SpringCloud Security OAuth2资源授权动态权限扩展

SpringCloud Security OAuth2资源授权动态权限扩展

在Spring Cloud Security 中,认证和授权都是通过FilterChainProxy(Servlet Filter过滤器)拦截然后进行操作的。FilterSecurityInterceptor过滤器拦截了受保护资源的请求,然后进行授权处理,授权验证的逻辑在其父类AbstractSecurityInterceptor实现。大致流程如下:

  1. 使用SecurityMetadataSource根据http请求获取对应拥有的权限。
  2. 使用Spring Security授权模块对用户访问的资源进行授权验证。

AbstractSecurityInterceptor的部分源码如下:

// AbstractSecurityInterceptor.java
 protected InterceptorStatusToken beforeInvocation(Object object) {
        ......
 
        // 根据http请求获取对应的配置的权限信息
  Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
    .getAttributes(object);
 
     ......
        // 对用户认证进行校验
  Authentication authenticated = authenticateIfRequired();
  try {
            // 对用户的权限与访问资源拥有的权限进行校验
   this.accessDecisionManager.decide(authenticated, object, attributes);
  }
  catch (AccessDeniedException accessDeniedException) {
   publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
     accessDeniedException));
 
   throw accessDeniedException;
  }
        ......
 }

默认的SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource实现中,会把资源服务器配置的权限信息全部加载到内存。如果要实现授权权限的动态修改,需要扩展SecurityMetadataSource,例如,使权限数据能够动态的从数据库获取。并且,自定义根据动态权限认证逻辑AccessDecisionVoter。

扩展SecurityMetadataSource

自定义PermissionFilterInvocationSecurityMetadataSource,参考默认的DefaultFilterInvocationSecurityMetadataSource实现从数据库动态的根据访问http请求获取配置的权限。由于每次都需要获取全部的有效的权限配置数据,可以对权限数据做一个本地缓存,提交查询效率。

在ConfigAttribute的子类实现中,可以使用SecurityConfig保存配置的权限. 访问规则ConfigAttribute。实现代码如下:

public class PermissionFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
 
    private final PermissionClient permissionClient;
 
    public PermissionFilterInvocationSecurityMetadataSource(PermissionClient permissionClient) {
        this.permissionClient = permissionClient;
    }
 
    /**
     * 转换权限列表
     */
    private Map<RequestMatcher, Collection<ConfigAttribute>> requestMatcherCollectionMap() {
 
        List<Permission> allPermissions = permissionClient.findAllList();
        if (CollectionUtils.isEmpty(allPermissions)) {
            return ImmutableMap.of();
        }
        return allPermissions.stream()
                .collect(Collectors.toMap(permission -> new AntPathRequestMatcher(permission.getUrl()),
                        permission -> Lists.newArrayList(new SecurityConfig(permission.getCode()))));
    }
 
 
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
 
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMatcherCollectionMap().entrySet()) {
            if (entry.getKey().matches(request)) {
                return entry.getValue();
            }
        }
        return null;
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return requestMatcherCollectionMap().values()
                .stream().flatMap(Collection::stream)
                .collect(Collectors.toList());
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

扩展根据权限授权逻辑

自定义PermissionsVoter类实现AccessDecisionVoter接口,实现了用户只要拥有访问资源的权限就可以访问。参考RoleVoter具体的实现逻辑,代码如下:

public class PermissionsVoter implements AccessDecisionVoter<Object> {
 
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return Objects.nonNull(attribute.getAttribute());
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
 
    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
 
        if (CollectionUtils.isEmpty(attributes)) {
            return ACCESS_DENIED;
        }
        // 用户授权的权限
        Collection<? extends GrantedAuthority> grantedAuthorities;
        if (Objects.isNull(authentication)
                || CollectionUtils.isEmpty(grantedAuthorities = extractAuthorities(authentication))
                || Objects.isNull(object)) {
 
            log.info("user no authentication!");
            return ACCESS_DENIED;
        }
        for (GrantedAuthority grantedAuthority : grantedAuthorities) {
 
            String authority;
            if (StringUtils.isNotBlank(authority = grantedAuthority.getAuthority())
                    && match(authority, attributes)) {
                return ACCESS_GRANTED;
            }
        }
        return ACCESS_DENIED;
    }
 
    private boolean match(String authority, Collection<ConfigAttribute> attributes) {
 
        for (ConfigAttribute configAttribute : attributes) {
            String attribute;
            if (StringUtils.isNotBlank(attribute = configAttribute.getAttribute())
                    && attribute.equals(authority)) {
                return true;
            }
        }
        return false;
    }
 
    /**
     * 获取用户权限列表
     */
    Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
        return authentication.getAuthorities();
    }
}

配置资源服务器

在配置资源服务器,主要是如下配置:

  1. SecurityMetadataSource,获取资源权限的设置
  2. AccessDecisionManager,自定义授权逻辑的配置

重点讲解下自定义AccessDecisionManager的情况,

  1. 选择AffirmativeBased(只要有一个授权处理通过则可以进行访问)。
  2. 配置RoleVoter(角色授权),AuthenticatedVoter(认证信息授权),WebExpressionVoter(EL描述授权)spring security默认的授权逻辑。
  3. 重点讲解WebExpressionVoter的初始化。在生成WebExpressionVoter时,需要设置其expressionHandler为 OAuth2WebSecurityExpressionHandler,这样在进行验证时才不会报错。在使用默认的AccessDecisionManager启动进行验证时,Spring Security使用ExpressionUrlAuthorizationConfigurer默认配置WebExpressionVoter,并且在设置expressionHandler为OAuth2WebSecurityExpressionHandler。使用默认配置资源服务器启动时,调试的结果如下:

![在这里插入图片描述](https://img-blog.csdnimg.cn/87530dda1f134dad9719be7f42b5dec7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ICB5p2o5bim5L2g55av54uC,size_20,color_FFFFFF,t_70,g_se,x_16

在这里插入图片描述
在资源资源服务器中的详细配置如下:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
 
    /**
     * 资源服务器内的资源访问控制
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
 
        http.authorizeRequests()
                .antMatchers("/webjars/**", "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html", "/swagger.json").permitAll()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
 
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// 权限获取自定义配置
                        fsi.setSecurityMetadataSource(new PermissionFilterInvocationSecurityMetadataSource(permissionClient));
                        return fsi;
                    }
                })
                .accessDecisionManager(accessDecisionManager());
    }
 
    private AccessDecisionManager accessDecisionManager() {
 
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(new OAuth2WebSecurityExpressionHandler());
        // 授权逻辑自定义配置
        return new AffirmativeBased(Lists.newArrayList(new PermissionsVoter(), new RoleVoter(),
                new AuthenticatedVoter(), webExpressionVoter));
    }
}

授权测试

在这里插入图片描述

在访问时,调用自定义PermissionFilterInvocationSecurityMetadataSource获取配置权限的截图如下
在这里插入图片描述
在进行授权处理时,调用自定义 PermissionsVoter进行授权认证,截图如下:
在这里插入图片描述
不足与优化之处

一般的实现中,在每个单独的微服务中配置资源服务器,资源授权成功以后,SecurityContextPersistenceFilter已经把当前登录用户信息存储到SecurityContextHolder上下文,直接根据security提供的SecurityContextHolder.getContext().getAuthentication().getPrincipal()就可以获取当前登录用户信息。如果,微服务不断地增加,一般常见的电商系统都有用户服务,商品服务,订单服务等等,这时,该如何配置资源服务器呢?大家可以思考一下。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值