JWT 介绍
JWT 介绍
JWT (JSON Web Token) 是一种开放标准 (RFC 7519),定义了一种紧凑、自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以通过数字签名验证其完整性,可用于身份认证、信息交换等场景。它本身不提供技术实现,而是定义了基于 Token 的会话管理规则,包括 Token 应包含的标准内容和生成过程。
JWT 的核心价值
JWT 解决了传统会话管理的痛点,特别是在分布式系统中。它通过将用户身份信息编码到 Token 中,实现了无状态认证,降低了服务器存储压力,提高了系统的可扩展性和安全性。
具体来说,JWT 的核心价值体现在以下几个方面:
- 无状态认证:服务器无需存储会话信息,Token 本身包含了完整的用户身份信息
- 分布式友好:在微服务架构中,Token 可以在多个服务间传递,避免了 session 共享的复杂性
- 跨平台兼容:不依赖 Cookie,可在不同平台(Web、移动应用、IoT设备)间无缝工作
- 减少数据库查询:用户信息直接包含在 Token 中,减少了频繁查询数据库的需求
- 易于扩展:可通过自定义声明添加额外的业务信息
JWT 的应用场景
JWT 在现代 Web 应用中有着广泛的应用,主要包括以下场景:
1. 身份认证
这是 JWT 最常见的应用场景。用户登录成功后,服务器生成 JWT 并返回给客户端,客户端后续请求携带此 JWT 进行身份验证。
2. 信息交换
JWT 可以安全地在各方之间传输信息,由于其数字签名机制,接收方可以验证信息的完整性和发送方的身份。
3. 授权控制
在复杂系统中,JWT 可以携带用户的角色和权限信息,实现细粒度的访问控制。
4. 单点登录 (SSO)
JWT 可以用于实现跨应用的单点登录,用户只需登录一次,即可访问多个相关应用。
5. API 安全
API 接口可以使用 JWT 进行身份验证和授权,确保只有合法用户可以访问特定的 API 资源。
JWT 知识体系结构
首先,俺们先来看看一个 JWT Token 长这样。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDQ1MTE3NDMsImp0aSI6IjYxYmVmNjkyLTE4M2ItNGYxYy1hZjE1LWUwMDM0MTczNzkxOSJ9.CZzB2-JI1oPRFxNMaoFz9-9cKGTYVXkOC2INMoEYNNA
仔细辨别会发现它由 A.B.C
三部分组成,这三部分依次是头部(Header)、负载(Payload)、签名(Signature),头部和负载以 JSON 形式存在,这就是 JWT 中的 JSON,三部分的内容都分别单独经过了 Base64 编码,以 .
拼接成一个 JWT Token。
头部(Header)
JWT 的 Header 中存储了所使用的加密算法和 Token 类型。
{ "alg": "HS256", "typ": "JWT" }
负载(Payload)
Payload 是负载,JWT 规范规定了一些字段,并推荐使用,开发者也可以自己指定字段和内容,例如下面的内容。
{ id: 1024 username: 'admin', exp: 1544602234 }
需要注意的是,Payload的内容只经过了 Base64 编码,对客户端来说当于明文存储,所以不要放置敏感信息。
签名(Signature)
Signature 部分用来验证 JWT Token 是否被篡改,所以这部分会使用一个 Secret 将前两部分加密,逻辑如下。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT 优势 & 问题
思考一下:
为什么要用JWT?
互联网服务离不开用户认证,传统的认证方式是通过cookie和session的方式进行认证,但是分布式web应用的普及,通过session 管理用户登录状态成本越来越高,你想想看,当业务服务分布在多台服务器时,该怎么进行session认证呢。
有人会说,每个业务服务都保持session同步不就好了,你细想,是不是要维护每台session,每台服务器都要读取 session,还有串session的风险,代价是不是很大。
有人还会说,像sso一样,所有请求先经过认证服务,session只在认证服务上保存不就好了。其实这种方式也为尝不可,只是每个用户都会产生一个session进行维护,客户端还需维护cookie,session量大起来了的话复杂度也随之提高了,session的存取都会占用服务器资源,用户量很大的情况下,会产生各种问题,性能比较低下。
优势
JWT 拥有基于 Token 的会话管理方式所拥有的一切优势,不依赖 Cookie,使得其可以防止 CSRF 攻击(通过伪装来自受信任用户的请求来利用受信任的网站),也能在禁用 Cookie 的浏览器环境中正常运行。
问题
而 JWT 的最大优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展,避免存储 Session 所需要引入的 Redis 等组件,降低了系统架构复杂度。但这也是 JWT 最大的劣势,由于有效期存储在 Token 中,JWT Token 一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁用用户,单纯使用 JWT 就无法做到了。
JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
基于 JWT 的实践
既然 JWT 依然存在诸多问题,甚至无法满足一些业务上的需求,但是我们依然可以基于 JWT 在实践中进行一些改进,来形成一个折中的方案
Token续期
对于了解jwt的同学来说,jwt有个弊端,jwt不允许续签时间,时间到期,token就过期。以下实践方案也是续签的一直思路。
在 JWT 的实践中,引入 Refresh Token,将会话管理流程改进如下。
- 客户端使用用户名密码进行认证
- 服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)
- 客户端访问需要认证的接口时,携带 Access Token
- 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
- 如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
- 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token(也可下发新的Refresh Token以达到未退出浏览器未关闭一直登录的状态)
- 客户端使用新的 Access Token 访问需要认证的接口
- 如果 Refresh Token 过期,则重新使用用户名密码进行认证

