1.简介
JWT(JSON Web Token)是一个开放标准(RFC 7519),它将用户信息存储在经过加密的JSON格式的字符串中,在各方之间安全地传输信息,进行身份认证。简单来说,JWT就是token的一种实现形式,是目前最流行的跨域身份验证解决方案之一。使用token进行身份认证的一般流程如下:
- 用户输入用户名和密码进行登录
- 服务端收到登录请求后,对接收到的用户名和密码进行验证
- 验证通过后,服务端会生成一个token返回给客户端
- 客户端在收到返回的token后存储起来
- 之后客户端在向服务端发起请求时携带服务端返回的token
- 服务端在收到非登录请求后,会拿到请求中的token并进行验证,如果验证通过,就正常返回数据给客户端
2.JWT结构
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
1.Header
JWT Header是一个JSON对象,用来描述JWT元数据。其中,alg属性表示签名使用的算法,默认为HMAC SHA256;typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload
Payload,即有效载荷,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据, JWT指定七个默认字段供选择。这些预定义的字段并不要求强制使用。
{
"iss": "发行人",
"exp": "到期时间",
"sub": "主题",
"aud": "用户",
"nbf": "在此之前不可用",
"iat": "发布时间",
"jti": "JWT ID用于标识该JWT"
}
除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,例如用户名等非敏感信息。但绝不能将用户密码存入载荷中,因为一般情况下JWT只采用了Base64,没有进行加密,相当于裸奔。
3.Signature
signature是对上面两部分数据进行签名得到的,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,从而确保数据不会被篡改。需要指定一个密钥(secretKey),该密钥仅保存在分发签名的服务器中。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)。
H
M
A
C
S
H
A
256
(
b
a
s
e
64
U
r
l
E
n
c
o
d
e
(
h
e
a
d
e
r
)
+
"
.
"
+
b
a
s
e
64
U
r
l
E
n
c
o
d
e
(
p
a
y
l
o
a
d
)
,
s
e
c
r
e
t
)
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
JWT中生成signature的部分源码如下:
private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s.%s", header, payload, signature);
}
3.JWT分类
JWT大致可以分为三类:
- nonsecure JWT:未经过签名
- JWS:经过加密生成签名的JWT
- JWE:payload部分经过加密的JWT
在日常开发中,我们主要用的是JWS,也就是生成签名的JWT;而加密的算法可以根据加密形式分为两类:
- 对称加密:密钥可以生成签名也能验签
- 进行对称加密的算法有:HMAC(哈希消息验证码)
- 非对称加密:有私钥和公钥,私钥只能用来生成签名,用公钥来进行验签
- 进行非对称加密的算法有:RSASSA(RSA签名算法)、ECDSA(椭圆曲线数据签名算法)
对称加密 JWT 和非对称加密 JWT 的比较
- 安全性: 非对称加密 JWT 更安全,因为私钥不需要共享,而对称加密 JWT 需要确保密钥的安全性。
- 复杂度: 对称加密 JWT 更简单高效,而非对称加密 JWT 需要管理公钥和私钥的配对,处理复杂度较高。
- 用途: 对称加密 JWT 适用于简单的场景,非对称加密 JWT 适用于更高安全要求的场景。
4.JWT使用
在Java中常用的JWT库有java-jwt和jjwt-root,下面以java-jwt举例
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
4.1对称加密
@Component
public class JwtUtils {
// 签名密钥
private static final String SECRET_KEY = "!MBGA&";
/**
* 生成JWT
* @param payLoad 载荷
* @param expiration 失效时间
* @return JWT
*/
public String generateToken(Map<String, String> payLoad, Date expiration) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTCreator.Builder jwtBuilder = JWT.create();
if (!CollectionUtils.isEmpty(payLoad)) {
payLoad.forEach(jwtBuilder::withClaim);
}
jwtBuilder.withExpiresAt(expiration);
return jwtBuilder.sign(algorithm);
}
/**
* 解析token
* @param token jwt
* @return 解析结果
*/
public DecodedJWT decodeToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
return jwtVerifier.verify(token);
}
/**
* 验证token
* @param token token
* @param claimMap 验证载荷
* @return 验证结果
*/
public boolean verifyToken(String token, Map<String, String> claimMap) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
Verification verification = JWT.require(algorithm);
if (!CollectionUtils.isEmpty(claimMap)) {
claimMap.forEach(verification::withClaim);
}
JWTVerifier jwtVerifier = verification.build();
jwtVerifier.verify(token);
return true;
}
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {
@Autowired
private JwtUtils jwtUtils;
@Test
public void testGenerateToken() {
Map<String, String> payloadMap = new HashMap<>();
payloadMap.put("userId", "007");
payloadMap.put("userName", "zhangsan");
String token = jwtUtils.generateToken(payloadMap, new Date(System.currentTimeMillis() + 1000 * 5));
System.out.println(token);
System.out.println("验证结果1:" + jwtUtils.verifyToken(token, null));
System.out.println("验证结果2:" + jwtUtils.verifyToken(token, payloadMap));
}
}
执行结果如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNzE3MDUwNTI0LCJ1c2VySWQiOiIwMDcifQ.oDEfhh2Ece8U2LVgzLwKejIdq9RrcFqG7DgYgYrGOb4
验证结果1:true
验证结果2:true
若在超时后再进行校验,则会报token已失效:
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {
@Autowired
private JwtUtils jwtUtils;
@Test
public void testGenerateToken() throws InterruptedException {
Map<String, String> payloadMap = new HashMap<>();
payloadMap.put("userId", "007");
payloadMap.put("userName", "zhangsan");
String token = jwtUtils.generateToken(payloadMap, new Date(System.currentTimeMillis() + 1000 * 5));
System.out.println(token);
Thread.sleep(1000 * 10);
System.out.println("验证结果1:" + jwtUtils.verifyToken(token, null));
System.out.println("验证结果2:" + jwtUtils.verifyToken(token, payloadMap));
}
}
com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on Thu May 30 14:30:20 CST 2024.
4.2非对称加密
由于非对称加密需要用公钥解析签名进行验签,因此还需要一对公钥和私钥;使用非对称加密的算法有ECDSA和RSASSA,下面以RSASSA进行举例。
在使用 RSA 算法生成 JWT(JSON Web Token)时,通常使用私钥进行签名(不是加密),这是因为 JWT 的签名部分需要保证数据的完整性和来源可信性,而私钥可以确保签名是由持有私钥的一方生成的,从而确保JWT的创建者是合法的,且在传输过程中没有被篡改,从而提供了数字签名的验证功能。在这个过程中,私钥只能由签发方持有,公钥由验证方持有。
例如,Alice 想发送一条带有数字签名的消息给 Bob:
- Alice 生成一对密钥,并将公钥发送给 Bob。
- Alice 使用自己的私钥对消息进行签名,然后将消息和签名一起发送给 Bob。
- Bob 使用 Alice 的公钥验证签名,以确保消息的来源和完整性。
@Component
public class JwtUtils {
// Base64编码的公钥、私钥
private static final String RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtG4t8uCjjkWUijuFxCP2SX7MtrreQRIdow4YrzGutVMqh/YJYDXsAY3nvOQnhYCOh3OBmQ8OCLsg+cM7s3sB+GHW7NRTRRPsY5NzwQFGWhAKer5pC++TAj8PgL36Jm+u9TtMSweBnzhMycedyHO6aNwnXGF3fMmx+YZIzJCzWD7TayJsOmTdAb3Oj4sUF+1SwAsXjFIuHUz00bqAI2WlaW9sTpKLZVrzSGxo/UkmTzrBXRkC7Dcnroa6Q7RrbcvQ/aXaufnqw+D1rJh4pmH34/L7FOVbVcb+iEWsL8MNDSq2jS5c2Ch4SEbtQEikRbRiyw4M2lLY0z96RYXl1Ob4MQIDAQAB";
private static final String RSA_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0bi3y4KOORZSKO4XEI/ZJfsy2ut5BEh2jDhivMa61UyqH9glgNewBjee85CeFgI6Hc4GZDw4IuyD5wzuzewH4Ydbs1FNFE+xjk3PBAUZaEAp6vmkL75MCPw+Avfomb671O0xLB4GfOEzJx53Ic7po3CdcYXd8ybH5hkjMkLNYPtNrImw6ZN0Bvc6PixQX7VLACxeMUi4dTPTRuoAjZaVpb2xOkotlWvNIbGj9SSZPOsFdGQLsNyeuhrpDtGtty9D9pdq5+erD4PWsmHimYffj8vsU5VtVxv6IRawvww0NKraNLlzYKHhIRu1ASKRFtGLLDgzaUtjTP3pFheXU5vgxAgMBAAECggEAfGV6SHmYSf6u6p8fGpuwsfs6KMGtei+DP2f4nNEkIt+z8wkubaa/kFLF8vVaSjDYE4sPTveSNXLOyWRRRO0J7rWF+MJqprWVxRFkPS0j0/Q7RWUCB7ilImdXyOsyDE3z0h+P+iqys5OdXYDCx/ECFEm02XxZZS+qBN8QUflxrOR44zE0cpvf5U3rNHbNeEKPYqvHkr1khdn02VwSGexRSiFGfcl9R5WGBzhpXYJWoca+CADGKdokjW0d7L4iv7E8Djf4gdfV/+oZSGkVK7u3khEz3ejWUCFiwvLdUvieXiw7T0RUBFgsVbFdoPObvfWqWevXI7mpqvgLTvQ4VSRSAQKBgQD1hdPCoaba6a5G+IREKZc548yEyTIeDW6QWWkKtGwX6O6Jz5yqvconGmT6G1kHMLoX3tyzUdRqE1YkmKMWn2YPYWQfsf+5HdQbnp6RRJX/PHo6aNsVY/qcT41h0NWUzBpBIgaNIHMwvnERw/1bes21zldoqLVQVUSppu8RAzu/dwKBgQC8IULs1qfIy6dePUUrZ1shWhifdjl+o3VERRhIhupw6QKahh8+HDfhxfhKf2yQDDSyeYdKSPqXTvE+29xNkQD50UPePTAWUY/489rqKjjqbzfP/0blKyMe7v0AuorshYlsvdTv8L/hv7IH+3ig/NeUmq8cfarfrDPMl1b4qft/lwKBgQDxuGH1esUVPX9xK/a8TN4wBlAyYwrOf5bc0soR2fOZzqAaWaX6i4Pc36WOfyI1bAmquqU6flPMY2EqAoR+H0yR0+aJNet4Sx+qWY3vo0Cx2s7TdqxK8PWossGVDc9ZAWDPwzoD83C1CqmzPevQBqVEWvO/fGmVv30sMceoTjCinQKBgQCU9q1chXejmgZzq4Y6oQNavFFk6qMJ8HopTaC1xQab0xT5sBvK/WMORcidjJo00UVk0K/clT6/UoXZpROi459nMFlR/xc1hO3ATa7Y3gflMZ16+AJ18ZtEoi35Rrw8Hly7mvCh8Pqu0TAx+9//cVVuWLExmRwVMfBuHqOCEG6RDwKBgEoIRzGrO7Mv1Hp8cwmd0I0AD1wKGsyIhaSybCFipzfE/or4c07YqLaqe71PCw2jL+y8iJi3sfzinyj+N8cof5PkkI3vaJ1pwsBzVL1U7zD7LU634QaEowyvnso73i2E9O5GJOjNuZePZr38DR3HXhpr2C58LxELHPQhKkhGBcKF";
/**
* 生成公私钥对
*/
public void createRSAKey() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
System.out.println(publicKeyBase64);
System.out.println(privateKeyBase64);
}
/**
* 生成非对称token
* @param payloadMap 载荷
* @param expiration 有效时间
* @return token
*/
public String generateAsymmetryToken(Map<String, String> payloadMap, Date expiration) {
RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
Algorithm algorithm = Algorithm.RSA256((RSAPrivateKey) rsa.getPrivateKey());
JWTCreator.Builder jwtBuilder = JWT.create();
if (!CollectionUtils.isEmpty(payloadMap)) {
payloadMap.forEach(jwtBuilder::withClaim);
}
jwtBuilder.withExpiresAt(expiration);
return jwtBuilder.sign(algorithm);
}
/**
* 解析非对称token
* @param token jwt
* @return 解析结果
*/
public DecodedJWT decodeAsymmetryToken(String token) {
RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) rsa.getPublicKey());
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
return jwtVerifier.verify(token);
}
/**
* 验证非对称token
* @param token jwt
* @param claimMap 验证载荷
* @return 验证结果
*/
public boolean verifyAsymmetryToken(String token, Map<String, String> claimMap) {
RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) rsa.getPublicKey());
Verification verification = JWT.require(algorithm);
if (!CollectionUtils.isEmpty(claimMap)) {
claimMap.forEach((k, v) -> verification.withClaim(k, v));
}
JWTVerifier jwtVerifier = verification.build();
jwtVerifier.verify(token);
return true;
}
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {
@Autowired
private JwtUtils jwtUtils;
@Test
public void testGenerateToken() throws InterruptedException, NoSuchAlgorithmException {
Map<String, String> payloadMap = new HashMap<>();
payloadMap.put("userId", "007");
payloadMap.put("userName", "zhangsan");
Date expiration = new Date(System.currentTimeMillis() + 1000 * 5);
String token = jwtUtils.generateAsymmetryToken(payloadMap, expiration);
System.out.println(token);
//Thread.sleep(1000 * 10);
System.out.println("验证结果1:" + jwtUtils.verifyAsymmetryToken(token, null));
System.out.println("验证结果2:" + jwtUtils.verifyAsymmetryToken(token, payloadMap));
}
}
执行结果如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyTmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNzE3MDUyOTA5LCJ1c2VySWQiOiIwMDcifQ.jiVFjX1WQiaJiMBjT1gTCENKq4D3e6RuvkP9bv0LDcz9yw9DxPkzh9loR69zK9BQWOREIFPUe3OONiljmfJmJJtg69Qry98QmoiCeUCLWkWFWU1Jh0UvUL0jze5eRXk0lP8_-LvN0fXJCStzhf4LJWS-0hxr8-v8KkLsOL9RAzq5k5cceenINrPtCRMgYJFpmWRPFEbMrtFSGJREZJzb3I_yAHspYMNwU-R_vIx9OfkRp60rtD7WifC2rxVWuXtf_SZMkws1yIVbMsYzg-nI-mCeWqVjaEVYnzWFml6Y4Uh7ZQ4-5WfctzPTAydQSNy3wX6W5FSXwPTvyZ13k-zk2Q
验证结果1:true
验证结果2:true
非对称加密也广泛应用在安全通信方面,此时公钥由发送方持有,私钥由接收方持有,例如:
Alice 想给 Bob 发送一条加密信息:
- Bob 生成一对密钥,并将公钥发送给 Alice。
- Alice 使用 Bob 的公钥加密信息,然后发送给 Bob。
- Bob 使用自己的私钥解密信息。
无论是在数字签名还是安全通信中,私钥都必须保密,只能由密钥对的所有者持有使用;而公钥可以公开分发,需要进行加密通信或验证签名的一方都可以持有公钥。