美文网首页spring系列我爱编程Spring Security
spring security动态配置url权限认证

spring security动态配置url权限认证

作者: 说你还是说我 | 来源:发表于2018-04-02 23:08 被阅读1948次

    本文介绍的spring security动态配置url权限认证基于的是spring-boot-2.0.0、spring-security 5.X来编写的。

    笔者浏览完spring security官方文档之后,发现并没有详细的介绍说明如何动态的配置我们的url权限认证。spring security默认的权限配置确只会在启动工程的时候初始化一次url权限配置。但是实际情况我们项目的权限会随时动态的更改,这样我们就需要重新启动项目以便新配置的权限生效。这样的处理显然不合理。当然spring是具有非常好的拓展性,我们就抓主spring的这个特性,模仿默认的认证方式来拓展我们需要的认证规则。
    在spring security的官方文档的Spring Security FAQ里有这么一个问题解答44.4.6. How do I define the secured URLs within an application dynamically?
    这里的解答非常重要
    1.需要提供认证数据规则源数据(类似默认配置在代码里的url权限数据)
    2.自定义一个拦截器然后把它添加到spring security的filterChain中
    通过这两个主要思路,在逐步通过来代码来实现我们的动态url权限配置

    具体步骤

    1.配置认证数据源

    public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
    
        /*
         * 这个例子放在构造方法里初始化url权限数据,我们只要保证在 getAttributes()之前初始好数据就可以了
         */
        public MyFilterSecurityMetadataSource() {
            Map<RequestMatcher, Collection<ConfigAttribute>> map = new HashMap<>();
    
            AntPathRequestMatcher matcher = new AntPathRequestMatcher("/home");
            SecurityConfig config = new SecurityConfig("ROLE_ADMIN");
            ArrayList<ConfigAttribute> configs = new ArrayList<>();
            configs.add(config);
            map.put(matcher,configs);
    
            AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/");
            SecurityConfig config2 = new SecurityConfig("ROLE_ADMIN");
            ArrayList<ConfigAttribute> configs2 = new ArrayList<>();
            configs2.add(config2);
            map.put(matcher2,configs2);
    
            this.requestMap = map;
        }
    
    
        /**
         * 在我们初始化的权限数据中找到对应当前url的权限数据
         *
         * @param object
         * @return
         * @throws IllegalArgumentException
         */
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            FilterInvocation fi = (FilterInvocation) object;
            HttpServletRequest request = fi.getRequest();
            String url = fi.getRequestUrl();
            String httpMethod = fi.getRequest().getMethod();
    
            // Lookup your database (or other source) using this information and populate the
            // list of attributes (这里初始话你的权限数据)
            //List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
    
            //遍历我们初始化的权限数据,找到对应的url对应的权限
            for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
                    .entrySet()) {
                if (entry.getKey().matches(request)) {
                    return entry.getValue();
                }
            }
            return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return FilterInvocation.class.isAssignableFrom(clazz);
        }
    

    这里使用不了WebExpressionConfigAttribute这个类配置ConfigAttribute,应为它只能在package中使用,这也导致了在认证管理器中,我们不能使用对应WebExpressionVoter来解析我们使用的SecurityConfig加载的数据。具体原因如下:


    image.png

    这里做了一个类的实例检查。所以在后面使用了RoleVoter来做认证数据的解析。

    2.自定义动态数据拦截器

    public class DynamicallyUrlInterceptor extends AbstractSecurityInterceptor implements Filter {
    
        //标记自定义的url拦截器已经加载
        private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied_dynamically";
    
        private FilterInvocationSecurityMetadataSource securityMetadataSource;
        private boolean observeOncePerRequest = true;
    
    
        @Override
        public Class<?> getSecureObjectClass() {
            return FilterInvocation.class;
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(request, response, chain);
            invoke(fi);
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void destroy() {
        }
    
        public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
            return this.securityMetadataSource;
        }
    
        public SecurityMetadataSource obtainSecurityMetadataSource() {
            return this.securityMetadataSource;
        }
    
        public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
            this.securityMetadataSource = newSource;
        }
    
        @Override
        public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
            super.setAccessDecisionManager(accessDecisionManager);
        }
    
        public void invoke(FilterInvocation fi) throws IOException, ServletException {
    
            if ((fi.getRequest() != null)
                    && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                    && observeOncePerRequest) {
                // 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);
                }
    
                InterceptorStatusToken token = super.beforeInvocation(fi);
    
                try {
                    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                }
                finally {
                    super.finallyInvocation(token);
                }
    
                super.afterInvocation(token, null);
            }
        }
    

    这个类参考的是FilterSecurityInterceptor这个类写的,只是修改了FILTER_APPLIED的值。主要是重写了父类的方法,添加认证数据源值的设置。使父类在调用方法是能找到对应的数据。

    3.提供一个认证管理器

    public class DynamicallyUrlAccessDecisionManager extends AbstractAccessDecisionManager {
    
        public DynamicallyUrlAccessDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) {
            super(decisionVoters);
        }
    
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
                throws AccessDeniedException, InsufficientAuthenticationException {
            int deny = 0;
    
            for (AccessDecisionVoter voter : getDecisionVoters()) {
                int result = voter.vote(authentication, object, configAttributes);
    
                if (logger.isDebugEnabled()) {
                    logger.debug("Voter: " + voter + ", returned: " + result);
                }
    
                switch (result) {
                    case AccessDecisionVoter.ACCESS_GRANTED:
                        return;
    
                    case AccessDecisionVoter.ACCESS_DENIED:
                        deny++;
    
                        break;
    
                    default:
                        break;
                }
            }
    
            if (deny > 0) {
                throw new AccessDeniedException(messages.getMessage(
                        "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
            }
    
            // To get this far, every AccessDecisionVoter abstained
            checkAllowIfAllAbstainDecisions();
        }
    }
    

    spring security默认提供了


    image.png

    有这么几种具体的认证决策,这里最终使用的是RoleVoter这个决策认证类。

    4.配置filter

        @Bean
        public DynamicallyUrlInterceptor dynamicallyUrlInterceptor(){
            DynamicallyUrlInterceptor interceptor = new DynamicallyUrlInterceptor();
            interceptor.setSecurityMetadataSource(new MyFilterSecurityMetadataSource());
    
            //配置RoleVoter决策
            List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
            decisionVoters.add(new RoleVoter());
            //设置认证决策管理器
            interceptor.setAccessDecisionManager(new DynamicallyUrlAccessDecisionManager(decisionVoters));
            return interceptor;
        }
    

    然后在把我们的filter添加到spring security的filter中


    image.png

    附上spring security的fiter信息的连接
    https://docs.spring.io/spring-security/site/docs/4.2.4.RELEASE/reference/htmlsingle/#ns-custom-filters

    最后,本例子只是技术探讨。具体使用这种动态的url验证机制是否合适,还需要结合实际情况来分析。

    项目地址:https://gitee.com/longguiyunjosh/spring-security-dynamically-demo

    参考文章:
    https://segmentfault.com/a/1190000010672041
    https://www.cnblogs.com/visoncheng/p/3335768.html

    相关文章

      网友评论

      • bb4525538822:请问自定义的的DynamicallyUrlAccessDecisionManager有什么意义,我看了代码,和官方代码AffirmativeBased是一模一样的
        说你还是说我:@神笔马奶 但是只能把filter插入到spring的filterChain中,spring security是不会使用这个类的MyFilterSecurityMetadataSource
        bb4525538822:@说你还是说我 也就是说,其实只要实现MyFilterSecurityMetadataSource就行了,其它不用改,对吧
        说你还是说我:官方的url权限都是在代码中配置死的。这里其实就是参照官方的代码写的,步骤一中里面只是模拟了两个请求路径的权限规则。如果要做成动态获取权限数据,就想办法把你的权限数据放到requestMap中。
      • bb4525538822:请问,换成__spring_security_filterSecurityInterceptor_filterApplied_dynamically有什么意义吗
        说你还是说我:这个类似保存到map中的key一样,只要保证拓展的拦截器的key与Spring Security中集成号的拦截器key的不冲突就好。如果要深入了解的话,就需要看Spring Security加载拦截器的过程了。
      • zhangxiaojieU:如何在动态url权限控制中,使用hasAnyrole,一个URL可以匹配多个role
        zhangxiaojieU:@说你还是说我 是的是的,已解决。其实是按照url设置roles,反过来也是一个url匹配多个role。
        :+1:
        说你还是说我:在加载url权限的时候,数据源里是url对应的权限其实是一个list集合。步骤1中的url权限里的SecurityConfig最后是存入到list集合中。也就是一个url可以对应多个role。
      • 迷糊醉:虽然看不懂但感觉很厉害!加油

      本文标题:spring security动态配置url权限认证

      本文链接:https://www.haomeiwen.com/subject/pqhfhftx.html