序言
SpringSecurity 进行的权限验证,有时候可能并不太满足我们的需求。有时候呢可能需要你自己去扩展达到一个对自己业务满意的验证,这时候怎么办呢?第一呢, 先不要百度,你要懂权限验证的一个流程,不懂的话可以去看我之前的博客,第二呢,就是放开手按照自己假象的思路去实践!
思路
我说一下我的需求,我想判断那个角色拥有某些url的权限,这时候该怎么进行扩展呢?
我的思路是对 自定义 AccessDecisionVoter
然后我们知道在 FilterSecurityInterceptor
里是从 SecurityMetadataSource
中拿到的权限配置项,知道了这些后,就可以做出扩展。
- 定义一个自己的
SecurityMetadataSource
使其从数据库中查询权限自己构建ConfigAttribute
- 定义一个具有自己需求的
AccessDecisionVoter
- 规定一个自己的
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的权限校验做出更好的扩展。
顺便说下我们这个自定义权限并不会影响到权限注解,权限注解是进行了切面处理所以还会在走一遍权限认证器和投票的。