Spring-security五完美权限管理系统授权过程分析

34次阅读

共计 11001 个字符,预计需要花费 28 分钟才能阅读完成。

1. 权限管理相关概念

  权限管理是一个几乎所有后台系统的都会涉及的一个重要组成部分,主要目的是对整个后台管理系统进行权限的控制。常见的基于角色的访问控制,其授权模型为“用户 - 角色 - 权限”,简明的说,一个用户拥有多个角色,一个角色拥有多个权限。其中,

  • 用户: 不用多讲,大家也知道了;
  • 角色: 一个集合的概念,角色管理是确定角色具备哪些权限的一个过程;
  • 权限:
    1). 页面权限,控制你可以看到哪个页面,看不到哪个页面;

    2). 操作权限,控制你可以在页面上进行哪些操作(查询、删除、编辑等);
    3). 数据权限,是控制你可以看到哪些数据。

实质是:
 权限(Permission)= 资源(Resource)+ 操作(Privilege)
 角色(Role)= 权限的集合(a set of low-level permissions)
 用户(User)= 角色的集合(high-level roles)

权限管理过程:

  1. 鉴权管理,即权限判断逻辑,如菜单管理(普通业务人员登录系统后,是看不到【用户管理】菜单的)、功能权限管理(URL 访问的管理)、行级权限管理等
  2. 授权管理,即权限分配过程,如直接对用户授权,直接分配到用户的权限具有最优先级别、对用户所属岗位授权,用户所属岗位信息可以看作是一个分组,和角色的作用一样,但是每个用户只能关联一个岗位信息等。

  在实际项目中用户数量多,逐一的为每个系统用户授权,这是极其繁琐的事,所以可以学习 linux 文件管理系统一样,设置 group 模式,一组有多个用户,可以为用户组授权相同的权限,简便多了。这样模式下:
  每个用户的所有权限 = 用户个人的权限 + 用户组所用的权限
用户组、用户、与角色三者关系如下:

再结合权限管理的页面权限、操作权限,如菜单的访问、功能模块的操作、按钮的操作等等,可把功能操作与资源统一管理,即让它们直接与权限关联起来,关系图如下:

2. 授权过程分析

### 2.1 授权访问权限工作流程:

        FilterSecurityInterceptor
                             doFilter()->invoke()
                               ->AbstractSecurityInterceptor
                                   beforeInvocation()
                           ->SecurityMetadataSource 获取 ConfigAttribute 属性信息(从数据库或者其他数据源地方)getAttributes()
                                   ->AccessDecisionManager()  基于 AccessDecisionVoter 实现授权访问
                                           Decide()
                                       ->AccessDecisionVoter  受 AccessDecisionManager 委托实现授权访问
                                               vote()

默认授权过程会使用这样的工作流程,接下来来分析各个组件的功能与源码。
### 2.2 AbstractSecurityInterceptor 分析
  FilterSecurityInterceptor 为授权拦截器,在 FilterSecurityInterceptor 中有一个封装了 过滤链 request 以及 responseFilterInvocation对象进行操作,在 FilterSecurityInterceptor,主要由invoke() 调用其父类 AbstractSecurityInterceptor 的方法。

invoke()分析:

 
 public void invoke(FilterInvocation fi) throws IOException, ServletException {
                 .....
                 // 获取 accessDecisionManager 权限决策后结果状态、以及权限属性
        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {super.finallyInvocation(token);
        }

        super.afterInvocation(token, null);
    }
}

  AbstractSecurityInterceptor 的授权过滤器主要方法 beforeInvocation(),afterInvocation() 以及authenticateIfRequired(), 其最主要的方法beforeInvocation() 分析如下:

protected InterceptorStatusToken beforeInvocation(Object object) {
       ....
       // 从 SecurityMetadataSource 的权限属性
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
            .getAttributes(object);
    if (attributes == null || attributes.isEmpty()) {
             .....
        publishEvent(new PublicInvocationEvent(object));
        return null; // no further work post-invocation
    }

    // 调用认证环节获取 authenticated(包含用户的详细信息)Authentication authenticated = authenticateIfRequired();

    // Attempt authorization
    try {
        // 进行关键的一步:授权的最终决策  
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                accessDeniedException));

        throw accessDeniedException;
    }

    

    // Attempt to run as a different user
    Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
            attributes);

    if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object");
        }

        // no further work post-invocation
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                attributes, object);
    }
    else {if (debug) {logger.debug("Switching to RunAs Authentication:" + runAs);
        }

        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
        SecurityContextHolder.getContext().setAuthentication(runAs);

        // need to revert to token.Authenticated post-invocation 
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
}

### 2.3 SecurityMetadataSource
  SecurityMetadataSource是从数据库或者其他数据源中加载ConfigAttribute,为了在AccessDecisionManager.decide() 最终决策中进行 match。其有三个方法:

Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;// 加载权限资源

Collection<ConfigAttribute> getAllConfigAttributes();// 加载所有权限资源

