时间:2021-05-19
缘起
标准的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 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,该过滤器实现了主要的鉴权逻辑,最核心的代码在这里:
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); } }从上面可以看出,要实现动态鉴权,可以从两方面着手:
下面来看分别如何实现。
自定义AccessDecisionManager
官方的三个AccessDecisionManager都是基于AccessDecisionVoter来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了。
自定义主要是实现AccessDecisionVoter接口,我们可以仿照官方的RoleVoter实现一个:
如何加入动态权限呢?
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。实际项目里可以根据需要灵活选择。
延伸阅读:
Spring Security 架构与源码分析
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
spring-security里自带了oauth2,正好YIIU里也用到了spring-security做权限部分,那为何不直接集成上第三方登录呢?然后我开始了
这里使用的是spring-security和原生的jasigcas包来进行整合,为什么没有直接使用spring提供的spring-security-cas,后面
本文实例讲述了Java实现url加密处理的方法。分享给大家供大家参考,具体如下:packagetest;importjava.security.Key;impo
国际化配置basename中配置的是消息文件的路径在spring-security-core-3.2.0.M1.jar包中的org.springframewor
Spring依赖注入(DI)的三种方式,分别为:1.接口注入2.Setter方法注入3.构造方法注入下面介绍一下这三种依赖注入在Spring中是怎么样实现的。首