序言

SpringSecurity 进行的权限验证,有时候可能并不太满足我们的需求。有时候呢可能需要你自己去扩展达到一个对自己业务满意的验证,这时候怎么办呢?第一呢, 先不要百度,你要懂权限验证的一个流程,不懂的话可以去看我之前的博客,第二呢,就是放开手按照自己假象的思路去实践!

思路

我说一下我的需求,我想判断那个角色拥有某些url的权限,这时候该怎么进行扩展呢?

我的思路是对 自定义 AccessDecisionVoter 然后我们知道在 FilterSecurityInterceptor 里是从 SecurityMetadataSource 中拿到的权限配置项,知道了这些后,就可以做出扩展。

  1. 定义一个自己的SecurityMetadataSource 使其从数据库中查询权限自己构建 ConfigAttribute
  2. 定义一个具有自己需求的AccessDecisionVoter
  3. 规定一个自己的AccessDecisionManager

实践

SecurityMetadataSource

首先呢我们先定义一个自己的 SecurityMetadataSource ,然后每次调用的时候都去数据库里去查询。

public class DynamicFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {


    private FilterInvocationSecurityMetadataSource superMetadataSource;


    public DynamicFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource) {
        this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;
        //TODO 在这里去查询你的数据库
    }

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 假设这就是从数据库中查询到的数据
     * 意思就是 ROLE_JAVA 的角色 才能访问 /tt
     */
    private final Map<String, String> urlRoleMap = new HashMap<String, String>() {{
        put("/tt", "ROLE_JAVA");
    }};

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();
        for (Map.Entry<String, String> entry : urlRoleMap.entrySet()) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                return SecurityConfig.createList(entry.getValue());
            }
        }
        //如果没有匹配到就拿 咱们自定义的配置
        return superMetadataSource.getAttributes(object);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }


    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

AccessDecisionVoter

紧接着我们定义一个自己的 AccessDecisionVoter 这一步的作用是对我们自己构造的 SecurityConfig.createList(entry.getValue()); 进行一个角色验证。

public class MyDynamicVoter implements AccessDecisionVoter<Object> {
    /**
     * supports 方法说明这个投票器是否可以传递到下一个投票器
     * 可以支持传递,则返回true
     *
     * @param attribute
     * @return
     */
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        //如果没有进行认证则永远返回 -1
        if (authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
        for (ConfigAttribute attribute : attributes) {
            if (attribute.getAttribute() == null) {
                continue;
            }
            if (this.supports(attribute)) {
                result = ACCESS_DENIED;
                for (GrantedAuthority authority : authorities) {
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }
        return result;
    }

    private Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
        return authentication.getAuthorities();
    }

}

AccessDecisionManager

最后呢我们可以选择重写或者使用 Spring Security 提供的验证器 看你自己。这里呢我还是使用 Spring Security提供的 AffirmativeBased

@Bean
 public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    decisionVoters.add(new AuthenticatedVoter());
    decisionVoters.add(new WebExpressionVoter());
    decisionVoters.add(new MyDynamicVoter());
    return new AffirmativeBased(decisionVoters);
 }

这里顺便把WebExpressionVoter 投票器加入,让其验证除我们自定义的权限配置。

加入配置项

最后呢把我们定义好的条条框框都加入到我们的配置链中

protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic()  //httpBasic 登录
        http.formLogin()
                .failureHandler(failureAuthenticationHandler) // 自定义登录失败处理
                .successHandler(successAuthenticationHandler) // 自定义登录成功处理
                .and()
                .logout()
                .logoutUrl("/logout")
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/authentication/form") // 自定义登录路径
                .and()
                .authorizeRequests()// 对请求授权
                // 自定义FilterInvocationSecurityMetadataSource
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                        fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
                        fsi.setAccessDecisionManager(accessDecisionManager());
                        return fsi;
                    }
                })
                .antMatchers("/login", "/authentication/require",
                        "/authentication/form").permitAll()
                .anyRequest()
                .authenticated().and().exceptionHandling().accessDeniedHandler(accessDeniedAuthenticationHandler)
                .and()
                .csrf().disable();// 禁用跨站攻击
    }


@Bean
public DynamicFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
   return new DynamicFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
}


@Bean
public AccessDecisionManager accessDecisionManager() {
   List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
   decisionVoters.add(new AuthenticatedVoter());
   decisionVoters.add(new WebExpressionVoter());
   decisionVoters.add(new MyDynamicVoter());
   return new AffirmativeBased(decisionVoters);
}

然后定义一个controller

@RestController
public class PermissionController {


    @RequestMapping("/tt")
    public String tt() {
        System.out.println("1");
        return "tt";
    }
}

让登录用户拥有ROLE_JAVA 角色的时候,即可访问/tt

当然这只是我的一个需求,具体的怎么扩展决定在你自己,只要懂了大概的流程,既可以对Spring Security的权限校验做出更好的扩展。

顺便说下我们这个自定义权限并不会影响到权限注解,权限注解是进行了切面处理所以还会在走一遍权限认证器和投票的。