What is JSON Web Token?
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。此信息可以被验证和信任,因为它是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA的公钥/私钥对进行签名
紧凑!
由于其大小,它可以通过URL,POST参数或HTTP头发送。此外,由于它的尺寸,它的传输速度很快。自包含!
有效负载包含关于用户的所有必需信息,以避免多次查询数据库
JWT 的组成与结构
- Header 报头
- Payload 有效载荷
- Signature 签名
所以 JWT 的结构一般为
xxx.yyyy.zzzzz
Header 报头
也称标头,Header由两部分组成
- 令牌类型 typ
- 签名算法 alg
Header 示例
{
"alg": "HS256",
"typ": "JWT"
}
关于签名算法
签名算法的作用
- 数据完整性:签名算法确保 JWT 的数据完整性。通过对 JWT 的头部 (Header)、载荷 (Payload) 和密钥进行签名,可以验证 JWT 在传输过程中是否被篡改。如果 JWT 的内容被修改,签名将无法匹配,从而检测到数据的不完整性。
- 身份验证:签名算法用于验证 JWT 的发布者身份。只有持有正确密钥的发布者才能生成有效的签名。验证者可以使用对应的密钥验证签名,从而确认 JWT 是由可信的发布者签发的。
- 防止伪造:签名算法防止他人伪造 JWT。如果攻击者试图伪造 JWT,由于没有正确的密钥,无法生成有效的签名,因此伪造的 JWT 将在验证过程中被拒绝。
- 不可否认性:签名算法提供了不可否认性。一旦 JWT 被签名,发布者就无法否认其签发了该 JWT。这对于某些应用场景(如数字合同、授权确认等)非常重要
常见的签名算法
a. HS256 (HMAC with SHA-256):
- 使用对称加密算法 HMAC-SHA256 进行签名。
- 需要使用相同的密钥进行签名和验证。
- 适用于双方共享相同密钥的情况。
b. HS384 (HMAC with SHA-384):
- 使用对称加密算法 HMAC-SHA384 进行签名。
- 与 HS256 类似,但使用更长的哈希值(384 位)。
c. HS512 (HMAC with SHA-512):
- 使用对称加密算法 HMAC-SHA512 进行签名。
- 与 HS256 类似,但使用更长的哈希值(512 位)。
d. RS256 (RSA with SHA-256):
- 使用非对称加密算法 RSA 和 SHA-256 哈希函数进行签名。
- 需要使用私钥进行签名,使用公钥进行验证。
- 适用于需要高度安全性和信任第三方的情况。
e. ES256 (ECDSA with SHA-256):
- 使用椭圆曲线数字签名算法 (ECDSA) 和 SHA-256 哈希函数进行签名。
- 与 RS256 类似,但使用椭圆曲线密码学,具有更高的安全性和更小的密钥尺寸。
f. PS256 (RSASSA-PSS with SHA-256):
- 使用 RSASSA-PSS 签名算法和 SHA-256 哈希函数进行签名。
- 与 RS256 类似,但使用了一种更安全的填充方案。
Payload 有效负载
令牌的第二部分,Payload 包含 claims(声明)
Claims(声明)是关于用户和附加数据的数据,claims 的名称只有 3 个字符长
声明有以下三种
- Registered claims 注册声明
预定义的 claims 包含 iss(发行者)、exp(到期时间)、sub(主题)、aud(受众)
sub 主题通常表示与 JWT 相关的用户或实体的标识符- Public claims 公共声明
自定义部分,由 JWT 使用者定义使用- Private claims 私有声明
自定义部分,这些自定义声明是为了在同意使用它们的各方之间共享信息而建立的,既不是注册的也不是公开的声明
Payload 示例
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Payload 经过 Base64Url 编码后,形成 JWT 的第二部分
Signature 签名
Signature 用于验证消息发送过程中国是否被篡改,并且在使用私钥签名的 token 时可用于验证 JWT 的发送者是否是它所说的那个人
创建 Signature 签名前,需要获取 已编码的Header、已编码的Payload 、secret和指定签名算法
secret 是一个由自己指定的字符串,在 建议在 spring boot 中的yaml文件中指定
示例:application.yaml
jwt: secret: Xf5n8p3m9Kl6Rq2Tj1Uh0Vw7Yd4Gb2Hc3Jk8Lz6Nt1Qx9Rs5Pm7Fh4Sd3Ew1Cv0By #可自定义密钥
类中的使用
@Value("${jwt.secret}") private String secret;
创建 Signation签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
JAVA中创建使用 JWT
- 生成JWT
// 从配置文件中读取 JWT 密钥
@Value("${jwt.secret}")
private String jwtSecret;
// 从配置文件中读取 JWT 过期时间(单位:秒)
@Value("${jwt.expiration}")
private long jwtExpiration;
// 生成 JWT
public String generateToken(String username) {
// 设置 JWT 的过期时间
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpiration * 1000);
// 创建 JWT 的头部(Header)
Map<String, Object> header = new HashMap<>();
header.put("typ", "JWT");
header.put("alg", "HS256");
// 创建 JWT 的载荷(Payload)
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
// 生成 JWT
return Jwts.builder()
.setHeader(header) // 设置头部
.setClaims(claims) // 设置载荷
.setSubject(username) // 设置主题
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(expiryDate) // 设置过期时间
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 设置签名算法和密钥
.compact(); // 生成 JWT 字符串
}
示例JWT
对于已签名的令牌,尽管这些信息受到保护,不会被篡改,但任何人都可以读取。不要将秘密信息放在JWT的有效载荷或头部元素中,除非它被加密
- 解析 JWT
// 解析 JWT
public Claims parseToken(String token) {
try {
// 使用密钥解析 JWT,并返回 Claims
return Jwts.parserBuilder()
.setSigningKey(getSigningKey()) // 设置签名密钥
.build()
.parseClaimsJws(token) // 解析 JWT
.getBody(); // 获取 Claims
} catch (Exception e) {
// JWT 解析失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
- 刷新JWT
// 刷新 JWT
public String refreshToken(String token) {
try {
// 解析 JWT 获取 Claims
Claims claims = parseToken(token);
// 获取用户名
String username = claims.getSubject();
// 生成新的 JWT
return generateToken(username);
} catch (Exception e) {
// JWT 刷新失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
- 验证JWT是否过期
// 验证 JWT 是否过期
public boolean isTokenExpired(String token) {
try {
// 解析 JWT 获取 Claims
Claims claims = parseToken(token);
// 判断 JWT 是否过期
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
// JWT 验证失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
- 获取签名密钥
// 获取签名密钥
private SecretKey getSigningKey() {
// 将密钥字符串转换为字节数组
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
// 使用 HMAC-SHA256 算法生成签名密钥
return Keys.hmacShaKeyFor(keyBytes);
}
JWT工具类
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
// 从配置文件中读取 JWT 密钥
@Value("${jwt.secret}")
private String jwtSecret;
// 从配置文件中读取 JWT 过期时间(单位:秒)
@Value("${jwt.expiration}")
private long jwtExpiration;
// 生成 JWT
public String generateToken(String username) {
// 设置 JWT 的过期时间
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpiration * 1000);
// 创建 JWT 的头部(Header)
Map<String, Object> header = new HashMap<>();
header.put("typ", "JWT");
header.put("alg", "HS256");
// 创建 JWT 的载荷(Payload)
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
// 生成 JWT
return Jwts.builder()
.setHeader(header) // 设置头部
.setClaims(claims) // 设置载荷
.setSubject(username) // 设置主题
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(expiryDate) // 设置过期时间
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 设置签名算法和密钥
.compact(); // 生成 JWT 字符串
}
// 解析 JWT
public Claims parseToken(String token) {
try {
// 使用密钥解析 JWT,并返回 Claims
return Jwts.parserBuilder()
.setSigningKey(getSigningKey()) // 设置签名密钥
.build()
.parseClaimsJws(token) // 解析 JWT
.getBody(); // 获取 Claims
} catch (Exception e) {
// JWT 解析失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
// 刷新 JWT
public String refreshToken(String token) {
try {
// 解析 JWT 获取 Claims
Claims claims = parseToken(token);
// 获取用户名
String username = claims.getSubject();
// 生成新的 JWT
return generateToken(username);
} catch (Exception e) {
// JWT 刷新失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
// 验证 JWT 是否过期
public boolean isTokenExpired(String token) {
try {
// 解析 JWT 获取 Claims
Claims claims = parseToken(token);
// 判断 JWT 是否过期
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
// JWT 验证失败,抛出异常
throw new IllegalArgumentException("Invalid token");
}
}
// 获取签名密钥
private SecretKey getSigningKey() {
// 将密钥字符串转换为字节数组
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
// 使用 HMAC-SHA256 算法生成签名密钥
return Keys.hmacShaKeyFor(keyBytes);
}
}
pom.xml
<dependencies>
<!-- JWT库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
application.yaml
jwt:
secret: Xf5n8p3m9Kl6Rq2Tj1Uh0Vw7Yd4Gb2Hc3Jk8Lz6Nt1Qx9Rs5Pm7Fh4Sd3Ew1Cv0By #记得设置得足够长
expiration: 10000 #单位秒
- 使用实例
@Autowired
private JwtUtils jwtUtils;
// 生成 JWT
String token = jwtUtils.generateToken("john");
// 解析 JWT
Claims claims = jwtUtils.parseToken(token);
String username = claims.getSubject();
// 刷新 JWT
String refreshedToken = jwtUtils.refreshToken(token);
// 验证 JWT 是否过期
boolean isExpired = jwtUtils.isTokenExpired(token);
官方在线 JWT 信息校验
JWT的工作流程
JWT 不使用cookie,所以不存在 跨域资源共享(OSRF)问题
JWT 用户身份验证的流程
- Browser 客户端(一般是浏览器)
- Server 服务器
- POST /users/login with username and password:
- 客户端(浏览器)向服务器发送 POST 请求,请求路径为 /users/login,请求体中包含用户名和密码。
- 这一步是用户登录的过程,客户端将用户的登录凭证发送给服务器进行验证。
- Creates a JWT with a secret:
- 服务器接收到登录请求后,首先验证用户名和密码的正确性。
- 如果验证通过,服务器会使用一个秘密的密钥(secret)创建一个 JWT。
- JWT 中包含了一些关于用户身份的信息(如用户ID、用户名等),以及一些元数据(如过期时间、签发时间等)。
- Returns the JWT to the browser:
- 服务器生成 JWT 后,将其作为登录成功的响应返回给客户端(浏览器)。
- 客户端收到 JWT 后,通常会将其存储在本地(如 localStorage 或 Cookie 中)以便后续使用。
- Sends the JWT on the Authorization header:
- 客户端在后续的请求中,将之前存储的 JWT 放在请求的 Authorization 头部中发送给服务器。
- 这个头部通常的格式为 “Bearer <JWT>”。
- Checks JWT signature. Get user information from the JWT:
- 服务器接收到客户端的请求后,会首先检查请求头部中的 JWT 签名是否有效。
- 服务器使用之前用于创建 JWT 的密钥(secret)对 JWT 进行验证,确保 JWT 没有被篡改。
- 如果 JWT 签名有效,服务器会从 JWT 中解析出用户的身份信息。
- Sends response to the client:
- 服务器根据从 JWT 中获取的用户信息,进行相应的业务逻辑处理。
- 处理完成后,服务器将响应结果返回给客户端。
如果使用 HTTP 标头发送 JWT 令牌,应该尽量使 token 变得太大,有些服务器不接受 8KB 的头文件
JWT工作流程
- Application Client 客户端应用程序
- Authorization Server 授权服务器
- Resource Server 资源服务器
- 客户端应用程序向授权服务器发送请求,以获取访问令牌(Access Token)。这个请求通常包括客户端的身份验证信息(如客户端ID和密钥)以及所请求的权限范围(Scope)。
- 授权服务器验证客户端的身份,并检查请求的有效性。如果验证通过,授权服务器会生成一个访问令牌(Access Token),并将其返回给客户端应用程序。这个访问令牌通常是一个JWT,其中包含了关于客户端、用户、权限范围以及令牌有效期等信息。
- 客户端应用程序收到访问令牌后,会将其存储在本地(通常存储在内存中或安全的存储中)。每当客户端需要访问受保护的资源时,它会将访问令牌附加到请求的Authorization头中,然后发送给资源服务器。
- 资源服务器接收到客户端的请求后,会从请求的Authorization头中提取访问令牌。资源服务器会验证访问令牌的有效性,检查其签名、过期时间以及所包含的权限范围等信息。
- 如果访问令牌有效,资源服务器会根据令牌中包含的权限范围,授予客户端对相应资源的访问权限。资源服务器会处理客户端的请求,并将响应结果返回给客户端。
- 客户端应用程序接收到资源服务器的响应,并根据需要处理返回的数据或执行相应的操作。
- 如果访问令牌过期或者客户端需要访问其他受保护的资源,客户端可以重复步骤1到步骤6,以获取新的访问令牌并访问所需的资源。
JWT与SWT、SAML
- JWT JSON Web 令牌
- SWT 简单Web令牌
- SAML 安全断言标记语言令牌
为什么选择JWT而不是SWT与SAML?
体积方面:因为 JSON 没有 XML 那么冗长(动辄开头结尾声明好几行),所以在编码时它的体积更小,更紧凑,更适合在 HTML 和 HTTP 环境中传递
安全方面:SWT只能通过HMAC算法进行对称签名,但是JWT和SAML可以使用X.509证书的形式的公钥/密钥对进行签名,同时,使用XML数字签名对XML进行签名而不引入模糊的安全漏洞比较难(总之就是JWT是这三个中最安全的)
便捷性:几乎所有编程语言都存在JSON解析器,可以直接映射到对象,而XML需要手动配置对象映射,使得JWT在多平台能够更直接地使用,且SAML编码的长度很长
JSON Web Token介绍-jwt.io — JSON Web Token Introduction - jwt.io