写点什么

深入理解 Spring Security 授权机制原理

用户头像
朱季谦
关注
发布于: 2021 年 03 月 26 日

原创/朱季谦

在 Spring Security 权限框架里,若要对后端 http 接口实现权限授权控制,有两种实现方式。

一、一种是基于注解方法级的鉴权,其中,注解方式又有 @Secured 和 @PreAuthorize 两种。

@Secured 如:


  1 @PostMapping("/test")  2  @Secured({WebResRole.ROLE_PEOPLE_W})  3  public void test(){  4  ......  5  return null;  6  }
复制代码


@PreAuthorize 如:

  1 @PostMapping("save")  2 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {  4     ValiParamUtils.ValiParamReq(result);  5     return sysUserService.save(sysUser);  6 }
复制代码


二、一种基于 config 配置类,需在对应 config 类配置 @EnableGlobalMethodSecurity(prePostEnabled = true)注解才能生效,其权限控制方式如下:

  1 @Override  2 protected void configure(HttpSecurity httpSecurity) throws Exception {  3     //使用的是JWT,禁用csrf  4     httpSecurity.cors().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 }
复制代码


这两种方式各有各的特点,在日常开发当中,普通程序员接触比较多的,则是注解方式的接口权限控制。

那么问题来了,我们配置这些注解或者类,其 security 框是如何帮做到能针对具体的后端 API 接口做权限控制的呢?

单从一行 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")注解上看,是看不出任何头绪来的,若要回答这个问题,还需深入到源码层面,方能对 security 授权机制有更好理解。

若要对这个过程做一个总的概述,笔者整体以自己的思考稍作了总结,可以简单几句话说明其整体实现,以该接口为例:


  1 @PostMapping("save")  2 @PreAuthorize("hasAuthority('sys:user:add')")  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {  4     ValiParamUtils.ValiParamReq(result);  5     return sysUserService.save(sysUser);  6 }
复制代码


即,认证通过的用户,发起请求要访问“/save”接口,若该 url 请求在配置类里设置为必须进行权限认证的,就会被 security 框架使用 filter 拦截器对该请求进行拦截认证。拦截过程主要一个动作,是把该请求所拥有的权限集与 @PreAuthorize 设置的权限字符“sys:user:add”进行匹配,若能匹配上,说明该请求是拥有调用“/save”接口的权限,那么,就可以被允许执行该接口资源。

 

在 springboot+security+jwt 框架中,通过一系列内置或者自行定义的过滤器 Filter 来达到权限控制,如何设置自定义的过滤器 Filter 呢?例如,可以通过设置 httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)来自定义一个基于 JWT 拦截的过滤器 JwtFilter,这里的 addFilterBefore 方法将在下一篇文详细分析,这里暂不展开,该方法大概意思就是,将自定义过滤器 JwtFilter 加入到 Security 框架里,成为其中的一个优先安全 Filter,代码层面就是将自定义过滤器添加到 List<Filter> filters。

 

