前言
现在很多系统都是都用上了springboot、springcloud,系统也偏向分布式部署、管理,最早的用户令牌方案:session、cookie已经不能够满足系统的需求,使用一些特殊操作完成令牌的生成及校验会造成更多的服务器开销及客户端开销,为此许多项目都使用上了token。
token的原理即为将一串加密字符,寄存在请求头中,随着请求头往返与前后端,以校验该访问是否有权限。
如果每一个系统都去写一套token的生成和验证,是一个很繁琐的重复造轮子,让人有点难受。所以趁着空隙,生成了我使用注解就可以验证token的想法,并且也有一个提供token的方式,那我每个项目只需要一个接口、一个注解就能完成token的验证机制岂不是很方便?说干就干!
设计思路
首先需要一个注解,该注解可以在controller的类上生效,也可以在controller的接口方法上生效,即我可以指定某个方法需要验证,也可以指定某个类下的所有方法可以生效。
该注解进行了token的验证,验证是否过期,是否能够被解密,是否能够解析等,完成这一系列之后才可以继续进行该次请求,否则会返回相应的错误信息。流程图如下:
流程
同时还要配置忽略地址,用于灵活配置token的验证位置。
验证token首先得有token,所以也要提供一个生成token的位置。
实现方案
注解及aop切面实现
首先,实现一个注解,当类或者方法加上这个注解就能让系统知道必须要检验token:
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenVerification {
}
我把这个注解命名为TokenVerification。注解目标使用Type和METHOD,这样可以使注解在类及方法上生效。具体可查看源码:
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,将验证放在接口方法进行前:
@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进行加解密,私钥进行加密,公钥进行解密。公私密钥需要统一生成,生成代码:
/**
* 注意:下面都是生成密钥对相关方法,除特殊情况外无需调用
* 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的生成:
@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的解析:
@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;
}
以下是全篇的代码:
/**
* 加密方式
*/
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不存在");
thr