要说清token和jwt之前一定会谈起cookie和session,因为Http协议本身是一个无状态协议,每次请求都是单独的。但是当web等项目开发环境中,当前请求可能需要与其他http请求的上下文进行关联,才能完成业务逻辑,所以cookie和session就诞生了。
基于RFC6265规范(Http state Management Mechanism),允许服务端生成Cookie信息在响应中通过Set-Cookie头部告知客户端(并且允许多个Set-Cookie头部传递值信息),客户端得到Cookie后,后续请求中都会自动将Cookie头部信息携带到请求中,这样服务端就可以根据Cookie中的sessio-id获取当前请求的上下文信息,如用户登录状态信息等,以执行正常的业务逻辑。当分布式环境中使用session时,如果服务端的节点数较少时可以使用复制的方式同步各个服务器(如:Tomcat)的session会话信息;如果数据节点较多时需要使用集中式session管理(如:存放redis中)。
Cookie协议在设计上的问题:
基于OAuth2.0协议(一) - 授权码许可流程得知,token可以是一个唯一性、不连续性、不可猜性的字符串(可以是一个加密的字符串、也可以是普通的uuid等),在服务端需要与资源权限进行绑定。并且基本上系统开发时都会基于OAuth2.0规范,使用Authorization Request Header Field【授权请求头部字段】的请求方式设计token,如
Authorization: Bearer b1a64d5c-5e0c-4a70-9711-7af6568a61fb
jwt(JSON Web Token)基于[RFC7519]规范 ,一种紧凑的、自包含的结构化(json)封装方式来生成token。jwt的声明主要用于身份提供者(授权服务)和服务提供者(受保护资源)之前传递被认证的身份信息。jwt的结构为head(头部)、payload(数据体)、signature(签名三部分构成),如下(可以使用https://jwt.io/进行在线解析jwt信息):header.payload.signature
head头部信息主要是两部分信息:
playload存放有效的服务相关的字段信息(标准的声明但是并不强制使用,更多可以参考RFC7519规范):
signture 表示对 JWT 信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。
谈完规范,就是将规范进行落地,而jjwt提供了java的jwt的创建和验证的类库。需要注意jjwt高于0.9.1的版本,其自带的decode与0.9.1及以下版本可能有不兼容的情况,并且支持的加密算法有:
io.jsonwebtoken
jjwt-api
0.11.2
io.jsonwebtoken
jjwt-impl
0.11.2
runtime
io.jsonwebtoken
jjwt-jackson
0.11.2
runtime
// 创建token
String jwt = Jwts.builder()
.setSubject(String.valueOf(appTokenRequest.getUserId()))
.claim(JwtInfoKeyConstant.USER_ID_KEY, appTokenRequest.getUserId())
.claim(JwtInfoKeyConstant.USER_NAME_KEY, appTokenRequest.getUserName())
.setExpiration(expiredDate)
.signWith(jwtPrivateKey, SignatureAlgorithm.RS256)
.compact();
// 验证token,并解析token信息
@Override
protected AccessToken parserToken(String token) throws Exception {
Jws claimsJws;
try {
claimsJws = Jwts.parserBuilder().setSigningKey(jwtPublicKey).build().parseClaimsJws(token);
} catch (ExpiredJwtException expiredJwtException) {
throw new BusinessException(ErrorCodeEnum.AUTHORIZED_EXPIRE.getCode(), ErrorCodeEnum.AUTHORIZED_EXPIRE.getMessage());
} catch (Exception e) {
throw new BusinessException(ErrorCodeEnum.INVALID_TOKEN.getCode(), ErrorCodeEnum.INVALID_TOKEN.getMessage());
}
Claims body = claimsJws.getBody();
Date expiration = claimsJws.getBody().getExpiration();
if (expiration.before(new Date())) {
throw new BusinessException(ErrorCodeEnum.AUTHORIZED_EXPIRE.getCode(), ErrorCodeEnum.AUTHORIZED_EXPIRE.getMessage());
}
Long userId = body.get(JwtInfoKeyConstant.USER_ID_KEY, Long.class);
String userName = body.get(JwtInfoKeyConstant.USER_NAME_KEY, String.class);
return new UserNameAccessToken(userId, userName, token);
}
其中使用的RSA公钥和私钥可以使用 jdk的Tool工具包生成,也可以使用工具类生成,将生成的公钥和私钥文件存放在spring boot的resource目录下,并且引入项目中
public class RsaKeyGen {
public static void main(String[] args) throws Exception{
genKeyPair();
}
public static void genKeyPair() throws Exception {
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,密钥大小为96-1024位
keyPairGen.initialize(2048,new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥
String publicKeyString = Base64Utils.encodeToString(publicKey.getEncoded());
// 得到私钥字符串
String privateKeyString = Base64Utils.encodeToString((privateKey.getEncoded()));
File privateKeyFile = new File("E:\\test\\jwt-private-key.der");
writeFile(privateKeyFile,privateKey.getEncoded());
File publicKeyFile = new File("E:\\test\\jwt-public-key.der");
writeFile(publicKeyFile,publicKey.getEncoded());
}
private static void writeFile(File file,byte[] content) throws Exception{
OutputStream out = new FileOutputStream(file);
out.write(content);
if (out != null) out.close();
}
}