JWT全面理解
JWT 的本质是一串经过加密签名的 JSON 格式字符串,用于在客户端和服务器之间安全地传递 “声明”(Claims,即用户身份、权限等信息)。分布式系统中服务间通信(如微服务 A 向微服务 B 传递用户权限信息)、跨组织数据交换(如第三方登录后,平台间传递用户基本信息)。(如 role: admin、permissions: ["read", "write"]),服务器验证 JWT 后,直接。减
目录
JWT是一种基于JSON的轻量级身份认证与信息交换标准,广泛应用于前后端分离、分布式系统、跨服务通信等场景。它通过数字签名保证信息的完整性和可靠性,核心价值在于解决了传统认证方案(如 Session)在跨域、分布式架构下的局限性。
一、JWT是什么
JWT 的本质是一串经过加密签名的 JSON 格式字符串,用于在客户端和服务器之间安全地传递 “声明”(Claims,即用户身份、权限等信息)。其核心作用可概括为三类:
1、身份认证(最核心用途)
解决 “用户是谁” 的问题,替代传统的 Session 认证,流程如下:
- 用户通过账号密码登录,服务器验证通过后,生成包含用户唯一标识(如 user_id)、过期时间等信息的 JWT;
- 服务器将 JWT 直接返回给客户端(无需存储在服务器);
- 后续客户端发起请求时,在 HTTP 头(如 Authorization: Bearer <JWT>)中携带 JWT;
- 服务器接收请求后,验证 JWT 的签名和有效性(如是否过期、是否被篡改),验证通过即可确认用户身份,无需查询数据库或缓存。
2、信息交换
解决 “安全传递数据” 的问题。由于 JWT 带有数字签名,接收方可以通过签名验证信息是否来自合法发送方,且未被篡改。
分布式系统中服务间通信(如微服务 A 向微服务 B 传递用户权限信息)、跨组织数据交换(如第三方登录后,平台间传递用户基本信息)。
3、授权控制
解决 “用户能做什么” 的问题。可在 JWT 中嵌入用户权限信息(如 role: admin、permissions: ["read", "write"]),服务器验证 JWT 后,直接从载荷中读取权限,无需额外查询权限数据库。
后台管理系统中,根据 JWT 中的 role 字段判断用户是否有权访问某接口(如仅管理员可调用 “删除用户” 接口)。
二、JWT的核心价值
对比传统的 Session 认证(服务器存储用户会话,客户端保存 Session ID),JWT 的价值主要体现在以下 4 点:
|
特性 |
JWT 方案 |
传统 Session 方案 |
价值体现 |
|
无状态(Stateless) |
服务器不存储任何会话信息,仅通过 JWT 验证身份 |
服务器需存储 Session(内存 / 数据库 / Redis) |
减轻服务器存储压力,支持高并发;无需考虑 Session 同步(分布式系统友好) |
|
跨域 / 跨服务 |
基于 HTTP 头传递,支持跨域名、跨服务通信 |
依赖 Cookie,受 “同源策略” 限制,跨域需额外配置 |
适配前后端分离(前端部署在 CDN,后端在另一域名)、微服务架构 |
|
轻量化 |
字符串格式,体积小,传输速度快 |
需携带 Session ID,且服务器需查询 Session 详情 |
减少网络传输开销,提升接口响应速度(尤其移动端弱网场景) |
|
自包含(Self-contained) |
载荷中包含用户核心信息(如 ID、权限) |
仅存储 Session ID,需查询数据库获取用户信息 |
减少数据库查询次数(验证身份时无需查库),提升系统性能 |
三、如何理解JWT的结构和工作原理
JWT 由三部分组成,用英文句号(.)分隔,格式为 Header.Payload.Signature,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX2lkIjoiMTIzIiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTY4OTAxNjQwMH0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1、三部分结构解析
- Header(头部):声明 JWT 的类型(typ,固定为 JWT)和使用的签名算法(alg,如 HS256 哈希算法、RS256 非对称加密算法)。格式是JSON字符串,是可逆的,仅编码而非加密。
- Payload(载荷):存储核心 “声明”(用户信息、过期时间等)。JSON 字符串,同样经过 Base64 编码(可逆!不要存储密码、手机号等敏感信息)。
- Signature(签名):保证 JWT 不被篡改,是 JWT 安全性的核心。拼接编码后的 Header 和 Payload,使用 Header 中声明的算法(如 HS256),结合服务器端的密钥(Secret) 对拼接字符串进行加密,得到签名。
2、核心工作流程
- 签发:用户登录成功 → 服务器根据用户信息构建 Payload → 结合 Header 和密钥生成 Signature → 拼接三部分得到 JWT 并返回给客户端;
- 存储:客户端将 JWT 存储在 localStorage、sessionStorage 或 Cookie 中;
- 携带:客户端发起请求时,在 Authorization 头中携带 JWT(Bearer <JWT>);
- 验证:服务器解析 JWT → 验证签名(防篡改)→ 检查过期时间(exp)→ 提取用户信息 → 处理业务逻辑。
四、JWT的使用步骤
例如我现在实现用户登录的功能,我要通过JWT进行安全认证的步骤如下:
1、添加依赖
<dependencies>
<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT 工具库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Redis 依赖 (用于存储刷新令牌) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2、添加配置文件
spring:
redis:
host: localhost
port: 6379
# JWT配置
jwt:
secret: your-secret-key-1234567890-abcdefg # 密钥,生产环境要复杂且保密
access-token-expire: 3600000 # 访问令牌过期时间(毫秒),1小时
refresh-token-expire: 604800000 # 刷新令牌过期时间(毫秒),7天
3、创建实体类
package com.example.jwtdemo.entity;
import lombok.Data;
@Data
public class Result<T> {
private int code; // 状态码:200成功,401未授权,500服务器错误等
private String message; // 提示信息
private T data; // 响应数据
// 成功返回
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("成功");
result.setData(data);
return result;
}
// 失败返回
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
package com.example.jwtdemo.entity;
import lombok.Data;
@Data
public class TokenDTO {
private String accessToken; // 访问令牌
private String refreshToken; // 刷新令牌
}
package com.example.jwtdemo.entity;
import lombok.Data;
@Data
public class User {
private Integer id;
private String username;
private String password;
private String role; // 角色,如"USER"或"ADMIN"
}
4、创建JWT工具类
package com.example.jwtdemo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.access-token-expire}")
private long accessTokenExpire;
@Value("${jwt.refresh-token-expire}")
private long refreshTokenExpire;
// 从令牌中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从令牌中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从令牌中获取自定义声明
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 解析令牌,获取所有声明
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}
// 检查令牌是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成访问令牌
public String generateAccessToken(String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role); // 存储角色信息
return doGenerateToken(claims, username, accessTokenExpire);
}
// 生成刷新令牌
public String generateRefreshToken(String username) {
return doGenerateToken(new HashMap<>(), username, refreshTokenExpire);
}
// 实际生成令牌的方法
private String doGenerateToken(Map<String, Object> claims, String subject, long expiration) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject) // 用户名作为主题
.setIssuedAt(new Date(System.currentTimeMillis())) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + expiration)) // 过期时间
.signWith(SignatureAlgorithm.HS512, secretKey) // 签名算法和密钥
.compact();
}
// 验证令牌
public Boolean validateToken(String token, String username) {
final String tokenUsername = getUsernameFromToken(token);
return (tokenUsername.equals(username) && !isTokenExpired(token));
}
}
5、创建JWT认证拦截器
package com.example.jwtdemo.config;
import com.example.jwtdemo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取令牌
String authorizationHeader = request.getHeader("Authorization");
// 检查令牌是否存在且格式正确
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("未提供有效的令牌");
return false;
}
// 提取令牌(去掉"Bearer "前缀)
String token = authorizationHeader.substring(7);
try {
// 从令牌中获取用户名
String username = jwtUtil.getUsernameFromToken(token);
// 验证令牌
if (!jwtUtil.validateToken(token, username)) {
throw new Exception("令牌无效");
}
// 令牌有效,将用户名存入请求属性,供后续使用
request.setAttribute("username", username);
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌验证失败:" + e.getMessage());
return false;
}
}
}
7、配置拦截器和Redis
package com.example.jwtdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 注册JWT拦截器
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置拦截器应用的路径
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/api/**") // 拦截/api/**路径的请求
.excludePathPatterns("/api/login") // 不拦截登录接口
.excludePathPatterns("/api/refresh-token"); // 不拦截刷新令牌接口
}
}
8、创建服务层
package com.example.jwtdemo.service;
import com.example.jwtdemo.entity.TokenDTO;
import com.example.jwtdemo.entity.User;
public interface UserService {
// 用户登录
TokenDTO login(String username, String password);
// 刷新访问令牌
String refreshToken(String refreshToken);
// 注销登录
void logout(String username);
// 根据用户名获取用户信息
User getUserByUsername(String username);
}
package com.example.jwtdemo.service.impl;
import com.example.jwtdemo.entity.TokenDTO;
import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.service.UserService;
import com.example.jwtdemo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private StringRedisTemplate redisTemplate;
// 模拟数据库中的用户
private static final User mockUser = new User();
static {
mockUser.setId(1);
mockUser.setUsername("admin");
mockUser.setPassword("123456");
mockUser.setRole("ADMIN");
}
@Override
public TokenDTO login(String username, String password) {
// 验证用户名密码(实际项目中应查询数据库)
if (!mockUser.getUsername().equals(username) || !mockUser.getPassword().equals(password)) {
throw new RuntimeException("用户名或密码错误");
}
// 生成令牌
String accessToken = jwtUtil.generateAccessToken(username, mockUser.getRole());
String refreshToken = jwtUtil.generateRefreshToken(username);
// 将刷新令牌存入Redis,设置过期时间
redisTemplate.opsForValue().set(
"refresh_token:" + username,
refreshToken,
7,
TimeUnit.DAYS
);
TokenDTO tokenDTO = new TokenDTO();
tokenDTO.setAccessToken(accessToken);
tokenDTO.setRefreshToken(refreshToken);
return tokenDTO;
}
@Override
public String refreshToken(String refreshToken) {
try {
// 从刷新令牌中获取用户名
String username = jwtUtil.getUsernameFromToken(refreshToken);
// 验证刷新令牌是否有效
if (!jwtUtil.validateToken(refreshToken, username)) {
throw new RuntimeException("刷新令牌无效");
}
// 检查Redis中是否存在该刷新令牌
String storedToken = redisTemplate.opsForValue().get("refresh_token:" + username);
if (storedToken == null || !storedToken.equals(refreshToken)) {
throw new RuntimeException("刷新令牌已过期或已被注销");
}
// 生成新的访问令牌
User user = getUserByUsername(username);
return jwtUtil.generateAccessToken(username, user.getRole());
} catch (Exception e) {
throw new RuntimeException("刷新令牌失败:" + e.getMessage());
}
}
@Override
public void logout(String username) {
// 从Redis中删除刷新令牌
redisTemplate.delete("refresh_token:" + username);
}
@Override
public User getUserByUsername(String username) {
// 实际项目中应查询数据库
if (mockUser.getUsername().equals(username)) {
return mockUser;
}
throw new RuntimeException("用户不存在");
}
}
9、创建控制器
package com.example.jwtdemo.controller;
import com.example.jwtdemo.entity.Result;
import com.example.jwtdemo.entity.TokenDTO;
import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// 登录接口
@PostMapping("/login")
public Result<TokenDTO> login(
@RequestParam String username,
@RequestParam String password) {
try {
TokenDTO tokenDTO = userService.login(username, password);
return Result.success(tokenDTO);
} catch (Exception e) {
return Result.error(401, e.getMessage());
}
}
// 刷新令牌接口
@PostMapping("/refresh-token")
public Result<String> refreshToken(@RequestParam String refreshToken) {
try {
String newAccessToken = userService.refreshToken(refreshToken);
return Result.success(newAccessToken);
} catch (Exception e) {
return Result.error(401, e.getMessage());
}
}
// 注销接口
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request) {
try {
String username = (String) request.getAttribute("username");
userService.logout(username);
return Result.success(null);
} catch (Exception e) {
return Result.error(500, e.getMessage());
}
}
// 获取用户信息接口(需要认证)
@GetMapping("/user/info")
public Result<User> getUserInfo(HttpServletRequest request) {
try {
String username = (String) request.getAttribute("username");
User user = userService.getUserByUsername(username);
// 出于安全考虑,清除密码信息
user.setPassword(null);
return Result.success(user);
} catch (Exception e) {
return Result.error(500, e.getMessage());
}
}
}
五、核心实现
1、登录流程:用户提交用户名密码 → 验证通过 → 生成 accessToken 和 refreshToken → 返回给客户端
2、认证流程:客户端请求带 accessToken → 拦截器验证 token → 验证通过则允许访问
3、令牌刷新:accessToken 过期 → 用 refreshToken 获取新的 accessToken → 避免重新登录
4、安全机制:
- 密码验证确保用户身份
- 令牌签名防止篡改
- 短期 accessToken 减少泄露风险
- refreshToken 存储在 Redis 中便于注销
六、核心知识点的回答
1、生成token时如何设置过期时间
1.1、直接指定过期时间
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtExample {
private static final String SECRET_KEY = "your-secret-key";
public static String generateToken(String username) {
// 直接设置过期时间为 2024-12-31 23:59:59(硬编码,不推荐)
Date expirationDate = new Date(1735689599000L); // 时间戳(毫秒)
return Jwts.builder()
.setSubject(username) // 存储用户名
.setExpiration(expirationDate) // 设置过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名
.compact();
}
}
1.2、动态计算过期时间
- 获取当前时间戳(System.currentTimeMillis());
- 加上有效期(毫秒单位,如 1 小时 = 3600 * 1000 毫秒);
- 转换为 Date 对象传入 setExpiration()。
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtExample {
private static final String SECRET_KEY = "your-secret-key";
// 有效期:1小时(3600秒 * 1000毫秒)
private static final long EXPIRATION_TIME = 3600 * 1000;
public static String generateToken(String username) {
// 计算过期时间:当前时间 + 有效期
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME);
return Jwts.builder()
.setSubject(username) // 存储用户名
.setIssuedAt(new Date()) // 签发时间(可选,便于追踪)
.setExpiration(expirationDate) // 设置过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法+密钥
.compact();
}
}
1.3、在配置文件中管理过期时间
- 配置文件
- 通过@Value注入配置,动态生成过期时间
jwt: secret: your-secret-key access-token-expire: 3600000 # 访问令牌有效期:1小时(毫秒) refresh-token-expire: 604800000 # 刷新令牌有效期:7天(毫秒)import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtil { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.access-token-expire}") private long accessTokenExpire; // 从配置文件注入有效期(毫秒) // 生成访问令牌(短期有效) public String generateAccessToken(String username) { Date expirationDate = new Date(System.currentTimeMillis() + accessTokenExpire); return Jwts.builder() .setSubject(username) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } }2、JWT有哪些安全隐患?如何解决
- 隐患 1:Payload 可解码,泄露非敏感信息(如用户名);解决:不存储敏感信息,仅存用户 ID 等非敏感标识。
- 隐患 2:令牌泄露后可被恶意使用(无法主动吊销);解决:短期有效期 + 刷新令牌机制 + 令牌黑名单(Redis)。
- 隐患 3:密钥泄露导致令牌可伪造;解决:密钥通过环境变量存储,定期更换;使用 RS256 非对称加密(私钥签名,公钥验证)。
- 隐患 4:传输过程中被劫持;解决:全程使用 HTTPS 加密传输。
3、JWT 和 Session 的区别是什么?
更多推荐


所有评论(0)