设置增加自行定义的过滤器 Filter 伪代码如下:


  1 @Configuration  2 @EnableWebSecurity  3 @EnableGlobalMethodSecurity(prePostEnabled = true)  4 public class SecurityConfig extends WebSecurityConfigurerAdapter {  5     ......  6     @Override  7     protected void configure(HttpSecurity httpSecurity) throws Exception {  8         //使用的是JWT,禁用csrf  9         httpSecurity.cors().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()), UsernamePasswordAuthenticationFilter.class); 21     } 22 }
复制代码


该过滤器类 extrends 继承 BasicAuthenticationFilter,而 BasicAuthenticationFilter 是继承 OncePerRequestFilter,该过滤器确保在一次请求只通过一次 filter,而不需要重复执行。这样配置后,当请求过来时,会自动被 JwtFilter 类拦截,这时,将执行重写的 doFilterInternal 方法,在 SecurityContextHolder.getContext().setAuthentication(authentication)认证通过后,会执行过滤器链 FilterChain 的方法 chain.doFilter(request, response);


  1 public class JwtFilter  extends BasicAuthenticationFilter {  2   3     @Autowired  4     public JwtFilter(AuthenticationManager authenticationManager) {  5         super(authenticationManager);  6     }  7   8    @Override  9    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 10        // 获取token, 并检查登录状态 11        // 获取令牌并根据令牌获取登录认证信息 12        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); 13        // 设置登录认证信息到上下文 14        SecurityContextHolder.getContext().setAuthentication(authentication); 15  16        chain.doFilter(request, response); 17    } 18  19 }
复制代码


那么,问题来了,过滤器链 FilterChain 究竟是什么?

这里,先点进去看下其类源码:


  1 package javax.servlet;  2   3 import java.io.IOException;  4   5 public interface FilterChain {  6     void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;  7 }
复制代码


FilterChain 只有一个 doFilter 方法,这个方法的作用就是将请求 request 转发到下一个过滤器 filter 进行过滤处理操作,执行过程如下:

image


过滤器链像一条铁链,把相关的过滤器链接起来,请求线程如蚂蚁一样,会沿着这条链一直爬过去-----即,通过 chain.doFilter(request, response)方法,一层嵌套一层地传递下去,当传递到该请求对应的最后一个过滤器,就会将处理完成的请求转发返回。因此,通过过滤器链,可实现在不同的过滤器当中对请求 request 做处理,且过滤器之间彼此互不干扰。

这其实是一种责任链的设计模式。在这种模式当中,通常每个接受者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么,它就会把相同的请求传给下一个接收者,以此类推。

 

Spring Security 框架上过滤器链上都有哪些过滤器呢?

 

可以在 DefaultSecurityFilterChain 类根据输出相关 log 或者 debug 来查看 Security 都有哪些过滤器,如在 DefaultSecurityFilterChain 类中的构造器中打断点,如图所示,可以看到,自定义的 JwtFilter 过滤器也包含其中:


image


这些过滤器都在同一条过滤器链上,即通过 chain.doFilter(request, response)可将请求一层接一层转发,处理请求接口是否授权的主要过滤器是 FilterSecurityInterceptor,其主要作用如下:

1. 获取到需访问接口的权限信息,即 @Secured({WebResRole.ROLE_PEOPLE_W}) 或 @PreAuthorize 定义的权限信息;

2. 根据 SecurityContextHolder 中存储的 authentication 用户信息,来判断是否包含与需访问接口的权限信息,若包含,则说明拥有该接口权限;

3. 主要授权功能在父类 AbstractSecurityInterceptor 中实现;

  

我们将从 FilterSecurityInterceptor 这里开始重点分析 Security 授权机制原理的实现。

过滤器链将请求传递转发 FilterSecurityInterceptor 时,会执行 FilterSecurityInterceptor 的 doFilter 方法:

  1 public void doFilter(ServletRequest request, ServletResponse response,  2       FilterChain chain) throws IOException, ServletException {  3    FilterInvocation fi = new FilterInvocation(request, response, chain);  4    invoke(fi);  5 }
复制代码

在这段代码当中,FilterInvocation 类是一个有意思的存在,其实它的功能很简单,就是将上一个过滤器传递过滤的 request,response,chain 复制保存到 FilterInvocation 里,专门供 FilterSecurityInterceptor 过滤器使用。它的有意思之处在于,是将多个参数统一归纳到一个类当中,其到统一管理作用,你想,若是 N 多个参数,传进来都分散到类的各个地方,参数多了,代码多了,方法过于分散时,可能就很容易造成阅读过程中,弄糊涂这些个参数都是哪里来了。但若统一归纳到一个类里,就能很快定位其来源,方便代码阅读。网上有人提到该 FilterInvocation 类还起到解耦作用,即避免与其他过滤器使用同样的引用变量。

总而言之,这个地方的设定虽简单,但很值得我们学习一番,将其思想运用到实际开发当中,不外乎也是一种能简化代码的方法。

FilterInvocation 主要源码如下:


  1 public class FilterInvocation {  2   3    private FilterChain chain;  4    private HttpServletRequest request;  5    private HttpServletResponse response;  6   7   8    public FilterInvocation(ServletRequest request, ServletResponse response,  9          FilterChain chain) { 10       if ((request == null) || (response == null) || (chain == null)) { 11          throw new IllegalArgumentException("Cannot pass null values to constructor"); 12       } 13  14       this.request = (HttpServletRequest) request; 15       this.response = (HttpServletResponse) response; 16       this.chain = chain; 17    } 18    ...... 19 }
复制代码


FilterSecurityInterceptor 的 doFilter 方法里调用 invoke(fi)方法:


  1 public void invoke(FilterInvocation fi) throws IOException, ServletException {  2    if ((fi.getRequest() != null)  3          && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)  4          && observeOncePerRequest) {  5      //筛选器已应用于此请求,每个请求处理一次,所以不需重新进行安全检查   6       fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  7    }  8    else {  9       // 第一次调用此请求时,需执行安全检查 10       if (fi.getRequest() != null && observeOncePerRequest) { 11          fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); 12       } 13        //1.授权具体实现入口 14       InterceptorStatusToken token = super.beforeInvocation(fi); 15       try { 16        //2.授权通过后执行的业务 17          fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 18       } 19       finally { 20          super.finallyInvocation(token); 21       } 22        //3.后续处理 23       super.afterInvocation(token, null); 24    } 25 }
复制代码


授权机制实现的入口是 super.beforeInvocation(fi),其具体实现在父类 AbstractSecurityInterceptor 中实现,beforeInvocation(Object object)的实现主要包括以下步骤:

 

一、获取需访问的接口权限,这里 debug 的例子是调用了前文提到的“/save”接口,其权限设置是 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根据下面截图,可知变量 attributes 获取了到该请求接口的权限:


image


二、获取认证通过之后保存在 SecurityContextHolder 的用户信息,其中,authorities 是一个保存用户所拥有全部权限的集合;


image


这里 authenticateIfRequired()方法核心实现:

  1 private Authentication authenticateIfRequired() {  2    Authentication authentication = SecurityContextHolder.getContext()  3          .getAuthentication();  4    if (authentication.isAuthenticated() && !alwaysReauthenticate) {  5      ......  6       return authentication;  7    }  8    authentication = authenticationManager.authenticate(authentication);  9    SecurityContextHolder.getContext().setAuthentication(authentication); 10    return authentication; 11 }
复制代码


在认证过程通过后,执行 SecurityContextHolder.getContext().setAuthentication(authentication)将用户信息保存在 Security 框架当中,之后可通过 SecurityContextHolder.getContext().getAuthentication()获取到保存的用户信息;

 

三、尝试授权,用户信息 authenticated、请求携带对象信息 object、所访问接口的权限信息 attributes,传入到 decide 方法;


image


decide()是决策管理器 AccessDecisionManager 定义的一个方法。

  1 public interface AccessDecisionManager {  2    void decide(Authentication authentication, Object object,  3          Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,  4          InsufficientAuthenticationException;  5    boolean supports(ConfigAttribute attribute);  6    boolean supports(Class<?> clazz);  7 }
复制代码


AccessDecisionManager 是一个 interface 接口,这是授权体系的核心。FilterSecurityInterceptor 在鉴权时,就是通过调用 AccessDecisionManager 的 decide()方法来进行授权决策,若能通过,则可访问对应的接口。

AccessDecisionManager 类的方法具体实现都在子类当中,包含 AffirmativeBased、ConsensusBased、UnanimousBased 三个子类;


image


AffirmativeBased 表示一票通过,这是 AccessDecisionManager 默认类;

ConsensusBased 表示少数服从多数;

UnanimousBased 表示一票反对;

如何理解这个投票机制呢?

点进去 AffirmativeBased 类里,可以看到里面有一行代码 int result = voter.vote(authentication, object, configAttributes):


image


这里的 AccessDecisionVoter 是一个投票器,用到委托设计模式,即 AffirmativeBased 类会委托投票器进行选举,然后将选举结果返回赋值给 result,然后判断 result 结果值,若为 1,等于 ACCESS_GRANTED 值时,则表示可一票通过,也就是,允许访问该接口的权限。

这里,ACCESS_GRANTED 表示同意、ACCESS_DENIED 表示拒绝、ACCESS_ABSTAIN 表示弃权:

  1 public interface AccessDecisionVoter<S> {  2    int ACCESS_GRANTED = 1;//表示同意  3    int ACCESS_ABSTAIN = 0;//表示弃权  4    int ACCESS_DENIED = -1;//表示拒绝  5    ......  6    }
复制代码


那么,什么情况下,投票结果 result 为 1 呢?

这里需要研究一下投票器接口 AccessDecisionVoter,该接口的实现如下图所示:


image


这里简单介绍两个常用的:

1. RoleVoter:这是用来判断 url 请求是否具备接口需要的角色,这种主要用于使用注解 @Secured 处理的权限;

2. PreInvocationAuthorizationAdviceVoter:针对类似注解 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")处理的权限;


image


到这一步,代码就开始难懂了,这部分封装地过于复杂,总体的逻辑,是将用户信息所具有的权限与该接口的权限表达式做匹配,若能匹配成功,返回 true,在三目运算符中,

allowed ? ACCESS_GRANTED : ACCESS_DENIED,就会返回 ACCESS_GRANTED ,即表示通过,这样,返回给 result 的值就为 1 了。


image


image


到此为止,本文就结束了,笔者仍存在不足之处,欢迎各位读者能够给予珍贵的反馈,也算是对笔者写作的一种鼓励。


发布于: 2021 年 03 月 26 日阅读数: 8
用户头像

朱季谦

关注

一个以编程为生的荒诞小说家 2018.06.29 加入

汤圆创作APP签约作者; 简书优秀认证作者; Java后端程序员; PMP认证人员; 学生时代著有《黑色玫瑰》一书已上架微信读书APP;

评论

发布
暂无评论
深入理解Spring Security授权机制原理