如何利用自定义注解放行 Spring Security 项目的接口

x33g5p2x  于2022-04-12 转载在 Spring  
字(6.8k)|赞(0)|评价(0)|浏览(537)

在实际项目中使用到了springsecurity作为安全框架,我们会遇到需要放行一些接口,使其能匿名访问的业务需求。但是每当需要当需要放行时,都需要在security的配置类中进行修改,感觉非常的不优雅。

例如这样:


图片

所以想通过自定义一个注解,来进行接口匿名访问。在实现需求前,我们先了解一下security的两种方行思路。

第一种就是在 configure(WebSecurity web)方法中配置放行,像下面这样:

  1. @Override
  2. public void configure(WebSecurity web) throws Exception {
  3.     web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
  4. }

第二种方式是在 configure(HttpSecurity http)方法中进行配置:

  1. @Override
  2. protected void configure(HttpSecurity httpSecurity) throws Exception
  3. {
  4.  httpSecurity
  5.     .authorizeRequests()
  6.           .antMatchers("/hello").permitAll()
  7.           .anyRequest().authenticated()
  8. }

两种方式最大的区别在于,第一种方式是不走 Spring Security 过滤器链,而第二种方式走 Spring Security 过滤器链,在过滤器链中,给请求放行。如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

在我们使用 Spring Security 的时候,有的资源可以使用第一种方式额外放行,不需要验证,例如前端页面的静态资源,就可以按照第一种方式配置放行。

有的资源放行,则必须使用第二种方式,例如登录接口。大家知道,登录接口也是必须要暴露出来的,不需要登录就能访问到的,但是我们却不能将登录接口用第一种方式暴露出来,登录请求必须要走 Spring Security 过滤器链,因为在这个过程中,还有其他事情要做,具体的登录流程想了解的可以自行百度。

了解完了security的两种放行策略后,我们开始实现

首先创建一个自定义注解

  1. @Target({ElementType.METHOD}) //注解放置的目标位置,METHOD是可注解在方法级别上
  2. @Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
  3. @Documented //生成文档
  4. public @interface IgnoreAuth {
  5. }

这里说明一下,@Target({ElementType.METHOD})我的实现方式,注解只能标记在带有@RequestMapping注解的方法上。具体为什么下面的实现方式看完就懂了。

接下来创建一个security的配置类SecurityConfig并继承WebSecurityConfigurerAdapter

  1. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter
  3. {
  4.     @Autowired
  5.     private RequestMappingHandlerMapping requestMappingHandlerMapping;
  6.     /**
  7.      * @ description: 使用这种方式放行的接口,不走 Spring Security 过滤器链,
  8.      *                无法通过 SecurityContextHolder 获取到登录用户信息的,
  9.      *                因为它一开始没经过 SecurityContextPersistenceFilter 过滤器链。
  10.      * @ dateTime: 2021/7/19 10:22
  11.      */
  12.     @Override
  13.     public void configure(WebSecurity web) throws Exception {
  14.         WebSecurity and = web.ignoring().and();
  15.         Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
  16.         handlerMethods.forEach((info, method) -> {
  17.             // 带IgnoreAuth注解的方法直接放行
  18.             if (StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))) {
  19.                 // 根据请求类型做不同的处理
  20.                 info.getMethodsCondition().getMethods().forEach(requestMethod -> {
  21.                     switch (requestMethod) {
  22.                         case GET:
  23.                             // getPatternsCondition得到请求url数组,遍历处理
  24.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  25.                                 // 放行
  26.                                 and.ignoring().antMatchers(HttpMethod.GET, pattern);
  27.                             });
  28.                             break;
  29.                         case POST:
  30.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  31.                                 and.ignoring().antMatchers(HttpMethod.POST, pattern);
  32.                             });
  33.                             break;
  34.                         case DELETE:
  35.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  36.                                 and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
  37.                             });
  38.                             break;
  39.                         case PUT:
  40.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  41.                                 and.ignoring().antMatchers(HttpMethod.PUT, pattern);
  42.                             });
  43.                             break;
  44.                         default:
  45.                             break;
  46.                     }
  47.                 });
  48.             }
  49.         });
  50.     }
  51. }

在这里使用Spring为我们提供的RequestMappingHandlerMapping类,我们可以通过requestMappingHandlerMapping.getHandlerMethods();获取到所有的RequestMappingInfo信息。如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

以下是源码部分,可不看,看了可以加深理解

这里简单说一下RequestMappingHandlerMapping的工作流程,便于理解。我们通过翻看源码


图片

继承关系如上图所示。

AbstractHandlerMethodMapping实现了InitializingBean 接口

  1. public interface InitializingBean {
  2.     void afterPropertiesSet() throws Exception;
  3. }

