前言
JWT(JSON Web Tokens)是目前比较流行的跨域认证解决方案,遵循RFC 7519 标准,我们可以使用JWT在用户和服务器之间传递安全可靠的信息。
我们可以在 JWT的官网 了解到更多关于JWT的信息。
正文
构成
JWT主要由头部、载荷与签名三部分构成,三部分的信息用英文逗号“.”分割。
一个完整的JWT如下:
它的头部部分(Header)为红色标注部分,载荷(Payload)为蓝色部分,签名(Signature)为橘色部分。
它们均使用Base64进行编码,我们将上述Base64解码后可以看到如下:
Header部分Json:
1 2 3 4
| { "typ": "JWT", "alg": "HS256" }
|
这儿Header声明了使用的加密算法(HS256)及token类型(JWT)。
Payload部分Json如下:
1 2 3 4 5 6 7
| { "phone": "1888888888", "sessionId": "111111111111", "exp": 1548052800, "userId": "1433223", "platform": "APP" }
|
其中除exp字段其它都是我自定义的字段。JWT 规定了7个官方字段,供我们选用,如下:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
PS:可以看到信息仅仅使用了Base64进行了一下编码,非加密,如果我们想是信息更安全可以对JWT生成的token进行可逆加密。
Signature部分:
会对上面两部分进行签名,通常使用RSA,Hmac或者ECDSA等签名方式。用于防止上面两部分的数据遭到篡改。
源码
要想在Java项目里使用JWT,需要引入以下依赖。
1 2 3 4 5 6
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.3.0</version> </dependency>
|
我们来简单的看一下它的源码部分。
先看一下包的结构:
algorithm:各种签名的包。
exceptions:自定义异常类的包。
interfaces和impl:JWT的接口和实现类包。
在algorithm包里我们可以看到我们刚才描述的几种签名算法(RSA,HMAC,ECDSA)。
在JWTCreator类中,我们可以看到Signature部分是通过Header,Payload经过签名算法得来的。
同时载荷Payload里JWT规定的几个可使用字段也能看到。
生成签名,放入Header信息及Payload信息,使用指定签名算法生成JWT token。
JWTDecoder类为解密token的类,可以看到它的处理方法,获取headerJson和payloadJson,还是比较好理解的。
应用
我们使用JWT生成token并使用。
我们知道,前后端使用token进行交互,服务器端可以不用保存session状态,减轻压力。
我们定义一个Vo,用于存放一些用户数据,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Data @Accessors(chain = true) public class UserVo {
private String phone;
private String userId;
private String sessionId;
private long expiresAt;
private String platform; }
|
可以认为这些为公共部分,用户登录后应该携带这些信息。
这样我们就可以使用JWT在该用户登录后生成一个有效token,为保证信息安全,我们可以对生成的token进行加密,如下:
我们使用AES算法对token进行加解密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| @Slf4j public class AESUtils { private final static String ENCODING_UTF8 = "utf-8"; private static final String KEY_ALGORITHM = "AES"; private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; private String secretKeySeed = null; private String ivParameterSeed = null; public AESUtils(String secretKeySeed,String ivParameterSeed) { this.secretKeySeed = secretKeySeed; if (ivParameterSeed.length() != 16) { throw new RuntimeException("iv向量长度必须为16"); } this.ivParameterSeed=ivParameterSeed; } public AESUtils(String secretKeySeed) { if (secretKeySeed.length() != 16) { throw new RuntimeException("iv向量长度必须为16"); } this.secretKeySeed = secretKeySeed; this.ivParameterSeed=secretKeySeed; }
public String aesEncrypt(String content) throws Exception { byte[] encryptStr = encrypt(content, secretKeySeed,ivParameterSeed); return Base64.encodeBase64String(encryptStr); }
public String aesDecrypt(String encryptStr) throws Exception { byte[] decodeBase64 = Base64.decodeBase64(encryptStr); return new String(decrypt(decodeBase64, secretKeySeed,ivParameterSeed),ENCODING_UTF8); }
private static SecretKeySpec getSecretKeySpec(final String secretKeySeed) { try { return new SecretKeySpec(secretKeySeed.getBytes(), "AES"); } catch (Exception ex) { log.error("生成加密密钥异常",ex); } return null; }
private static IvParameterSpec getIvParameterSpec(final String ivParameterSeed) { return new IvParameterSpec(ivParameterSeed.getBytes()); }
private static byte[] encrypt(String content, String secretKeySeed,String ivParameterSeed) throws Exception { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, getSecretKeySpec(secretKeySeed),getIvParameterSpec(ivParameterSeed)); return cipher.doFinal(content.getBytes(ENCODING_UTF8)); }
private static byte[] decrypt(byte[] content, String secretKeySeed,String ivParameterSeed) throws Exception { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, getSecretKeySpec(secretKeySeed),getIvParameterSpec(ivParameterSeed)); return cipher.doFinal(content); } }
|
同时,根据刚才我们的说明创建一个JWT帮助类用于生成token,大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| @Slf4j public class JWTUtils { private String JWT_HEADER = ""; private Algorithm algorithm; private long expireTimeMillis=2*60*60*1000; private AtomicBoolean initState = new AtomicBoolean(false);
private static AESUtils aesUtils=null;
private static class SingletonHolder { static final JWTUtils instance = new JWTUtils(); }
public static JWTUtils getInstance() { return JWTUtils.SingletonHolder.instance; }
public void init(String jwtSecretKey,long jwtExpireTimeSeconds,String aesSecretKeySeed,String aesIvParameterSeed) throws UnsupportedEncodingException { if (initState.compareAndSet(false, true)) { this.algorithm = Algorithm.HMAC256(jwtSecretKey); if(jwtExpireTimeSeconds>0) { this.expireTimeMillis = jwtExpireTimeSeconds * 1000; } this.JWT_HEADER = StringUtils.substringBefore(JWT.create().sign(Algorithm.HMAC256(jwtSecretKey)), ".")+"."; aesUtils = new AESUtils(aesSecretKeySeed, aesIvParameterSeed); }else{ log.error("重复初始化jwt"); } }
public String encodeJWT(UserVo userVo) { return encodeJWT(userVo,this.expireTimeMillis); }
public String encodeJWT(UserVo userVo,long expireTimeMillis){ String token = JWT.create() .withExpiresAt(new Date(System.currentTimeMillis()+expireTimeMillis)) .withClaim("phone",userVo.getPhone()) .withClaim("userId",userVo.getUserId()) .withClaim("sessionId",userVo.getSessionId()) .withClaim("platform",userVo.getPlatform()) .sign(algorithm); System.out.println("token----> "+token); try { token = aesUtils.aesEncrypt(StringUtils.removeStart(token, JWT_HEADER)); } catch (Exception ex) { log.error("加密异常",ex); token = ""; } return token; }
public UserVo decodeJWT(String token) { UserVo userVo =new UserVo(); try { if (StringUtils.isBlank(token)) { throw new RuntimeException("无效token"); } String decryptJwtToken = aesUtils.aesDecrypt(token); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(StringUtils.join(JWT_HEADER,decryptJwtToken)); long expiresAt = jwt.getExpiresAt()==null?0:jwt.getExpiresAt().getTime(); if(System.currentTimeMillis()>expiresAt){ throw new RuntimeException("token有效期超期"); } Map<String, Claim> claims = jwt.getClaims(); userVo.setPhone(claims.get("phone")==null?"":claims.get("phone").asString()); userVo.setUserId(claims.get("userId")==null?"":claims.get("userId").asString()); userVo.setSessionId(claims.get("sessionId")==null?"":claims.get("sessionId").asString()); userVo.setPlatform(claims.get("platform")==null?"":claims.get("platform").asString()); userVo.setExpiresAt(expiresAt);
} catch (Exception exception){ throw new RuntimeException("无效token"); } return userVo; } }
|
这样,一个简单的JWT工具类就搞定了,可以用于生成token。
测试
我们测试一下效果,新建方法如下:
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) throws Exception{ JWTUtils jwtUtils = JWTUtils.getInstance(); jwtUtils.init("sakuratears",1000,"1234567891111111","test111111111111"); UserVo userVo = new UserVo(); userVo.setUserId("1433223").setPhone("1888888888").setPlatform("APP").setSessionId("111111111111"); String token = jwtUtils.encodeJWT(userVo); System.out.println("加密token----> "+token); Thread.sleep(1); UserVo vo = jwtUtils.decodeJWT(token); System.out.println("解密token得到结果----> "+vo.toString()); }
|
运行结果如下:
我们尝试缩短token失效时间,增加线程等待时间。如下:
1 2 3
| jwtUtils.init("sakuratears",1,"1234567891111111","test111111111111"); ...... Thread.sleep(10000);
|
再次运行,可以看到token已失效。
所以我们在为客户端颁发token后,应该设置合理的token失效时间,当token失效后,再次请求,应告诉用户需要重新登录了。
总结
经过上面的一些描述,我们可以知道JWT的一些特点。
JWT默认是不加密的,为了保证安全,可以对JWT(token)进行可逆加密处理。
JWT可以用于信息交换,比如上面UserVo里面的手机号,这样我们不用在使用userId在对用户进行数据库查询,提高系统性能。
可以看到,JWT一旦签发生成token,如果不设置超时时间或者设置不合理(过长),在有效期内,token始终是有效的,除非服务器进行额外的处理。所以如果token泄露或被盗用,将是十分危险的,故应当设置合理的过期时间。
为了减少泄露或者盗用风险,JWT一般使用HTTPS协议进行传输。若使用HTTP协议,务必对token进行可逆加密处理后在进行传输。