前后端分离项目中JWT Token身份验证全流程详解
在前后端分离的Web开发架构中,身份验证是保障系统安全的核心环节,JWT(JSON Web Token)凭借其无状态、易扩展的特性成为主流方案。本文将结合实际代码,详细拆解Token身份验证的完整实现思路、核心代码逻辑及设计原则。
一、Token身份验证的核心设计理念
传统的Session-Cookie验证依赖服务端存储会话信息,在分布式系统中需解决会话共享问题;而JWT Token采用无状态验证模式,将用户身份信息加密后存储在Token中,服务端只需验证Token的合法性即可完成身份校验,无需存储会话数据,大幅提升系统扩展性。
核心优势:
**1. 无状态:**服务端无需存储会话,便于集群部署;
**2. 跨域友好:**Token通过请求头传递,适配前后端分离的跨域场景;
**3. 过期可控:**可自定义Token有效期,降低被盗用风险;
**4. 轻量化:**基于JSON格式,传输和解析成本低。
二、Token身份验证完整流程(附核心代码)
阶段1:登录生成Token(客户端→服务端)
1. 前端登录请求代码
用户输入账号密码后,前端通过AJAX发起登录请求,将参数转为JSON格式传递:
javascript
function login() {
$.ajax({
type: "post",
url: "/user/login",
contentType: "application/json",
data: JSON.stringify({
userName: $("#username").val(),
password: $("#password").val()
}),
success: function(result) {
if (result.code == 200 && result.data != null) {
// 登录成功后存储Token和用户ID
localStorage.setItem("loginUserId", result.data.userId);
localStorage.setItem("token", result.data.token);
location.href = "blog_list.html"; // 跳转到业务页面
}
}
});
}
2. 后端验证并生成Token
后端接收登录请求后,校验账号密码合法性,验证通过则生成JWT Token:
java
// UserServiceImpl核心逻辑
public LoginResponse check(LoginRequest loginRequest) {
// 1. 从数据库查询用户信息
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserInfo::getUserName, loginRequest.getUserName());
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
// 2. 校验用户是否存在、密码是否正确
if (userInfo == null) {
throw new BlogException("用户名不正确");
}
if (!userInfo.getPassword().equals(loginRequest.getPassword())) {
throw new BlogException("密码不正确");
}
// 3. 生成JWT Token
String token = JwtUtils.createToken(userInfo.getId().toString());
// 4. 返回Token和用户ID
LoginResponse loginResponse = new LoginResponse();
loginResponse.setUserId(userInfo.getId());
loginResponse.setToken(token);
return loginResponse;
}
阶段2:Token的本地存储(客户端)
登录成功后,前端将后端返回的Token存储在浏览器的 localStorage 中:
javascript
localStorage.setItem("token", result.data.token);
localStorage 是浏览器的本地存储机制,Token会持久化保存(除非手动清除),保证用户在关闭浏览器重新打开后仍可保留登录状态。
阶段3:请求自动携带Token(客户端→服务端)
为避免在每个请求中手动添加Token,前端通过jQuery的全局AJAX拦截器,在所有请求发送前自动将Token放入请求头:
javascript
// 全局AJAX请求拦截器:添加Token到请求头
$(document).ajaxSend(function(e, xhr, opt){
let userToken = localStorage.getItem("token");
xhr.setRequestHeader("user_token", userToken);
});
核心作用:所有需要权限的请求都会自动携带 user_token 请求头,无需重复编写Token传递逻辑。
阶段4:服务端Token校验(拦截器实现)
1. 登录拦截器(LoginInterceptor):核心校验逻辑
java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头获取Token
String userToken = request.getHeader("user_token");
// 2. 校验Token合法性(是否过期、签名是否正确)
Boolean check = JwtUtils.check(userToken);
if (check) {
// Token有效,放行请求
return true;
} else {
// Token无效,返回401未授权状态码
response.setStatus(401);
return false;
}
}
}
2. 拦截器配置(WebConfig):规则管理
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 无需拦截的路径:登录接口、静态资源等
List<String> excludePath = List.of(
"/user/login",
"/**/**.html",
"/pic/**",
"/js/**",
"/css/**"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns(excludePath); // 排除公开路径
}
}
-
LoginInterceptor :负责具体的Token校验逻辑(执行者);
-
WebConfig :负责拦截器的注册和规则配置(配置者);
-
分工设计遵循"单一职责原则",便于后续扩展(如添加日志拦截器、权限拦截器)。
阶段5:Token失效处理(客户端)
当Token过期/无效时,服务端返回401状态码,前端通过全局AJAX错误拦截器统一处理,自动跳转到登录页:
javascript
// 全局AJAX错误拦截器:处理401未授权
$(document).ajaxError(function (event, xhr, options, exc) {
if (xhr.status == 401) {
location.href = "blog_login.html"; // 跳转到登录页
}
});
三、关键配套代码说明
1. 全局异常处理(保证错误信息统一返回)
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
// 处理自定义业务异常
@ExceptionHandler(BlogException.class)
public Result error(BlogException e) {
log.error("业务异常:", e);
return Result.error(e.getMessage());
}
// 处理全局异常
@ExceptionHandler(Exception.class)
public Result error(Exception e) {
log.error("系统异常:", e);
return Result.error(e.getMessage());
}
}
2. 统一响应结果(Result类)
@Data
public class Result<T> {
private int code; // 响应码:200成功/其他失败
private String errMsg; // 错误信息
private T data; // 响应数据
// 成功响应
public static <T> Result<T> ok(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setData(data);
return result;
}
// 失败响应
public static <T> Result<T> error(String errMsg) {
Result<T> result = new Result<>();
result.setCode(-1);
result.setErrMsg(errMsg);
return result;
}
}
四、核心设计原则与最佳实践
1. 职责分离原则
- 前端:Token的存储、自动携带、失效跳转分离实现;
- 后端:Token生成、校验、拦截规则配置分离实现;
- 优势:代码模块化,便于维护和扩展(如新增权限拦截器只需修改配置类)。
2. 安全性设计
- 密码校验:将数据库密码放在equals方法前,避免空指针( userInfo.getPassword().equals(loginRequest.getPassword()) );
- Token传输:通过HTTPS传输,防止Token被窃取;
- 过期控制:JWT Token设置合理有效期(如2小时),降低被盗用风险。
3. 用户体验优化
- 全局拦截:前端自动携带Token、自动处理401跳转,无需用户手动操作;
- 统一错误信息:后端通过自定义异常返回明确的错误提示(如"用户名不正确"),前端无需解析复杂错误格式。
五、总结
JWT Token身份验证的核心是"生成-存储-携带-校验-失效处理"的闭环流程:
1. 登录时生成Token并返回前端;
2. 前端存储Token并在请求时自动携带;
3. 后端通过拦截器统一校验Token合法性;
4. Token失效时前端自动引导用户重新登录。
该方案既解决了传统Session的分布式共享问题,又通过前后端的全局拦截器实现了代码的统一管理,是前后端分离项目中身份验证的最优实践之一。