Spring Security源码:权限访问控制是如何做到的?
Posted 木兮同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security源码:权限访问控制是如何做到的?相关的知识,希望对你有一定的参考价值。
文章目录
〇、前文回顾
在实战篇《手把手教你如何使用Spring Security(下):访问控制》我们学习了Spring Security强大的访问控制能力,只需要进行寥寥几行的配置就能做到权限的控制,本篇来看看它到底是如何做到的。
一、再聊过滤器链
源码篇中反复提到,请求进来需要经过的是一堆过滤器形成的过滤器链,走完过滤器链未抛出异常则可以继续访问后台接口资源,而最后一个过滤器就是来判断请求是否有权限继续访问后台资源,如果没有则会将拒绝访问的异常往上向异常过滤器抛,异常过滤器会对异常进行翻译,然后响应给客户端。
- 所以,一般情况下最后一个过滤器是做
权限访问控制的核心过滤器FilterSecurityInterceptor
,而倒数第二个是异常翻译过滤器ExceptionTranslationFilter
,将异常进行翻译然后响应给客户端。 - 比如我们实战项目过滤器链图解
二、过滤器的创建
FilterSecurityInterceptor的创建
- 这个过滤器的配置器是
ExpressionUrlAuthorizationConfigurer
,它的父类AbstractInterceptUrlConfigurer
中的configure()
方法创建了这个过滤器。
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);
...
- 这个过滤器的配置器是在
HttpSecurity
的authorizeRequests()
方法中apply进来的,在我们自己配置的核心配置器中使用的就是该种基于HttpServletRequest
限制访问的方式。
ExceptionTranslationFilter的创建
- 这个过滤器的配置器是
ExceptionHandlingConfigurer
,它自己的configure()
方法中创建了这个过滤器。
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);
...
- 这个过滤器的配置器是在
HttpSecurity
的exceptionHandling()
方法中apply进来的,和上面不同的是,这个过滤器配置器会默认被apply进HttpSecurity
,在WebSecurityConfigurerAdapter
中的init()
方法,里面调用了getHttp()
方法,这里定义了很多默认的过滤器配置,其中就包括当前过滤器配置。
三、源码流程
FilterSecurityInterceptor
- 进入:
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- 进入:
invoke(FilterInvocation fi)
- 进入:
beforeInvocation(Object object)
这个方法里面有个
attributes
,里面获取的就是当前request请求所能匹配中的权限Spel表达式,比如这里是hasRole('ROLE_BUYER')
方法源码如下,继续往下走
protected InterceptorStatusToken beforeInvocation(Object object)
...
// 获取当前request请求所能匹配中的权限Spel表达式
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;
...
- 进入:
decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
这里有个投票器,投票结果为1表示可以访问直接返回,投票结果为-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();
- 进入:
vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes)
这里面其实就是使用Spring的Spel表达式进行投票,使用请求中的权限表达式组装Expression,使用Token令牌中的权限组装EvaluationContext,然后调用
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()
方法里面就是调用Expression的getValue()
方法,获取实际的匹配结果,如下图Spel表达式为hasRole('ROLE_BUYER')
所以它实际调用的是SecurityExpressionRoot#hasRole
方法(关于权限表达式对应实际调用的方法,在《手把手教你如何使用Spring Security(下):访问控制》文章中已贴出,下面文章也补充一份),里面的逻辑其实就是判断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;
- 如果投票成功,则会一直返回到
invoke()
方法,再执行后续过滤器,未抛异常表示该请求已经有访问权限了 - 假如投票失败,在
decide()
方法中会向上抛拒绝访问异常,一直往上抛直到被处理,往上反向跟踪发现这个过滤器一直没有处理拒绝访问异常,那就继续往上个过滤器抛,就到了我们的异常翻译过滤器ExceptionTranslationFilter
。
ExceptionTranslationFilter
- 该过滤器的
doFilter()
方法很简单,没有逻辑处理,只对后续过滤器抛出的异常进行处理,源码如下:
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);
- 当抛出拒绝访问异常后,继续调用
handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception)
方法,方法里面主要将异常信息和错误码设置到响应头
,然后响应到客户端,请求结束。
补充:权限表达式
权限表达式(ExpressionUrlAuthorizationConfigurer) | 说明 | Spel表达式 | Spel表达式实际执行方法(SecurityExpressionOperations) |
---|---|---|---|
permitAll() | 表示允许所有,永远返回true | permitAll | permitAll() |
denyAll() | 表示拒绝所有,永远返回false | denyAll | denyAll() |
anonymous() | 当前用户是anonymous时返回true | anonymous | isAnonymous() |
rememberMe() | 当前用户是rememberMe用户时返回true | rememberMe | isRememberMe() |
authenticated() | 当前用户不是anonymous时返回true | authenticated | isAuthenticated() |
fullyAuthenticated() | 当前用户既不是anonymous也不是rememberMe用户时返回true | fullyAuthenticated | isFullyAuthenticated() |
hasRole(“BUYER”) | 用户拥有指定权限时返回true | hasRole(‘ROLE_BUYER’) | hasRole(String role) |
hasAnyRole(“BUYER”,“SELLER”) | 用于拥有任意一个角色权限时返回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’) | 请求发送的Ip匹配时返回true | hasIpAddress(‘192.168.1.0/24’) | hasIpAddress(String ipAddress),该方法在WebSecurityExpressionRoot类中 |
access("@rbacService.hasPermission(request, authentication)") | 可以自定义Spel表达式 | @rbacService.hasPermission (request, authentication) | hasPermission(request, authentication) ,该方法在自定义的RbacServiceImpl类中 |
四、总结
- 访问控制的核心过滤器是
FilterSecurityInterceptor
,当然这个是可选的,我们完全也可以自定义一个过滤器去处理权限访问。 - 处理访问异常处理的过滤器是
ExceptionTranslationFilter
,里面逻辑很简单,给response设置异常信息错误码,再返回给客户端。
五、系列文章
Spring Security 系列
- 《手把手教你如何使用Spring Security(上):登录授权》
- 《手把手教你如何使用Spring Security(中):接口认证》
- 《手把手教你如何使用Spring Security(下):访问控制》
- 《Spring Security源码(一):整体框架设计》
- 《Spring Security源码(二):建造者详解》
- 《Spring Security源码(三):HttpSecurity详解》
- 《Spring Security源码(四):配置器详解》
- 《Spring Security源码(五):FilterChainProxy是如何创建的?》
- 《Spring Security源码(六):FilterChainProxy是如何运行的?》
- 《Spring Security源码(七):设计模式在框架中的应用》
- 《Spring Security源码(八):登录认证源码流程》
- 《Spring Security源码(九):过滤器链上的过滤器是如何排序的?》
- 《Spring Security源码(十):权限访问控制是如何做到的?》
Spring Security OAuth 系列
以上是关于Spring Security源码:权限访问控制是如何做到的?的主要内容,如果未能解决你的问题,请参考以下文章
手把手教你如何使用Spring Security(下):访问控制
spring security需要登录后才能访问的路径的权限配置是怎么样的
SpringBoot集成Spring Security——权限控制