SpringBoot 2整合SpringSecurity权限管理(五)基于数据库的角色授权

本篇文章部分参考了https://www.ktanx.com/blog/p/4929

前言

上一篇文章中,我们已经实现了基于数据库的用户认证,接下来要做的就是基于数据库的角色授权,即动态的权限验证,实现Spring Security的决策管理器AccessDecisionManager。

要实现动态的权限验证,当然要先有对应的访问权限资源了。Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限,所以第一步需要实现SecurityMetadataSource。

SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但FilterInvocationSecurityMetadataSource只是一个标识接口,对应于FilterInvocation,本身并无任何内容:

/**
 * Marker interface for <code>SecurityMetadataSource</code> implementations that are
 * designed to perform lookups keyed on {@link FilterInvocation}s.
 *
 * @author Ben Alex
 */
public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
}

因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource,这是因为Spring Security中很多web才使用的类参数类型都是FilterInvocationSecurityMetadataSource。

下面我们自定义实现类CustomInvocationSecurityMetadataSource,它的主要责任就是当访问一个url时返回这个url所需要的访问权限。

一、新增CustomInvocationSecurityMetadataSource类实现FilterInvocationSecurityMetadataSource接口

 *
 * @author ZHANGCHAO
 * @date 2020/3/18 8:06
 * @since 1.0.0
 */
@Slf4j
@Service
public class CustomInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private ISysResourceService resourceService;
    @Autowired
    private ISysRoleResourceService roleResourceService;
    @Autowired
    private ISysRoleService roleService;

    /**
     * key是url+method
     * value是对应url资源的角色列表
     **/
    private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();

    /**
     * 组装资源路径和其对应的权限列表map
     * 注意:
     * @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
     * 此方法必须在将类放入服务之前调用,且只执行一次。
     * 
     * @Author ZHANGCHAO
     * @Date 2020/3/18 14:06
     * @param 
     * @retrun void     
     **/
    @PostConstruct
    public void init(){
        log.info("[自定义权限资源数据源]:{}","初始化权限资源");
        List<SysResource> resourceList = (List<SysResource>) resourceService.list().getData();
        resourceList.forEach(sysResource -> {
            List<SysRoleResource> roleResources = (List<SysRoleResource>) roleResourceService.list(new QueryWrapper<SysRoleResource>().lambda().eq(SysRoleResource::getResourceCode,sysResource.getCode())).getData();
            if (isNotEmpty(roleResources)){
                List<String> rolesNames = new ArrayList<>();
                roleResources.forEach(sysRoleResource -> {
                    SysRole role = (SysRole) roleService.getOne(new QueryWrapper<SysRole>().lambda().eq(SysRole::getCode,sysRoleResource.getRoleCode())).getData();
                    rolesNames.add(role.getName());
                });
                if (isNotEmpty(rolesNames)){
                    List<ConfigAttribute> configAttributes = new ArrayList<>();
                    rolesNames.forEach(roleName -> configAttributes.add(new SecurityConfig(roleName)));
                    // url == 权限列表
                    requestMap.put(new AntPathRequestMatcher(sysResource.getUrl()),configAttributes);
                }
            }
        });
        log.info("requestMap:==> "+requestMap);
    }
    
    /**
     * getAttributes方法返回本次访问需要的权限,可以有多个权限。
     * 在上面的实现中如果没有匹配的url直接返回null,
     * 也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。
     *
     * 访问配置属性(ConfigAttribute)用于给定安全对象(通过的验证)
     *
     * @param o 安全的对象
     * @return 用于传入的安全对象的属性。 如果没有适用的属性,则应返回空集合。
     * @throws IllegalArgumentException 如果传递的对象不是SecurityDatasource实现支持的类型,则抛出异常
     **/
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        log.info("[自定义权限资源数据源]:{}","获取本次访问需要的权限");
        if (requestMap.isEmpty()){
            init();
        }
        FilterInvocation fi = (FilterInvocation) o;
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(fi.getHttpRequest())) {
                log.info("[自定义权限资源数据源]:当前路径[{}]需要的资源权限:[{}] ==> 触发鉴权决策管理器",entry.getKey(),entry.getValue().toString());
                return entry.getValue();
            }
        }
        log.info("[自定义权限资源数据源]:{}==> {}","白名单路径",fi.getHttpRequest().getRequestURI());
        return null;
    }

    /**
     * getAllConfigAttributes方法如果返回了所有定义的权限资源,
     * Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。
     *
     * 如果可用,则返回由实现类定义的所有ConfigAttribute。
     *
     * AbstractSecurityInterceptor使用它对针对它ConfigAttribute的每个配置属性执行启动时验证。
     *
     * @return ConfigAttribute,如果没有适用的,就返回null
     **/
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        List<ConfigAttribute> allAttributes = new ArrayList<>();
        requestMap.forEach((key,value) -> {allAttributes.addAll(value);});
        log.info("[自定义权限资源数据源]:获取所有的角色==> {}",allAttributes.toString());
        return allAttributes;
    }

    /**
     * AbstractSecurityInterceptor 调用
     * supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
     *
     * @param aClass 正在查询的类
     * @return 如果实现可以处理指定的类,则为true
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

注意:@PostConstruct注解的方法将会在依赖注入完成后被自动调用。

getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回null,也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。

getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。

supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。

在加载的时候,这里的url是key,value是访问需要的权限码,一个权限码可以对应多个url,一个url也可以有多个权限码,想要怎么玩都可以在这里实现,示例中只是最简单的。

PS: 另外说一下FilterInvocation类:

这个类的作用本身很简单,就是把doFilter传进来的request,response和FilterChain对象保存起来,供FilterSecurityInterceptor的处理代码调用。
一般写FilterSecurityInterceptor类的代码时都会直接把doFilter的参数要么保存在FilterSecurityInterceptor类的相关属性里,要么就是直接传进来用,并且不断地在各个方法中传递这些参数。由此可见springSecurity的作者这个小小的设计使得代码的可阅读性和藕合性大大降低,因为FilterInvocation类替代了这些参数在FilterSecurityInterceptor类中各处游动,,这样通过该类屏蔽了web filter过滤器环境。

有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。这需要通过实现自定义的AccessDecisionManager来实现。Spring Security内置的几个AccessDecisionManager就不讲了,在web项目中基本用不到。

二、自定义CustomAccessDecisionManager 类,实现AccessDecisionManager接口

重写权限决策,根据URL资源权限和用户角色,进行鉴权。

/**
 * 权限决策管理器
 *
 * @author ZHANGCHAO
 * @date 2020/3/18 16:52
 * @since 1.0.0
 */
