spring security動態(tài)配置url權限的2種實現(xiàn)方法
緣起
標準的RABC, 權限需要支持動態(tài)配置,spring security默認是在代碼里約定好權限,真實的業(yè)務場景通常需要可以支持動態(tài)配置角色訪問權限,即在運行時去配置url對應的訪問角色。
基于spring security,如何實現(xiàn)這個需求呢?
最簡單的方法就是自定義一個Filter去完成權限判斷,但這脫離了spring security框架,如何基于spring security優(yōu)雅的實現(xiàn)呢?
spring security 授權回顧
spring security 通過FilterChainProxy作為注冊到web的filter,F(xiàn)ilterChainProxy里面一次包含了內(nèi)置的多個過濾器,我們首先需要了解spring security內(nèi)置的各種filter:
| Alias | Filter Class | Namespace Element or Attribute |
|---|---|---|
| CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
| SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
| CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
| HEADERS_FILTER | HeaderWriterFilter | http/headers |
| CSRF_FILTER | CsrfFilter | http/csrf |
| LOGOUT_FILTER | LogoutFilter | http/logout |
| X509_FILTER | X509AuthenticationFilter | http/x509 |
| PRE_AUTH_FILTER | AbstractPreAuthenticatedProcessingFilter Subclasses | N/A |
| CAS_FILTER | CasAuthenticationFilter | N/A |
| FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
| BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
| SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http/@servlet-api-provision |
| JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http/@jaas-api-provision |
| REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
| ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
| SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
| EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
| FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
| SWITCH_USER_FILTER | SwitchUserFilter | N/A |
最重要的是FilterSecurityInterceptor,該過濾器實現(xiàn)了主要的鑒權邏輯,最核心的代碼在這里:
protected InterceptorStatusToken beforeInvocation(Object object) {
// 獲取訪問URL所需權限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
// 通過accessDecisionManager鑒權
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
從上面可以看出,要實現(xiàn)動態(tài)鑒權,可以從兩方面著手:
- 自定義SecurityMetadataSource,實現(xiàn)從數(shù)據(jù)庫加載ConfigAttribute
- 另外就是可以自定義accessDecisionManager,官方的UnanimousBased其實足夠使用,并且他是基于AccessDecisionVoter來實現(xiàn)權限認證的,因此我們只需要自定義一個AccessDecisionVoter就可以了
下面來看分別如何實現(xiàn)。
自定義AccessDecisionManager
官方的三個AccessDecisionManager都是基于AccessDecisionVoter來實現(xiàn)權限認證的,因此我們只需要自定義一個AccessDecisionVoter就可以了。
自定義主要是實現(xiàn)AccessDecisionVoter接口,我們可以仿照官方的RoleVoter實現(xiàn)一個:
public class RoleBasedVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
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;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return authentication.getAuthorities();
}
@Override
public boolean supports(Class clazz) {
return true;
}
}
如何加入動態(tài)權限呢?
vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)里的Object object的類型是FilterInvocation,可以通過getRequestUrl獲取當前請求的URL:
FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl();
因此這里擴展空間就大了,可以從DB動態(tài)加載,然后判斷URL的ConfigAttribute就可以了。
如何使用這個RoleBasedVoter呢?在configure里使用accessDecisionManager方法自定義,我們還是使用官方的UnanimousBased,然后將自定義的RoleBasedVoter加入即可。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 自定義accessDecisionManager
.accessDecisionManager(accessDecisionManager())
.and()
.apply(securityConfigurerAdapter());
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
// new RoleVoter(),
new RoleBasedVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
自定義SecurityMetadataSource
自定義FilterInvocationSecurityMetadataSource只要實現(xiàn)接口即可,在接口里從DB動態(tài)加載規(guī)則。
為了復用代碼里的定義,我們可以將代碼里生成的SecurityMetadataSource帶上,在構造函數(shù)里傳入默認的FilterInvocationSecurityMetadataSource。
public class AppFilterInvocationSecurityMetadataSource implements org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource {
private FilterInvocationSecurityMetadataSource superMetadataSource;
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public AppFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource){
this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;
// TODO 從數(shù)據(jù)庫加載權限配置
}
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
// 這里的需要從DB加載
private final Map<String,String> urlRoleMap = new HashMap<String,String>(){{
put("/open/**","ROLE_ANONYMOUS");
put("/health","ROLE_ANONYMOUS");
put("/restart","ROLE_ADMIN");
put("/demo","ROLE_USER");
}};
@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 boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
怎么使用?和accessDecisionManager不一樣,ExpressionUrlAuthorizationConfigurer 并沒有提供set方法設置FilterSecurityInterceptor的FilterInvocationSecurityMetadataSource,how to do?
發(fā)現(xiàn)一個擴展方法withObjectPostProcessor,通過該方法自定義一個處理FilterSecurityInterceptor類型的ObjectPostProcessor就可以修改FilterSecurityInterceptor。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 自定義FilterInvocationSecurityMetadataSource
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
return fsi;
}
})
.and()
.apply(securityConfigurerAdapter());
}
@Bean
public AppFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
AppFilterInvocationSecurityMetadataSource securityMetadataSource = new AppFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return securityMetadataSource;
}
小結
本文介紹了兩種基于spring security實現(xiàn)動態(tài)權限的方法,一是自定義accessDecisionManager,二是自定義FilterInvocationSecurityMetadataSource。實際項目里可以根據(jù)需要靈活選擇。
延伸閱讀:
總結
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- 詳解Spring Security 中的四種權限控制方式
- java中自定義Spring Security權限控制管理示例(實戰(zhàn)篇)
- SpringSecurity動態(tài)加載用戶角色權限實現(xiàn)登錄及鑒權功能
- Spring security實現(xiàn)登陸和權限角色控制
- 解決Spring Security的權限配置不生效問題
- SpringBoot整合Security實現(xiàn)權限控制框架(案例詳解)
- Spring security實現(xiàn)權限管理示例
- SpringBoot2.0 整合 SpringSecurity 框架實現(xiàn)用戶權限安全管理方法
- Spring Security動態(tài)權限的實現(xiàn)方法詳解
- 基于Spring Security的動態(tài)權限系統(tǒng)設計與實現(xiàn)
相關文章
Java后端Tomcat實現(xiàn)WebSocket實例教程
WebSocket protocol 是HTML5一種新的協(xié)議。它實現(xiàn)了瀏覽器與服務器全雙工通信(full-duplex)。一開始的握手需要借助HTTP請求完成握手。本文給大家介紹Java后端Tomcat實現(xiàn)WebSocket實例教程,感興趣的朋友一起學習吧2016-05-05
基于springboot實現(xiàn)數(shù)據(jù)可視化的示例代碼
本文主要介紹了基于springboot實現(xiàn)數(shù)據(jù)可視化,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧<BR>2022-07-07
Spring Boot 集成 ElasticSearch應用小結
這篇文章主要介紹了Spring Boot 集成 ElasticSearch應用小結,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-11-11

