前言
对于 Spring Boot 项目:
- 如果已经在使用 Spring Security,优先考虑 JJWT,因为它与 Spring 生态系统更兼容
- 如果希望代码更简洁,或者需要与 Auth0 服务集成,考虑 java-jwt
对于非 Spring 项目:
- java-jwt 通常是更好的选择,因为它更轻量、API 更现代
对于初学者:
- java-jwt 的链式 API 更容易理解和使用
使用 java-jwt 实现 Spring Boot 2.7.13 项目的 JWT 认证
我们先不使用spring secrity 框架,搞个更轻量的
一、创建 JWT 工具类
首先创建一个工具类来处理 JWT 的生成和验证:
package com.neuedu.hisweb.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.neuedu.hisweb.entity.Customer;
import com.neuedu.hisweb.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* JWT工具类,用于生成和验证JSON Web Token
*
* JWT由三部分组成:
* 1. Header: 包含令牌类型和签名算法
* 2. Payload: 包含用户信息和元数据
* 3. Signature: 用于验证令牌的完整性
*
* 格式: Header.Payload.Signature
*/
@Component
public class JwtUtils {
// 签名密钥,用于生成和验证JWT签名
public static final String SECRET = "SECRET";
// 应用级别的密钥,用于额外的安全验证
private String secretkey;
// JWT过期时间(秒),从配置文件注入,默认1年
private Long expireTime;
// 从配置文件中注入应用密钥
@Value("${jwt.secretkey}")
public void setSecretkey(String secretkey) {
this.secretkey = secretkey;
}
// 从配置文件中注入JWT过期时间,默认值为1年(31536000秒)
@Value("${jwt.expireTime:31536000}")
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
/**
* 根据用户对象生成JWT令牌
*
* @param object 用户对象,可以是User或Customer类型
* @return 生成的JWT令牌
*/
public String sign(Object object) {
// 计算过期时间(毫秒),将配置的秒转换为毫秒
Date expireDate = new Date(System.currentTimeMillis() + expireTime * 1000);
// 创建JWT构建器,添加通用声明
JWTCreator.Builder builder = JWT.create()
.withClaim("secretkey", secretkey) // 添加应用密钥作为声明
.withExpiresAt(expireDate); // 设置过期时间
// 根据用户类型添加不同的声明
if (object instanceof Customer) {
Customer customer = (Customer) object;
return builder.withClaim("id", customer.getId()) // 添加客户ID
.sign(Algorithm.HMAC256(SECRET)); // 使用HMAC256算法签名
} else if (object instanceof User) {
User user = (User) object;
return builder.withClaim("realName", user.getRealName()) // 添加真实姓名
.withClaim("userName", user.getUserName()) // 添加用户名
.withClaim("id", user.getId()) // 添加用户ID
.sign(Algorithm.HMAC256(SECRET)); // 使用HMAC256算法签名
}
// 如果对象类型不支持,抛出异常
throw new IllegalArgumentException("Unsupported object type: " + object.getClass().getName());
}
/**
* 验证JWT令牌的有效性
*
* @param token 待验证的JWT令牌
* @return 验证结果,true表示有效,false表示无效
*/
public boolean verify(String token) {
try {
// 创建JWT验证器,使用相同的密钥和算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
// 验证令牌,如果验证失败会抛出异常
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
// 捕获验证异常,返回验证失败
return false;
}
}
/**
* 从JWT令牌中获取用户名
*
* @param token JWT令牌
* @return 用户名,如果令牌无效则返回null
*/
public String getUserNameByToken(String token) {
try {
// 解码JWT令牌,获取声明信息
DecodedJWT decodedJWT = JWT.decode(token);
// 获取userName声明
return decodedJWT.getClaim("userName").asString();
} catch (JWTDecodeException e) {
// 处理解码异常,返回null表示获取失败
return null;
}
}
/**
* 从JWT令牌中获取用户对象
*
* @param token JWT令牌
* @return 用户对象(User或Customer),如果令牌无效则返回null
*/
public Object getUserByToken(String token) {
try {
// 解码JWT令牌,获取声明信息
DecodedJWT decodedJWT = JWT.decode(token);
// 根据是否存在userName声明判断用户类型
if (decodedJWT.getClaim("userName").isNull()) {
// 没有userName声明,创建Customer对象
Customer customer = new Customer();
customer.setId(decodedJWT.getClaim("id").asInt());
return customer;
}
// 有userName声明,创建User对象
User user = new User();
user.setUserName(decodedJWT.getClaim("userName").asString());
user.setRealName(decodedJWT.getClaim("realName").asString());
user.setId(decodedJWT.getClaim("id").asInt());
return user;
} catch (JWTDecodeException e) {
// 处理解码异常,返回null表示获取失败
return null;
}
}
/**
* 验证JWT令牌并返回解码后的JWT对象
*
* @param token JWT令牌
* @return 解码后的JWT对象
*/
private DecodedJWT verifyAndGetJWT(String token) {
// 创建验证器并验证令牌
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
return verifier.verify(token);
}
}
verify
方法
verify
方法验证 JWT 的原理,本质是检查令牌的完整性、合法性以及时效性,确保令牌是服务端签发且未被篡改、未过期,核心围绕 JWT 的结构和签名机制展开,用大白话详细拆解如下:
1. 先理解 JWT 的 “身份”:三部分组成
JWT 令牌本质是个字符串,格式为 Header.Payload.Signature
(三部分用 .
拼接):
- Header(头):存令牌类型(固定
JWT
)和签名算法(比如这里的HMAC256
),格式是 JSON,会被 Base64 编码。 - Payload(载荷):存业务数据(比如用户 ID、用户名)和元数据(比如过期时间
exp
),也是 JSON 后 Base64 编码。 - Signature(签名):用
Header
里的算法 + 服务端密钥,对Header.Payload
进行加密生成,用于防篡改。
2. verify
验证的核心逻辑
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
verifier.verify(token);
这两行代码做了这些事,最终实现 “验证令牌是否合法”:
(1)“搭环境”:准备验证器
JWT.require(Algorithm.HMAC256(SECRET)).build()
:
- 告诉验证器:“用
HMAC256
算法,且用服务端的SECRET
密钥” 。 - 相当于给验证器配好 “解密 / 验签工具”,让它知道怎么去核对令牌的签名。
(2)“验身份”:检查令牌是否合法
verifier.verify(token)
会依次做这些校验(只要有一个不通过,就抛 JWTVerificationException
):
① 检查签名是否被篡改:
验证器会按 JWT 格式,把令牌拆成Header
、Payload
、Signature
三部分。
然后用和签发时相同的算法(HMAC256)+ 相同的密钥(SECRET),重新计算Header.Payload
的签名。- 如果重新计算的签名 ≠ 令牌里的 Signature,说明令牌被改过(比如 Payload 里的用户 ID 被偷偷改了),验证失败。
② 检查令牌是否过期:
验证器会解析Payload
里的exp
(过期时间)字段,对比当前系统时间:- 如果
当前时间 > exp
,说明令牌过期,验证失败。
- 如果
③ 检查其他 “合法性”(可选,这里代码没配,但原理通用):
除了签名和过期,还能校验更多规则(比如检查iss
发行人、aud
受众是否符合预期),不过你代码里没配这些,所以主要校验前两项。
3. 总结验证原理
简单说,verify
就是:
用和签发时相同的算法 + 密钥,重新生成签名,对比令牌里的签名(防篡改);同时检查令牌里的过期时间(防过期)。
只有这两项(以及其他你配置的规则)都通过,才认为令牌合法,返回 true
;否则返回 false
。
可以理解成:
把 JWT 当成一张 “身份证”,verify
就是 “警察叔叔”:
- 先看身份证上的 “防伪标记”(签名)对不对 → 防篡改。
- 再看 “有效期” 过没过期 → 防过期。
- 都没问题,才承认这张 “身份证” 是真的 。
二、添加配置属性
在 application.properties
中添加 JWT 相关配置:
# 密钥配置
#secretkey: hisweb
jwt:
secretkey: hisweb # 应用级别的密钥,用于额外安全验证
expireTime: 3600 # JWT过期时间(秒),默认值为1年(31536000)
三、用户登录发Token
写登录接口:
@PostMapping("/login") public JsonResult<User> login(HttpServletRequest request, @RequestBody User user){ String uname = user.getUserName(); String pwd = user.getPassword(); // 构建查询条件:用户名、密码匹配且未删除 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName, uname) .eq(User::getPassword, pwd) .eq(User::getDelMark, 1); // 调用服务层查询用户 user = iUserService.getOne(wrapper); JsonResult<User> jsonResult; if (user == null) { // 登录失败 jsonResult = new JsonResult<User>("用户名或密码不正确!"); } else { // 登录成功,将用户信息存入会话 request.getSession().setAttribute("user", user); // 生成JWT令牌(通过注入的jwtUtils实例调用sign方法) String token = jwtUtils.createToken(user); // 返回用户信息和令牌 jsonResult = new JsonResult<>(user, token); } return jsonResult; }
四、保护其他接口
写个拦截器检查token:
package com.neuedu.hisweb.interceptor;
import com.neuedu.hisweb.entity.Customer;
import com.neuedu.hisweb.entity.JsonResult;
import com.neuedu.hisweb.entity.User;
import com.neuedu.hisweb.utils.JwtUtils;
import com.neuedu.hisweb.utils.UserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
// 添加@Component注解,让Spring管理这个拦截器
@Component
public class JwtInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);
// 注入JwtUtils实例
@Autowired
private JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 通过注入的实例调用verify方法
if (null == token || "".equals(token) || !jwtUtils.verify(token)) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try (PrintWriter writer = response.getWriter()) {
writer.print(new JsonResult<User>("未登录"));
} catch (Exception e) {
logger.error("login token error is {}", e.getMessage());
}
return false;
}
// 通过注入的实例调用getUserByToken方法
Object userObj = jwtUtils.getUserByToken(token);
if (userObj instanceof Customer){
UserUtils.setLoginCustomer((Customer) userObj);
}
else{
UserUtils.setLoginUser((User) userObj);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行了拦截器的postHandle方法");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserUtils.removeUser();
}
}
注册拦截器:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/api/**") // 保护所有/api开头的接口 .excludePathPatterns("/login"); // 不拦截登录接口 } }
五、测试使用
写个测试接口:
@RestController @RequestMapping("/api") public class TestController { @GetMapping("/hello") public String hello() { return "需要token才能访问的数据"; } }
测试步骤:
先用Postman访问
/login
获取token访问
/api/hello
时,在Headers加:Authorization: Bearer 你的token
六、注意事项
密钥保管好:别把密钥写在代码里,可以放配置文件
token过期:前端发现401错误要自动跳转到登录页
敏感操作:重要操作(如修改密码)即使有token也要再输密码