spring boot2 +java-jwt轻量实现jwt
前言
-
对于 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实例@Autowiredprivate JwtUtils jwtUtils;@Overridepublic 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;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("执行了拦截器的postHandle方法");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserUtils.removeUser();}
}
-
注册拦截器:
@Configuration public class WebConfig implements WebMvcConfigurer {@Overridepublic 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也要再输密码