(五)Spring Security基于数据库的权限授权


我们接着上一章(四)Spring Security基于数据库的用户认证,进行开发

一:重写并实现了基于数据库的权限数据源

/**
 * 权限资源
 * FilterInvocationSecurityMetadataSource的默认实现是
 * DefaultFilterInvocationSecurityMetadataSource
 */
@Service
public class CustomInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    private static final Logger log = LoggerFactory.getLogger(CustomInvocationSecurityMetadataSourceService.class);

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

    @Autowired
    public CustomInvocationSecurityMetadataSourceService(PermissionRepository permissionRepository) {
        this.permissionRepository = permissionRepository;
    }

    /**
     *  注意:
     *  @PostConstruct 用于在依赖关系注入完成之后需要执行的方法,以执行任何初始化。
     *  此方法必须在将类放入服务之前调用,且只执行一次。
     */
    @PostConstruct
    public void init(){
        log.info("[自定义权限资源数据源]:{}","初始化权限资源");
        List<Permission> permissions = permissionRepository.findAll();
        permissions.forEach(item->{
            Set<String> roleNames = item.getRoleNames();
            List<ConfigAttribute> configAttributes = new ArrayList<>();
            for (String roleName : roleNames) {
                configAttributes.add(new SecurityConfig(roleName));
            }
            requestMap.put(new AntPathRequestMatcher(item.getUrl()),configAttributes);
        });
        System.out.println(requestMap.toString());
    }


    /**
     * getAttributes方法返回本次访问需要的权限,可以有多个权限。
     * 在上面的实现中如果没有匹配的url直接返回null,
     * 也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。
     *
     * 访问配置属性(ConfigAttribute)用于给定安全对象(通过的验证)
     *
     * @param object 安全的对象
     * @return 用于传入的安全对象的属性。 如果没有适用的属性,则应返回空集合。
     * @throws IllegalArgumentException 如果传递的对象不是SecurityDatasource实现支持的类型,则抛出异常
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        log.info("[自定义权限资源数据源]:{}","获取本次访问需要的权限");
        if(requestMap.isEmpty()){
            init();
        }
        final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(request)) {
                log.info("[自定义权限资源数据源]:当前路径[{}]需要的资源权限:[{}] ==> 触发鉴权决策管理器",entry.getKey(),entry.getValue().toString());
              return entry.getValue();
            }
        }
       log.info("[自定义权限资源数据源]:{}==> {}","白名单路径",request.getRequestURI());
        return null;
    }


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

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

二:重写权限决策

根据URL资源权限和用户角色,进行鉴权

@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {

    private static final Logger log = LoggerFactory.getLogger(CustomAccessDecisionManager.class);

    /**
     * 权限鉴定
     *
     * @param authentication   from SecurityContextHolder.getContext() => userDetails.getAuthorities()
     * @param object           就是FilterInvocation对象,可以得到request等web资源。
     * @param configAttributes from MetaDataSource.getAttributes(),已经被框架做了非空判断
     * @throws AccessDeniedException   如果由于身份验证不具有所需的权限或ACL特权而拒绝访问
     * @throws InsufficientAuthenticationException 如果由于身份验证没有提供足够的信任级别而拒绝访问
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        log.info("****************************************权限鉴定********************************************");
        /*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一个URL
        log.info("[当前路径[{}]需要的资源权限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
        log.info("[登录用户[{}]权限]:{}",authentication.getName(),authentication.getAuthorities());

        if(configAttributes == null){
            return;
        }

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

    /**
     * AbstractSecurityInterceptor 调用,遍历ConfigAttribute集合,筛选出不支持的attribute
     *
     * @param attribute 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 attribute) {
        return true;
    }

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

三:实现AbstractSecurityInterceptor

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

@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    private static final Logger log = LoggerFactory.getLogger(CustomFilterSecurityInterceptor.class);

    private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";


    private final CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService;
    private final CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    public CustomFilterSecurityInterceptor(CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService, CustomAccessDecisionManager customAccessDecisionManager) {
        this.customInvocationSecurityMetadataSourceService = customInvocationSecurityMetadataSourceService;
        this.customAccessDecisionManager = customAccessDecisionManager;
    }

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

    /**
     * 向父类提供要处理的安全对象类型,因为父亲被调用的方法参数类型大多是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.customInvocationSecurityMetadataSourceService;
    }

    /**
     *
     * 由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 chain   提供对链中下一个Filter的访问,以便此Filter将请求和响应传递给以进行进一步处理
     * @throws IOException      如果在此筛选器处理请求期间发生I/O错误
     * @throws ServletException 如果由于其他原因处理失败
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("[自定义过滤器]:{}","CustomFilterSecurityInterceptor.doFilter()");
        FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
        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 fi) throws IOException, ServletException {
        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
            // first time this request being called, so perform security checking
            if (fi.getRequest() != null ) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            /* 调用父类的beforeInvocation ==> accessDecisionManager.decide(..) */
            InterceptorStatusToken token = super.beforeInvocation(fi);

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

注意:在spring容器托管的AbstractSecurityInterceptor的bean,都会自动加入到servlet的filter chain,不用在websecurityconfig配置
示例
在这里插入图片描述
当权限不足时,会自动跳转到403
在这里插入图片描述

四:项目地址

(五)Spring Security基于数据库的权限授权

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章