序言
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的權限校驗做出更好的擴展。
順便說下我們這個自定義權限並不會影響到權限註解,權限註解是進行了切面處理所以還會在走一遍權限認證器和投票的。
本博文是基於springboot2.x 和security 5 如果有什麼不對的請在下方留言。