spring security動態(tài)配置url權(quán)限的2種實(shí)現(xiàn)方法
緣起
標(biāo)準(zhǔn)的RABC, 權(quán)限需要支持動態(tài)配置,spring security默認(rèn)是在代碼里約定好權(quán)限,真實(shí)的業(yè)務(wù)場景通常需要可以支持動態(tài)配置角色訪問權(quán)限,即在運(yùn)行時去配置url對應(yīng)的訪問角色。
基于spring security,如何實(shí)現(xiàn)這個需求呢?
最簡單的方法就是自定義一個Filter去完成權(quán)限判斷,但這脫離了spring security框架,如何基于spring security優(yōu)雅的實(shí)現(xiàn)呢?
spring security 授權(quán)回顧
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,該過濾器實(shí)現(xiàn)了主要的鑒權(quán)邏輯,最核心的代碼在這里:
protected InterceptorStatusToken beforeInvocation(Object object) { // 獲取訪問URL所需權(quán)限 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); Authentication authenticated = authenticateIfRequired(); // 通過accessDecisionManager鑒權(quán) 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); } }
從上面可以看出,要實(shí)現(xiàn)動態(tài)鑒權(quán),可以從兩方面著手:
- 自定義SecurityMetadataSource,實(shí)現(xiàn)從數(shù)據(jù)庫加載ConfigAttribute
- 另外就是可以自定義accessDecisionManager,官方的UnanimousBased其實(shí)足夠使用,并且他是基于AccessDecisionVoter來實(shí)現(xiàn)權(quán)限認(rèn)證的,因此我們只需要自定義一個AccessDecisionVoter就可以了
下面來看分別如何實(shí)現(xiàn)。
自定義AccessDecisionManager
官方的三個AccessDecisionManager都是基于AccessDecisionVoter來實(shí)現(xiàn)權(quán)限認(rèn)證的,因此我們只需要自定義一個AccessDecisionVoter就可以了。
自定義主要是實(shí)現(xiàn)AccessDecisionVoter接口,我們可以仿照官方的RoleVoter實(shí)現(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)權(quán)限呢?
vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)
里的Object object的類型是FilterInvocation,可以通過getRequestUrl獲取當(dāng)前請求的URL:
FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl();
因此這里擴(kuò)展空間就大了,可以從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只要實(shí)現(xiàn)接口即可,在接口里從DB動態(tài)加載規(guī)則。
為了復(fù)用代碼里的定義,我們可以將代碼里生成的SecurityMetadataSource帶上,在構(gòu)造函數(shù)里傳入默認(rèn)的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ù)庫加載權(quán)限配置 } 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()); } } // 返回代碼定義的默認(rèn)配置 return superMetadataSource.getAttributes(object); } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
怎么使用?和accessDecisionManager不一樣,ExpressionUrlAuthorizationConfigurer 并沒有提供set方法設(shè)置FilterSecurityInterceptor的FilterInvocationSecurityMetadataSource,how to do?
發(fā)現(xiàn)一個擴(kuò)展方法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; }
小結(jié)
本文介紹了兩種基于spring security實(shí)現(xiàn)動態(tài)權(quán)限的方法,一是自定義accessDecisionManager,二是自定義FilterInvocationSecurityMetadataSource。實(shí)際項(xiàng)目里可以根據(jù)需要靈活選擇。
延伸閱讀:
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
在Java中輕松將HTML格式文本轉(zhuǎn)換為純文本的方法示例(保留換行)
這篇文章主要介紹了在Java中輕松將HTML格式文本轉(zhuǎn)換為純文本的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04springcloud中Feign超時提示Read timed out executing
Feign接口調(diào)用分兩層,Ribbon的調(diào)用和Hystrix調(diào)用,理論上設(shè)置Ribbon的時間即可,但是Ribbon的超時時間和Hystrix的超時時間需要結(jié)合起來,這篇文章給大家介紹springcloud之Feign超時提示Read timed out executing POST問題及解決方法,感興趣的朋友一起看看吧2024-01-01SpringCloud Netflix Ribbon源碼解析(推薦)
這篇文章主要介紹了SpringCloud Netflix Ribbon源碼解析,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03基于javascript實(shí)現(xiàn)獲取最短路徑算法代碼實(shí)例
這篇文章主要介紹了基于javascript實(shí)現(xiàn)獲取最短路徑算法代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02