缘起
标准的 RABC, 权限需要支持动态配置, spring security 默认是在代码里约定好权限, 真实的业务场景通常需要可以支持动态配置角色访问权限, 即在运行时去配置 url 对应的访问角色.
基于 spring security, 如何实现这个需求呢?
最简单的方法就是自定义一个 Filter 去完成权限判断, 但这脱离了 spring security 框架, 如何基于 spring security 优雅的实现呢?
spring security 授权回顾
spring security 通过 FilterChainProxy 作为注册到 web 的 filter,FilterChainProxy 里面一次包含了内置的多个过滤器, 我们首先需要了解 spring security 内置的各种 filter:
Alias | Filter Class | Namespace Element or Attribute |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | mailto: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 | mailto:http/@servlet-api-provision |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | mailto: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
, 该过滤器实现了主要的鉴权逻辑, 最核心的代码在这里:
- 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);
- }
- }
从上面可以看出, 要实现动态鉴权, 可以从两方面着手:
自定义 SecurityMetadataSource, 实现从数据库加载 ConfigAttribute
另外就是可以自定义 accessDecisionManager, 官方的 UnanimousBased 其实足够使用, 并且他是基于 AccessDecisionVoter 来实现权限认证的, 因此我们只需要自定义一个 AccessDecisionVoter 就可以了
下面来看分别如何实现.
自定义 AccessDecisionManager
官方的三个 AccessDecisionManager 都是基于 AccessDecisionVoter 来实现权限认证的, 因此我们只需要自定义一个 AccessDecisionVoter 就可以了.
自定义主要是实现
AccessDecisionVoter
接口, 我们可以仿照官方的 RoleVoter 实现一个:
- 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;
- }
- }
如何加入动态权限呢?
vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)
里的 Object object 的类型是 FilterInvocation, 可以通过 getRequestUrl 获取当前请求的 URL:
- FilterInvocation fi = (FilterInvocation) object;
- String url = fi.getRequestUrl();
因此这里扩展空间就大了, 可以从 DB 动态加载, 然后判断 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 只要实现接口即可, 在接口里从 DB 动态加载规则.
为了复用代码里的定义, 我们可以将代码里生成的 SecurityMetadataSource 带上, 在构造函数里传入默认的 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 从数据库加载权限配置
- }
- 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?
发现一个扩展方法
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 实现动态权限的方法, 一是自定义 accessDecisionManager, 二是自定义 FilterInvocationSecurityMetadataSource. 实际项目里可以根据需要灵活选择.
来源: https://www.cnblogs.com/xiaoqi/p/spring-security-rabc.html