AbstractHandlerMethodMapping类中通过afterPropertiesSet方法调用initHandlerMethods进行初始化

  1. public void afterPropertiesSet() {
  2.         this.initHandlerMethods();
  3.     }
  4.     protected void initHandlerMethods() {
  5.         String[] var1 = this.getCandidateBeanNames();
  6.         int var2 = var1.length;
  7.         for(int var3 = 0; var3 < var2; ++var3) {
  8.             String beanName = var1[var3];
  9.             if (!beanName.startsWith("scopedTarget.")) {
  10.                 this.processCandidateBean(beanName);
  11.             }
  12.         }
  13.         this.handlerMethodsInitialized(this.getHandlerMethods());
  14.     }

再调用processCandidateBean方法:

  1. protected void processCandidateBean(String beanName) {
  2.         Class beanType = null;
  3.         try {
  4.             beanType = this.obtainApplicationContext().getType(beanName);
  5.         } catch (Throwable var4) {
  6.             if (this.logger.isTraceEnabled()) {
  7.                 this.logger.trace("Could not resolve type for bean '" + beanName + "'", var4);
  8.             }
  9.         }
  10.         if (beanType != null && this.isHandler(beanType)) {
  11.             this.detectHandlerMethods(beanName);
  12.         }
  13.     }

通过调用方法中的isHandler方法是不是requestHandler方法,可以看到源码是通过RequestMapping,Controller 注解进行判断的。

  1. protected boolean isHandler(Class<?> beanType) {
  2.         return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class);
  3.     }

判断通过后,调用detectHandlerMethods 方法将handler注册到HandlerMethod的缓存中。如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

  1. protected void detectHandlerMethods(Object handler) {
  2.         Class<?> handlerType = handler instanceof String ? this.obtainApplicationContext().getType((String)handler) : handler.getClass();
  3.         if (handlerType != null) {
  4.             Class<?> userType = ClassUtils.getUserClass(handlerType);
  5.             Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (method) -> {
  6.                 try {
  7.                     return this.getMappingForMethod(method, userType);
  8.                 } catch (Throwable var4) {
  9.                     throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, var4);
  10.                 }
  11.             });
  12.             if (this.logger.isTraceEnabled()) {
  13.                 this.logger.trace(this.formatMappings(userType, methods));
  14.             }
  15.             methods.forEach((method, mapping) -> {
  16.                 Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
  17.                 this.registerHandlerMethod(handler, invocableMethod, mapping);
  18.             });
  19.         }
  20.     }

通过registerHandlerMethod方法将handler放到private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap();map中。

requestMappingHandlerMapping.getHandlerMethods()方法就是获取所有的HandlerMapping。

  1. public Map<T, HandlerMethod> getHandlerMethods() {
  2.     this.mappingRegistry.acquireReadLock();
  3.     Map var1;
  4.     try {
  5.         var1 = Collections.unmodifiableMap(this.mappingRegistry.getMappings());
  6.     } finally {
  7.         this.mappingRegistry.releaseReadLock();
  8.     }
  9.     return var1;
  10. }

最后就是对map进行遍历,判断是否带有IgnoreAuth.class注解,然后针对不同的请求方式进行放行。

  1. handlerMethods.forEach((info, method) -> {
  2.             // 带IgnoreAuth注解的方法直接放行
  3.             if (StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))) {
  4.                 // 根据请求类型做不同的处理
  5.                 info.getMethodsCondition().getMethods().forEach(requestMethod -> {
  6.                     switch (requestMethod) {
  7.                         case GET:
  8.                             // getPatternsCondition得到请求url数组,遍历处理
  9.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  10.                                 // 放行
  11.                                 and.ignoring().antMatchers(HttpMethod.GET, pattern);
  12.                             });
  13.                             break;
  14.                         case POST:
  15.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  16.                                 and.ignoring().antMatchers(HttpMethod.POST, pattern);
  17.                             });
  18.                             break;
  19.                         case DELETE:
  20.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  21.                                 and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
  22.                             });
  23.                             break;
  24.                         case PUT:
  25.                             info.getPatternsCondition().getPatterns().forEach(pattern -> {
  26.                                 and.ignoring().antMatchers(HttpMethod.PUT, pattern);
  27.                             });
  28.                             break;
  29.                         default:
  30.                             break;
  31.                     }
  32.                 });
  33.             }
  34.         });

看到这里就能理解我最开始的强调的需标记在带有@RequestMapping注解的方法上。我这里使用到的是configure(WebSecurity web)的放行方式。它是不走security的过滤链,是无法通过 SecurityContextHolder 获取到登录用户信息的,这点问题是需要注意的。

来源:https://blog.csdn.net/weixin_45089791/article/details/118890274

相关文章