Spring Security基于HttpRequest配置權(quán)限示例詳解
前言
今天我們重點聊聊授權(quán)方式的另外一種:基于HttpServletRequest配置權(quán)限
基于HttpServletRequest配置權(quán)限
一個典型的配置demo
http.authorizeHttpRequests(requestMatcherRegstry -> // /admin/** 需要有AMIND角色 requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN") // /log/** 只要有AMIND、USER角色之一 .requestMatchers("/log/**").hasAnyRole("ADMIN", "USER") // 任意請求 只要登錄了即可訪問 .anyRequest().authenticated() );
從這里也可以看出,要實現(xiàn)基于RBAC,還是比較容易的。也比較容易使用。但是如果想要動態(tài)的增加角色,就需要我們定制AuthorizationManager。
配置原理
HttpSecurity是負(fù)責(zé)構(gòu)建DefaultSecurityFilterChain的。而這個安全過濾器鏈,則是允許我們進(jìn)行配置的。而authorizeHttpRequests
方法,正是配置AuthorizationFilter的。而我們傳入的入?yún)?lambada表達(dá)式-則是指引如何配置AuthorizationFilter的。
/** * 這個方法是HttpSecurity的方法。 * 作用是配置AuthorizationFilter。 * 其入?yún)uthorizeHttpRequestsCustomizer正是讓我們配置AuthorizationFilter的關(guān)鍵。 * Customizer:就是定制。原理比較容易理解,就是我把你需要配置的東西丟給你,你往里面賦值。 * AuthorizeHttpRequestsConfigurer<HttpSecurity>:這個是Configurer的實現(xiàn),負(fù)責(zé)引入過濾器的。這里明顯就是引入AuthorizationFilter * AuthorizationManagerRequestMatcherRegistry:這個就是我們最終配置的東西。而這個配置的正是我們上面的RequestMatcherDelegatingAuthorizationManager。說白了就是往里面添加哪些路徑對應(yīng)哪些AuthorizationManager。只不過,為了方便使用,也幫我們都封裝好了。不妨繼續(xù)往后看看。 */ public HttpSecurity authorizeHttpRequests( Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer) throws Exception { ApplicationContext context = getContext(); // 這里干了三個事情: // 1. 如果當(dāng)前HttpSecurity不存在AuthorizeHttpRequestsConfigurer,則創(chuàng)建一個,并注冊到當(dāng)前的HttpSecurity對象中。 // 2. 從AuthorizeHttpRequestsConfigurer拿到他的注冊器也就是AuthorizationManagerRequestMatcherRegistry // 3. 調(diào)用傳入的參數(shù)的customize。如此,我們傳入的lambda表達(dá)式就被調(diào)用了。 authorizeHttpRequestsCustomizer .customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry()); return HttpSecurity.this; }
public final class AuthorizationManagerRequestMatcherRegistry extends AbstractRequestMatcherRegistry<AuthorizedUrl> { /** * 這是父類的方法 * C代表的是AuthorizedUrl */ public C requestMatchers(String... patterns) { // 調(diào)用的重載方法第一個參數(shù)為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表達(dá)式:
requestMatcherRegstry -> requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN")
配置的正是AuthorizationManagerRequestMatcherRegistry
requestMachers方法,構(gòu)建出AuthorizedUrl,然后通過這個類的hasRole方法注冊當(dāng)前路徑所對應(yīng)的權(quán)限/角色。這個對應(yīng)關(guān)系由RequestMatcherEntry保存。key:RequestMatcher requestMatcher;value: AuthorizationManager。
值得一提的是,這個lambda表達(dá)式以及其鏈?zhǔn)秸{(diào)用看起來簡單方便,但是其內(nèi)部涉及多個類的方法調(diào)用,實在很容易犯迷糊,這是我覺得比較詬病的地方。在我看來,鏈?zhǔn)秸{(diào)用還是同一個返回值(每次都返回this)才能做到在方便至于也能清晰明了,容易理解。
而這里在lambda表達(dá)式內(nèi)部:
- 第一個方法是
requestMatcherRegstry.requestMatchers
是AbstractRequestMatcherRegistry
,也就是我們的AuthorizationManagerRequestMatcherRegistry
的父類。方法返回值是AuthorizedUrl。
- 第二個方法是
AuthorizedUrl.hasRole
而該方法的返回值為AuthorizationManagerRequestMatcherRegistry
。
發(fā)現(xiàn)什么了嗎?鏈?zhǔn)秸{(diào)用還能玩起遞歸,又回到最開始的第一個方法了。而要是我們配置HttpSecurity,直接一連串的鏈?zhǔn)秸{(diào)用,那更是沒譜了。經(jīng)常就是,你只能看著別人這樣配置,然后照貓畫虎。這個鏈?zhǔn)秸{(diào)用咋調(diào)回來的,一頭霧。因為中間可能跨越好幾個不同的類。。。
PS:可能官方也有些意識到這點,所以sample工程都是類似于本文開頭的那樣,傳入一個基于lambda表達(dá)式的Customizer。一個方法配置一個過濾器的SecurityConfigurer。但,如果你翻看源碼,你看到的就是一連串的鏈?zhǔn)秸{(diào)用。最為明顯的一個證明就是HttpSecurity#and
方法過期了。因此個人推薦大家用文章開頭的那種方法,相對清晰易理解。
我想說,這么玩是深怕別人搞明白了是嗎???更絕的是,即便你知曉了原理也沒有辦法直接注冊對應(yīng)關(guān)系,除非你使用反射!
這里給大家提個醒,如果你想搞明白你在使用SpringSecurity究竟在配置些什么,那么你就必須要搞明白上面的套路。
設(shè)計方案
Spring Security在5.5版本之后,在鑒權(quán)架構(gòu)上,進(jìn)行了較大的改動。以至于官方也出了遷移指南
組件 | 5.5之前 | 5.5之后 |
---|---|---|
過濾器 | FilterSecurityInterceptor | AuthorizationFilter |
鑒權(quán)管理器 | AccessDecisionManager | AuthorizationManager |
訪問決策投票員 | AccessDecisionVoter | - |
而原來的設(shè)計方案,相較于新的方案,更為復(fù)雜。這里給大家一張官方的UML感受感受:
除卻過濾器外,還需要三個組件來構(gòu)建完整的鑒權(quán):
AccessDecisionManager 、AccessDecisionVoter 、ConfigAttribute。
感興趣的同學(xué)可以自己琢磨琢磨,但已經(jīng)廢棄的方案,這里就不討論了。
5.6之后的新方案
新方案只有一個包羅萬象、且極具擴展性的AuthorizationManager
我們前面的配置demo,本質(zhì)上都是在配置RequestMatcherDelegatingAuthorizationManager
。他主要是記錄每一個路徑對應(yīng)的AuthorizationManager<HttpServletRequest>
。當(dāng)有請求過來時,只需要遍歷每一個路徑,當(dāng)找到匹配者就委托該AuthorizationManager<HttpServletRequest>
進(jìn)行鑒權(quán)。
在我們的配置demo中,對應(yīng)的是AuthoriztyAuthorizationManager
和AuthenticatedAuthorizationManager
。前者,意味著我們配置的是角色/權(quán)限,后者對應(yīng)的是authenticated()
這個方法。
如果你認(rèn)真看了這個關(guān)系圖,那么一定會發(fā)現(xiàn)右邊的4個實現(xiàn)類正是我們在上一文講述基于方法配置權(quán)限中所使用到的。
鑒權(quán)源碼分析
權(quán)限過濾的入口:AuthorizationFilter
public class AuthorizationFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException { // 類型轉(zhuǎn)換 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; // 是否需要執(zhí)行鑒權(quán) if (this.observeOncePerRequest && isApplied(request)) { chain.doFilter(request, response); return; } // /error和異步請求不處理 if (skipDispatch(request)) { chain.doFilter(request, response); return; } // 是否已經(jīng)執(zhí)行過鑒權(quán)邏輯了 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { // 從SecurityContextHolder中獲取憑證,并通過AuthorizationManager做出決策 AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); // 發(fā)布鑒權(quán)事件 this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision); if (decision != null && !decision.isGranted()) { // 拒絕訪問異常 throw new AccessDeniedException("Access Denied"); } // 正常執(zhí)行后續(xù)業(yè)務(wù)邏輯 chain.doFilter(request, response); } finally { // 處理完業(yè)務(wù)邏輯后,為當(dāng)前請求清理標(biāo)識 request.removeAttribute(alreadyFilteredAttributeName); } } }
RequestMatcherDelegatingAuthorizationManager
public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> { @Override public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) { // 遍歷每一個已經(jīng)登錄好的路徑,找到對應(yīng)的AuthorizationManager<RequestAuthorizationContext>> for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) { RequestMatcher matcher = mapping.getRequestMatcher(); // 匹配當(dāng)前請求 MatchResult matchResult = matcher.matcher(request); if (matchResult.isMatch()) { // 找到匹配的AuthorizationManager就直接調(diào)用check方法并返回鑒權(quán)結(jié)果 AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry(); return manager.check(authentication, new RequestAuthorizationContext(request, matchResult.getVariables())); } } // 沒有匹配的AuthorizationManager則返回拒絕當(dāng)前請求 return DENY; } }
可見,在沒有匹配的AuthorizationManager的情況下,默認(rèn)是拒絕請求的。
總結(jié)
1.我們在配置中配置的url被封裝成RequestMatcher,而hasRole被封裝成AuthorityAuthorizationManager。進(jìn)行注冊,在請求過來時,便通過遍歷所有注冊好的RequestMatch進(jìn)行匹配,存在匹配就調(diào)用AuthorizationManager<RequestAuthorizationContext>#check
方法。
2.配置的鏈?zhǔn)秸{(diào)用,會跨越多個不同的類,最終又回到第一個對象的類型。
后記
本文我們聊了基于HttpRequest配置權(quán)限的方方面面。相信這里有一個點應(yīng)該會引起大家的注意:配置。下一次,我們聊聊Spring Security的配置體系。
相關(guān)文章
ZooKeeper入門教程二在單機和集群環(huán)境下的安裝搭建及使用
本文是ZooKeeper入門系列教程,涵蓋ZooKeeper的安裝使及單機集群環(huán)境搭建,通過實例和大量圖表,結(jié)合實戰(zhàn),幫助學(xué)習(xí)者理解和運用,有需要的朋友可以借鑒參考下2022-01-01SpringCloud及Nacos服務(wù)注冊IP選擇問題解決方法
這篇文章主要介紹了SpringCloud及Nacos服務(wù)注冊IP選擇問題,為什么注冊的IP和真實IP不符合呢,原因是Nacos客戶端在注冊服務(wù)時會從機器網(wǎng)卡中選擇一個IP來注冊,所以,當(dāng)注冊了的是非真實IP后,另一臺機器調(diào)用時是不可能調(diào)通的,知道問題原因就是解決方法,一起看看吧2024-01-01Java 獲取網(wǎng)絡(luò)302重定向URL的方法
在本篇文章里小編給大家整理的是關(guān)于Java 獲取網(wǎng)絡(luò)302重定向URL的方法以及相關(guān)知識點,有興趣的朋友們參考下。2019-08-08spring mvc @PathVariable綁定URI模板變量值方式
這篇文章主要介紹了spring mvc @PathVariable綁定URI模板變量值方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11