@Slf4j
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {

    /**
     * 权限鉴定
     *
     * @param authentication   from SecurityContextHolder.getContext() => userDetails.getAuthorities()
     * @param o           就是FilterInvocation对象,可以得到request等web资源。
     * @param collection from MetaDataSource.getAttributes(),已经被框架做了非空判断
     * @throws AccessDeniedException   如果由于身份验证不具有所需的权限或ACL特权而拒绝访问
     * @throws InsufficientAuthenticationException 如果由于身份验证没有提供足够的信任级别而拒绝访问
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        log.info("****************************************权限鉴定********************************************");
        /*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一个URL
        log.info("[当前路径[{}]需要的资源权限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
        log.info("[登录用户[{}]权限]:{}",authentication.getName(),authentication.getAuthorities());

        if(collection == null){
            return;
        }

        for (ConfigAttribute configAttribute : collection) {
            // 资源的权限
            String attribute = configAttribute.getAttribute();
            // 当前用户的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities){
                if (attribute.equals(authority.getAuthority())){
                    log.info("[权限决策管理器]:登录用户[{}]权限匹配=[{}]",authentication.getName(),attribute);
                    return;
                }
            }
        }
        log.info("[权限决策管理器]:登录用户[{}]权限不足",authentication.getName());
        throw new AccessDeniedException("权限不足");
    }

    /**
     * AbstractSecurityInterceptor 调用,遍历ConfigAttribute集合,筛选出不支持的attribute
     *
     * @param configAttribute a configuration attribute that has been configured against the
     *                  <code>AbstractSecurityInterceptor</code>
     * @return true if this <code>AccessDecisionManager</code> can support the passed
     * configuration attribute
     */
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    /**
     * AbstractSecurityInterceptor 调用,验证AccessDecisionManager是否支持这个安全对象的类型。
     * supports(Class)方法被安全拦截器实现调用,
     * 包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
     *
     * @param aClass the class that is being queried
     * @return <code>true</code> if the implementation can process the indicated class
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

同样的也有三个方法,其它两个和SecurityMetadataSource类似,这里主要讲讲decide方法。

decide方法的三个参数中:

  • authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
  • object就是FilterInvocation对象,可以得到request等web资源。
  • collection是本次访问需要的权限。

上面的实现中,当需要多个权限时只要有一个符合则校验通过,即的关系,想要的关系只需要修改这里的逻辑即可。

三、自定义CustomFilterSecurityInterceptor,继承AbstractSecurityInterceptor

/**
 * @author ZHANGCHAO
 * @date 2020/3/19 11:32
 * @since 1.0.0
 */
@Slf4j
@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private CustomInvocationSecurityMetadataSource securityMetadataSource;
    @Autowired
    private CustomAccessDecisionManager accessDecisionManager;

    private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";

    /**
     *  初始化时将定义的DecisionManager,注入到父类AbstractSecurityInterceptor中
     *  注意:
     *  @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
     *  此方法必须在将类放入服务之前调用,且只执行一次。
     */
    @PostConstruct
    public void init() {
        log.info("设置==========================================鉴权决策管理器");
        super.setAccessDecisionManager(accessDecisionManager);
    }

    /**
     * 向父类提供要处理的安全对象类型,因为父亲被调用的方法参数类型大多是Object,框架需要保证传递进去的安全对象类型相同
     *
     * @return 子类为其提供服务的安全对象的类型
     */
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    /**
     * 获取到自定义MetadataSource的方法
     *
     * 启动时会有3次调用
     * 第一次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
     * 第二次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
     * 第三次调用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
     *
     * 登录时调用
     * 调用:{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
     *
     * @return  权限资源映射的数据源
     */
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    /**
     *
     * 由Web容器调用,以向filter指示正在将其放入服务中。
     * servlet容器在实例化filter后,只调用一个init方法。
     * 在要求filter执行任何过滤之前,init方法必须成功完成。
     * 如果init方法满足以下条件之一,则web容器无法将筛选器放入服务:抛出 servletException
     * 在web容器定义的时间内不返回
     * 默认实现时NO-OP
     * @param filterConfig 与正在初始化的filter实例关联的配置信息
     * @throws ServletException 如果实例化失败
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("filer==========================================init");
    }

    /**
     * 每当request/response对由于客户端请求链末端的资源而通过链时,容器调用过滤器的doFilter方法。
     * 传入此方法的filter chain 允许Filter传递请求并响应链中的下一个实体。
     *
     * 此方法的典型实现将遵循以下模式:
     * 1.检查请求
     * 2.也可以使用自定义实现包装请求对象,输入filter的内容或头
     * 3.(可选)使用自定义实现包装响应对象,以Filter 内容或头进行输出过滤
     * 4.使用FilterChain对象的chain.doFilter()调用链中的下一个实体
     * 5.在调用FilterChain中的下一个实体后,直接在响应上设置头。
     * @param request  要处理的请求
     * @param response 与请求关联的响应
     * @param filterChain   提供对链中下一个Filter的访问,以便此Filter将请求和响应传递给以进行进一步处理
     * @throws IOException      如果在此筛选器处理请求期间发生I/O错误
     * @throws ServletException 如果由于其他原因处理失败
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        log.info("[自定义过滤器]:{}","CustomFilterSecurityInterceptor.doFilter()");
        FilterInvocation filterInvocation = new FilterInvocation(request,response,filterChain);
        invoke(filterInvocation);
    }

    /**
     * 由Web容器调用,以向filter指示它正在退出服务。
     * 只有当filter的doFilter方法中的所有线程都退出或超出时间间后,才调用此方法。
     * 在web容器调用此方法之后,它将不再在此filter的实例上调用doFilter方法。
     * 此方法使filter有机会clean正在保留的任何资源(例如内存、文件句柄、线程)
     * 并确保任何持久状态与filter在内存中的当前状态同步。
     *
     * 默认实现时NO-OP
     */
    @Override
    public void destroy() {
        log.info("filer==========================================destroy");
    }

    private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        if (filterInvocation.getRequest() != null && filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null) {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());
        } else {
            if (filterInvocation.getRequest() != null) {
                filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }
            // 调用父类的beforeInvocation ==> accessDecisionManager.decide(..)
            InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
            try {
                filterInvocation.getChain().doFilter(filterInvocation.getRequest(),filterInvocation.getResponse());
            } finally {
                super.finallyInvocation(token);
            }
            super.afterInvocation(token,null);
        }
    }
}

