
在分布式系统和微服务架构普及的今天,传统的Session-Cookie认证机制逐渐暴露出短板:Session依赖服务端存储,分布式环境下需要Redis等中间件实现共享,跨域场景下Cookie的传输受限,移动端适配也存在诸多不便。而JSON Web Token(JWT)凭借无状态、自包含、跨域友好的特性,成为解决这些问题的主流方案。本文将从底层原理到企业级实战,全方位拆解JWT的设计逻辑、安全机制与落地实践,让你既能吃透原理,又能直接应用到生产环境。
JWT(JSON Web Token)是一种紧凑的、自包含的安全传输协议,通过JSON格式在各方之间传递信息。它的核心价值在于:信息本身可验证——接收方通过签名就能确认数据未被篡改,无需依赖第三方存储。
JWT由三部分组成,用.分隔,格式为Header.Payload.Signature。例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header用于描述JWT的元数据,包含两个核心字段:
alg:签名算法(如HS256(HMAC-SHA256)、RS256(RSA-SHA256))typ:令牌类型,固定为JWT示例Header(JSON):
{
"alg": "HS256",
"typ": "JWT"
}
最终会被Base64Url编码为第一段字符串。
Payload是JWT的核心数据区,包含声明(Claims)——即需要传递的信息。声明分为三类:
iss:签发者(Issuer)exp:过期时间(Expiration Time),必须是NumericDate格式(Unix时间戳)sub:主题(Subject)aud:受众(Audience)iat:签发时间(Issued At)示例Payload(JSON):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"userId": 1001,
"role": "ADMIN"
}
同样会被Base64Url编码为第二段字符串。注意:Base64Url是可逆编码,Payload中绝对不能存放密码、令牌等敏感信息!
Signature是JWT的安全核心,用于验证数据完整性和来源合法性。生成规则:
.拼接以HS256算法为例,签名公式:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
最终签名结果作为第三段字符串。
服务端验证时,会重新计算签名并与接收的Signature对比:若一致,说明数据未被篡改且来源合法;若不一致,则令牌无效。
JWT的认证流程完全基于令牌传递,无需服务端存储状态,具体步骤如下:

Authorization头(格式:Bearer <token>)携带。<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>jwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt-demo</name>
<description>JWT实战演示项目</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- SpringBoot Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- 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>
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
<scope>runtime</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring:
# 数据库配置
datasource:
url:jdbc:mysql://localhost:3306/jwt_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username:root
password:root
driver-class-name:com.mysql.cj.jdbc.Driver
# 日志配置
logging:
level:
com.jam.demo.mapper:debug
# MyBatisPlus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case:true
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type:auto
# JWT自定义配置
jwt:
secret:76a9f2a89e7d4b3a8f2d7c6b5a4s3d2f1g7h8j9k0l1p2o3i4u5y6t7r8e9w0q# 生产环境需用强密钥(建议256位以上)
expiration:3600000# 令牌过期时间(1小时,单位:毫秒)
refresh-expiration:86400000# 刷新令牌过期时间(24小时)
CREATE DATABASEIFNOTEXISTS jwt_demo DEFAULTCHARACTERSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jwt_demo;
CREATETABLE`user` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`username`varchar(50) NOTNULLCOMMENT'用户名',
`password`varchar(100) NOTNULLCOMMENT'密码(BCrypt加密)',
`role`varchar(20) NOTNULLCOMMENT'角色(ADMIN/USER)',
`create_time` datetime DEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`update_time` datetime DEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_username` (`username`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 插入测试数据(密码:123456)
INSERTINTO`user` (`username`, `password`, `role`)
VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'ADMIN'),
('user', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'USER');
package com.jam.demo.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
/**
* JWT工具类,提供令牌生成、验证、解析功能
* @author ken
*/
@Component
@Slf4j
publicclass JwtUtil {
/**
* JWT签名密钥
*/
@Value("${jwt.secret}")
private String secret;
/**
* 访问令牌过期时间(毫秒)
*/
@Value("${jwt.expiration}")
privatelong expiration;
/**
* 刷新令牌过期时间(毫秒)
*/
@Value("${jwt.refresh-expiration}")
privatelong refreshExpiration;
/**
* 生成访问令牌
* @param claims 自定义声明
* @return JWT令牌
*/
public String generateAccessToken(Map<String, Object> claims) {
return generateToken(claims, expiration);
}
/**
* 生成刷新令牌
* @param claims 自定义声明
* @return 刷新令牌
*/
public String generateRefreshToken(Map<String, Object> claims) {
return generateToken(claims, refreshExpiration);
}
/**
* 生成JWT令牌
* @param claims 自定义声明
* @param expiration 过期时间(毫秒)
* @return JWT令牌
*/
private String generateToken(Map<String, Object> claims, long expiration) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析JWT令牌
* @param token JWT令牌
* @return Claims对象
*/
public Claims parseToken(String token) {
if (!StringUtils.hasText(token)) {
thrownew IllegalArgumentException("令牌不能为空");
}
try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.error("令牌已过期:{}", e.getMessage());
thrownew RuntimeException("令牌已过期");
} catch (UnsupportedJwtException e) {
log.error("不支持的令牌格式:{}", e.getMessage());
thrownew RuntimeException("不支持的令牌格式");
} catch (MalformedJwtException e) {
log.error("令牌格式错误:{}", e.getMessage());
thrownew RuntimeException("令牌格式错误");
} catch (SignatureException e) {
log.error("令牌签名无效:{}", e.getMessage());
thrownew RuntimeException("令牌签名无效");
} catch (IllegalArgumentException e) {
log.error("令牌参数错误:{}", e.getMessage());
thrownew RuntimeException("令牌参数错误");
}
}
/**
* 验证令牌是否有效
* @param token JWT令牌
* @return true-有效,false-无效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
returntrue;
} catch (Exception e) {
returnfalse;
}
}
/**
* 从令牌中获取用户ID
* @param token JWT令牌
* @return 用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
/**
* 从令牌中获取用户名
* @param token JWT令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("username", String.class);
}
}
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("user")
publicclass User {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码(BCrypt加密)
*/
private String password;
/**
* 角色(ADMIN/USER)
*/
private String role;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
package com.jam.demo.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatisPlus字段自动填充
* @author ken
*/
@Component
@Slf4j
publicclass MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("开始插入填充...");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("开始更新填充...");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;
/**
* 用户服务接口
* @author ken
*/
publicinterface UserService extends IService<User> {
/**
* 用户登录
* @param loginVo 登录参数
* @return 令牌信息
*/
TokenVo login(LoginVo loginVo);
/**
* 刷新令牌
* @param refreshToken 刷新令牌
* @return 新的令牌信息
*/
TokenVo refreshToken(String refreshToken);
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import com.jam.demo.util.JwtUtil;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 用户服务实现类
* @author ken
*/
@Service
@Slf4j
publicclass UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private JwtUtil jwtUtil;
/**
* BCrypt密码编码器
*/
privatefinal BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override
public TokenVo login(LoginVo loginVo) {
// 参数校验
String username = loginVo.getUsername();
String password = loginVo.getPassword();
StringUtils.hasText(username, "用户名不能为空");
StringUtils.hasText(password, "密码不能为空");
// 查询用户
User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null) {
thrownew RuntimeException("用户名不存在");
}
// 验证密码
if (!passwordEncoder.matches(password, user.getPassword())) {
thrownew RuntimeException("密码错误");
}
// 构建JWT声明
Map<String, Object> claims = Maps.newHashMap();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("role", user.getRole());
// 生成令牌
String accessToken = jwtUtil.generateAccessToken(claims);
String refreshToken = jwtUtil.generateRefreshToken(claims);
// 返回令牌信息
TokenVo tokenVo = new TokenVo();
tokenVo.setAccessToken(accessToken);
tokenVo.setRefreshToken(refreshToken);
tokenVo.setExpiresIn(jwtUtil.getExpiration() / 1000); // 秒级返回
log.info("用户{}登录成功,生成令牌", username);
return tokenVo;
}
@Override
public TokenVo refreshToken(String refreshToken) {
// 验证刷新令牌
if (!jwtUtil.validateToken(refreshToken)) {
thrownew RuntimeException("刷新令牌无效");
}
// 解析刷新令牌获取用户信息
Long userId = jwtUtil.getUserIdFromToken(refreshToken);
String username = jwtUtil.getUsernameFromToken(refreshToken);
User user = this.getById(userId);
if (user == null) {
thrownew RuntimeException("用户不存在");
}
// 重新生成访问令牌
Map<String, Object> claims = Maps.newHashMap();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("role", user.getRole());
String newAccessToken = jwtUtil.generateAccessToken(claims);
TokenVo tokenVo = new TokenVo();
tokenVo.setAccessToken(newAccessToken);
tokenVo.setRefreshToken(refreshToken); // 刷新令牌未过期可复用
tokenVo.setExpiresIn(jwtUtil.getExpiration() / 1000);
log.info("用户{}刷新令牌成功", username);
return tokenVo;
}
}
package com.jam.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 登录请求VO
* @author ken
*/
@Data
@Schema(description = "登录请求参数")
publicclass LoginVo {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码")
private String password;
}
package com.jam.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 令牌返回VO
* @author ken
*/
@Data
@Schema(description = "令牌信息")
publicclass TokenVo {
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "访问令牌过期时间(秒)")
private Long expiresIn;
}
package com.jam.demo.interceptor;
import com.jam.demo.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* JWT拦截器,验证请求令牌
* @author ken
*/
@Component
@Slf4j
publicclass JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的令牌
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
token = token.substring(7); // 截取Bearer后的令牌
}
// 验证令牌
if (!jwtUtil.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: 令牌无效或已过期");
returnfalse;
}
// 将用户信息存入ThreadLocal(可选,便于业务层获取)
Long userId = jwtUtil.getUserIdFromToken(token);
String username = jwtUtil.getUsernameFromToken(token);
request.setAttribute("userId", userId);
request.setAttribute("username", username);
returntrue;
}
}
package com.jam.demo.config;
import com.jam.demo.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类,注册拦截器
* @author ken
*/
@Configuration
publicclass WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**") // 需要拦截的路径
.excludePathPatterns("/api/auth/login", "/api/auth/refresh"); // 排除登录和刷新令牌接口
}
}
package com.jam.demo.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger3配置类
* @author ken
*/
@Configuration
publicclass SwaggerConfig {
@Bean
public OpenAPI openAPI() {
returnnew OpenAPI()
.info(new Info()
.title("JWT认证接口文档")
.description("JWT实战项目的API接口文档")
.version("1.0.0"));
}
}
package com.jam.demo.controller;
import com.jam.demo.service.UserService;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
/**
* 认证控制器
* @author ken
*/
@RestController
@RequestMapping("/api/auth")
@Tag(name = "认证接口", description = "登录、刷新令牌")
@Slf4j
publicclass AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
@Operation(summary = "用户登录", description = "验证用户名密码并返回令牌")
public TokenVo login(@Valid @RequestBody LoginVo loginVo) {
return userService.login(loginVo);
}
@PostMapping("/refresh")
@Operation(summary = "刷新令牌", description = "用刷新令牌获取新的访问令牌")
public TokenVo refreshToken(@RequestParam String refreshToken) {
return userService.refreshToken(refreshToken);
}
}
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试控制器(需认证)
* @author ken
*/
@RestController
@RequestMapping("/api/test")
@Tag(name = "测试接口", description = "需要JWT认证的测试接口")
publicclass TestController {
@GetMapping("/info")
@Operation(summary = "获取用户信息", description = "获取当前登录用户的信息")
public String getInfo(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
String username = (String) request.getAttribute("username");
return String.format("用户ID:%s,用户名:%s,认证成功!", userId, username);
}
}
访问Swagger3文档地址:http://localhost:8080/swagger-ui/index.html,可看到所有接口。
调用/api/auth/login接口,传入参数:
{
"username": "admin",
"password": "123456"
}
返回结果:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 3600
}
调用/api/test/info接口,请求头添加Authorization: Bearer <accessToken>,返回:
用户ID:1,用户名:admin,认证成功!
调用/api/auth/refresh接口,传入刷新令牌,返回新的访问令牌。
JWT的安全性完全依赖于实现细节,以下是企业级开发必须遵守的最佳实践:
Keys.secretKeyFor(SignatureAlgorithm.HS256)生成安全密钥。exp(过期时间)、iss(签发者)、aud(受众)等声明,避免无效令牌。特性 | JWT认证 | Session认证 |
|---|---|---|
存储位置 | 客户端(localStorage/cookie) | 服务端(内存/Redis) |
状态 | 无状态(服务端无需存储) | 有状态(服务端需维护Session) |
分布式支持 | 天然支持(无需共享) | 需Redis等中间件共享Session |
跨域支持 | 友好(令牌可跨域传递) | 受限(Cookie跨域需配置) |
性能 | 高(无数据库/缓存查询) | 中(需查询Session) |
扩展性 | 强(支持多端) | 弱(依赖Cookie) |
安全性 | 依赖实现(需HTTPS+强密钥) | 较高(Cookie可设HttpOnly) |
令牌撤销 | 复杂(需黑名单) | 简单(直接删除Session) |
访问令牌短期有效,刷新令牌长期有效。当访问令牌过期时,客户端用刷新令牌获取新的访问令牌,避免用户频繁登录。实现要点:
JWT本身无法主动撤销,企业级需结合Redis实现黑名单:
JWT作为无状态认证方案,完美解决了分布式系统的认证痛点,但它并非银弹——需严格遵守安全最佳实践,才能发挥其优势。本文从原理到实战,覆盖了JWT的核心知识和企业级落地细节,你可直接基于示例代码搭建生产级认证系统。记住:JWT的安全性在于细节,强密钥、HTTPS、合理的过期时间是保障系统安全的基石。