在前后端分离项目中,传统的Session登录认证已无法满足需求------Session依赖服务器内存存储,分布式部署时会出现认证失效问题,且存在跨域、安全性不足等弊端。而JWT(JSON Web Token)作为一种无状态、轻量级的认证方式,无需在服务器存储会话信息,仅通过客户端携带Token即可完成认证,完美解决上述问题。
本文将基于SpringBoot框架,结合JJWT工具包,完整实现"Token生成→登录认证→Token解析→登录人信息查询"全流程,全程结合可直接复用的代码,拆解每一步核心逻辑,从依赖配置到接口实现,再到避坑指南,覆盖实战开发所有关键要点,新手也能快速上手落地。
核心技术栈:SpringBoot(后端框架)+ JJWT(JWT工具包,简化Token生成与解析)+ SpringMVC(接口开发),无需复杂配置,依赖引入后即可快速开发,代码可直接集成到管理系统、APP后端等各类前后端分离项目中。
一、核心原理:JWT为什么能实现无状态认证?
在讲解代码实现前,先简单理解JWT的核心逻辑,避免只懂代码、不懂原理:
-
JWT结构:由三部分组成(Header+Payload+Signature),通过Base64编码和签名拼接而成,整体为一串字符串,可直接在HTTP请求头中携带。
-
Header(头部):指定签名算法(如HS256)和Token类型,Base64编码后不可加密(仅编码,非加密)。
-
Payload(载荷):存储核心业务信息(如用户ID、用户名),可设置过期时间,同样是Base64编码(注意:Payload不加密,不可存储敏感信息,如密码)。
-
Signature(签名):用自定义密钥对Header和Payload进行签名,确保Token不被篡改------服务器接收Token后,会用相同的密钥验证签名,若签名不一致则Token无效。
-
-
认证流程:
-
- 客户端提交用户名/密码,服务器验证通过后,生成JWT Token并返回给客户端。
-
- 客户端接收Token后,存储在本地(如localStorage、Cookie),后续所有请求都在请求头中携带Token。
-
- 服务器拦截所有需要认证的请求,解析请求头中的Token,验证Token有效性(签名是否正确、是否过期)。
-
- Token验证通过后,解析出Payload中的用户标识(如用户ID),根据标识查询登录人信息,返回给客户端。
-
-
核心优势:无状态(服务器无需存储会话信息,减轻服务器压力)、支持跨域、可分布式部署、轻量级(Token字符串体积小,传输效率高)。
二、前期准备:依赖配置与核心参数配置
实现JWT认证,需先引入JJWT依赖(负责Token的生成、解析和验证),再配置JWT核心参数(密钥、过期时间),这是实现后续功能的基础。
2.1 引入JJWT依赖(pom.xml)
本文使用JJWT 0.11.5版本(稳定版本,兼容SpringBoot 2.x/3.x),需引入3个依赖(api、impl、jackson,分别负责核心API、运行时实现、JSON序列化):
XML
<!-- JWT核心依赖:提供Token生成、解析的API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- JWT运行时依赖:实现核心API的底层逻辑 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope> <!-- 仅运行时生效,编译时不依赖 -->
</dependency>
<!-- JWT JSON序列化依赖:用于Payload中JSON数据的序列化/反序列化 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.2 配置JWT核心参数(application.yml)
将JWT的密钥(secret)和Token过期时间(expiration)配置在配置文件yaml中,避免硬编码,便于后续修改和部署,参数说明如下:
-
secret:密钥,必须至少32位(JJWT的HS256算法要求密钥长度不低于256位,即32个字符),可自定义(如字母+数字组合),核心作用是签名Token,必须保密,不可泄露。
-
expiration:Token过期时间,单位为秒(本文配置7200秒,即2小时,可根据业务需求调整,如后台管理系统可设置1小时,APP可设置7天)。
XML
jwt:
secret: "zxcvbnmlkjhgfdsaqwertyuiop123456" # 自定义密钥,长度≥32位,建议生产环境使用更复杂的密钥
expiration: 7200 # Token过期时间(秒),7200秒=2小时
三、核心功能实现(结合代码,逐模块拆解)
本文实现3个核心功能,串联起Token登录与信息查询的全流程:1. JWT工具类(Token生成、解析、验证);2. 登录接口(验证用户身份,生成Token并返回);3. 登录人信息查询接口(解析Token,获取用户标识,查询并返回用户信息)。
模块1:JWT工具类(JwtUtil)------ 核心工具
封装JWT的核心操作:生成Token、解析Token获取用户标识、验证Token有效性,该类交给Spring管理(@Component),通过@Value注入配置文件中的参数,可在其他类中直接注入使用。
java
package com.qcby.aischool.util;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component // 交给Spring管理,支持依赖注入
public class JwtUtil {
// 从配置文件注入Token过期时间(秒)
@Value("${jwt.expiration}")
private long expiration;
// 从配置文件注入JWT密钥
@Value("${jwt.secret}")
private String secret;
/**
* 生成Token(核心方法)
* @param id 存储在Token中的用户标识(如用户ID、用户名,本文用用户ID)
* @return 生成的JWT Token字符串
*/
public String generateToken(String id) {
// 1. 根据密钥生成加密Key(HS256算法要求Key为HmacSHA256类型)
Key key = Keys.hmacShaKeyFor(secret.getBytes());
// 2. 构建Token并返回
return Jwts.builder()
.setSubject(id) // 设置Payload:存储用户标识(核心信息)
.setIssuedAt(new Date()) // 设置签发时间(当前时间)
// 设置过期时间:当前时间 + 过期时间(秒转毫秒)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(key, SignatureAlgorithm.HS256) // 设置签名算法和密钥
.compact(); // 生成最终的Token字符串
}
/**
* 解析Token,获取存储在Payload中的用户标识(如用户ID)
* @param token 客户端传递的Token字符串
* @return 用户标识(本文返回用户ID)
*/
public String getUsernameFromToken(String token) {
// 1. 生成加密Key(与生成Token时的密钥一致)
Key key = Keys.hmacShaKeyFor(secret.getBytes());
// 2. 解析Token,获取Payload中的subject(即用户标识)
return Jwts.parserBuilder()
.setSigningKey(key) // 设置验证签名的密钥
.build() // 构建解析器
.parseClaimsJws(token) // 解析Token(若Token无效,会抛出JwtException)
.getBody() // 获取Payload部分
.getSubject(); // 获取subject(用户标识)
}
/**
* 验证Token是否有效(核心方法)
* @param token 客户端传递的Token字符串
* @return true:Token有效;false:Token无效(签名错误、已过期、格式错误等)
*/
public boolean validateToken(String token) {
try {
// 1. 生成加密Key
Key key = Keys.hmacShaKeyFor(secret.getBytes());
// 2. 解析Token,若解析成功且未过期,则Token有效
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// 捕获异常:JwtException(Token签名错误、已过期等)、IllegalArgumentException(Token格式错误)
return false;
}
}
}
关键细节与避坑点:
-
密钥生成:必须使用Keys.hmacShaKeyFor(secret.getBytes())生成Key,不可手动构建,否则会导致签名验证失败。
-
Payload存储:仅存储非敏感信息(如用户ID、用户名),绝对不能存储密码、手机号等敏感数据(因为Payload是Base64编码,可直接解码查看)。
-
异常捕获:validateToken方法中捕获JwtException(Token相关异常)和IllegalArgumentException(参数异常),避免因Token无效导致程序崩溃。
-
方法命名:getUsernameFromToken方法名可根据实际存储的信息修改(如getUserIdFromToken),本文因代码中存储的是用户ID,实际功能是获取用户ID。
模块2:登录接口(login)------ Token生成入口
登录接口是Token生成的入口,核心逻辑:接收客户端提交的用户名/密码,验证用户身份(查询数据库,判断用户名密码是否正确),验证通过后,调用JwtUtil生成Token,将Token返回给客户端,客户端后续请求携带该Token即可完成认证。
java
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController // 标记为控制器,处理HTTP请求
public class LoginController {
// 注入管理员服务(用于查询数据库,验证用户身份,替换为自己项目中的Service)
@Autowired
private AdministratorService administratorService;
// 注入JWT工具类(用于生成Token)
@Autowired
private JwtUtil jwtUtil;
/**
* 登录接口(客户端提交用户名/密码,获取Token)
* @param user 客户端传递的登录信息(包含用户名、密码,封装为Administrator实体类)
* @return 响应结果(成功返回Token,失败返回错误信息)
*/
@PostMapping("/login")
@ResponseBody
public Result login(@RequestBody Administrator user){
// 1. 验证用户身份:查询数据库,判断用户名/密码是否正确(实际项目中需加密密码后比对)
List<Administrator> users = administratorService.login(user);
// 2. 验证通过(查询到唯一用户),生成Token
if (users.size() == 1){
// 获取登录用户的ID,作为Token的Payload(用户标识)
String userId = users.get(0).getId().toString();
// 调用JwtUtil生成Token
String token = jwtUtil.generateToken(userId);
// 构建响应数据,将Token返回给客户端
Map<String,String> data = new HashMap<>();
data.put("token", token);
// 返回成功响应(Result为自定义响应实体,包含code、msg、data)
return new Result().success(data);
}else {
// 验证失败(未查询到用户或查询到多个用户),返回错误信息
return new Result().error("用户名或密码错误");
}
}
}
核心注意点与优化建议:
-
密码验证:实际项目中,数据库存储的密码必须加密(如使用BCrypt加密),客户端传递的密码需先加密,再与数据库中的加密密码比对,避免明文密码传输和存储(本文代码为简化演示,未做加密处理,实际需补充)。
-
用户校验:users.size() == 1 是简化校验,实际项目中可优化为"查询到唯一用户且密码匹配",避免因数据库数据异常(如重复用户)导致的问题。
-
响应格式:使用自定义Result实体类统一响应格式(包含状态码、提示信息、数据),便于前端统一解析(如成功返回code=200,失败返回code=500)。
模块3:登录人信息查询接口(info)------ Token解析与信息查询
客户端登录成功并获取Token后,后续请求(如查询个人信息)需携带Token,服务器通过解析Token获取用户标识,再根据标识查询用户信息并返回。本文中,Token解析逻辑由拦截器完成(代码中未展示拦截器,后续补充),拦截器解析Token后,将用户ID存入request属性中,接口直接从request中获取用户ID即可。
java
/**
* 登录人信息查询接口(需携带Token访问)
* 前端请求路径:/api/user/info(可根据实际需求修改)
* @param request 请求对象,用于获取拦截器解析后的用户ID
* @return 登录人的完整信息(如管理员信息)
*/
@GetMapping("/info")
@ResponseBody
public Result getLoginAdministratorInfo(HttpServletRequest request) {
System.out.println("进入查询登录人信息的接口");
// 1. 从request中获取拦截器解析的用户ID(拦截器已验证Token有效性)
String id = (String) request.getAttribute("id");
// 2. 根据用户ID查询数据库,获取登录人完整信息
Administrator administrator = administratorService.findAdministratorById(Integer.valueOf(id));
// 3. 返回登录人信息
return Result.success(administrator);
}
关键补充:Token拦截器(必加,代码补充):
上述接口能直接从request中获取用户ID,核心是Token拦截器------拦截所有需要认证的请求,验证Token有效性,解析Token中的用户ID,存入request属性中。补充拦截器代码(可直接复用):
java
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从Header中获取Token(前端需将Token放在Authorization中)
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 去掉"Bearer "前缀
}
// 2. 验证Token
if (token == null || !jwtUtil.validateToken(token)) {
response.setStatus(401); // 未授权
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
return false;
}
// 3. Token有效,将用户ID存入request属性中
String userId = jwtUtil.getUsernameFromToken(token);
request.setAttribute("id", userId);
// 4. 继续处理请求
return true;
}
}
拦截器配置(需注册到Spring,让拦截器生效):
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
// 加自定义拦截器JwtInterceptor,设置拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/administrator/login");
}
}
拦截器核心逻辑说明:
-
Token获取:前端需将Token放在请求头的Authorization字段中,格式为"Bearer 生成的Token"(如Bearer eyJhbGciOiJIUzI1NiJ9...),拦截器截取前缀后获取真实Token。
-
拦截范围:拦截所有请求(/**),排除登录接口(/login)------登录接口是获取Token的入口,无需认证即可访问。
-
权限控制:Token无效或未携带时,返回401未授权,阻止访问接口;Token有效则放行,并将用户ID存入request,供接口查询使用。
四、完整流程串联与测试步骤
4.1 全流程串联(清晰理解Token的作用)
-
客户端提交用户名/密码,发送POST请求到/login接口。
-
服务器验证用户名/密码正确,调用JwtUtil.generateToken()生成Token(Payload中存储用户ID),返回Token给客户端。
-
客户端存储Token(如localStorage),后续访问/info接口时,在请求头中携带Token(Authorization: Bearer Token)。
-
服务器拦截器拦截/info请求,获取请求头中的Token,调用JwtUtil.validateToken()验证Token有效性。
-
Token有效,调用JwtUtil.getUsernameFromToken()解析出用户ID,存入request属性中。
-
/info接口从request中获取用户ID,查询数据库得到登录人信息,返回给客户端。
4.2 测试步骤(快速验证功能,可直接操作)
-
环境准备:启动SpringBoot项目,确保数据库连接正常(AdministratorService能正常查询用户),拦截器已注册生效。
-
登录测试:使用Postman发送POST请求(http://localhost:8080/login),请求体为JSON格式({"username":"admin","password":"123456"}),成功返回Token。
-
信息查询测试:发送GET请求(http://localhost:8080/info),在请求头中添加Authorization字段,值为"Bearer 上一步返回的Token",成功返回登录人的管理员信息。
-
无效Token测试:修改Token中的任意字符,再次发送/info请求,服务器返回401未授权,验证拦截器生效。
五、落地优化与避坑指南(实战必备)
上述代码可直接复用,但在实际生产环境中,需注意以下优化点和坑点,避免线上问题,提升系统安全性和稳定性。
5.1 核心优化点
-
密码加密:数据库存储的密码必须使用BCrypt等不可逆加密算法加密,客户端传递的密码需先加密再与数据库比对,避免明文传输和存储。
-
密钥安全:生产环境中,JWT密钥(secret)不能硬编码在配置文件中,建议通过环境变量、配置中心(如Nacos)注入,避免密钥泄露。
-
Token过期处理:前端需监听Token过期时间,在Token过期前自动刷新Token(如调用刷新Token接口),避免用户频繁登录;服务器可设置Token刷新机制(如 refreshToken)。
-
日志优化:在拦截器、JwtUtil、接口中添加日志(如SLF4J+Logback),记录Token生成、验证、解析的关键信息,便于问题排查(如Token无效原因、登录失败原因)。
-
接口权限细化:拦截器可根据用户角色(如管理员、普通用户)进行权限控制,不同角色可访问的接口不同,而非仅验证Token有效性。
5.2 常见坑点与解决方案
-
坑点1:Token验证失败,提示签名错误------解决方案:检查生成Token和验证Token时使用的密钥是否一致,确保secret长度≥32位,避免密钥拼写错误。
-
坑点2:Token解析时抛出JwtException(过期)------解决方案:检查Token过期时间配置,前端及时刷新Token,服务器返回401后,前端引导用户重新登录。
-
坑点3:拦截器未生效,无需Token即可访问接口------解决方案:检查拦截器是否添加@Component注解(交给Spring管理),是否在WebMvcConfig中注册,拦截路径和排除路径是否配置正确。
-
坑点4:前端携带Token后,服务器仍返回401------解决方案:检查请求头中Token的格式是否正确(是否添加Bearer前缀),Token是否被篡改,拦截器中Token截取逻辑是否正确。
-
坑点5:Payload中存储敏感信息导致泄露------解决方案:仅存储非敏感信息(用户ID、用户名),密码、手机号等敏感信息绝对不能存入Payload。
六、总结与扩展
本文基于SpringBoot+JJWT,完整实现了Token登录认证与登录人信息查询功能,核心优势在于:
-
无状态认证:服务器无需存储会话信息,减轻服务器压力,支持分布式部署,解决传统Session认证的弊端。
-
代码简洁可复用:JWT工具类、拦截器、接口代码均可直接复用,只需替换实体类和Service,即可快速集成到各类前后端分离项目中。
-
安全性高:通过密钥签名Token,防止Token被篡改;Payload不存储敏感信息,降低信息泄露风险;拦截器统一拦截,确保接口安全。
扩展方向:
-
Token刷新机制:实现refreshToken接口,用户登录后返回accessToken(短期有效)和refreshToken(长期有效),accessToken过期后,用refreshToken刷新获取新的accessToken。
-
多端适配:支持PC端、APP端、小程序端统一认证,通过Token中的设备信息,实现多端登录管理(如同一账号最多允许3台设备登录)。
-
权限细化:结合Spring Security,实现基于角色的权限控制(RBAC),不同角色访问不同接口,提升系统安全性。
-
Token黑名单:实现Token注销功能(如用户退出登录),将注销的Token加入黑名单,即使Token未过期,也不允许访问接口。
如果在实现过程中遇到问题,可结合JJWT官方文档排查,也可留言交流探讨,助力快速落地JWT认证功能。