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

    1. .antMatchers("/captchaImage").anonymous()

    所以想通过自定义一个注解,来进行接口匿名访问。在实现需求前,先了解一下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 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信息。
    以下是源码部分,可不看,看了可以加深理解
    这里简单说一下RequestMappingHandlerMapping的工作流程,便于理解。通过翻看源码
    使用自定义注解放行 Spring Security 项目的接口 - 图1
    继承关系如上图所示。
    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的缓存中。

    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 获取到登录用户信息的,这点问题是需要注意的。