SpringMVC 参数注入的两种方式

之前有一篇博文写到如何用自定义JWT Token来保护你的项目,于是我很快在所有项目运用上了。

Spring Security 初探 & 自定义身份认证

而其中,我们知道JWT Token是可以储存Claims的,作为这个令牌的负荷。我在上面放入了一个储存用户部分基本信息例如用户ID。但是怎么解析和接受这些Claims,上篇文章没有解释。

其实我想到的思路也很简单,参照以前SpringMVC是如何处理Session的。获得Session中储存的属性可以通过一个@ModelAttribute注解获得。于是我想到,我的Token虽然无状态,但是它和SESSION一样储存了一些属性(Claims)。只不过Session的内容由后端储存,前端传回的是一个SESSIONID,而Token的一切信息都在token里面,后端只负责解析。

我想到的做法是模仿Session,我自定义了一个注解@TokenUser,然后拦截这个注解并注入这个参数。我实现过的做法一共有两种。

# SecurityFilter

这个Filter是在前一篇文章提到的,无需修改。不过我们要利用到里面其中一条代码。

  SecurityContextHolder.getContext().setAuthentication(authentication);

我们在一个“安全上下文”中,填入了Token解析后的封装实体。然后在SecurityConfig中,关闭了认证后清空上下文的配置,因为我们需要在处理一个请求的时候,获取这个包含了用户信息的封装实体。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth
                .eraseCredentials(false);
    }

# 定义注解@TokenUser

package team.a9043.yiluwiki.security.tokenuser;

import java.lang.annotation.*;

/**
 * 获得Token中的 SisUser
 *
 * @author a9043
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenUser {
    boolean required() default true;
}

我们新建一个TokenUser的注解,其中一样有required属性。

# 第一种——SpringAOP

新建一个Aspect,为了可行性,我选择了环绕通知@Around的方式。不过该代码我忽略了require属性,因为在某一个项目中不需要使用,于是我删除了,懒得写回去了。

package team.a9043.project_name.security.tokenuser;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import team.a9043.project_name.pojo.User;
import team.a9043.project_name.security.entity.AuthenticationToken;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.IntStream;

@Component
@Aspect
@Order()
public class TokenUserAspect {
    @Around(value = "execution(" +
        "* team.a9043.project_name.controller.*.*(..,@TokenUser (*), ..))",
        argNames = "pjp")
    public Object getUser(ProceedingJoinPoint pjp) throws Throwable {
        // 获得方法签名
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        // 获得方法
        Method method = signature.getMethod();
        // 获得参数拒接
        Annotation[][] methodAnnotations = method.getParameterAnnotations();
        // 获得参数
        Object[] args = pjp.getArgs();
        // 获得参数类型
        Class[] argTypes = Arrays.stream(args).map(arg -> arg != null ?
            arg.getClass() : null).toArray(Class[]::new);
        // 临时变量,TokenUser的参数索引位置
        int userArgsIdx;

        // 遍历参数,寻找类型等于所需要的用户实体类型和注解类型的参数索引
        userArgsIdx = IntStream.range(0, args.length)
            .filter(i -> argTypes[i].equals(User.class))
            .filter(i -> Arrays.stream(methodAnnotations[i]).anyMatch(annotation -> annotation.annotationType().equals(TokenUser.class)))
            .findFirst()
            .orElse(-1);

        // 注入参数,从之前的放入的“安全上下文”中获取所需要的实体并注入
        args[userArgsIdx] =
            ((AuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getSisUser();
        // 继续方法
        return pjp.proceed(args);
    }
}

这种方式使用Spring环绕通知,在方法进行之前进行参数检查并注入,不过代码有些少复杂和繁琐。后来再学习SpringMVC的时候发现有更好用的HandlerMethodArgumentResolver接口。

# HandlerMethodArgumentResolver

这种方法比较简单,只需要实现这个接口并注册到SpringMVC Config就可以了。

# TokenUserMethodArgumentResolver

我用类TokenUserMethodArgumentResolver实现该接口

package team.a9043.yiluwiki.security.tokenuser;

import org.springframework.core.MethodParameter;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import team.a9043.yiluwiki.security.entity.YwAuthenticationToken;

public class TokenUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(TokenUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 获取授权实体
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 检验未授权,和required属性的处理
        if (authentication instanceof AnonymousAuthenticationToken) {
            TokenUser tokenUser = parameter.getMethodAnnotation(TokenUser.class);
            if (null != tokenUser && tokenUser.required())
                throw new TokenUserException("No user found but required");
            return null;
        }
        // 返回用户实体
        return ((YwAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getYwUser();
    }
}

该接口有两个方法。

一个是supportParameter,如果该方法返回false,那么后面的参数注入就不会进行。在这里我选择检验是否含有这个参数注解TokenUser.class

另一个是resolveArgument,描述了注入参数的方法,返回值就是注入的参数。那么很简单了,我们将以前的AOP代码改造一下就可以了,如上。

# WebMvcConfig

package team.a9043.yiluwiki.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import team.a9043.yiluwiki.security.tokenuser.TokenUserMethodArgumentResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new TokenUserMethodArgumentResolver());
    }
}

如上,新建一个Config类并继承WebMvcConfigurer,复写其addArgumentResolvers方法,添加自己的参数解析器就完成了。

# 总结

至于哪种方法性能好,我没有做测试。但是从开发上,我是推荐时候第二种方法的,因为更简单更友好。

我认为理论上第二种方法性能会好很多,因为第二种使用HandlerMethodArgumentResolverComposite进行统一参数处理。 而第一种使用AOP对所有Controller都环绕一层。