之前有一篇博文写到如何用自定义JWT Token来保护你的项目,于是我很快在所有项目运用上了。
而其中,我们知道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
都环绕一层。