Leverage custom annotations to line the interface of a Spring Security project

In the actual project using springsecurity as a security framework, we will encounter business requirements that need to release some interfaces so that they can be accessed anonymously. But whenever you need to release, you need to modify it in the security configuration class, which feels very inelegant. \

For example this:

picture

picture

So I want to make anonymous access to the interface by customizing an annotation. Before implementing the requirements, let's first understand the two ways of thinking about security.

The first is  configure(WebSecurity web)to configure the release in the method, like this:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}
复制代码

The second way is  configure(HttpSecurity http)to configure it in the method:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
 httpSecurity
    .authorizeRequests()
          .antMatchers("/hello").permitAll()
          .anyRequest().authenticated()
}
复制代码

The biggest difference between the two methods is that the first method does not follow the Spring Security filter chain, while the second method follows the Spring Security filter chain, and in the filter chain, the request is released. If you are learning Spring Boot, I recommend a free tutorial that has been serialized for many years and continues to be updated: blog.didispace.com/spring-boot…

When we use Spring Security, some resources can be released additionally using the first method without verification. For example, the static resources of the front-end page can be configured and released according to the first method.

For some resources to be released, the second method must be used, such as the login interface. As we all know, the login interface must also be exposed, and it can be accessed without login, but we cannot expose the login interface in the first way. The login request must go through the Spring Security filter chain, because in this In the process, there are other things to do. If you want to know the specific login process, you can Baidu by yourself.

After understanding the two release strategies of security, we start to implement

First create a custom annotation

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

Here is a description of @Target({ElementType.METHOD})my implementation, annotations can only be marked on @RequestMappingmethods with annotations. Specifically why the following implementation will understand after reading it.

Next, create a security configuration class SecurityConfig and inheritWebSecurityConfigurerAdapter

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    /**
     * @ description: 使用这种方式放行的接口,不走 Spring Security 过滤器链,
     *                无法通过 SecurityContextHolder 获取到登录用户信息的,
     *                因为它一开始没经过 SecurityContextPersistenceFilter 过滤器链。
     * @ dateTime: 2021/7/19 10:22
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        WebSecurity and = web.ignoring().and();
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        handlerMethods.forEach((info, method) -> {
            // 带IgnoreAuth注解的方法直接放行
            if (StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))) {
                // 根据请求类型做不同的处理
                info.getMethodsCondition().getMethods().forEach(requestMethod -> {
                    switch (requestMethod) {
                        case GET:
                            // getPatternsCondition得到请求url数组,遍历处理
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                // 放行
                                and.ignoring().antMatchers(HttpMethod.GET, pattern);
                            });
                            break;
                        case POST:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.POST, pattern);
                            });
                            break;
                        case DELETE:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
                            });
                            break;
                        case PUT:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.PUT, pattern);
                            });
                            break;
                        default:
                            break;
                    }
                });
            }
        });
    }
}
复制代码

Using the classes provided by Spring here RequestMappingHandlerMapping, we can requestMappingHandlerMapping.getHandlerMethods();get all the RequestMappingInfoinformation through.

The following is the source code part, but don't read it, you can deepen your understanding after reading it

Here is a brief description RequestMappingHandlerMappingof the workflow for easy understanding. By looking at the source code

picture

picture

The inheritance relationship is shown in the figure above.

AbstractHandlerMethodMappingimplements the InitializingBean interface

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}
复制代码

AbstractHandlerMethodMappingThe class is initialized by a afterPropertiesSetmethod callinitHandlerMethods

 public void afterPropertiesSet() {
        this.initHandlerMethods();
    }

    protected void initHandlerMethods() {
        String[] var1 = this.getCandidateBeanNames();
        int var2 = var1.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String beanName = var1[var3];
            if (!beanName.startsWith("scopedTarget.")) {
                this.processCandidateBean(beanName);
            }
        }

        this.handlerMethodsInitialized(this.getHandlerMethods());
    }
复制代码

Call the processCandidateBeanmethod again:

 protected void processCandidateBean(String beanName) {
        Class beanType = null;

        try {
            beanType = this.obtainApplicationContext().getType(beanName);
        } catch (Throwable var4) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Could not resolve type for bean '" + beanName + "'", var4);
            }
        }

        if (beanType != null && this.isHandler(beanType)) {
            this.detectHandlerMethods(beanName);
        }

    }
复制代码

By calling the isHandler method in the method is not a requestHandlermethod, you can see that the source code is passed RequestMapping, and the Controller annotation is used to judge.

protected boolean isHandler(Class<?> beanType) {
        return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class);
    }
复制代码

After the judgment is passed, the calling detectHandlerMethods method registers the handler in the cache of HandlerMethod. If you are learning Spring Boot, I recommend a free tutorial that has been serialized for many years and continues to be updated: blog.didispace.com/spring-boot…

protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = handler instanceof String ? this.obtainApplicationContext().getType((String)handler) : handler.getClass();
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (method) -> {
                try {
                    return this.getMappingForMethod(method, userType);
                } catch (Throwable var4) {
                    throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, var4);
                }
            });
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(this.formatMappings(userType, methods));
            }

            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                this.registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }

    }
复制代码

registerHandlerMethodThe handler is put into the private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap();map by the method.

The requestMappingHandlerMapping.getHandlerMethods()method is to get all HandlerMappings.

public Map<T, HandlerMethod> getHandlerMethods() {
    this.mappingRegistry.acquireReadLock();

    Map var1;
    try {
        var1 = Collections.unmodifiableMap(this.mappingRegistry.getMappings());
    } finally {
        this.mappingRegistry.releaseReadLock();
    }

    return var1;
}
复制代码

The last step is to traverse the map, determine whether it is IgnoreAuth.classannotated or not, and then release it for different request methods.

handlerMethods.forEach((info, method) -> {
            // 带IgnoreAuth注解的方法直接放行
            if (StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))) {
                // 根据请求类型做不同的处理
                info.getMethodsCondition().getMethods().forEach(requestMethod -> {
                    switch (requestMethod) {
                        case GET:
                            // getPatternsCondition得到请求url数组,遍历处理
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                // 放行
                                and.ignoring().antMatchers(HttpMethod.GET, pattern);
                            });
                            break;
                        case POST:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.POST, pattern);
                            });
                            break;
                        case DELETE:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.DELETE, pattern);
                            });
                            break;
                        case PUT:
                            info.getPatternsCondition().getPatterns().forEach(pattern -> {
                                and.ignoring().antMatchers(HttpMethod.PUT, pattern);
                            });
                            break;
                        default:
                            break;
                    }
                });
            }
        });
复制代码

Seeing this, you can understand my initial emphasis on marking @RequestMappingmethods with annotations. What I am using here is configure(WebSecurity web)the release method. It does not follow the security filter chain, and cannot  SecurityContextHolder obtain the login user information. This problem needs to be paid attention to.

Guess you like

Origin juejin.im/post/7086632592715284487