44

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

 4 years ago
source link: https://zhuanlan.zhihu.com/p/86662663?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

公众号【Ccww笔记】
关注公众号【Ccww笔记】,领取干货资料。

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笔记】,一起学习。加群,每天会分享干货,还有学习视频领取!




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK