Spring Security系列四 自定义决策管理器(动态权限码)

来源:互联网 发布:霍金对人工智能的看法 编辑:程序博客网 时间:2024/05/21 08:00

前言

前面我们已经实现了用户的自定义登录及密码的加密,接下来就是动态的权限验证了,也就是实现Spring Security的决策管理器AccessDecisionManager

权限资源 SecurityMetadataSource

要实现动态的权限验证,当然要先有对应的访问权限资源了。Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限,所以第一步需要实现SecurityMetadataSource

SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但FilterInvocationSecurityMetadataSource只是一个标识接口,对应于FilterInvocation,本身并无任何内容:

  1. /**
  2. * Marker interface for <code>SecurityMetadataSource</code> implementations that are
  3. * designed to perform lookups keyed on {@link FilterInvocation}s.
  4. *
  5. * @author Ben Alex
  6. */
  7. public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
  8. }

因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource,这是因为Spring Security中很多web才使用的类参数类型都是FilterInvocationSecurityMetadataSource

下面是一个自定义实现类CustomSecurityMetadataSource的示例代码,它的主要责任就是当访问一个url时返回这个url所需要的访问权限。

  1. /**
  2. * Created by liyd on 16/12/9.
  3. */
  4. public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
  5. public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
  6. FilterInvocation fi = (FilterInvocation) object;
  7. Map<String, Collection<ConfigAttribute>> metadataSource = CustomSecurityContext.getMetadataSource();
  8. for (Map.Entry<String, Collection<ConfigAttribute>> entry : metadataSource.entrySet()) {
  9. String uri = entry.getKey();
  10. RequestMatcher requestMatcher = new AntPathRequestMatcher(uri);
  11. if (requestMatcher.matches(fi.getHttpRequest())) {
  12. return entry.getValue();
  13. }
  14. }
  15. return null;
  16. }
  17. public Collection<ConfigAttribute> getAllConfigAttributes() {
  18. return null;
  19. }
  20. public boolean supports(Class<?> clazz) {
  21. return FilterInvocation.class.isAssignableFrom(clazz);
  22. }
  23. }

getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回null,也就是没有配置权限的url默认都为白名单,想要换成默认是黑名单只要修改这里即可。

getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute是否配置正确,不需要校验直接返回null。

supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。

在上面我们主要定义了两个权限码:

  1. user=/user
  2. admin=/admin

也就是CustomSecurityContext.getMetadataSource()加载的内容,主要加载代码如下:

  1. ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
  2. Resource[] resources = resourcePatternResolver.getResources("classpath*:/security/*.properties");
  3. if (ArrayUtils.isEmpty(resources)) {
  4. return;
  5. }
  6. Properties properties = new Properties();
  7. for (Resource resource : resources) {
  8. properties.load(resource.getInputStream());
  9. }
  10. for (Map.Entry<Object, Object> entry : properties.entrySet()) {
  11. String key = (String) entry.getKey();
  12. String value = (String) entry.getValue();
  13. String[] values = StringUtils.split(value, ",");
  14. Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
  15. ConfigAttribute configAttribute = new SecurityConfig(key);
  16. configAttributes.add(configAttribute);
  17. for (String v : values) {
  18. METADATA_SOURCE_MAP.put(StringUtils.trim(v), configAttributes);
  19. }
  20. }

这里我们把权限的配置信息写在了properties文件中,当然你也可以存在数据库中或其它任何地方。

在加载的时候,这里的url是key,value是访问需要的权限码,一个权限码可以对应多个url,一个url也可以有多个权限码,想要怎么玩都可以在这里实现,示例中只是最简单的。

权限决策 AccessDecisionManager

有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。

这需要通过实现自定义的AccessDecisionManager来实现。Spring Security内置的几个AccessDecisionManager就不讲了,在web项目中基本用不到。

以下是示例代码:

  1. public class CustomAccessDecisionManager implements AccessDecisionManager {
  2. public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
  3. Iterator<ConfigAttribute> iterator = configAttributes.iterator();
  4. while (iterator.hasNext()) {
  5. if (authentication == null) {
  6. throw new AccessDeniedException("当前访问没有权限");
  7. }
  8. ConfigAttribute configAttribute = iterator.next();
  9. String needCode = configAttribute.getAttribute();
  10. Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
  11. for (GrantedAuthority authority : authorities) {
  12. if (StringUtils.equals(authority.getAuthority(), "ROLE_" + needCode)) {
  13. return;
  14. }
  15. }
  16. }
  17. throw new AccessDeniedException("当前访问没有权限");
  18. }
  19. public boolean supports(ConfigAttribute attribute) {
  20. return true;
  21. }
  22. public boolean supports(Class<?> clazz) {
  23. return FilterInvocation.class.isAssignableFrom(clazz);
  24. }
  25. }

