使用注解的形式对token进行验证
@[TOC](使用注解的形式对token进行验证)
# 前言
现在很多系统都是都用上了springboot、springcloud,系统也偏向分布式部署、管理,最早的用户令牌方案:session、cookie已经不能够满足系统的需求,使用一些特殊操作完成令牌的生成及校验会造成更多的服务器开销及客户端开销,为此许多项目都使用上了token。
token的原理即为将一串加密字符,寄存在请求头中,随着请求头往返与前后端,以校验该访问是否有权限。
如果每一个系统都去写一套token的生成和验证,是一个很繁琐的重复造轮子,让人有点难受。所以趁着空隙,生成了我使用注解就可以验证token的想法,并且也有一个提供token的方式,那我每个项目只需要一个接口、一个注解就能完成token的验证机制岂不是很方便?说干就干!
# 设计思路
首先需要一个注解,该注解可以在controller的类上生效,也可以在controller的接口方法上生效,即我可以指定某个方法需要验证,也可以指定某个类下的所有方法可以生效。
该注解进行了token的验证,验证是否过期,是否能够被解密,是否能够解析等,完成这一系列之后才可以继续进行该次请求,否则会返回相应的错误信息。流程图如下:

同时还要配置忽略地址,用于灵活配置token的验证位置。
验证token首先得有token,所以也要提供一个生成token的位置。
# 实现方案
## 注解及aop切面实现
首先,实现一个注解,当类或者方法加上这个注解就能让系统知道必须要检验token:
```java
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenVerification {
}
```
我把这个注解命名为TokenVerification。注解目标使用Type和METHOD,这样可以使注解在类及方法上生效。具体可查看源码:
```java
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,
/** Field declaration (includes enum constants) */
    FIELD,
/** Method declaration */
    METHOD,
/** Formal parameter declaration */
    PARAMETER,
/** Constructor declaration */
    CONSTRUCTOR,
/** Local variable declaration */
    LOCAL_VARIABLE,
/** Annotation type declaration */
    ANNOTATION_TYPE,
/** Package declaration */
    PACKAGE,
/**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,
/**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}
```
想让这个注解在接口方法进行前就进行生效,接入AOP切面,使用@before,将验证放在接口方法进行前:
```java
@Aspect
@Component
@Order(1)
@Slf4j
public class JwtAspect {
	
	/**
	 * token验证主入口
	 * @throws Throwable 异常抛出
	 */
	@Before(value = " @within(com.wyb.util.annotation.TokenVerification) || @annotation(com.wyb.util.annotation.TokenVerification)")
	public void verifyTokenForClass() throws Throwable{
		checkToken();
	}
```
这里使用了@within和@annotation来判定触发注解的范围,因为单独使用annotation的话在方法上添加注解不生效,故此添加@within使方法上的注解也可生效。
## token的加密生成及解析
token采用Jwt加密生成,加密算法使用了RSA算法,同时采用了64位序列化的公私密钥对token进行加解密,私钥进行加密,公钥进行解密。公私密钥需要统一生成,生成代码:
```java
/**
	 * 注意:下面都是生成密钥对相关方法,除特殊情况外无需调用
	 * main 方法用于生成密钥对,配置密钥时使用
	 * 已经生成好,考虑后期添加入配置中,目前写成final,公私密钥必须同时生成
	 * @param args args
	 * @throws NoSuchAlgorithmException 解析异常
	 */
	public static void main(String[] args) throws NoSuchAlgorithmException {
		KeyPair keyPair = generateKeyPair();
		String privateKey = base64Encode(keyPair.getPrivate());
		String publicKey = base64Encode(keyPair.getPublic());
		System.out.printf("Private Key: %s\nPublic Key: %s", privateKey, publicKey);
	}
/**
	 * 生成密钥对
	 *
	 * @return KeyPair
	 * @throws NoSuchAlgorithmException 解析异常
	 */
	private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
		// algorithm RSA
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_FAMILY_NAME);
		return keyPairGenerator.genKeyPair();
	}
/**
	 * 把私钥转化成base64字符串
	 *
	 * @param key 密钥
	 * @return 序列化后得密钥
	 */
	private static String base64Encode(PrivateKey key) {
		return new String(Base64Utils.encode(key.getEncoded()));
	}
/**
	 * 把公钥转化成base64字符串
	 * @param key 密钥
	 * @return 序列化后得密钥
	 */
	private static String base64Encode(PublicKey key) {
		return new String(Base64Utils.encode(key.getEncoded()));
	}
```
生成了公私密钥之后,利用jwt的token生成方法,生成包含用户或者应用信息的已经加密的token字符串,该字符串即为token令牌,请求接口时,带上该字符串即可。以下为token的生成,及解析代码:
token的生成:
```java
@Override
	public String generateToken(Object object, Date expireAt, String subject) {
		//建立jwt
		JwtBuilder jwtBuilder = Jwts.builder();
//设置jwt的body
		subject = subject != null ? subject: UUID.randomUUID().toString();
		Map<String,Object> objectMap = new HashMap<>();
		if(Objects.nonNull(object)){
			objectMap.put("data",object);
		}
		jwtBuilder.setClaims(objectMap).setSubject(subject);
//设置过期时间-时间设置要放在最后,否则设置了body之后会把时间盖掉,无法获取到时间
		if(Objects.isNull(expireAt)){
			log.info("没有设置过期时间,自动设置过期时间三十分钟");
			long currentTime = System.currentTimeMillis();
			currentTime += 30*60*1000;
			Date newDate = new Date(currentTime);
			jwtBuilder.setExpiration(newDate);
		}else{
			//设置过期时间
			jwtBuilder.setExpiration(expireAt);
		}
		//生成加密jwt
		return jwtBuilder.signWith(SignatureAlgorithm.RS256,privateKeyFromBase64()).compact();
	}
```
token的解析:
```java
	@Override
	public JwtBody signatureToken(String token) {
Jws<Claims> claimsJws = authToken(token);
return new JwtBody(claimsJws.getBody());
	}
/**
	 * 解析token
	 * @param token token
	 * @return claims
	 */
	private Jws<Claims> authToken(String token){
		if(null == token){
			log.info("本次请求token不存在");
			throw new TokenException(TokenReturnCode.RESOLVE_FAILED);
		}
		Jws<Claims> claimsJws;
		//解析token
		try {
			//如果传来的token不对,会对异常进行捕获
			claimsJws = Jwts.parser().setSigningKey(publicKeyFromBase64()).parseClaimsJws(token);
		}catch (JwtException e){
			log.error("token:\"{}\" 不正确",token);
			log.error(e.getMessage());
			throw new TokenException(TokenReturnCode.RESOLVE_FAILED);
		}
return claimsJws;
	}
```
以下是全篇的代码:
```java
	/**
	 * 加密方式
	 */
	private static final String ALGORITHM_FAMILY_NAME = "RSA";
/**
	 * 私钥,加密用
	 */
	private static final String PRIVATE_KEY = "生成的私钥";
/**
	 * 公钥,解密用
	 */
	private static final String PUBLIC_KEY = "生成的公钥";
@Override
	public JwtBody signatureToken(String token) {
Jws<Claims> claimsJws = authToken(token);
return new JwtBody(claimsJws.getBody());
	}
@Override
	public String generateToken(Object object, Date expireAt, String subject) {
		//建立jwt
		JwtBuilder jwtBuilder = Jwts.builder();
//设置jwt的body
		subject = subject != null ? subject: UUID.randomUUID().toString();
		Map<String,Object> objectMap = new HashMap<>();
		if(Objects.nonNull(object)){
			objectMap.put("data",object);
		}
		jwtBuilder.setClaims(objectMap).setSubject(subject);
//设置过期时间-时间设置要放在最后,否则设置了body之后会把时间盖掉,无法获取到时间
		if(Objects.isNull(expireAt)){
			log.info("没有设置过期时间,自动设置过期时间三十分钟");
			long currentTime = System.currentTimeMillis();
			currentTime += 30*60*1000;
			Date newDate = new Date(currentTime);
			jwtBuilder.setExpiration(newDate);
		}else{
			//设置过期时间
			jwtBuilder.setExpiration(expireAt);
		}
		//生成加密jwt
		return jwtBuilder.signWith(SignatureAlgorithm.RS256,privateKeyFromBase64()).compact();
	}
/**
	 * 解析token
	 * @param token token
	 * @return claims
	 */
	private Jws<Claims> authToken(String token){
		if(null == token){
			log.info("本次请求token不存在");
			throw new TokenException(TokenReturnCode.RESOLVE_FAILED);
		}
		Jws<Claims> claimsJws;
		//解析token
		try {
			//如果传来的token不对,会对异常进行捕获
			claimsJws = Jwts.parser().setSigningKey(publicKeyFromBase64()).parseClaimsJws(token);
		}catch (JwtException e){
			log.error("token:\"{}\" 不正确",token);
			log.error(e.getMessage());
			throw new TokenException(TokenReturnCode.RESOLVE_FAILED);
		}
return claimsJws;
	}
/**
	 * 公钥64位序列化
	 * @return PublicKey
	 */
	private static PublicKey publicKeyFromBase64() {
		try {
			byte[] keyBytes = Base64Utils.decodeFromString(PUBLIC_KEY);
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
			KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_FAMILY_NAME);
			return keyFactory.generatePublic(keySpec);
		} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
			throw new IllegalArgumentException(ex);
		}
	}
/**
	 * 私钥64位序列化
	 * @return PrivateKey
	 */
	private static PrivateKey privateKeyFromBase64() {
		try {
			byte[] keyBytes = Base64Utils.decodeFromString(PRIVATE_KEY);
			PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
			KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_FAMILY_NAME);
			return keyFactory.generatePrivate(keySpec);
		} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
			throw new IllegalArgumentException(ex);
		}
	}
```
因为在解密token后会产生claims,也就是token的body,里面包含了用户需要在token里传递的信息,所以解析后需要有一个方式传递给接口。这里采用了全局参数注入,使用了拦截器,将token解析方法产生的jwtBody注入全局,使接口可以自行获取:
全局参数handler:
```java
/**
 * 创建时间:2021/1/25 17:24
 * 实现拦截并注入参数
 * @author wyb
 */
public class JwtArgumentResolver implements HandlerMethodArgumentResolver {
private TokenUtils tokenUtils;
private String tokenHeader;
@Override
	public boolean supportsParameter(MethodParameter methodParameter) {
		return methodParameter.getParameterType().equals(JwtBody.class);
	}
@Override
	public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
		//将解析后的jwtBody作为全局参数,所有接口均可调用
		return tokenUtils.signatureToken(nativeWebRequest.getHeader(tokenHeader));
	}
/**
	 * 构造方法,因为使用注解注入的形式不生效
	 * @param tokenUtils token工具bean
	 * @param tokenHeader 请求头名字
	 */
	JwtArgumentResolver(TokenUtils tokenUtils, String tokenHeader){
		this.tokenHeader = tokenHeader;
		this.tokenUtils = tokenUtils;
	}
}
```
拦截器设置:
```java
/**
 * 创建时间:2021/1/26 10:55
 * 创建拦截器
 * @author wyb
 */
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
	private TokenUtils tokenUtils;
@Value("${token.header:token}")
	private String tokenHeader;
@Override
	protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		argumentResolvers.add(new JwtArgumentResolver(tokenUtils,tokenHeader));
	}
}
```
可能大家很奇怪为什么TokenUtils和配置参数需要通过构造函数传入解析器,因为在解析器中并不会自动注入bean,只能手动实例化,所以只能使用构造函数的形式在配置bean中注入。
这里注入的全局参数是jwtBody:
```java
@Data
public class JwtBody {
private String subject;
private Object object;
public JwtBody(Claims claims){
		if(Objects.nonNull(claims)){
			this.subject = claims.getSubject();
			Map map = new HashMap<>(claims);
			map.remove("sub");
			this.object = map.get("data");
		}
	}
}
```
用于存放token解析后的信息返回给开发者,让开发者专注于信息的匹配及获取。
到此,使用注解验证token的方案就实现完了。该工具需要在配置文件中加上一下支持:
```yaml
token:
	header: token
	ignore: /test,/example,/aaa
```
header为token在请求头中的名字,如果不配置则默认为token,ignore则为验证忽略地址,配置后请求该地址后可忽略验证直接访问。
# 总结
在方案实现过程中也遇到二比较多的坑,这里列出总结一下:
1.注解虽然可以添加target注解规定它可以使用的范围,但是进入切面后,不一定会在这个范围内检测到该注解,单纯的使用exclution和annotation不能达到我们想要的效果。使用within注解指定注解,所有使用该注解的方法、包、类均可以检测到,但是不能在切面方法使用参数。
2.全局参数的注入问题,网上许多资料都写明加载解析器就可以,但是因为版本不同,所以加入的方式不通,解析器一定要手动加入:
```java
@Override
	protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		argumentResolvers.add(new JwtArgumentResolver(tokenUtils,tokenHeader));
	}
```
这里是在webmvc的拦截器中手动加入,如果你不加,解析器就不能生效。
3.同时为了配备项目,我还自定义了异常抛出,用以抛出token解析中的异常,具体的方案不在本文的主题中,则不列举了。
代码地址:[utils](https://github.com/wangyb-may/utils)
使用注解的形式对token进行验证的更多相关文章
- .NET CORE TOKEN 权限验证
		原文:.NET CORE TOKEN 权限验证 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/u012601647/article/details/ ... 
- .NET WebAPI 用ActionFilterAttribute实现token令牌验证与对Action的权限控制
		项目背景是一个社区类的APP(求轻吐...),博主主要负责后台业务及接口.以前没玩过webAPI,但是领导要求必须用这个(具体原因鬼知道),只好硬着头皮上了. 最近刚做完权限这一块,分享出来给大家.欢 ... 
- WebAPI 用ActionFilterAttribute实现token令牌验证与对Action的权限控制
		.NET WebAPI 用ActionFilterAttribute实现token令牌验证与对Action的权限控制 项目背景是一个社区类的APP(求轻吐...),博主主要负责后台业务及接口.以前没玩 ... 
- Spring Security框架下Restful Token的验证方案
		项目使用Restful的规范,权限内容的访问,考虑使用Token验证的权限解决方案. 验证方案(简要概括): 首先,用户需要登陆,成功登陆后返回一个Token串: 然后用户访问有权限的内容时需要上传T ... 
- Python itsdangerous 生成token和验证token
		代码如下 class AuthToken(object): # 用于处理token信息流程: # 1.更加给定的用户信息生成token # 2.保存生成的token,以便于后面验证 # 3.对用户请求 ... 
- token登录验证机制
		一张图解释 token登录验证机制 
- 使用flask搭建微信公众号:完成token的验证
		上一篇文章讨论了官方给的例子验证token失败的解决方法:微信公众号token验证失败 想了一下,还是决定不适用web.py这个框架.因为搜了一下他的中文文档不多,学起来可能会有点麻烦.而且看着他没有 ... 
- 基于JWT的Token身份验证
		 身份验证,是指通过一定的手段,完成对用户身份的确认.为了及时的识别发送请求的用户身份,我们调研了常见的几种认证方式,cookie.session和token. 1.Cookie  cookie是 ... 
- Springboot token令牌验证解决方案 在SpringBoot实现基于Token的用户身份验证
		1.首先了解一下Token 1.token也称作令牌,由uid+time+sign[+固定参数]组成: uid: 用户唯一身份标识 time: 当前时间的时间戳 sign: 签名, 使用 hash/e ... 
随机推荐
- 它听键盘声就知道你敲的是什么——GitHub 热点速览 Vol.51
			作者:HelloGitHub-小鱼干 本以为本周的 GitHub 和十二月一样平平无奇就那么度过了,结果 BackgroundMattingV2 重新刷新了本人的认知,还能这种骚操作在线实时抠视频去背 ... 
- 使用form表单上传文件
			在使用form表单上传文件时候,input[type='file']是必然会用的,其中有一些小坑需要避免. 1.form的 enctype="multipart/form-data" ... 
- CentOS7部署GeoServer
			CentOS7部署GeoServer 一.安装JDK81.下载jdk1.8 wget http://download.oracle.com/otn-pub/java/jdk/8u181-b13/96a ... 
- Mac苹果电脑单片机开发
			1.安装虚拟机 可以阅读往期文章:Mac苹果电脑安装虚拟机 2.在虚拟机上安装CH340驱动,keil4,PZ-ISP, 下载 CH340驱动安装 下载keil4破解及汉化 下载普中科技烧录软件 
- 前端面试题归类-HTML2
			一. SGML . HTML .XML 和 XHTML 的区别? SGML 是标准通用标记语言,是一种定义电子文档结构和描述其内容的国际标准语言,是所有电子文档标记语言的起源. HTML 是超文本标记 ... 
- JVM调试说明
			-XX:+<option>:表示开启option选项 -XX:-<option>:表示关闭option选项 -XX:<option>=<value>:表 ... 
- Putty或MobaXTerm无法连接VMware虚拟机 报Network error: Connection timed out的解决方案
			当出现无法连接的问题时, 我们要先对可能出现的问题进行梳理, 然后进行排查, 以下我先整理一些可能出现问题的地方: 1. 通过 ping 查看两台终端是否均有联网 windows下通过控制台 cmd ... 
- 使用Lists.partition切分性能优化
			项目实战 影拓邦电影同步中,使用Lists.partition按500条长度进行切分,来实现es的同步. 切分的List为 使用介绍及示例 将list集合按指定长度进行切分,返回新的List<L ... 
- openstack octavia的实现与分析(一)openstack负载均衡的现状与发展以及lvs,Nginx,Haproxy三种负载均衡机制的基本架构和对比
			[负载均衡] 大量用户发起请求的情况下,服务器负载过高,导致部分请求无法被响应或者及时响应. 负载均衡根据一定的算法将请求分发到不同的后端,保证所有的请求都可以被正常的下发并返回. [主流实现-LVS ... 
- HP(惠普)服务器 修复 Intelligent Provisioning(摘录)
			摘录自:https://www.xxshell.com/1219.html 我们在给惠普服务器安装操作系统和配置RAID最常用的就是通过F10(Intelligent Provisioning)进行, ... 
