Spring Security源碼解析之權(quán)限訪問控制是如何做到的
〇、前文回顧
在實(shí)戰(zhàn)篇《話說Spring Security權(quán)限管理(源碼詳解)》我們學(xué)習(xí)了Spring Security強(qiáng)大的訪問控制能力,只需要進(jìn)行寥寥幾行的配置就能做到權(quán)限的控制,本篇來看看它到底是如何做到的。
一、再聊過濾器鏈
源碼篇中反復(fù)提到,請(qǐng)求進(jìn)來需要經(jīng)過的是一堆過濾器形成的過濾器鏈,走完過濾器鏈未拋出異常則可以繼續(xù)訪問后臺(tái)接口資源,而最后一個(gè)過濾器就是來判斷請(qǐng)求是否有權(quán)限繼續(xù)訪問后臺(tái)資源,如果沒有則會(huì)將拒絕訪問的異常往上向異常過濾器拋,異常過濾器會(huì)對(duì)異常進(jìn)行翻譯,然后響應(yīng)給客戶端。
所以,一般情況下最后一個(gè)過濾器是做權(quán)限訪問控制的核心過濾器FilterSecurityInterceptor ,而倒數(shù)第二個(gè)是異常翻譯過濾器ExceptionTranslationFilter ,將異常進(jìn)行翻譯然后響應(yīng)給客戶端。比如我們實(shí)戰(zhàn)項(xiàng)目過濾器鏈圖解

二、過濾器的創(chuàng)建
FilterSecurityInterceptor的創(chuàng)建
這個(gè)過濾器的配置器是 ExpressionUrlAuthorizationConfigurer ,它的父類 AbstractInterceptUrlConfigurer 中的 configure() 方法創(chuàng)建了這個(gè)過濾器。
abstract class AbstractInterceptUrlConfigurer<C extends AbstractInterceptUrlConfigurer<C, H>, H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<C, H> {
...
@Override
public void configure(H http) throws Exception {
FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
if (metadataSource == null) {
return;
}
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
if (filterSecurityInterceptorOncePerRequest != null) {
securityInterceptor
.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
}
securityInterceptor = postProcess(securityInterceptor);
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
...
}
這個(gè)過濾器的配置器是在 HttpSecurity 的 authorizeRequests() 方法中apply進(jìn)來的,在我們自己配置的核心配置器中使用的就是該種基于 HttpServletRequest 限制訪問的方式。

ExceptionTranslationFilter的創(chuàng)建
這個(gè)過濾器的配置器是 ExceptionHandlingConfigurer ,它自己的 configure() 方法中創(chuàng)建了這個(gè)過濾器。
public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<ExceptionHandlingConfigurer<H>, H> {
...
@Override
public void configure(H http) throws Exception {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
entryPoint, getRequestCache(http));
if (accessDeniedHandler != null) {
exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler);
}
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
...
}
這個(gè)過濾器的配置器是在 HttpSecurity 的 exceptionHandling() 方法中apply進(jìn)來的,和上面不同的是,這個(gè)過濾器配置器會(huì)默認(rèn)被apply進(jìn) HttpSecurity,在 WebSecurityConfigurerAdapter 中的 init() 方法,里面調(diào)用了 getHttp() 方法,這里定義了很多默認(rèn)的過濾器配置,其中就包括當(dāng)前過濾器配置。