抽象类AbstractSecurityInterceptor默认实现是FilterSecurityInterceptor,进行访问资源时,会通过这个拦截器拦截访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截, 其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息, 还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等), 如果权限足够,则返回,权限不够则报错并调用权限不足页面。

AbstractSecurityInterceptor这个过滤器基本上控制着spring security 的整个流程。Spring Security核心流程具体每条的实现由AbstractSecurityInterceptor 实现,也就是说AbstractSecurityInterceptor 只是定义了一些行为,然后这些行为的安排,也就是执行流程则是由具体的子类所实现,AbstractSecurityInterceptor 虽然也叫Interceptor ,但是并没有继承和实现任何和过滤器相关的类,具体和过滤器有关的部分是由子类所定义。每一种受保护对象都拥有继承自AbstrachSecurityInterceptor的拦截器类。spring security 提供了两个具体实现类,MethodSecurityInterceptor 将用于受保护的方法,FilterSecurityInterceptor 用于受保护的web 请求。

AbstractSecurityInterceptor 的两个实现都具有一致的逻辑:

  1. 先将正在请求调用的受保护对象传递给beforeInvocation()方法进行权限鉴定。
  2. 权限鉴定失败就直接抛出异常了。
  3. 鉴定成功将尝试调用受保护对象,调用完成后,不管是成功调用,还是抛出异常,都将执行finallyInvocation()。
  4. 如果在调用受保护对象后没有抛出异常,则调用afterInvocation()。

AbstractSecurityInterceptor 中的一些方法:

  1. afterPropertiesSet():主要是对类的属性进行校验。
  2. beforeInvocation():这个方法实现了对访问对象的权限校验,内部使用了AccessDecisionManager 和 AuthenticationManager。
  3. finallyInvocation():这个方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()的方法。
  4. finallyInvocation():方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。

注意:在spring容器托管的AbstractSecurityInterceptor的bean,都会自动加入到servlet的filter chain,不用在websecurityconfig配置

四、启动项目测试

image-20200319152625402

以上

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值