一、JWT简介
JWT
全称(Json web token)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用 JSON 对象在各方之间安全地传输信息。此信息是经过数字签名的,因此可以验证和信任。翻译自JWT官网,有兴趣同学可以去跳转-JWT官网文档地址
What is JSON Web Token?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
二、场景
在讲JWT之前,想先分享下它的用途,这样大家就可以从实际场景出发更透彻的认识这个工具。JWT的使用场景主要包含两个部分摘自官网
- 授权:这是使用JWT最常见的场景。用户登录后,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是目前广泛使用JWT的一个特性,因为它的开销很小,并且能够轻松地跨不同的域使用。
- 信息交换:JSON Web令牌是各方之间安全传输信息的好方法。因为可以对jwt进行签名(例如,使用公钥/私钥对),所以可以确保发送者就是他们所说的那个人。此外,由于签名是使用报头和有效负载计算的,因此您还可以验证内容是否未被篡改。
知道用途后下面聊到JWT基本结构、特点和具体场景中的Demo
三、结构
JWT是有三部分组成:Header
、Payload
、Signature
,三部分组合生成一个由英文.
连接类似xxxxx.yyyyy.zzzzz
格式的加密的Token,其中三部分内容是:
"JwtConfig": {
"SecretKey": "5610703d-d774-2b4b-836e-996ada2bb75b", // 密钥
"Issuer": "wuyun", // 颁发者
"Audience": "user", // 接收者
"Expired": 1 // 过期时间(30min)
},
-
Header:头部是一个Json格式包裹,其中包含两部分类似:
json { "alg": "HS256", "typ": "JWT" }
,Token 类型和加密算法类型。可以根据自己需求进行选择适合的加密算法,当然不设置JWT也会有默认值。 -
Payload:负载部分保存用户加密的数据,通过签名和见
-
Signature:使用编码后的header和payload以及我们提供的一个密钥,然后使用header中指定的签名算法进行签名,签名的作用是保证JWT没有被篡改过,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
这三部分均用base64进行编码,并使用 . 进行分隔。一个典型的 JWT 格式的 token 类似
四、特点
优点
- 自包含:经过加密的Payload可以存放负载信息
- 无状态:交互中减少资源消耗,直接在请求上下文获取负载信息
- 简洁:可以通过 Post/Get请求通过参数或header存储,且数据量小
缺点
- 占带宽:实际业务中相比SessionId更长,占用请求带宽会更多
- 劫持问题:因为 token是无状态,会导致前端注销后段仍然存储,会导致劫持问题
- 性能问题:大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,对于两个层面的签名会增加CPU开销
备注
:建议JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息,这样可以避免一些安全问题,由于JWT需要验证签名,因此会耗费较多的CPU资源,特别是对单线程的环境友好。
五、场景演示(Java)
- 导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
- 授权场景
package com.bs.service.impl;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.bs.service.JwtService;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.security.*;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
/**
* @name: JwtServiceImpl
* @description: JWT实现类
* @author: BrownSugar
* @date: 2024-01-19 09:54:34
* @version: 1.0.0
* @see JwtService
**/
@Service
@RequiredArgsConstructor
public class JwtServiceImpl implements JwtService {
private static final Logger LOG = LoggerFactory.getLogger(JwtServiceImpl.class);
private final static String ISSUER = "BS";
private final static String SUBJECT = "JWT-SUBJECT";
/**
* 私钥
*/
private static PrivateKey PRIVATE_KEY;
/**
* 公钥
*/
private static PublicKey PUBLIC_KEY;
/**
* @description: 载入密钥
* @param: []
* @author: BrownSugar
* @date: 2024-01-12 04:49:34
**/
@PostConstruct
public void loadPrivateKey() {
try{
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PUBLIC_KEY = keyPair.getPublic();
PRIVATE_KEY = keyPair.getPrivate();
}catch (Exception e){
LOG.error("类名:{} 异常消息 : {}", this.getClass().getSimpleName(), e);
}
}
/**
* @return {@code String }
* @description: 对象/对象集合加签
* @param: [key, value, dateField, offset]
* @author: BrownSugar
* @date: 2024-01-22 02:13:56
**/
@Override
public String sign(String key, Object value, DateField dateField, int offset) {
DateTime date = DateUtil.date();
String compact = Jwts
.builder()
.setSubject(SUBJECT)
.setIssuer(ISSUER)
.claim(key, JSONUtil.toJsonStr(value))
.setId(this.createJWTID())
.setExpiration(DateUtil.offset(date, dateField, offset))
.signWith(PRIVATE_KEY, SignatureAlgorithm.RS256)
.compact();
return compact;
}
/**
* @return {@code T }
* @description: 对象解析
* @param: [key, token, clazz]
* @author: BrownSugar
* @date: 2024-01-12 05:57:16
**/
@Override
public <T> T unSign(String key, String token, Class<T> clazz) {
Object o = Jwts.parser()
.setSigningKey(PUBLIC_KEY)
.parseClaimsJws(token)
.getBody()
//根据name和type取中相应的value
.get(key);
return JSONUtil.toBean((String) o, clazz);
}
/**
* @return {@code List<T> }
* @description: 对象集合解析
* @param: [key, token, clazz]
* @author: BrownSugar
* @date: 2024-01-22 02:14:57
**/
@Override
public <T> List<T> unSignToList(String key, String token, Class<T> clazz) {
Object o = Jwts.parser()
.setSigningKey(PUBLIC_KEY)
.parseClaimsJws(token)
.getBody()
//根据name和type取中相应的value
.get(key);
return JSONUtil.toList((String) o, clazz);
}
/**
* @return {@code String }
* @description: 生成JWT编号
* @param: []
* @author: BrownSugar
* @date: 2024-01-12 04:27:23
**/
private String createJWTID() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
}
@Test
public void signUserInfoTest() {
User user = new User();
user.setId(1);
user.setNickName("张三");
user.setUsername("Miss Zhang");
//LOG.warn("user:{}", user);
String token = jwtService.sign("user", user, DateField.MINUTE, 5);
LOG.warn("token:{}", token);
User unSignUser = jwtService.unSign("user", token, User.class);
LOG.warn("unSignUser:{}", unSignUser);
}
测试结果
:
- 信息交换
对文件加载接口进行加密,提前准备一个文件加载接口,在业务中需要加载文件地方返回给客户端一个带有token的请求地址,再通过文件加载接口解析token并将文件流返回给客户端,这样用户可以在无感且加密的状态下获取文件,而且还可以在生成token的时候设置过期时间,这样保证了文件的安全性。
@Test
public void signFileIdTest() {
// 业务接口返回文件地址
String fileId = "1";
LOG.warn("fileId:{}", fileId);
String token = jwtService.sign("fileId", fileId, DateField.MINUTE, 5);
LOG.warn("token:{}", token);
String url = "https://www.csdn.com/file/load/" + token;
LOG.warn("请求地址:{}", url);
// load接口逻辑,参数:(HttpServletResponse response, String token)
String unSignFileId = jwtService.unSign("fileId", token, String.class);
// TODO 这里通过文件ID获取文件流,将文件流和文件类型存放在Response中并返回
LOG.warn("value:{}", unSignFileId);
}
测试结果
:
六、总结
JWT的使用优于传统的SessionId,因为它减少了服务器端的负担,并且不需要在服务器端保存会话信息。尽管JWT在小型Web应用程序中使用较为常见,但它同样适用于大型分布式系统,只要正确实施了安全措施。总结来说,JWT是一种轻量级、高效且安全的认证和授权解决方案,适合现代Web开发和应用场景