三、源碼流程
FilterSecurityInterceptor
- 進(jìn)入:
doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - 進(jìn)入:
invoke(FilterInvocation fi) - 進(jìn)入:
beforeInvocation(Object object)
這個(gè)方法里面有個(gè)
attributes,里面獲取的就是當(dāng)前request請(qǐng)求所能匹配中的權(quán)限Spel表達(dá)式,比如這里是hasRole('ROLE_BUYER')
方法源碼如下,繼續(xù)往下走
protected InterceptorStatusToken beforeInvocation(Object object) {
...
// 獲取當(dāng)前request請(qǐng)求所能匹配中的權(quán)限Spel表達(dá)式
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
...
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
...
}
進(jìn)入:decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
這里有個(gè)投票器,投票結(jié)果為1表示可以訪問直接返回,投票結(jié)果為-1表示拒絕訪問,向上拋拒絕訪問異常,這里使用的投票器是
WebExpressionVoter
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
進(jìn)入:vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes)
這里面其實(shí)就是使用Spring的Spel表達(dá)式進(jìn)行投票,使用請(qǐng)求中的權(quán)限表達(dá)式組裝Expression,使用Token令牌中的權(quán)限組裝EvaluationContext,然后調(diào)用
ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx),
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
evaluateAsBoolean()方法里面就是調(diào)用Expression的getValue()方法,獲取實(shí)際的匹配結(jié)果,如下圖Spel表達(dá)式為hasRole('ROLE_BUYER')
所以它實(shí)際調(diào)用的是SecurityExpressionRoot#hasRole方法(關(guān)于權(quán)限表達(dá)式對(duì)應(yīng)實(shí)際調(diào)用的方法,在《手把手教你如何使用Spring Security(下):訪問控制》文章中已貼出,下面文章也補(bǔ)充一份),里面的邏輯其實(shí)就是判斷Token令牌中是否包含有ROLE_BUYER的角色,有的話返回true,否則返回false,如下為SecurityExpressionRoot#hasRole方法源碼:
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
- 如果投票成功,則會(huì)一直返回到
invoke()方法,再執(zhí)行后續(xù)過濾器,未拋異常表示該請(qǐng)求已經(jīng)有訪問權(quán)限了 - 假如投票失敗,在
decide()方法中會(huì)向上拋拒絕訪問異常,一直往上拋直到被處理,往上反向跟蹤發(fā)現(xiàn)這個(gè)過濾器一直沒有處理拒絕訪問異常,那就繼續(xù)往上個(gè)過濾器拋,就到了我們的異常翻譯過濾器ExceptionTranslationFilter。
ExceptionTranslationFilter
該過濾器的 doFilter() 方法很簡單,沒有邏輯處理,只對(duì)后續(xù)過濾器拋出的異常進(jìn)行處理,源碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
當(dāng)拋出拒絕訪問異常后,繼續(xù)調(diào)用 handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) 方法,方法里面主要將異常信息和錯(cuò)誤碼設(shè)置到響應(yīng)頭,然后響應(yīng)到客戶端,請(qǐng)求結(jié)束。
補(bǔ)充:權(quán)限表達(dá)式
| 權(quán)限表達(dá)式(ExpressionUrlAuthorizationConfigurer) | 說明 | Spel表達(dá)式 | Spel表達(dá)式實(shí)際執(zhí)行方法(SecurityExpressionOperations) |
|---|---|---|---|
| permitAll() | 表示允許所有,永遠(yuǎn)返回true | permitAll | permitAll() |
| denyAll() | 表示拒絕所有,永遠(yuǎn)返回false | denyAll | denyAll() |
| anonymous() | 當(dāng)前用戶是anonymous時(shí)返回true | anonymous | isAnonymous() |
| rememberMe() | 當(dāng)前用戶是rememberMe用戶時(shí)返回true | rememberMe | isRememberMe() |
| authenticated() | 當(dāng)前用戶不是anonymous時(shí)返回true | authenticated | isAuthenticated() |
| fullyAuthenticated() | 當(dāng)前用戶既不是anonymous也不是rememberMe用戶時(shí)返回true | fullyAuthenticated | isFullyAuthenticated() |
| hasRole(“BUYER”) | 用戶擁有指定權(quán)限時(shí)返回true | hasRole(‘ROLE_BUYER') | hasRole(String role) |
| hasAnyRole(“BUYER”,“SELLER”) | 用于擁有任意一個(gè)角色權(quán)限時(shí)返回true | hasAnyRole (‘ROLE_BUYER',‘ROLE_BUYER') | hasAnyRole(String… roles) |
| hasAuthority(“BUYER”) | 同hasRole | hasAuthority(‘ROLE_BUYER') | hasAuthority(String role) |
| hasAnyAuthority(“BUYER”,“SELLER”) | 同hasAnyRole | hasAnyAuthority (‘ROLE_BUYER',‘ROLE_BUYER') | hasAnyAuthority(String… authorities) |
| hasIpAddress(‘192.168.1.0/24') | 請(qǐng)求發(fā)送的Ip匹配時(shí)返回true | hasIpAddress(‘192.168.1.0/24') | hasIpAddress(String ipAddress),該方法在WebSecurityExpressionRoot類中 |
| access("@rbacService.hasPermission(request, authentication)") | 可以自定義Spel表達(dá)式 | @rbacService.hasPermission (request, authentication) | hasPermission(request, authentication) ,該方法在自定義的RbacServiceImpl類中 |
四、總結(jié)
- 訪問控制的核心過濾器是
FilterSecurityInterceptor,當(dāng)然這個(gè)是可選的,我們完全也可以自定義一個(gè)過濾器去處理權(quán)限訪問。 - 處理訪問異常處理的過濾器是
ExceptionTranslationFilter,里面邏輯很簡單,給response設(shè)置異常信息錯(cuò)誤碼,再返回給客戶端。
以上就是Spring Security源碼解析之權(quán)限訪問控制是如何做到的的詳細(xì)內(nèi)容,更多關(guān)于Spring Security權(quán)限訪問控制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java多線程的調(diào)度_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
有多個(gè)線程,如何控制它們執(zhí)行的先后次序呢?下文給大家分享四種方法及java多線程調(diào)度的實(shí)例代碼,需要的朋友參考下吧2017-05-05
Java如何避免死鎖和競(jìng)態(tài)條件的實(shí)現(xiàn)
本文主要介紹了Java如何避免死鎖和競(jìng)態(tài)條件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
布隆過濾器詳解以及其在Java中的實(shí)際應(yīng)用
布隆過濾器是一種數(shù)據(jù)結(jié)構(gòu),比較巧妙的概率型數(shù)據(jù)結(jié)構(gòu)(probabilistic data structure),特點(diǎn)是高效地插入和查詢,這篇文章主要給大家介紹了關(guān)于布隆過濾器詳解以及其在Java中的實(shí)際應(yīng)用,需要的朋友可以參考下2023-12-12
SpringBoot中處理跨域請(qǐng)求CORS的全面指南
跨域資源共享是一種安全機(jī)制,它允許Web應(yīng)用程序在一個(gè)域上的資源請(qǐng)求另一個(gè)域上的資源,下面就跟隨小編一起來深入了解下SpringBoot中處理跨域請(qǐng)求CORS的具體操作吧2025-04-04
SpringBoot集成ShedLock實(shí)現(xiàn)分布式定時(shí)任務(wù)
ShedLock 是一個(gè) Java 庫,通常用于分布式系統(tǒng)中,確保定時(shí)任務(wù)(Scheduled Tasks)在集群環(huán)境下只被某一個(gè)實(shí)例執(zhí)行一次,它通過在共享資源中添加鎖的方式,本文給大家介紹了SpringBoot集成ShedLock實(shí)現(xiàn)分布式定時(shí)任務(wù),需要的朋友可以參考下2024-11-11
Java Web項(xiàng)目創(chuàng)建并實(shí)現(xiàn)前后端交互
本文主要介紹了Java Web項(xiàng)目創(chuàng)建并實(shí)現(xiàn)前后端交互,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Springboot SseEmitter流式輸出的實(shí)現(xiàn)代碼
本文介紹了Spring Boot中使用SseEmitter實(shí)現(xiàn)流式輸出的原理和示例代碼,通過SseEmitter,可以實(shí)現(xiàn)客戶端和服務(wù)器之間的實(shí)時(shí)通信,服務(wù)器可以分塊發(fā)送數(shù)據(jù),而客戶端可以實(shí)時(shí)接收和處理這些數(shù)據(jù),,感興趣的朋友一起看看吧2025-03-03
IDEA2022搭建Spring?Cloud多模塊項(xiàng)目的詳細(xì)過程
這篇文章主要介紹了IDEA2022搭建Spring?Cloud多模塊項(xiàng)目,網(wǎng)上有很多教程父模塊都是通過maven的方式創(chuàng)建的,然后子模塊是通過Spring?Initalizr方式創(chuàng)建,這種方式父模塊無法管理子模塊的依賴仲裁,需要每個(gè)子模塊自行管理,就失去了父模塊的用處了2022-10-10


方法源碼如下,繼續(xù)往下走