boolean supports(Class<?> var1);

### 2.4 AccessDecisionManager
  AccessDecisionManagerAbstractSecurityInterceptor 拦截器调用进行最终访问控制决策。
而且由AuthenticationManager 创建的 Authentication object 中的 GrantedAuthority,首先被授权模块中的 AccessDecisionManager读取使用,当复杂的 GrantedAuthority,getAuthority()为 null,因此需要 AccessDecisionManager 专门支持 GrantedAuthority 实现以便了解其内容。

AccessDecisionManager接口方法:

   void decide(Authentication authentication, Object secureObject,    Collection<ConfigAttribute> attrs) throws AccessDeniedException;

   boolean supports(ConfigAttribute attribute);

   boolean supports(Class clazz);

2.5 AccessDecisionVoter

  AccessDecisionManager.decide()将使用 AccessDecisionVoter 进行投票决策。AccessDecisionVoter进行投票访问控制决策,访问不通过就抛出AccessDeniedException

AccessDecisionVoter接口方法:

   int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

   boolean supports(ConfigAttribute attribute);

   boolean supports(Class clazz);

AccessDecisionVoter 核心方法 vote() 通常是获取 Authentication 的 GrantedAuthority已定义好的 ConfigAttributes进行match,如果成功为投同意票,匹配不成功为拒绝票,当 ConfigAttributes 中无属性时,才投弃票。

Spring Security 提供了三种投票方式去实现 AccessDecisionManager 接口进行投票访问控制决策:

  • ConsensusBased: 大多数 voter 同意访问就授权访问
  • AffirmativeBased: 只要一个以上 voter 同意访问就授权访问,全部
  • UnanimousBased: 只有全体同意了才授权访问

AccessDecisionVoter 用三个静态变量表示 voter 投票情况:

  • ACCESS_ABSTAIN: 弃权
  • ACCESS_DENIED: 拒绝访问
  • ACCESS_GRANTED: 允许访问

Note: 当所有 voter 都弃权时使用变量 allowIfEqualGrantedDeniedDecisions 来判断,true 为通过,false 抛出 AccessDeniedException。

此外可自定义 AccessDecisionManager 实现接口,因为可能某些 AccessDecisionVoter 具有权重比高投票权或者某些 AccessDecisionVoter 具有一票否定权。AccessDecisionVoter 的 Spring security 实现类 RoleVoterAuthenticatedVoterRoleVoter 为最为常见的 AccessDecisionVoter,其为简单的权限表示,并以前缀为 ROLE_,vote 匹配规则也跟上面一样。

源码分析:

 Public int vote(Authentication authentication,Object object,Collection<ConfigAttribute>attributes){
    // 用户传递的 authentication 为 null,拒绝访问
   if(authentication==null){return ACCESS_DENIED;}

   int result=ACCESS_ABSTAIN;

   Collection<?extendsGrantedAuthority>authorities=extractAuthorities(authentication);


    // 依次进行投票
   for(ConfigAttributeattribute:attributes){if(this.supports(attribute)){

           result=ACCESS_DENIED;

           //Attempt to find a matching granted authority

           for(GrantedAuthorityauthority:authorities){if(attribute.getAttribute().equals(authority.getAuthority())){returnACCESS_GRANTED;}
      }
   }
 }

3. 案例 - 自定义组件

自定义组件:

  1. 自定义 FilterSecurityInterceptor,可仿写 FilterSecurityInterceptor,实现抽象类AbstractSecurityInterceptor 以及 Filter 接口,其主要的是把 自定义的 SecurityMetadataSource 自定义 accessDecisionManager配置到自定义 FilterSecurityInterceptor 的拦截器中
  2. 自定义 SecurityMetadataSource,实现接口 FilterInvocationSecurityMetadataSource,实现从数据库或者其他数据源中加载 ConfigAttribute(即是从数据库或者其他数据源中加载资源权限)
  3. 自定义 accessDecisionManager,可使用基于 AccessDecisionVoter 实现权限认证的官方 UnanimousBased
  4. 自定义 AccessDecisionVoter

### 3.1 自定义 MyFilterSecurityInterceptor
自定义 MyFilterSecurityInterceptor主要工作为:

  • 加载自定义的 SecurityMetadataSource 到自定义的 FilterSecurityInterceptor 中;
  • 加载自定义的 AccessDecisionManager 到自定义的 FilterSecurityInterceptor 中;
  • 重写 invoke 方法

     @Component
     public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
         private FilterInvocationSecurityMetadataSource securityMetadataSource;
     
     
         @Override
         public void init(FilterConfig filterConfig) throws ServletException { }
     
         @Override
         public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);
             invoke(fi);
         }
     
         private void invoke(FilterInvocation fi) throws IOException, ServletException {
             //fi 里面有一个被拦截的 url
             // 里面调用 MyInvocationSecurityMetadataSource 的 getAttributes(Object object)这个方法获取 fi 对应的所有权限
             // 再调用 MyAccessDecisionManager 的 decide 方法来校验用户的权限是否足够
             InterceptorStatusToken token = super.beforeInvocation(fi);
             try {
                 // 执行下一个拦截器
                 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
             } finally {super.afterInvocation(token, null);
             }
         }
     
         @Override
         public void destroy() {}
     
         @Override
         public Class<?> getSecureObjectClass() {return null;}
     
         @Override
         public SecurityMetadataSource obtainSecurityMetadataSource() {return this.securityMetadataSource;}
     
         public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {return this.securityMetadataSource;}
     
         // 设置自定义的 FilterInvocationSecurityMetadataSource
         @Autowired
         public void setSecurityMetadataSource(MyFilterInvocationSecurityMetadataSource messageSource) {this.securityMetadataSource = messageSource;}
     
         // 设置自定义的 AccessDecisionManager
         @Override
         @Autowired
         public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {super.setAccessDecisionManager(accessDecisionManager);
         }
      }

