转载

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

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

  • 用户: 不用多讲,大家也知道了;
  • 角色: 一个集合的概念,角色管理是确定角色具备哪些权限的一个过程 ;
  • 权限: 1).页面权限,控制你可以看到哪个页面,看不到哪个页面;
    2). 操作权限,控制你可以在页面上进行哪些操作(查询、删除、编辑等);
    3).数据权限,是控制你可以看到哪些数据。

实质是:

权限(Permission) = 资源(Resource) + 操作(Privilege)

角色(Role) = 权限的集合(a set of low-level permissions)

用户(User) = 角色的集合(high-level roles)

权限管理过程:

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

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

每个用户的所有权限=用户个人的权限+用户组所用的权限

用户组、用户、与角色三者关系如下:

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

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

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

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

AccessDecisionManager被 AbstractSecurityInterceptor 拦截器调用进行最终访问控制决策。 而且由 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完美的权限管理系统。

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

Spring security(五)-完美权限管理系统(授权过程分析)
原文  https://juejin.im/post/5d0a0398e51d45772a49ad4e
正文到此结束
Loading...