在前后端分离的项目开发中,接口的登录校验是保障系统安全的第一道防线。传统的 Session-Cookie 校验方式受限于跨域问题,难以适配分布式架构,而JWT(JSON Web Token) 凭借无状态、跨域友好的特性,成为前后端分离项目的主流登录校验方案。
在实际项目中,我们通过5 个核心类的紧密配合,基于 JWT 封装了一套完整的登录校验机制,实现了 "Token 生成 - 请求拦截 - Token 校验 - 用户信息传递" 的全流程自动化处理,既保证了接口安全,又让用户信息传递更优雅。本文将从核心类职责、代码实现、执行流程三个维度,完整解析这套 JWT 登录校验方案的设计与实现。
一、JWT 登录校验核心思想:无状态的身份认证
在讲解具体实现前,先理解 JWT 登录校验的核心思想,这是整个方案的设计基础:
- 无状态:服务端不存储任何用户登录信息,所有身份信息都加密存储在 JWT Token 中,服务端只需通过秘钥校验 Token 的合法性,无需依赖数据库 / 缓存查询,适配分布式架构;
- 一次登录,全程有效:用户登录成功后,服务端生成包含用户核心信息(如用户 ID)的 Token 返回给前端,后续前端请求需在请求头中携带该 Token,服务端通过校验 Token 完成身份认证;
- 全程自动化:通过 Spring MVC 拦截器实现请求的自动拦截与 Token 校验,校验通过后自动将用户信息存入 ThreadLocal,后续业务层可无感获取,无需手动传参。
整个方案的核心是5 个类各司其职、协同工作,分别承担 "配置读取、Token 处理、请求拦截、注册配置、信息传递" 的职责,形成一套完整的接口安全防护体系。
二、五大核心类:各司其职,构筑 JWT 校验链路
以下按 "执行流程顺序" 讲解每个核心类的具体职责、核心代码和设计细节,所有代码均为项目实战可直接使用的版本,贴合企业级开发规范。
1. JwtProperties:配置读取类 ------ 统一管理 JWT 配置项
核心职责 :专门读取application.yml中的 JWT 相关配置(如秘钥、过期时间、Token 请求头名称),通过 Spring Boot 的配置绑定功能,将配置文件中的数据封装为 Java 对象,供其他类注入使用。设计优势:将配置项与业务代码解耦,后续修改 JWT 配置(如更换秘钥、调整过期时间),只需修改配置文件,无需改动任何业务代码,符合 "配置与代码分离" 的开发原则。
核心代码实现
java
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* JWT配置属性类,绑定application.yml中的JWT配置
*/
@Data // Lombok注解,自动生成get/set方法
@Component // 交给Spring容器管理
@ConfigurationProperties(prefix = "sky.jwt") // 绑定配置文件中前缀为sky.jwt的配置
public class JwtProperties {
/**
* 管理端用户生成Token的秘钥
*/
private String adminSecretKey;
/**
* 管理端用户Token的过期时间(毫秒)
*/
private long adminTtl;
/**
* 管理端用户Token在请求头中的名称
*/
private String adminTokenName;
}
对应 application.yml 配置
# JWT配置
sky:
jwt:
admin-secret-key: sky-admin-2024 # 管理端秘钥,建议生产环境使用复杂随机字符串
admin-ttl: 7200000 # Token过期时间,2小时(毫秒)
admin-token-name: token # 前端请求头中携带Token的名称
2. JwtUtil:工具类 ------Token 的 "生成器" 与 "解析器"
核心职责 :JWT 操作的通用工具类,封装 Token 的生成 和解析 两个核心方法,提供给登录接口和拦截器使用,是 JWT 校验的基础工具。设计优势 :工具类方法设计为static,无需实例化即可调用;核心依赖 JJWT 框架实现 JWT 的加密与解密,屏蔽底层复杂的加密逻辑,对外提供简洁的调用接口。
核心代码实现
java
package com.sky.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
/**
* JWT工具类,生成和解析Token
*/
public class JwtUtil {
/**
* 生成JWT Token
* @param secretKey 加密秘钥
* @param ttlMillis Token过期时间(毫秒)
* @param claims 需存入Token的自定义数据(如用户ID)
* @return 加密后的Token字符串
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 1. 设置Token过期时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date expDate = new Date(expMillis);
// 2. 构建JWT Token
return Jwts.builder()
.setClaims(claims) // 存入自定义数据
.setExpiration(expDate) // 设置过期时间
.signWith(SignatureAlgorithm.HS256, secretKey) // 指定加密算法和秘钥
.compact(); // 生成并返回Token字符串
}
/**
* 解析JWT Token,获取其中的自定义数据
* @param secretKey 加密秘钥(需与生成时一致)
* @param token 待解析的Token字符串
* @return 包含自定义数据的Claims对象
*/
public static Claims parseJWT(String secretKey, String token) {
// 解析Token并获取Claims(存储自定义数据的容器)
return Jwts.parser()
.setSigningKey(secretKey) // 设置解密秘钥
.parseClaimsJws(token) // 解析Token
.getBody(); // 获取自定义数据
}
}
关键说明
- Claims:JJWT 框架提供的容器,用于存储需要加密到 Token 中的自定义数据(如用户 ID),解析 Token 后可直接从 Claims 中获取;
- 加密算法:这里使用 HS256 对称加密算法,生成和解析时使用相同的秘钥,保证 Token 不被篡改;
- 过期时间:生成 Token 时指定过期时间,解析时若 Token 已过期,会直接抛出异常,实现 Token 的自动失效。
3. JwtTokenAdminInterceptor:拦截器 ------ 接口的 "安保人员"
核心职责 :整个 JWT 登录校验的核心类 ,相当于系统的 "安保人员"。实现 Spring MVC 的HandlerInterceptor接口,在请求到达 Controller 前拦截请求,完成 Token 的提取、校验,并将用户信息存入 ThreadLocal。核心时机 :重写preHandle方法,在Controller 方法执行前执行拦截逻辑,校验不通过则直接拦截请求,避免非法请求进入业务层。
核心代码实现
java
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.properties.JwtProperties;
import com.sky.utils.BaseContext;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 管理端JWT Token拦截器,校验请求的Token合法性
*/
@Component // 交给Spring容器管理,方便后续注册
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties; // 注入JWT配置
/**
* 前置拦截:请求到达Controller前执行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("开始拦截管理端请求,校验JWT Token...");
// 1. 从请求头中提取Token(配置文件中指定的头名称,如token)
String token = request.getHeader(jwtProperties.getAdminTokenName());
try {
// 2. 调用JwtUtil解析Token,校验合法性(秘钥不匹配/Token过期都会抛出异常)
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
// 3. 从Claims中获取用户ID(登录时存入的核心信息)
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前登录用户ID:{}", empId);
// 4. 将用户ID存入ThreadLocal,供后续业务层使用(核心!)
BaseContext.setCurrentId(empId);
// 5. 校验通过,放行请求
return true;
} catch (Exception e) {
// 6. 校验失败(Token非法/过期/空),返回401未授权状态码,拦截请求
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401状态码
log.error("JWT Token校验失败:{}", e.getMessage());
return false;
}
}
}
关键细节
- 异常捕获:解析 Token 时的所有异常(如 Token 为空、秘钥不匹配、Token 过期、格式错误)都统一捕获,避免程序崩溃,同时返回 401 状态码告知前端;
- 用户 ID 传递 :校验通过后,将用户 ID 存入
BaseContext(基于 ThreadLocal 的上下文工具类),实现同一请求线程内的用户信息无感传递,后续 Service 层可直接获取; - 只拦截管理端 :该拦截器专为管理端接口设计,后续可通过相同方式实现用户端 JWT 拦截器(
JwtTokenUserInterceptor)。
4. WebMvcConfiguration:注册配置类 ------ 让拦截器 "生效"
核心职责 :Spring MVC 的全局配置类,完成两件核心事:① 将 JWT 拦截器注册到 Spring MVC 的拦截器链中;② 配置拦截规则(拦截哪些路径、排除哪些路径)。实现方式 :继承WebMvcConfigurationSupport,重写addInterceptors方法,通过InterceptorRegistry完成拦截器的注册和规则配置。
核心代码实现
java
package com.sky.config;
import com.sky.interceptor.JwtTokenAdminInterceptor;
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.WebMvcConfigurationSupport;
/**
* Spring MVC全局配置类,注册拦截器、消息转换器等
*/
@Configuration // 标记为配置类,交给Spring容器管理
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; // 注入JWT拦截器
/**
* 注册拦截器,配置拦截规则
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册JWT拦截器及拦截规则...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**") // 拦截所有管理端接口(/admin/开头的所有路径)
.excludePathPatterns("/admin/employee/login"); // 排除登录接口(无需校验Token)
}
}
关键拦截规则说明
addPathPatterns("/admin/**"):拦截所有以/admin/开头的请求,即所有管理端接口,保证管理端接口的安全性;excludePathPatterns("/admin/employee/login"):排除登录接口,因为登录接口是获取 Token 的入口,此时用户尚未登录,无 Token 可校验,必须放行;- 可扩展 :若有其他无需校验的接口(如验证码接口),直接添加到
excludePathPatterns中即可。
5. BaseContext:上下文工具类 ------ 用户 ID 的 "线程传递容器"
核心职责 :基于ThreadLocal实现同一请求线程内的用户 ID 传递,是 JWT 拦截器与业务层之间的 "桥梁"。拦截器将用户 ID 存入其中,Service/Mapper 层可直接获取,无需通过方法参数层层传递。设计基础:Tomcat 为每个 HTTP 请求分配独立线程,请求的整个处理流程(拦截器→Controller→Service→Mapper)都在该线程中执行,ThreadLocal 保证了线程内数据共享、线程间数据隔离。
核心代码实现
java
package com.sky.utils;
/**
* 基于ThreadLocal的上下文工具类,传递当前登录用户ID
*/
public class BaseContext {
// 定义ThreadLocal变量,存储Long类型的用户ID
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 存入用户ID
* @param id 当前登录用户ID
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取用户ID
* @return 当前登录用户ID
*/
public static Long getCurrentId() {
return threadLocal.get();
}
/**
* 移除用户ID,避免内存泄漏
*/
public static void removeCurrentId() {
threadLocal.remove();
}
}
业务层使用示例
在 Service 层中,无需任何参数传递,直接调用BaseContext.getCurrentId()即可获取当前登录用户 ID,用于记录操作人(如创建人、修改人),完美配合公共字段自动填充功能:
java
// 新增员工时,获取当前登录管理员ID,作为创建人
employee.setCreateUser(BaseContext.getCurrentId());
// 修改菜品时,获取当前登录管理员ID,作为修改人
dish.setUpdateUser(BaseContext.getCurrentId());
三、完整执行流程:从请求到来至业务层执行
当五大核心类配置完成后,整个 JWT 登录校验流程将自动化执行 ,对前端和业务层开发者完全透明。以下以管理端新增员工为例,讲解从请求到来至业务层执行的完整流程:
前提:用户已完成登录,前端持有有效 Token
用户在管理端登录时,后端登录接口(/admin/employee/login)校验账号密码成功后,通过JwtUtil.createJWT()生成包含用户 ID 的 Token,返回给前端,前端将 Token 存储在本地(如 localStorage),后续所有请求都在请求头中携带该 Token。
正式执行流程(共 6 步)
- 请求发送 :前端发送新增员工请求(
/admin/employee/save),在请求头中携带 Token(如token: eyJhbGciOiJIUzI1NiJ9...); - 规则匹配 :Spring MVC 通过
WebMvcConfiguration中的配置,发现请求路径/admin/employee/save匹配/admin/**,触发 JWT 拦截器; - Token 提取 :
JwtTokenAdminInterceptor拦截请求,从请求头中提取 Token(根据JwtProperties配置的adminTokenName); - Token 校验 :拦截器调用
JwtUtil.parseJWT(),使用JwtProperties中的秘钥解析 Token,校验其合法性(秘钥是否匹配、是否过期); - 信息传递 :校验通过后,从解析后的 Claims 中获取用户 ID,调用
BaseContext.setCurrentId()将 ID 存入 ThreadLocal,放行请求; - 业务执行 :请求进入 Controller→Service 层,业务层通过
BaseContext.getCurrentId()获取当前登录用户 ID,完成业务逻辑执行(如记录创建人)。
异常情况处理
若前端请求未携带 Token、Token 非法、Token 过期,拦截器会直接捕获异常,设置响应状态码为 401 并拦截请求,请求无法进入业务层,保证接口安全。
四、登录接口实现:Token 的生成入口
为了让整个方案更完整,这里补充管理端登录接口的核心代码,这是 Token 的生成入口,与 JWT 校验机制形成完整的闭环:
1. 登录请求 DTO(接收前端参数)
java
package com.sky.dto;
import lombok.Data;
@Data
public class EmployeeLoginDTO {
private String username; // 用户名
private String password; // 密码
}
2. 登录接口 Controller 层
java
package com.sky.controller.admin;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import com.sky.vo.EmployeeLoginVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/admin/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 员工登录
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
// 1. 业务层校验账号密码,返回登录成功的员工信息
Employee employee = employeeService.login(employeeLoginDTO);
// 2. 生成JWT Token,存入用户ID
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims
);
// 3. 封装返回结果,将Token和用户信息返回给前端
EmployeeLoginVO vo = new EmployeeLoginVO();
vo.setId(employee.getId());
vo.setName(employee.getName());
vo.setToken(token);
return Result.success(vo);
}
}
五、方案优势:为什么这套 JWT 校验机制适用于企业级开发?
这套基于 5 大核心类的 JWT 登录校验机制,并非简单的 JWT 工具类调用,而是结合 Spring MVC、ThreadLocal 的企业级最佳实践,相比简单的 JWT 实现,具有以下核心优势:
- 配置与代码解耦 :通过
JwtProperties统一管理 JWT 配置,修改配置无需改动业务代码,符合开闭原则; - 接口安全全覆盖:通过拦截器实现管理端接口的全量拦截,仅排除登录接口,从入口处保障接口安全;
- 用户信息无感传递 :基于 ThreadLocal 的
BaseContext,实现用户 ID 的跨层传递,避免方法参数层层传递的繁琐,让代码更优雅; - 无状态设计,适配分布式:服务端不存储任何登录信息,Token 自包含所有身份信息,可直接部署在分布式集群中,无需考虑 Session 共享问题;
- 可扩展性强 :可快速扩展用户端 JWT 校验(新增
JwtTokenUserInterceptor,配置/user/**拦截规则),与管理端校验逻辑互不干扰; - 异常友好:统一的异常捕获和 401 状态码返回,前端可根据状态码统一处理未登录 / Token 过期场景,提升用户体验;
- 代码复用性高 :
JwtUtil作为通用工具类,可在整个项目中复用,登录接口、拦截器直接调用,无需重复开发。
六、生产环境优化建议
以上实现为项目基础版 JWT 校验机制,在生产环境中,可根据实际需求进行以下优化,让方案更安全、更健壮:
- 使用非对称加密算法:将 HS256 对称加密替换为 RSA 非对称加密,生成 Token 用私钥,解析 Token 用公钥,提升 Token 的安全性,防止秘钥泄露;
- 增加 Token 刷新机制:为 Token 添加刷新令牌(Refresh Token),当 Access Token 过期时,前端可通过 Refresh Token 获取新的 Access Token,避免用户频繁登录;
- Token 黑名单机制:针对需要强制下线的用户(如修改密码、账号冻结),添加 Redis 实现的 Token 黑名单,拦截已失效但未过期的 Token;
- 请求头防篡改:结合请求头签名,防止 Token 被篡改或伪造;
- 限制 Token 请求频率:添加接口限流,防止针对 JWT 接口的暴力破解攻击;
- 完善日志记录:在拦截器中增加详细的日志记录(如请求 IP、Token 校验结果、用户 ID),便于生产环境问题排查;
- 秘钥安全管理:生产环境的 JWT 秘钥不直接写在配置文件中,通过配置中心(如 Nacos、Apollo)或环境变量注入,防止秘钥泄露。
七、总结
本文讲解的 JWT 登录校验机制,是Spring Boot + JWT + AOP(拦截器) 在企业级项目中的经典落地实践,核心是通过 5 个核心类的协同工作,实现了从 Token 生成、请求拦截、Token 校验到用户信息传递的全流程自动化。
这套方案的核心价值不仅在于保障了接口安全,更在于让安全校验与业务逻辑完全解耦------ 开发者无需在业务层编写任何校验代码,只需专注于业务逻辑实现;同时通过 ThreadLocal 实现用户信息的无感传递,让代码更简洁、更优雅。
在实际项目中,这套方案可直接复用,并根据业务需求扩展用户端校验、Token 刷新、黑名单等功能。掌握这套方案,不仅能解决前后端分离项目的登录校验问题,更能理解 Spring MVC 拦截器、配置绑定、ThreadLocal 等核心技术的综合应用,提升企业级项目的开发能力。
JWT 的核心是无状态的身份认证,而优秀的项目实现,是让这份无状态的安全,与 Spring 生态完美融合,形成一套可扩展、可维护、高安全的完整解决方案。