zoukankan      html  css  js  c++  java
  • 深入理解Spring Security授权机制原理

    原创/朱季谦

    在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

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

  • 相关阅读:
    软件项目管理阅读笔记02
    大二下周总结(11)
    “帮你APP”团队冲刺9
    “帮你APP”团队冲刺8
    “帮你APP”团队冲刺7
    “帮你APP”团队冲刺6
    “帮你APP”团队冲刺5
    单词统计
    “帮你APP”团队冲刺4
    “帮你APP”团队冲刺3
  • 原文地址:https://www.cnblogs.com/zhujiqian/p/14186219.html
Copyright © 2011-2022 走看看