同样的也有三个方法,其它两个和SecurityMetadataSource类似,这里主要讲讲decide方法。

decide方法的三个参数中:

  • authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities
  • object就是FilterInvocation对象,可以得到request等web资源。
  • configAttributes是本次访问需要的权限。

上面的实现中,当需要多个权限时只要有一个符合则校验通过,即的关系,想要的关系只需要修改这里的逻辑即可。

配置使用自定义实现类

上面权限的资源和验证我们已经都实现了,接下来就是指定让Spring Security使用我们自定义的实现类了。

在以前xml的配置中,一般都是自己实现一个FilterSecurityInterceptor,然后注入自定义的SecurityMetadataSourceAccessDecisionManager,就像下面这样:

  1. <b:bean id="customFilterSecurityInterceptor" class="com.dexcoder.security.CustomFilterSecurityInterceptor">
  2. <b:property name="authenticationManager" ref="customAuthenticationManager" />
  3. <b:property name="accessDecisionManager" ref="customAccessDecisionManager" />
  4. <b:property name="securityMetadataSource" ref="customSecurityMetadataSource" />
  5. </b:bean>

Spring BootJavaConfig中并没有这样的实现方式,但是提供了ObjectPostProcessor以让用户实现更多想要的高级配置。具体看下面代码,注意withObjectPostProcessor部分:

  1. @Bean
  2. public AccessDecisionManager accessDecisionManager() {
  3. return new CustomAccessDecisionManager();
  4. }
  5. @Bean
  6. public FilterInvocationSecurityMetadataSource securityMetadataSource() {
  7. return new CustomSecurityMetadataSource();
  8. }
  9. protected void configure(HttpSecurity http) throws Exception {
  10. http.authorizeRequests().antMatchers("/", "/index").permitAll().anyRequest().authenticated().and().formLogin()
  11. .loginPage("/login").defaultSuccessUrl("/user").permitAll().and().logout().permitAll()
  12. .and().authorizeRequests().anyRequest().authenticated()
  13. .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
  14. public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
  15. fsi.setAccessDecisionManager(accessDecisionManager());
  16. fsi.setSecurityMetadataSource(securityMetadataSource());
  17. return fsi;
  18. }
  19. });
  20. }

主要是在创建默认的FilterSecurityInterceptor的时候把我们的accessDecisionManagersecurityMetadataSource设置进去,至于authenticationManager因为我们已经声明了authenticationProvider并设置了userDetailService,所以这里可以省去。

既然扯到了FilterSecurityInterceptor这里再唠叨两句,Spring Security内部默认主要有三个实现,见下图:

Spring Security Interceptor

AspectJMethodSecurityInterceptorMethodSecurityInterceptorspring-security-core包内,FilterSecurityInterceptorspring-security-web包内,这也说明FilterSecurityInterceptor是web项目专用的。

在前面默认的实现中,Controller上加注解@PreAuthorize使用的是MethodSecurityInterceptor,但是在经过我们一番改造后,已经使用了FilterSecurityInterceptorMethodSecurityInterceptor已经没用到了。

当然你需要把Controller方法上的注解去掉:

  1. @RequestMapping(value = "/admin", method = RequestMethod.GET)
  2. // @PreAuthorize("hasAnyRole('admin')")
  3. public String helloAdmin() {
  4. return "admin";
  5. }
  6. @RequestMapping(value = "/user", method = RequestMethod.GET)
  7. // @PreAuthorize("hasAnyRole('admin','user')")
  8. public String helloUser() {
  9. return "user";
  10. }

测试

因为原来Controller中的helloUser方法注解上,hasAnyRole中有两个值,所在这里在授权的时候也需要授权两个,也就是数据库中admin要多加一条权限记录。

随后访问不同的url,发现跟基本一样,也是在我们预期当中。

最后

到这里一般情况下已经够用了,但是项目中往往会有很多“奇葩”的需求,下一章将讲解如何实现自定义的登录过滤器,实现各种客户端各种方式的登录,如区分手势密码、指纹登录及正常html外的.json.xml等方式的访问。

0 0
原创粉丝点击