将生成的 Refresh Token 以及过期时间存储在服务端的数据库中,由于 Refresh Token 不会在客户端请求业务接口时验证,只有在申请新的 Access Token 时才会验证,所以将 Refresh Token 存储在数据库中,不会对业务接口的响应时间造成影响,也不需要像 Session 一样一直保持在内存中以应对大量的请求。
上述的架构,提供了服务端禁用用户 Token 的方式,当用户需要登出或禁用用户时,只需要将服务端的 Refresh Token 禁用或删除,用户就会在 Access Token 过期后,由于无法获取到新的 Access Token 而再也无法访问需要认证的接口。这样的方式虽然会有一定的窗口期(取决于 Access Token 的失效时间),但是结合用户登出时客户端删除 Access Token 的操作,基本上可以适应常规情况下对用户认证鉴权的精度要求。
生产环境中JWT的典型实现方式:
在生产环境下,我们通常结合JWT的Access Token和Refresh Token机制来实现安全且用户友好的认证系统。以下是一个典型的实现方案:
1. 认证流程设计
2. 核心实现代码
1、倒入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
JWT工具类:
/**
* JWT工具类,提供token生成、验证和解析功能
*/
public class JwtUtils {
// 密钥,实际项目中应从配置文件或密钥管理服务获取
private static final String SECRET_KEY = "your-secret-key-keep-it-safe";
// Access Token过期时间(毫秒)
private static final long ACCESS_TOKEN_EXPIRE_TIME = 15 * 60 * 1000;
// Refresh Token过期时间(毫秒)
private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
/**
* 生成Access Token
* @param userId 用户ID
* @param username 用户名
* @param roles 用户角色
* @return Access Token
*/
public static String generateAccessToken(Long userId, String username, List<String> roles) {
return Jwts.builder()
.setSubject(userId.toString())
.claim("username", username)
.claim("roles", roles)
.claim("tokenType", "access")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
/**
* 生成Refresh Token
* @param userId 用户ID
* @return Refresh Token
*/
public static String generateRefreshToken(Long userId) {
return Jwts.builder()
.setSubject(userId.toString())
.claim("tokenType", "refresh")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
/**
* 验证Token有效性
* @param token 待验证的Token
* @return 验证结果
*/
public static boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
// 各种异常情况:过期、签名错误、格式错误等
return false;
}
}
/**
* 从Token中获取用户ID
* @param token Token
* @return 用户ID
*/
public static Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
// 其他辅助方法...
}
认证服务实现:
/**
* 认证服务实现类
*/
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 用户登录
* @param loginRequest 登录请求
* @return 认证响应
*/
@Override
public AuthResponse login(LoginRequest loginRequest) {
// 1. 验证用户身份
User user = userService.verifyUser(loginRequest.getUsername(), loginRequest.getPassword());
if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
// 2. 生成Access Token和Refresh Token
String accessToken = JwtUtils.generateAccessToken(
user.getId(), user.getUsername(), user.getRoles());
String refreshToken = JwtUtils.generateRefreshToken(user.getId());
// 3. 存储Refresh Token到Redis(设置过期时间)
String refreshTokenKey = "jwt:refresh:" + user.getId();
redisTemplate.opsForValue().set(
refreshTokenKey, refreshToken,
JwtUtils.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
// 4. 返回认证响应
return new AuthResponse(accessToken, refreshToken);
}
/**
* 刷新Access Token
* @param refreshToken Refresh Token
* @return 新的认证响应
*/
@Override
public AuthResponse refreshToken(String refreshToken) {
// 1. 验证Refresh Token有效性
if (!JwtUtils.validateToken(refreshToken)) {
throw new AuthenticationException("无效的Refresh Token");
}
// 2. 获取用户ID
Long userId = JwtUtils.getUserIdFromToken(refreshToken);
// 3. 验证Refresh Token是否与存储的一致
String storedRefreshToken = redisTemplate.opsForValue().get("jwt:refresh:" + userId);
if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
throw new AuthenticationException("Refresh Token已过期或无效");
}
// 4. 获取用户信息
User user = userService.getUserById(userId);
if (user == null) {
throw new AuthenticationException("用户不存在");
}
// 5. 生成新的Access Token和Refresh Token
String newAccessToken = JwtUtils.generateAccessToken(
user.getId(), user.getUsername(), user.getRoles());
String newRefreshToken = JwtUtils.generateRefreshToken(user.getId());
// 6. 更新存储的Refresh Token
redisTemplate.opsForValue().set(
"jwt:refresh:" + userId, newRefreshToken,
JwtUtils.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
// 7. 返回新的认证响应
return new AuthResponse(newAccessToken, newRefreshToken);
}
/**
* 用户登出
* @param userId 用户ID
*/
@Override
public void logout(Long userId) {
// 删除存储的Refresh Token
redisTemplate.delete("jwt:refresh:" + userId);
}
}
3. 实现要点与优势
核心要点:
- 双Token机制: Access Token(短期)用于API访问,Refresh Token(长期)用于获取新的Access Token
- 安全存储: Access Token存储在内存中(如Vuex/Redux),Refresh Token存储在本地存储(LocalStorage)或Cookie中
- 服务端控制: Refresh Token存储在Redis中,允许服务端主动失效Token
- 自动续期: 客户端检测Access Token即将过期时,自动使用Refresh Token获取新Token
优势:
- 安全性高: Access Token有效期短,即使泄露风险也小;Refresh Token存储在服务端可被主动失效
- 用户体验好: 用户无需频繁登录,Token自动续期
- 无状态设计: Access Token本身包含用户信息,服务端无需额外存储(除Refresh Token外)
- 跨平台支持: 不依赖Cookie,适用于Web、移动APP等各种客户端
- 灵活控制: 服务端可以通过Redis控制用户登录状态,支持单点登录、强制登出等功能
4. 避坑指南
- 密钥安全: 绝不要硬编码密钥,应使用配置文件或密钥管理服务
- Token存储: Access Token不要存储在本地存储,防止XSS攻击;Refresh Token存储时可设置HttpOnly标志
- 过期时间: Access Token有效期不宜过长(15-30分钟),Refresh Token有效期可适当延长(7-30天)
- 续期策略: 客户端应在Access Token过期前(如剩余5分钟)进行续期,避免请求失败
- 注销机制: 用户注销时,客户端应清除本地Token,服务端应删除存储的Refresh Token
- CSRF防护: 如果使用Cookie存储Refresh Token,需开启CSRF防护
- 限流措施: 对Refresh Token接口实施限流,防止暴力破解
5. 深度思考
思考题: 为什么生产环境中通常使用双Token机制(Access Token + Refresh Token)而不是单Token机制?
回答: 双Token机制平衡了安全性和用户体验。单Token机制如果有效期短,用户需要频繁登录;如果有效期长,泄露风险大。双Token机制中,Access Token有效期短(安全),Refresh Token有效期长(用户体验好),同时Refresh Token存储在服务端可被主动控制,进一步提高安全性。此外,双Token机制还便于实现Token自动续期,无需用户干预。
服务端可以随时踢掉用户当前的登录状态,服务端只需要重置token即可。随后的客户端请求所携带的token都会被服务端判定失效,把客户端打回登录界面。
用户登出,则只需要客户端删除存储的token即可,服务端不必理会。下次用户登录时,服务端自会重新给该用户分配一个新的token。