深⼊理解SpringSecurity授权机制原理
原创/朱季谦
在Spring Security权限框架⾥,若要对后端http接⼝实现权限授权控制,有两种实现⽅式。
⼀、⼀种是基于注解⽅法级的鉴权,其中,注解⽅式⼜有@Secured和@PreAuthorize两种。
@Secured如:
1 @PostMapping("/test")
2 @Secured({WebResRole.ROLE_PEOPLE_W})
3public void test(){
4 ......
5return null;
6 }
@PreAuthorize如:
1 @PostMapping("save")工商管理英文
2 @PreAuthorize("hasAuthority('sys:ur:add') AND hasAuthority('sys:ur:edit')")
3public RestRespon save(@RequestBody @Validated SysUr sysUr, BindingResult result) {
4 ValiParamUtils.ValiParamReq(result);
5return sysUrService.save(sysUr);
6 }
⼆、⼀种基于config配置类,需在对应config类配置@EnableGlobalMethodSecurity(prePostEnabled = true)注解才能⽣效,其权限控制⽅式如下:
1 @Override
2protected void configure(HttpSecurity httpSecurity) throws Exception {
3//使⽤的是JWT,禁⽤csrf
4 s().and().csrf().disable()
5//设置请求必须进⾏权限认证
6 .authorizeRequests()
7//⾸页和登录页⾯
8 .antMatchers("/").permitAll()
9 .antMatchers("/login").permitAll()
10// 其他所有请求需要⾝份认证
11 .anyRequest().authenticated();
12//退出登录处理
13 httpSecurity.logout().logoutSuccessHandler(...);
14//token验证过滤器
干萝卜条咸菜
15 httpSecurity.addFilterBefore(...);
16 }
这两种⽅式各有各的特点,在⽇常开发当中,普通程序员接触⽐较多的,则是注解⽅式的接⼝权限控制。
那么问题来了,我们配置这些注解或者类,其curity框是如何帮做到能针对具体的后端API接⼝做权限控制的呢?
单从⼀⾏@PreAuthorize("hasAuthority('sys:ur:add') AND hasAuthority('sys:ur:edit')")注解上看,是看不出任何头绪来的,若要回答这个问题,还需深⼊到源码层⾯,⽅能对curity授权机制有更好理解。
若要对这个过程做⼀个总的概述,笔者整体以⾃⼰的思考稍作了总结,可以简单⼏句话说明其整体实现,以该接⼝为例:
1 @PostMapping("save")
2 @PreAuthorize("hasAuthority('sys:ur:add')")
3public RestRespon save(@RequestBody @Validated SysUr sysUr, BindingResult result) {
4 ValiParamUtils.ValiParamReq(result);
5return sysUrService.save(sysUr);
6 }
即,认证通过的⽤户,发起请求要访问“/save”接⼝,若该url请求在配置类⾥设置为必须进⾏权限认证的,就会被curity框架使⽤filter拦截器对该请求进⾏拦截认证。拦截过程主要⼀个动作,是把该请求所拥有的权限集与@PreAuthorize设置的权限字符“sys:ur:add”进⾏匹配,若能匹配上,说明该请求是拥有调⽤“/save”接⼝的权限,那么,就可以被允许执⾏该接⼝资源。
在springboot+curity+jwt框架中,通过⼀系列内置或者⾃⾏定义的过滤器Filter来达到权限控制,如何设置⾃定义的过滤器Filter呢?例如,可以通过设置httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UrnamePasswordAuthenticationFilter.class)来⾃定义⼀个基于JWT拦截的过滤器JwtFilter,这⾥的addFilterBefore⽅法将在下⼀篇⽂详细分析,这⾥暂不展开,该⽅法⼤概意思就是,将⾃定义过滤器JwtFilter加⼊到Security框架⾥,成为其中的⼀个优先安全Filter,代码层⾯就是将⾃定义过滤器添加到List<Filter> filters。
设置增加⾃⾏定义的过滤器Filter伪代码如下:
1 @Configuration
2 @EnableWebSecurity
3 @EnableGlobalMethodSecurity(prePostEnabled = true)
4public class SecurityConfig extends WebSecurityConfigurerAdapter {
5 ......
6 @Override
7protected void configure(HttpSecurity httpSecurity) throws Exception {
8//使⽤的是JWT,禁⽤csrf
9 s().and().csrf().disable()
10//设置请求必须进⾏权限认证
11 .authorizeRequests()
12 ......
13//⾸页和登录页⾯
14 .antMatchers("/").permitAll()
15 .antMatchers("/login").permitAll()
16// 其他所有请求需要⾝份认证
17 .anyRequest().authenticated();
18 ......
19//token验证过滤器丑桔
20 httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UrnamePasswordAuthenticationFilter.class);
21 }
22 }
该过滤器类extrends继承BasicAuthenticationFilter,⽽BasicAuthenticationFilter是继承OncePerRequestFilter,该过滤器确保在⼀次请求只通过⼀次filter,⽽不需要重复执⾏。这样配置后,当请求过来时,会⾃动被JwtFilter类拦截,这时,将执⾏重写的doFilterInternal⽅法,在
1public class JwtFilter extends BasicAuthenticationFilter {
2
步步春3 @Autowired
4public JwtFilter(AuthenticationManager authenticationManager) {
5super(authenticationManager);
6 }
7
8 @Override
9protected void doFilterInternal(HttpServletRequest request, HttpServletRespon respon, FilterChain chain) throws IOException, ServletException { 10// 获取token, 并检查登录状态
11// 获取令牌并根据令牌获取登录认证信息
12 Authentication authentication = AuthenticationeFromToken(request);
13// 设置登录认证信息到上下⽂
14 Context().tAuthentication(authentication);
15
16 chain.doFilter(request, respon);
17 }
18
19 }
那么,问题来了,过滤器链FilterChain究竟是什么?
西工大附小这⾥,先点进去看下其类源码:
1package javax.rvlet;
2
3import java.io.IOException;
4
5public interface FilterChain {
6void doFilter(ServletRequest var1, ServletRespon var2) throws IOException, ServletException;
7 }
FilterChain只有⼀个 doFilter⽅法,这个⽅法的作⽤就是将请求request转发到下⼀个过滤器filter进⾏过滤处理操作,执⾏过程如下:
过滤器链就像⼀条铁链,中间的每个过滤器都包含对另⼀个过滤器的引⽤,从⽽把相关的过滤器链接起来,像⼀条链的样⼦。这时请求线程就如蚂蚁⼀样,会沿着这条链⼀直爬过去-----即,通过各过滤器调⽤另⼀个过滤器引⽤⽅法chain.doFilter(request, respon),实现⼀层嵌套⼀层地将请求传递下去,当该请求传递到能被处理的的过滤器时,就会被处理,处理完成后转发返回。通过过滤器链,可实现在不同的过滤器当中对请求request做处理,且过滤器之间彼此互不⼲扰。
Spring Security框架上过滤器链上都有哪些过滤器呢?
可以在DefaultSecurityFilterChain类根据输出相关log或者debug来查看Security都有哪些过滤器,如在DefaultSecurityFilterChain类中的构造器中打断点,如图所⽰,可以看到,⾃定义的JwtFilter过滤器也包含其中:
这些过滤器都在同⼀条过滤器链上,即通过chain.doFilter(request, respon)可将请求⼀层接⼀层转发,处理请求接⼝是否授权的主要过滤器是FilterSecurityInterceptor,其主要作⽤如下:
1. 获取到需访问接⼝的权限信息,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定义的权限信息;
2. 根据SecurityContextHolder中存储的authentication⽤户信息,来判断是否包含与需访问接⼝的权限信息,若包含,则说明拥有该接⼝权限;
3. 主要授权功能在⽗类AbstractSecurityInterceptor中实现;
我们将从FilterSecurityInterceptor这⾥开始重点分析Security授权机制原理的实现。
过滤器链将请求传递转发FilterSecurityInterceptor时,会执⾏FilterSecurityInterceptor的doFilter⽅法:
1public void doFilter(ServletRequest request, ServletRespon respon,
2 FilterChain chain) throws IOException, ServletException {
3 FilterInvocation fi = new FilterInvocation(request, respon, chain);
4 invoke(fi);
5 }
在这段代码当中,FilterInvocation类是⼀个有意思的存在,其实它的功能很简单,就是将上⼀个过滤器传递过滤的request,respon,chain复制保存到FilterInvocation⾥,专门供FilterSecurityInterceptor过滤器使⽤。它的有意思之处在于,是将多个参数统⼀归纳到⼀个类当中,
其到统⼀管理作⽤,你想,若是N多个参数,传进来都分散到类的各个地⽅,参数多了,代码多了,⽅法过于分散时,可能就很容易造成阅读过程中,弄糊涂这些个参数都是哪⾥来了。但若统⼀归纳到⼀个类⾥,就能很快定位其来源,⽅便代码阅读。⽹上有⼈提到该FilterInvocation类还起到解耦作⽤,即避免与其他过滤器使⽤同样的引⽤变量。
总⽽⾔之,这个地⽅的设定虽简单,但很值得我们学习⼀番,将其思想运⽤到实际开发当中,不外乎也是⼀种能简化代码的⽅法。
FilterInvocation主要源码如下:
1public class FilterInvocation {
学院风连衣裙2
3private FilterChain chain;
4private HttpServletRequest request;
5private HttpServletRespon respon;
6
7
8public FilterInvocation(ServletRequest request, ServletRespon respon,
9 FilterChain chain) {
10if ((request == null) || (respon == null) || (chain == null)) {
11throw new IllegalArgumentException("Cannot pass null values to constructor");
12 }
13
16this.chain = chain;
17 }
18 ......
19 }
FilterSecurityInterceptor的doFilter⽅法⾥调⽤invoke(fi)⽅法:
1public void invoke(FilterInvocation fi) throws IOException, ServletException {
2if ((fi.getRequest() != null)
3 && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
4 && obrveOncePerRequest) {
5//筛选器已应⽤于此请求,每个请求处理⼀次,所以不需重新进⾏安全检查
6 fi.getChain().Request(), fi.getRespon());
7 }
8el {
9// 第⼀次调⽤此请求时,需执⾏安全检查
10if (fi.getRequest() != null && obrveOncePerRequest) {
11 fi.getRequest().tAttribute(FILTER_APPLIED, Boolean.TRUE);
12 }
13//1.授权具体实现⼊⼝
14 InterceptorStatusToken token = super.beforeInvocation(fi);
15try {
16//2.授权通过后执⾏的业务
17 fi.getChain().Request(), fi.getRespon());
18 }
19finally {
20super.finallyInvocation(token);
21 }
22//3.后续处理
阜阳特色美食23super.afterInvocation(token, null);
24 }
25 }
授权机制实现的⼊⼝是super.beforeInvocation(fi),其具体实现在⽗类AbstractSecurityInterceptor中实现,beforeInvocation(Object object)的实现主要包括以下步骤:
⼀、获取需访问的接⼝权限,这⾥debug的例⼦是调⽤了前⽂提到的“/save”接⼝,其权限设置是@PreAuthorize("hasAuthority('sys:ur:add') AND
hasAuthority('sys:ur:edit')"),根据下⾯截图,可知变量attributes获取了到该请求接⼝的权限:
⼆、获取认证通过之后保存在 SecurityContextHolder的⽤户信息,其中,authorities是⼀个保存⽤户
所拥有全部权限的集合;
这⾥authenticateIfRequired()⽅法核⼼实现:
1private Authentication authenticateIfRequired() {
2 Authentication authentication = Context()
3 .getAuthentication();
4if (authentication.isAuthenticated() && !alwaysReauthenticate) {
5 ......
6return authentication;
7 }
8 authentication = authenticationManager.authenticate(authentication);
9 Context().tAuthentication(authentication);
10return authentication;
11 }
在认证过程通过后,执⾏Context().tAuthentication(authentication)将⽤户信息保存在Security框架当中,之后可通过Context().getAuthentication()获取到保存的⽤户信息;
三、尝试授权,⽤户信息authenticated、请求携带对象信息object、所访问接⼝的权限信息attributes,传⼊到decide⽅法;
decide()是决策管理器AccessDecisionManager定义的⼀个⽅法。
1public interface AccessDecisionManager {
2void decide(Authentication authentication, Object object,
3 Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
4 InsufficientAuthenticationException;
5boolean supports(ConfigAttribute attribute);
6boolean supports(Class<?> clazz);
7 }
AccessDecisionManager是⼀个interface接⼝,这是授权体系的核⼼。FilterSecurityInterceptor 在鉴权时,就是通过调⽤AccessDecisionManager的decide()⽅法来进⾏授权决策,若能通过,则可访问对应的接⼝。
AccessDecisionManager类的⽅法具体实现都在⼦类当中,包含AffirmativeBad、ConnsusBad、UnanimousBad三个⼦类;AffirmativeBad表⽰⼀票通过,这是AccessDecisionManager默认类;
ConnsusBad表⽰少数服从多数;
UnanimousBad表⽰⼀票反对;
如何理解这个投票机制呢?
点进去AffirmativeBad类⾥,可以看到⾥⾯有⼀⾏代码int result = voter.vote(authentication, object, configAttributes):
这⾥的AccessDecisionVoter是⼀个投票器,⽤到委托设计模式,即AffirmativeBad类会委托投票器进⾏选举,然后将选举结果返回赋值给result,然后判断result结果值,若为1,等于ACCESS_GRANTED值时,则表⽰可⼀票通过,也就是,允许访问该接⼝的权限。
这⾥,ACCESS_GRANTED表⽰同意、ACCESS_DENIED表⽰拒绝、ACCESS_ABSTAIN表⽰弃权:
1public interface AccessDecisionVoter<S> {
2int ACCESS_GRANTED = 1;//表⽰同意
3int ACCESS_ABSTAIN = 0;//表⽰弃权
4int ACCESS_DENIED = -1;//表⽰拒绝
5 ......
6 }
那么,什么情况下,投票结果result为1呢?
九一八纪念馆这⾥需要研究⼀下投票器接⼝AccessDecisionVoter,该接⼝的实现如下图所⽰:
这⾥简单介绍两个常⽤的:
1. RoleVoter:这是⽤来判断url请求是否具备接⼝需要的⾓⾊,这种主要⽤于使⽤注解@Secured处理的权限;
2. PreInvocationAuthorizationAdviceVoter:针对类似注解@PreAuthorize("hasAuthority('sys:ur:add') AND hasAuthority('sys:ur:edit')")处理的权限;
到这⼀步,代码就开始难懂了,这部分封装地过于复杂,总体的逻辑,是将⽤户信息所具有的权限与该接⼝的权限表达式做匹配,若能匹配成功,返回true,在三⽬运算符中,
allowed ? ACCESS_GRANTED : ACCESS_DENIED,就会返回ACCESS_GRANTED ,即表⽰通过,这样,返回给result的值就为1了。
到此为⽌,本⽂就结束了,笔者仍存在不⾜之处,欢迎各位读者能够给予珍贵的反馈,也算是对笔者写作的⼀种⿎励。