简介
说明
本文介绍JWT知识。包括:什么是JWT、JWT的消息组成、JWT实战(测试java-jwt和jjwt两个工具类)。
JWT是常用的TOKEN工具,本文介绍JWT知识。包括:什么是JWT、JWT的消息组成、JWT实战(java-jwt和jjwt两种客户端都测试一下)。
官网
JSON Web Tokens – jwt.io (可生成/解析 Token)
JWT简介
什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。JWT将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证;应用场景如用户登录。
JWT可通过URL、POST参数或HTTP header发送,数据量小;负载中可包含用户信息,避免多次查询数据库。
JWT定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT请求流程
- 用户发出post请求(包含账户和密码);
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中向服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
Cookie+Session与JWT对比
Cookie+Session
用户登录认证中,因为http是无状态的,所以采用session方式。用户登录成功,服务端会保存一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
Cookie+session这种模式通常是保存在服务器内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。
JWT
服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。简单便捷,无需通过Redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验。
JWT优点
- 占资源少
- 不在服务端保存信息。
- 扩展性
- 分布式中,Session需要做多机数据共享,通常存在数据库或者Redis里面。而JWT不需要。
- 跨语言
- Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;
JWT缺点
- 无法废弃已颁布的令牌
- JWT在到期之前会始终有效,无法中途废弃。例如:在payload中存储了一些信息,当信息需要更新时,重新签发一个JWT,但旧的jwt还没过期,旧的JWT仍然可以登录,登录后服务端从JWT中拿到的信息就是过时的。
- 解决方案:服务端部署额外的逻辑,例如:设置黑名单,一旦签发了新的JWT,旧的就加入黑名单(比如存到Redis里面),避免被再次使用。(违背JWT初衷)
- JWT在到期之前会始终有效,无法中途废弃。例如:在payload中存储了一些信息,当信息需要更新时,重新签发一个JWT,但旧的jwt还没过期,旧的JWT仍然可以登录,登录后服务端从JWT中拿到的信息就是过时的。
- 过期
- Cookie续签方案一般都是框架自带的,如:Session有效期30分钟,若30分钟内有访问,有效期刷新至30分钟。对于JWT,改变JWT的有效时间,就要签发新的JWT。
- 解决方案1:每次请求刷新JWT。即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。
- 解决方案2:每次请求检查Token过期时间,如果即将过期,则重新生成一个新Token返回给客户端。
- Cookie续签方案一般都是框架自带的,如:Session有效期30分钟,若30分钟内有访问,有效期刷新至30分钟。对于JWT,改变JWT的有效时间,就要签发新的JWT。
JWT消息构成
一个token分3部分,按顺序为:头部(header)、载荷(payload)、签证(signature)。三部分之间用.号做分隔。例如(编码后的格式):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
上边解码后结果如下图:
header
简介
{ "alg": "HS256", "typ": "JWT" }
包含两个信息:token类型和采用的加密算法。
token类型:本处是jwt
加密算法:通常直接使用 HMAC SHA256
加密算法
加密算法是单向函数散列算法,常见的有:MD5、SHA、HAMC。
- MD5(message-digest algorithm 5)信息-摘要 算法
- 广泛用于加解密,常用于文件校验。(每个文件都能生成唯一的MD5值)
- SHA (Secure Hash Algorithm)安全散列算法
- 数字签名等密码学应用中重要的工具,安全性高于MD5
- HMAC (Hash Message Authentication Code)散列消息鉴别码
- 基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
JWT验证和签名使用的算法列表如下:
JWS | 算法名称 |
---|---|
HS256 | HMAC256 |
HS384 | HMAC384 |
HS512 | HMAC512 |
RS256 | RSA256 |
RS384 | RSA384 |
RS512 | RSA512 |
ES256 | ECDSA256 |
ES384 | ECDSA384 |
ES512 | ECDSA512 |
payload
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
载荷:存放有效信息,存放的是claim(声明)。有两种声明:1.标准的声明 2.自定义的声明
不应该在JWT的payload存储敏感信息(比如密码),因为该部分是通过base64编码的,客户端可解码。payload的内容对客户端来说是可见的,但客户端无法修改 payload 中的信息或对其进行加密操作。在 JWT 的设计中,payload 部分是公开的,主要用于传递数据,而真正的安全性是 Signature 部分的校验,用来确保 token 的完整性和真实性。
标准的声明
键 | 含义 |
iss | Issuer。 jwt签发者 |
sub | Subject。用户。一般是用户名 |
aud | Audience。接收jwt的一方。一般写用户id |
exp | Expiration Time。过期时间戳(必须要大于签发时间) |
nbf | Not Before。生效时间戳。该时间之前,该jwt都是不可用的。 |
iat | Issued At。签发时间戳。 |
jti | JWT ID。JWT的唯一标识。 主要用来作为一次性token,从而回避重放攻击。 |
自定义的声明
这里可以写任何非敏感内容,比如:用户id、用户名、邮箱。
signature
简介
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
Signature 部分是对前两部分的签名,防止数据篡改。
1.指定一个密钥(secret)。这个secret保存在服务端,服务端根据这个密钥生成token和进行验证,要保护好,不能泄露给用户(客户端)。
2.使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名(Signature)。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
3.把 Header(base64加密后的)、Payload(base64加密后的)、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.
)分隔,返回给用户。
实战
java-jwt的github:https://github.com/auth0/java-jwt
jjwt的github:https://github.com/jwtk/jjwt
创建项目
引入依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.1</version> </dependency>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.12.5</version> </dependency>
说明:这两个引入一个即可。本处为了测试这两个库的用法,所以都引入了。
token实现库
token在java中共有6个实现库,见:JSON Web Tokens – jwt.io。
下边介绍常用的两个:
- java-jwt(推荐)
- jjwt(不太推荐)
- 不太推荐的原因:版本都是0.xx,还没发布1.xx版本。
工具类
本处会测试两个库:java-jwt和jjwt,所以写一个接口,两个实现类。
接口
package com.example.demo.util; public interface JwtUtil { String createToken(String userId); boolean verifyToken(String token); String getUserIdByToken(String token); }
java-jwt实现类
package com.example.demo.util.impl; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.example.demo.util.JwtUtil; import lombok.extern.slf4j.Slf4j; import java.time.Duration; import java.util.Date; @Slf4j public class JwtUtilJavaJwtImpl implements JwtUtil { // 过期时间 private final Duration TTL = Duration.ofSeconds(5); // 密钥(java-jwt对长度没有限制) private final String SECRET = "123456"; // 创建Token @Override public String createToken(String userId) { try { Date date = new Date(System.currentTimeMillis() + TTL.toMillis()); Algorithm algorithm = calculateKey(); return JWT.create() .withAudience(userId) // 将 user id 保存到 token 里面 .withExpiresAt(date) // date之后,token过期 // 自定义私有的payload的key-value // .withClaim("userName", "Tony") .sign(algorithm); // token 的密钥 } catch (Exception e) { log.error("创建token失败", e); return null; } } // 校验token @Override public boolean verifyToken(String token) { // 这里可以加一个非空校验 try { verifyAndParse(token); return true; } catch (Exception e) { log.error("解析token失败", e); return false; } } // 根据token获取userId @Override public String getUserIdByToken(String token) { // 这里可以加一个非空校验 try { DecodedJWT decodedJWT = verifyAndParse(token); // 不要这样写,如果这么写,token失效时不报错! // DecodedJWT decodedJWT = JWT.decode(token); // 可获得所有标准Claim // String subject = decodedJWT.getSubject(); // 可获得自定义Claim数据 // Map<String, Claim> claims = decodedJWT.getClaims(); // Map<String, String> claimMap = new HashMap<>(); // for (Map.Entry<String, Claim> entry : claims.entrySet()) { // claimMap.put(entry.getKey(), entry.getValue().asString()); // } // 可获得过期日期 // Date expiresAt = decodedJWT.getExpiresAt(); return decodedJWT.getAudience().get(0); } catch (Exception e) { log.error("解析token失败", e); return null; } } private Algorithm calculateKey() { return Algorithm.HMAC256(SECRET); } private DecodedJWT verifyAndParse(String token) { Algorithm algorithm = calculateKey(); JWTVerifier verifier = JWT.require(algorithm) // .withIssuer("auth0") // .withClaim("username", username) .build(); return verifier.verify(token); } }
jjwt实现类
package com.example.demo.util.impl; import com.example.demo.util.JwtUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SecureDigestAlgorithm; import lombok.extern.slf4j.Slf4j; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; @Slf4j public class JwtUtilJJwtImpl implements JwtUtil { // 过期时间 private final Duration TTL = Duration.ofSeconds(5); // 密钥(jjwt对长度有限制,必须大于32个字符) private final String SECRET = "123456789123456789123456789123456789"; // 加密算法 private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256; // 生成token(通过用户Id) @Override public String createToken(String userId) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + TTL.toMillis()); // 可自定义claim Map<String, String> claimMap = new HashMap<>(); claimMap.put("key1", "value1"); ClaimsBuilder claimsBuilder = Jwts.claims(); claimsBuilder.audience().add(userId); claimsBuilder.add(claimMap); Claims claims = claimsBuilder.build(); SecretKey secretKey = calculateKey(); return Jwts.builder() .issuedAt(nowDate) .expiration(expireDate) .claims(claims) .signWith(secretKey, ALGORITHM) .compact(); } @Override public boolean verifyToken(String token) { // 这里可以加一个非空校验 try { parseToken(token); return true; } catch (Exception e) { log.error("校验token失败", e); return false; } } // 从token中获取用户id @Override public String getUserIdByToken(String token) { Claims claims = parseToken(token); // 可获得过期时间 //claims.getExpiration(); Set<String> audienceSet = claims.getAudience(); return audienceSet.iterator().next(); } // 获取token中注册信息 private Claims parseToken(String token) { // 这里可以加一个非空校验 return Jwts.parser() .verifyWith(calculateKey()) .build() .parseSignedClaims(token) .getPayload(); } private SecretKey calculateKey() { return Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); } }
测试
1.测试java-jwt
package com.knife.example.auth; import com.knife.example.auth.impl.JwtUtilJavaJwtImpl; public class Demo { public static void main(String[] args) { JwtUtil jwtUtil = new JwtUtilJavaJwtImpl(); String token = jwtUtil.createToken("12"); System.out.println("生成的token:" + token); boolean verifySuccess = jwtUtil.verifyToken(token); System.out.println("token验证结果:" + verifySuccess); String userId = jwtUtil.getUserIdByToken(token); System.out.println("从token读出userId:" + userId); // 睡眠,等待token超时 try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } verifySuccess = jwtUtil.verifyToken(token); System.out.println("token验证结果:" + verifySuccess); } }
结果
生成的token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMiIsImV4cCI6MTcxMTA5NzM1N30.pyFIlnGCeCaYYSuQerbMxjWSuG4kn9lo7wZ6wAyDX04 token验证结果:true 从token读出userId:12 token验证结果:false
2.测试jjwt
package com.knife.example.auth; import com.knife.example.auth.impl.JwtUtilJJwtImpl; public class Demo { public static void main(String[] args) { JwtUtil jwtUtil = new JwtUtilJJwtImpl(); String token = jwtUtil.createToken("12"); System.out.println("生成的token:" + token); boolean verifySuccess = jwtUtil.verifyToken(token); System.out.println("token验证结果:" + verifySuccess); String userId = jwtUtil.getUserIdByToken(token); System.out.println("从token读出userId:" + userId); // 睡眠,等待token超时 try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } verifySuccess = jwtUtil.verifyToken(token); System.out.println("token验证结果:" + verifySuccess); } }
结果
生成的token:eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTEwOTc2NDUsImV4cCI6MTcxMTA5NzY1MCwiYXVkIjpbIjEyIl0sImtleTEiOiJ2YWx1ZTEifQ.N5ABZ9PnxkaLqTCahJ4XqI6lPHausKCOfv-zNoR5pww token验证结果:true 从token读出userId:12 16:54:16.402 [main] ERROR com.knife.example.auth.impl.JwtUtilJJwtImpl - 校验token失败 io.jsonwebtoken.ExpiredJwtException: JWT expired 6396 milliseconds ago at 2024-03-22T08:54:10.000Z. Current time: 2024-03-22T08:54:16.396Z. Allowed clock skew: 0 milliseconds. at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:682) at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:362) at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94) at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36) at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29) at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:821) at com.knife.example.auth.impl.JwtUtilJJwtImpl.parseToken(JwtUtilJJwtImpl.java:82) at com.knife.example.auth.impl.JwtUtilJJwtImpl.verifyToken(JwtUtilJJwtImpl.java:58) at com.knife.example.auth.Demo.main(Demo.java:26) token验证结果:false
请先
!