3.2 自定义 MyFilterInvocationSecurityMetadataSource

自定义 MyFilterInvocationSecurityMetadataSource主要工作为:

  • 从数据源中加载 ConfigAttribute 到 SecurityMetadataSource 资源器中
  • 重写 getAttributes()加载 ConfigAttribute 为 AccessDecisionManager.decide()授权决策做准备。

    @Component
    public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private Map<String, Collection<ConfigAttribute>> configAttubuteMap = null;
    
    private void loadResourceDefine() {
        //todo 加载数据库的所有权限
         Collection<ConfigAttribute> attributes;
    }
    
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        AntPathRequestMatcher matcher;
        String resUrl;
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        //1. 加载权限资源数据
        if (configAttubuteMap == null) {loadResourceDefine();
        }
        Iterator<String> iterator = configAttubuteMap.keySet().iterator();
        while (iterator.hasNext()) {resUrl = iterator.next();
            matcher = new AntPathRequestMatcher(resUrl);
            if (matcher.matches(request)) {return configAttubuteMap.get(resUrl);
            }
        }
        return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {return null;}
    
        @Override
        public boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);
        }

    }

3.3 自定义 MyAccessDecisionManager

自定义 MyAccessDecisionManager主要工作为:

  • 重写最终授权决策 decide(),自定义授权访问策略

     @Component
     public class MyAccessDecisionManager implements AccessDecisionManager {
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            ConfigAttribute c;
            String needRole;
            if(null== configAttributes || configAttributes.size() <=0) {return;}
            //1. 获取已定义的好资源权限配置
            Iterator<ConfigAttribute> iterable=configAttributes.iterator();
            while (iterable.hasNext()){c=iterable.next();
                needRole=c.getAttribute();
                //2. 依次比对用户角色对应的资源权限
                for (GrantedAuthority grantedAuthority:authentication.getAuthorities()){if(needRole.trim().equals(grantedAuthority.getAuthority())){return;}
                }
            }
    
        }
    
        @Override
        public boolean supports(ConfigAttribute attribute) {return true;}
    
        @Override
        public boolean supports(Class<?> clazz) {return true;}

    }

3.4 配置 SecurityConfig

配置 SecurityConfig主要工作为:

  • 将 FilterSecurityInterceptor 拦截器加载 WebSecurityConfig 中

        protected void configure(HttpSecurity http) throws Exception {http.headers().frameOptions().disable().and()
               // 表单登录
               .formLogin()
               .loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE)
               .loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL)
               .successHandler(authenticationSuccessHandler())
               .failureHandler(authenticationFailureHandler())
               .and()
               // 应用 sms 认证配置
               .apply(smsAuthenticationSecurityConfig)
               .and()
               // 允许通过
               .authorizeRequests()
               .antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL,
                            SecurityConstants.APP_USER_REGISTER_URL,
                           SecurityConstants.APP_FORM_LOGIN_INDEX_URL)
               .permitAll()// 以上的请求都不需要认证
               .and()
               //“记住我”配置
               .rememberMe()
               .tokenRepository(jdbcTokenRepository())//token 入库处理类
               .tokenValiditySeconds(SecurityConstants.REMEMBER_ME_VERIFY_TIME)//remember-me 有效时间设置
               .rememberMeParameter(SecurityConstants.REMEMBER_ME_PARAM_NAME)// 请求参数名设置
               .and()
               .csrf().disable();
            // 增加自定义权限授权拦截器
            http.addFilterBefore(myFilterSecurityInterceptor,FilterSecurityInterceptor.class);
      }
    

    ## 总结
      Spring Security 授权过程中,可以会涉主要涉及了上面上面所述的组件,其中主要的还是跟着源码多跑几遍,了解其中的原理,才能更加流畅的码代码。到此为止写完 Spring Security 的认证和授权分析流程,接下来会结合前面小节,写一个 Spring security 完美的权限管理系统。

    最后可关注公众号【Ccww 笔记】,一起学习。加群,每天会分享干货,还有学习视频领取!

正文完
 0