JWT 总结
1. 跨域身份验证
1.1 传统的 session 流程
- 浏览器发起请求登陆,向服务器发送用户名和密码
- 服务端验证身份,生成身份验证信息(用户角色、登录时间等),存储在服务端 的 session 中
- 服务器向用户返回 session_id,session 信息都会写入到用户的 Cookie
- 用户的每个后续请求都将通过在 Cookie 中取出 session_id 传给服务器
- 服务器收到 session_id 并对比之前保存的数据,确认用户的身份
- 服务器返回用户请求的内容
1.2 JWT 流程
- 浏览器发起请求登陆
- 服务端验证身份,根据算法,将用户标识符打包生成 JWT, 并且返回给浏览器
- 浏览器发起请求获取用户资料,把刚刚拿到的 JWT 一起发送给服务器
- 服务器发现数据中有 JWT,进行验证
- 服务器返回用户请求的内容
1.3 session 与 JWT 区别
- session 存储在服务端占用服务器资源,而 JWT 存储在客户端
- session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险,而 JWT 则通过参数传递
- session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用
- 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性
1.4 JWT 工作原理
在服务器身份验证之后,将生成一个 JSON 对象并将其发送回用户。之后,当用户与服务器通信时,客户在请求中发回 JSON 对象。服务器仅依赖于这个 JSON 对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展
2. 访问令牌的类型
3. JWT 的组成
JWTString=Base64(Header) + "." + Base64(Payload).HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
该对象为一个很长的字符串,字符之间通过 "." 分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名
3.1 Header(JWT 头)
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示:
{
"alg": "HS256",
"typ": "JWT"
}
- alg: 表示签名使用的算法,默认为 HMAC SHA256(写为 HS256)
- typ: 表示令牌的类型,JWT 令牌统一写为 JWT。
最后,使用 Base64URL 算法将上述 JSON 对象转换为字符串保存
3.2 Payload(有效载荷)
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据,包含三个部分:
- 标准声明
- 公共声明
- 私有声明
3.2.1 标准声明字段
interface Stantard {
iss?: string; // JWT的签发者,发行人
sub?: string; // 主题
aud?: string; // 接收JWT的一方,受众,用户
exp?: number; // JWT的过期时间,这个过期时间必须要大于签发时间
nbf?: number; // 定义在什么时间之前,该 JWT 都是不可用的
iat?: number; // 该JWT签发的时间,发布时间
jti?: number; //JWT的唯一身份标识,JWT ID
}
3.2.2 公共声明的字段
interface Public {
[key: string]: any;
}
公共声明字段可以添加任意信息,但是因为可以被解密出来,所以不建议存放敏感信息。
3.2.3 私有声明的字段
interface Private {
[key: string]: any;
}
3.2.4 示例
一个JWT 的 payload 如下:
{
"ip": "127.0.0.1",
"uuid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"iat": 1527523017
}
通过 Base64URL 算法加密生成第二部分的 payload
const payloadBuffer = Buffer.from(
JSON.stringify({
ip: "127.0.0.1",
uuid: "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
iat: 1527523017
})
);
const payload = payloadBuffer.toString("base64");
console.log(payload);
// eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9
默认情况下 JWT 是未加密的,因为只是采用 Base64URL 算法,拿到 JWT 字符串后可以转换回原本的 JSON 数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到 JWT 中,以防止信息泄露。JWT 只是适合在网络中传输一些非敏感的信息。
3.3 Signature(签名哈希)
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)生成签名:
signature = 加密算法(header + "." + payload, 密钥);
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用 "." 分隔,就构成整个 JWT 对象。
3.4 Base64URL 算法
JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。Base64 中用的三个字符是 "+","/" 和 "=",由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:"=" 去掉,"+" 用 "-" 替换,"/" 用 "_" 替换,这就是 Base64URL 算法。
4. JWT 安全性
4.1 Token如何做身份验证?
- 首先,JWT 的 Token 相当是明文,是可以解密的,任何存在 payload 的东西,都没有秘密可言,所以隐私数据不能签发 Token
- 而服务端,拿到 Token 后解密,即可知道用户信息,例如示例中的 uuid
- 有了 uuid,那么你就知道这个用户是谁,是否有权限进行下一步的操作
4.2 如何防止 Token 被串改?
- 此时 signature 字段就是关键了,能被解密出明文的只有 header 和 payload
- 假如被人串改了 payload,那么服务器可以通过 signature 去验证是否被篡改过,在服务端在执行一次 signature = 加密算法(header + "." + payload, 密钥);,然后对比 signature 是否一致,如果一致则说明没有被篡改。
- 所以服务器的密钥不能被泄漏。如果泄漏,将存在以下风险:
客户端可以自行签发 Token、其他人可以肆意篡改 Token
4.3 问题
- JWT 默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密
- 当 JWT 未加密方法时,一些私密数据无法通过 JWT 传输
- JWT 不仅可用于认证,还可用于信息交换。善用 JWT 有助于减少服务器请求数据库的次数
- JWT 的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦 JWT 签发,在有效期内将会一直有效
- JWT 本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT 的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证
- 为了减少盗用和窃取,JWT 不建议使用 HTTP 协议来传输代码,而是使用加密的 HTTPS 协议进行传输
5. JWT 种类
JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息,JWT 的具体实现可以分为以下几种:
- nonsecure JWT:未经过签名,不安全的 JWT
- JWS:经过签名的 JWT
- JWE:payload 部分经过加密的 JWT
5.1 nonsecure JWT
经过签名,不安全的 JWT。其 header 部分没有指定签名算法
{
"alg": "none",
"typ": "JWT"
}
并且也没有 Signature 部分
5.2 JWS
5.2.1 概念
JWS ,也就是 JWT Signature,其结构就是在之前 nonsecure JWT 的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证 JWT 不能被他人随意篡改。我们通常使用的 JWT 一般都是 JWS。
为了完成签名,除了用到 header 信息和 payload 信息外,还需要算法的密钥,也就是 secretKey。加密的算法一般有 2 类:
- 对称加密:secretKey 指加密密钥,可以生成签名与验签
- 非对称加密:secretKey 指私钥,只用来生成签名,不能用来验签(验签用的是公钥)
JWT 的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK
5.2.2 JWT 签名算法
到目前为止,JWT 的签名算法有三种:
- HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
- RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
- ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
5.3 JWE
JWS 是去验证数据的,而 JWE(JSON Web Encryption)是保护数据不被第三方的人看到的。通过 JWE,JWT 变得更加安全。
- JWE 和 JWS 的公钥私钥方案不相同,JWS 中,私钥持有者加密令牌,公钥持有者验证令牌。而 JWE 中,私钥一方应该是唯一可以解密令牌的一方。
- 在 JWE 中,公钥持有可以将新的数据放入 JWT 中,但是 JWS 中,公钥持有者只能验证数据,不能引入新的数据。因此,对于公钥/私钥的方案而言,JWS 和JWE 是互补的。
6. 使用
6.1 java-jwt
导入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
6.1.1 对称签名
public class TestJWT {
// Token的失效时间
private final static long EXPIRATION_TOKEN = 60 * 1000;
// Token的签名密钥
private final static String SECRET_KEY = "123456";
// 根据用户名生成 Token
public static String generateToken(String username){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTCreator.Builder jwt = JWT.create();
HashMap<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("type", "jwt");
// jwt.withHeader(header)
// JWT 的 header 部分,该 map 可以是空的,因为有默认值{"alg":HS256,"typ":"JWT"}
String token = jwt.withHeader(new HashMap<>())
.withClaim("userId", 7) // Payload
.withClaim("username", username)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN)) // 过期时间
.sign(algorithm);// 签名用的 secret
System.out.println("生成的 Token:" + token);
return token;
}
// 验证 Token
public static DecodedJWT verify(String token){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
Verification verification = JWT.require(algorithm);
JWTVerifier jwtVerifier = verification.build();
DecodedJWT decodedJWT = jwtVerifier.verify(token); // 通过密钥进行验证
return decodedJWT;
}
public static void main(String[] args) {
String token = generateToken("张三");
DecodedJWT decodedJWT = verify(token);
String header = decodedJWT.getHeader();
System.out.println("JWT 头:" + header);
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println("有效负载:" + claims);
Claim username = decodedJWT.getClaim("username");
System.out.println("用户名:" + username);
String signature = decodedJWT.getSignature();
System.out.println("签名:" + signature);
Date expiresAt = decodedJWT.getExpiresAt();
System.out.println("过期时间" + expiresAt);
}
}
6.1.2 非对称签名
public class TestJWT {
// Token的失效时间
private final static long EXPIRATION_TOKEN = 60 * 1000;
// 使用 Hutool 构建 RSA,当使用无参构造时,将自动生成随机的公钥私钥密钥对
private final static RSA rsa = new RSA();
// private static final String RSA_PRIVATE_KEY = "..."; 私钥
// private static final String RSA_PUBLIC_KEY = "..."; 公钥
// 生成 Token
public static String generateToken(String username){
// 获取 RSA 私钥
// RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
// RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();
String token = JWT.create().withHeader(new HashMap<>())
.withClaim("username", username)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
.sign(Algorithm.RSA256(null, rsaPrivateKey)); // 签名时传入私钥
return token;
}
// 验证 Token
public static DecodedJWT verify(String token){
// 获取 RSA 公钥
// RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
// RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
// 验签时传入公钥
JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(rsaPublicKey, null)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
public static void main(String[] args) {
String token = generateToken("张三");
DecodedJWT decodedJWT = verify(token);
String header = decodedJWT.getHeader();
System.out.println("JWT 头:" + header);
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println("有效负载:" + claims);
Claim username = decodedJWT.getClaim("username");
System.out.println("用户名:" + username);
String signature = decodedJWT.getSignature();
System.out.println("签名:" + signature);
Date expiresAt = decodedJWT.getExpiresAt();
System.out.println("过期时间" + expiresAt);
}
}
6.2 jjwt
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
6.2.1 对称签名
public class TestJWT {
// Token的失效时间
private final static long EXPIRATION_TOKEN = 60 * 1000;
// Token的密钥
private final static String SECRET_KEY = "123456";
// 根据用户名生成 Token
public static String generateToken(String username){
JwtBuilder jwtBuilder = Jwts.builder();
// jwtBuilder.setHeader(new HashMap<>());
String token = jwtBuilder.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("user")
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
.claim("username", username)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
System.out.println("生成的 Token:" + token);
return token;
}
// 验证 Token,从http请求头中获取 Token 字符串
public static Jws<Claims> verify(String token){
JwtParser jwtParser = Jwts.parser();
Jws<Claims> claimsJws = jwtParser.setSigningKey(SECRET_KEY).parseClaimsJws(token);
return claimsJws;
}
public static Jws<Claims> verify(HttpServletRequest request){
String token = request.getHeader("token");
JwtParser jwtParser = Jwts.parser();
Jws<Claims> claimsJws = jwtParser.setSigningKey(SECRET_KEY).parseClaimsJws(token);
return claimsJws;
}
public static void main(String[] args) {
String token = generateToken("张三");
Jws<Claims> claimsJws = verify(token);
JwsHeader header = claimsJws.getHeader();
System.out.println("Header:" + header);
Claims body = claimsJws.getBody();
System.out.println("PayLoad:" + body);
String subject = body.getSubject();
System.out.println("主题:" + subject);
String signature = claimsJws.getSignature();
System.out.println("签名:" + signature);
}
}
6.2.2 jjwt 的 0.10版本以后
pom依赖要引入多个
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
在 jjwt 0.10 版本之前,没有强制要求,secretKey 长度不满足要求时也可以签名成功。但是 0.10 版本后强制要求 secretKey 满足规范中的长度要求,否则生成 jws 时会抛出异常
- HS256:要求至少 256 bits (32 bytes)
- HS384:要求至少384 bits (48 bytes)
- HS512:要求至少512 bits (64 bytes)
- RS256 and PS256:至少2048 bits
- RS384 and PS384:至少3072 bits
- RS512 and PS512:至少4096 bits
- ES256:至少256 bits (32 bytes)
- ES384:至少384 bits (48 bytes)
- ES512:至少512 bits (64 bytes)
之前的签名和验签方法都是传入密钥的字符串,已经过时。最新的方法需要传入 Key 对象
public static String generateToken(String username){
String token = Jwts.builder()
.setHeaderParam("alg", "HS256")
.setSubject("user")
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
.claim("username", username)
// 传入Key对象
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
return token;
}
// 验证 Token
public static Jws<Claims> verify(String token) {
// 传入Key对象
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(token);
return claimsJws;
}
6.2.3 非对称签名
public class TestJWT {
// Token的失效时间
private final static long EXPIRATION_TOKEN = 60 * 1000;
// 使用 Hutool 构建 RSA
private final static RSA rsa = new RSA();
// 根据用户名生成 Token
public static String generateToken(String username){
// 获取 RSA 私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();
// jwtBuilder.setHeader(new HashMap<>());
String token = Jwts.builder().setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("user")
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
.claim("username", username)
.signWith(rsaPrivateKey, SignatureAlgorithm.RS256)
.compact();
System.out.println("生成的 Token:" + token);
return token;
}
// 验证 Token
public static Jws<Claims> verify(String token){
RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(rsaPublicKey).build().parseClaimsJws(token);
return claimsJws;
}
public static void main(String[] args) {
String token = generateToken("张三");
Jws<Claims> claimsJws = verify(token);
JwsHeader header = claimsJws.getHeader();
System.out.println("Header:" + header);
Claims body = claimsJws.getBody();
System.out.println("PayLoad:" + body);
String subject = body.getSubject();
System.out.println("主题:" + subject);
String signature = claimsJws.getSignature();
System.out.println("签名:" + signature);
}
}
7. 实际应用
- 在登录验证通过后,给用户生成一个对应的随机 Token (注意这个 Token 不是指 JWT,可以用 UUID 等算法生成),然后将这个Token 作为 Key 的一部分,用户信息作为 Value 存入 Redis,并设置过期时间,这个过期时间就是登录失效的时间
- 第1步中生成的随机 Token 作为 JWT 的 payload 生成 JWT 字符串返回给前端
- 前端之后每次请求都在请求头中的 Authorization 字段中携带 JWT 字符串
- 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的 Authorization 字段中取出 JWT 字符串并进行验证,验证通过后解析出 payload 中的随机 Token,然后再用这个随机 Token 得到 Key,从 Redis 中获取用户信息,如果能获取到就说明用户已经登录
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String JWT = request.getHeader("Authorization");
try {
// 1.校验JWT字符串
DecodedJWT decodedJWT = JWTUtils.decode(JWT);
// 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
...
return true;
}catch (SignatureVerificationException e){
System.out.println("无效签名");
e.printStackTrace();
}catch (TokenExpiredException e){
System.out.println("token已经过期");
e.printStackTrace();
}catch (AlgorithmMismatchException e){
System.out.println("算法不一致");
e.printStackTrace();
}catch (Exception e){
System.out.println("token无效");
e.printStackTrace();
}
return false;
}
}