網站首頁 編程語言 正文
前言
今天我們重點聊聊授權方式的另外一種:基于HttpServletRequest配置權限
基于HttpServletRequest配置權限
一個典型的配置demo
http.authorizeHttpRequests(requestMatcherRegstry ->
// /admin/** 需要有AMIND角色
requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN")
// /log/** 只要有AMIND、USER角色之一
.requestMatchers("/log/**").hasAnyRole("ADMIN", "USER")
// 任意請求 只要登錄了即可訪問
.anyRequest().authenticated()
);
從這里也可以看出,要實現基于RBAC,還是比較容易的。也比較容易使用。但是如果想要動態的增加角色,就需要我們定制AuthorizationManager。
配置原理
HttpSecurity是負責構建DefaultSecurityFilterChain的。而這個安全過濾器鏈,則是允許我們進行配置的。而authorizeHttpRequests
方法,正是配置AuthorizationFilter的。而我們傳入的入參-lambada表達式-則是指引如何配置AuthorizationFilter的。
/**
* 這個方法是HttpSecurity的方法。
* 作用是配置AuthorizationFilter。
* 其入參authorizeHttpRequestsCustomizer正是讓我們配置AuthorizationFilter的關鍵。
* Customizer:就是定制。原理比較容易理解,就是我把你需要配置的東西丟給你,你往里面賦值。
* AuthorizeHttpRequestsConfigurer<HttpSecurity>:這個是Configurer的實現,負責引入過濾器的。這里明顯就是引入AuthorizationFilter
* AuthorizationManagerRequestMatcherRegistry:這個就是我們最終配置的東西。而這個配置的正是我們上面的RequestMatcherDelegatingAuthorizationManager。說白了就是往里面添加哪些路徑對應哪些AuthorizationManager。只不過,為了方便使用,也幫我們都封裝好了。不妨繼續往后看看。
*/
public HttpSecurity authorizeHttpRequests(
Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)
throws Exception {
ApplicationContext context = getContext();
// 這里干了三個事情:
// 1. 如果當前HttpSecurity不存在AuthorizeHttpRequestsConfigurer,則創建一個,并注冊到當前的HttpSecurity對象中。
// 2. 從AuthorizeHttpRequestsConfigurer拿到他的注冊器也就是AuthorizationManagerRequestMatcherRegistry
// 3. 調用傳入的參數的customize。如此,我們傳入的lambda表達式就被調用了。
authorizeHttpRequestsCustomizer
.customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry());
return HttpSecurity.this;
}
public final class AuthorizationManagerRequestMatcherRegistry
extends AbstractRequestMatcherRegistry<AuthorizedUrl> {
/**
* 這是父類的方法
* C代表的是AuthorizedUrl
*/
public C requestMatchers(String... patterns) {
// 調用的重載方法第一個參數為HttpMethod,也就是說,我們還可以指定HTTP請求的方法,例如:POST、GET等
return requestMatchers(null, patterns);
}
@Override
protected AuthorizedUrl chainRequestMatchers(List<RequestMatcher> requestMatchers) {
this.unmappedMatchers = requestMatchers;
return new AuthorizedUrl(requestMatchers);
}
}
public class AuthorizedUrl {
private final List<? extends RequestMatcher> matchers;
public AuthorizationManagerRequestMatcherRegistry permitAll() {
return access(permitAllAuthorizationManager);
}
public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasRole(role)));
}
public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) {
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
}
public AuthorizationManagerRequestMatcherRegistry authenticated() {
return access(AuthenticatedAuthorizationManager.authenticated());
}
public AuthorizationManagerRequestMatcherRegistry access(
AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.notNull(manager, "manager cannot be null");
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
}
}
public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<AuthorizeHttpRequestsConfigurer<H>, H> {
private AuthorizationManagerRequestMatcherRegistry addMapping(List<? extends RequestMatcher> matchers,
AuthorizationManager<RequestAuthorizationContext> manager) {
for (RequestMatcher matcher : matchers) {
this.registry.addMapping(matcher, manager);
}
return this.registry;
}
}
我們通過lambda表達式:
requestMatcherRegstry -> requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN")
配置的正是AuthorizationManagerRequestMatcherRegistry
requestMachers方法,構建出AuthorizedUrl,然后通過這個類的hasRole方法注冊當前路徑所對應的權限/角色。這個對應關系由RequestMatcherEntry保存。key:RequestMatcher requestMatcher;value: AuthorizationManager。
值得一提的是,這個lambda表達式以及其鏈式調用看起來簡單方便,但是其內部涉及多個類的方法調用,實在很容易犯迷糊,這是我覺得比較詬病的地方。在我看來,鏈式調用還是同一個返回值(每次都返回this)才能做到在方便至于也能清晰明了,容易理解。
而這里在lambda表達式內部:
- 第一個方法是
requestMatcherRegstry.requestMatchers
是AbstractRequestMatcherRegistry
,也就是我們的AuthorizationManagerRequestMatcherRegistry
的父類。方法返回值是AuthorizedUrl。 - 第二個方法是
AuthorizedUrl.hasRole
而該方法的返回值為AuthorizationManagerRequestMatcherRegistry
。
發現什么了嗎?鏈式調用還能玩起遞歸,又回到最開始的第一個方法了。而要是我們配置HttpSecurity,直接一連串的鏈式調用,那更是沒譜了。經常就是,你只能看著別人這樣配置,然后照貓畫虎。這個鏈式調用咋調回來的,一頭霧。因為中間可能跨越好幾個不同的類。。。
PS:可能官方也有些意識到這點,所以sample工程都是類似于本文開頭的那樣,傳入一個基于lambda表達式的Customizer。一個方法配置一個過濾器的SecurityConfigurer。但,如果你翻看源碼,你看到的就是一連串的鏈式調用。最為明顯的一個證明就是HttpSecurity#and
方法過期了。因此個人推薦大家用文章開頭的那種方法,相對清晰易理解。
我想說,這么玩是深怕別人搞明白了是嗎???更絕的是,即便你知曉了原理也沒有辦法直接注冊對應關系,除非你使用反射!
這里給大家提個醒,如果你想搞明白你在使用SpringSecurity究竟在配置些什么,那么你就必須要搞明白上面的套路。
設計方案
Spring Security在5.5版本之后,在鑒權架構上,進行了較大的改動。以至于官方也出了遷移指南
組件 | 5.5之前 | 5.5之后 |
---|---|---|
過濾器 | FilterSecurityInterceptor | AuthorizationFilter |
鑒權管理器 | AccessDecisionManager | AuthorizationManager |
訪問決策投票員 | AccessDecisionVoter | - |
而原來的設計方案,相較于新的方案,更為復雜。這里給大家一張官方的UML感受感受:
除卻過濾器外,還需要三個組件來構建完整的鑒權:
AccessDecisionManager 、AccessDecisionVoter 、ConfigAttribute。
感興趣的同學可以自己琢磨琢磨,但已經廢棄的方案,這里就不討論了。
5.6之后的新方案
新方案只有一個包羅萬象、且極具擴展性的AuthorizationManager
我們前面的配置demo,本質上都是在配置RequestMatcherDelegatingAuthorizationManager
。他主要是記錄每一個路徑對應的AuthorizationManager<HttpServletRequest>
。當有請求過來時,只需要遍歷每一個路徑,當找到匹配者就委托該AuthorizationManager<HttpServletRequest>
進行鑒權。
在我們的配置demo中,對應的是AuthoriztyAuthorizationManager
和AuthenticatedAuthorizationManager
。前者,意味著我們配置的是角色/權限,后者對應的是authenticated()
這個方法。
如果你認真看了這個關系圖,那么一定會發現右邊的4個實現類正是我們在上一文講述基于方法配置權限中所使用到的。
鑒權源碼分析
權限過濾的入口:AuthorizationFilter
public class AuthorizationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws ServletException, IOException {
// 類型轉換
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 是否需要執行鑒權
if (this.observeOncePerRequest && isApplied(request)) {
chain.doFilter(request, response);
return;
}
// /error和異步請求不處理
if (skipDispatch(request)) {
chain.doFilter(request, response);
return;
}
// 是否已經執行過鑒權邏輯了
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 從SecurityContextHolder中獲取憑證,并通過AuthorizationManager做出決策
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
// 發布鑒權事件
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
if (decision != null && !decision.isGranted()) {
// 拒絕訪問異常
throw new AccessDeniedException("Access Denied");
}
// 正常執行后續業務邏輯
chain.doFilter(request, response);
}
finally {
// 處理完業務邏輯后,為當前請求清理標識
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
RequestMatcherDelegatingAuthorizationManager
public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
// 遍歷每一個已經登錄好的路徑,找到對應的AuthorizationManager<RequestAuthorizationContext>>
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
RequestMatcher matcher = mapping.getRequestMatcher();
// 匹配當前請求
MatchResult matchResult = matcher.matcher(request);
if (matchResult.isMatch()) {
// 找到匹配的AuthorizationManager就直接調用check方法并返回鑒權結果
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
return manager.check(authentication,
new RequestAuthorizationContext(request, matchResult.getVariables()));
}
}
// 沒有匹配的AuthorizationManager則返回拒絕當前請求
return DENY;
}
}
可見,在沒有匹配的AuthorizationManager的情況下,默認是拒絕請求的。
總結
-
我們在配置中配置的url被封裝成RequestMatcher,而hasRole被封裝成AuthorityAuthorizationManager。進行注冊,在請求過來時,便通過遍歷所有注冊好的RequestMatch進行匹配,存在匹配就調用
AuthorizationManager<RequestAuthorizationContext>#check
方法。 -
配置的鏈式調用,會跨越多個不同的類,最終又回到第一個對象的類型。
后記
本文我們聊了基于HttpRequest配置權限的方方面面。相信這里有一個點應該會引起大家的注意:配置。下一次,我們聊聊Spring Security的配置體系。
原文鏈接:https://blog.csdn.net/Evan_L/article/details/136605794
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2023-03-19 C語言利用goto語句設計實現一個關機程序_C 語言
- 2022-04-09 WPF圖表LiveChart使用詳解_基礎應用
- 2022-06-18 Redis實現單設備登錄的場景分析_Redis
- 2023-10-16 el-popover在原生table中,彈出多個以及內部取消按鈕無效問題
- 2022-10-17 Net?core中使用System.Drawing對上傳的圖片流進行壓縮(示例代碼)_實用技巧
- 2022-12-31 一文初探Go語言中的reflect反射包_Golang
- 2022-07-12 oracle?指定類型和指定位數創建序列號的代碼詳解_oracle
- 2022-04-30 利用Python生成Excel炫酷圖表_python
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支