JWT访问令牌

发布时间:2026/7/5 14:44:41
JWT访问令牌 JWT-概述全称JSON Web Token https://jwt.io/定义了一种简洁的、自包含的格式用于通信双方以json数据格式安全的传输信息。第一组成第一部分Header(头 记录令牌类型、签名算法等。 例如{alg:HS256,type:JWT}第二部分Payload(有效载荷携带一些自定义信息、默认信息等。 例如{id:1,username:Tom}第三部分Signature(签名防止Token被篡改、确保安全性。将header、payload并加入指定秘钥通过指定签名算法计算而来。示例如何使用JWT!--java-jwt坐标-- dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency在springboot项目的src\test\java\packageName 下测试​ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.junit.jupiter.api.Test; import java.util.Date; import java.util.HashMap; import java.util.Map; ​ public class JwtTest { ​ Test public void testGen() { MapString, Object claims new HashMap(); claims.put(id, 1); claims.put(username, 张三); //生成jwt的代码 String token JWT.create() .withClaim(user, claims)//添加载荷 .withExpiresAt(new Date(System.currentTimeMillis()1000*60*60 ))//添加过期时间 .sign(Algorithm.HMAC256(cafuc));//指定算法,配置秘钥 ​ System.out.println(token); ​ } ​ Test public void testParse() { //定义字符串,模拟用户传递过来的token String token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3ODI1NDAzMDV9.KQfy-u7CRcul5LolXPU-ETYMGdRm6LqgkWPXJ3Sp3uQ; ​ JWTVerifier jwtVerifier JWT.require(Algorithm.HMAC256(cafuc)).build(); ​ DecodedJWT decodedJWT jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象 MapString, Claim claims decodedJWT.getClaims(); System.out.println(claims.get(user)); ​ //如果篡改了头部和载荷部分的数据,那么验证失败 //如果秘钥改了,验证失败 //token过期 } }​​生成token和访问验登录生成token和访问验证token封装公共类import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; import java.util.Map; public class JwtUtil { private static final String KEY itheima; //接收业务数据,生成token并返回 public static String genToken(MapString, Object claims) { return JWT.create() .withClaim(claims, claims) .withExpiresAt(new Date(System.currentTimeMillis() 1000 * 60 * 60 )) .sign(Algorithm.HMAC256(KEY)); } //接收token,验证token,并返回业务数据 public static MapString, Object parseToken(String token) { return JWT.require(Algorithm.HMAC256(KEY)) .build() .verify(token) .getClaim(claims) .asMap(); } }登录生成token//判断密码是否正确 loginUser对象中的password是密文 if (true) { //登录成功 MapString, Object claims new HashMap(); claims.put(id, loginUser.getId()); claims.put(username, loginUser.getUsername()); String token JwtUtil.genToken(claims); //把token存储到redis中 //...先略过 return token; }访问验证token登录成功后会返回令牌后续每次请求中浏览器都需要在请求头Header携带到服务端请求头的名称为Authorization,值为token请求代码如下RequestMapping(value /list,method RequestMethod.GET) public Object loadData(RequestHeader(name Authorization) String token){ System.out.println(token); return dd; }这个时候浏览器直接访问接口会报错需要借助postman里设置请求头才能访问设置方式点击Headers在key里填写Authorization值value就是登录返回的tokenpostman设置请求头拦截器创建拦截器import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; ​ import java.util.Map; ​ Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //令牌验证 String token request.getHeader(Authorization); //验证token try { MapString, Object claims JwtUtil.parseToken(token); //放行 return true; } catch (Exception e) { //http响应状态码为401 response.setStatus(401); //不放行 return false; } } }​注册拦截器注意 注册拦截器的时候要记得放行不需要验证的链接如登录和注册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.WebMvcConfigurer; ​ Configuration public class WebConfig implements WebMvcConfigurer { ​ Autowired private LoginInterceptor loginInterceptor; ​ Override public void addInterceptors(InterceptorRegistry registry) { //登录接口和注册接口不拦截 registry.addInterceptor(loginInterceptor).excludePathPatterns(/user/login,/user/register); } }postman统一配置请求头给所有请求地址统一设置请求头pm.request.addHeader(Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3ODI1NDAzMDV9.KQfy-u7CRcul5LolXPU-ETYMGdRm6LqgkWPXJ3Sp3uQ...)ThreadLocal需求如果我们想要获取用户信息可以登录后从token中解析用户信息如用户名或用户ID了然后再从数据库里获取详情而不用依赖web前端回传用户名或用户ID这样可以提高安全性。GetMapping(/userInfo) public Object userInfo(RequestHeader(name Authorization) String token) { //根据用户名查询用户 MapString, Object map JwtUtil.parseToken(token); String username (String) map.get(username); User user userService.findByUserName(username); return user; }新需求 如果我们需要在每一个业务里用到用户ID该怎么办呢ThreadLocal介绍这里推荐用ThreadLocal提供线程局部变量用来存取数据: set()/get()使用ThreadLocal存储的数据, 线程安全import org.junit.jupiter.api.Test; public class ThreadLocalTest { Test public void testThreadLocalSetAndGet(){ //提供一个ThreadLocal对象 ThreadLocal tl new ThreadLocal(); //开启两个线程 //第一个线程 new Thread(()-{ tl.set(苹果); System.out.println(Thread.currentThread().getName(): tl.get()); System.out.println(Thread.currentThread().getName(): tl.get()); System.out.println(Thread.currentThread().getName(): tl.get()); },黑色).start(); //第二个线程 new Thread(()-{ tl.set(桃子); System.out.println(Thread.currentThread().getName(): tl.get()); System.out.println(Thread.currentThread().getName(): tl.get()); System.out.println(Thread.currentThread().getName(): tl.get()); },红色).start(); } }代码实现需求公共类import java.util.HashMap; import java.util.Map; /** * ThreadLocal 工具类 */ SuppressWarnings(all) public class ThreadLocalUtil { //提供一个全局唯一的ThreadLocal对象 private static final ThreadLocal THREAD_LOCAL new ThreadLocal(); //根据键获取值 public static T T get(){ return (T) THREAD_LOCAL.get(); } //存储键值对 public static void set(Object value){ THREAD_LOCAL.set(value); } //清除ThreadLocal 防止内存泄漏 public static void remove(){ THREAD_LOCAL.remove(); } }拦截器改进//在拦截器里把业务数据存储到ThreadLocal中即可核心代码ThreadLocalUtil.set(claims)import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; ​ import java.util.Map; ​ Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //令牌验证 String token request.getHeader(Authorization); //验证token try { MapString, Object claims JwtUtil.parseToken(token); //把业务数据存储到ThreadLocal中 ThreadLocalUtil.set(claims); //放行 return true; } catch (Exception e) { //http响应状态码为401 response.setStatus(401); //不放行 return false; } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清空ThreadLocal中的数据防止发生内存溢出 ThreadLocalUtil.remove(); } }业务调用GetMapping(/userInfo) public ResultUser userInfo(/*RequestHeader(name Authorization) String token*/) { //根据用户名查询用户 /* MapString, Object map JwtUtil.parseToken(token); String username (String) map.get(username);*/ MapString, Object map ThreadLocalUtil.get(); String username (String) map.get(username); User user userService.findByUserName(username); return Result.success(user); }redis改进springboot 集成redis详见 springboot整合redis登录成功后给浏览器响应令牌的同时把该令牌存储到redis中LoginInterceptor拦截器中需要验证浏览器携带的令牌并同时需要获取到redis中存储的与之相同的令牌当用户修改密码成功后删除redis中存储的旧令牌1.登录成功存储到redis登录成功后给浏览器响应令牌的同时把该令牌存储到redis中核心代码//数据库判断登录成功后,把token存储到redis中并设置有效时间 ValueOperationsString, String operations stringRedisTemplate.opsForValue(); operations.set(token,token,1, TimeUnit.HOURS);完整代码如下PostMapping(/login) public ResultString login(Pattern(regexp ^\\S{5,16}$) String username, Pattern(regexp ^\\S{5,16}$) String password) { //根据用户名查询用户 User loginUser userService.findByUserName(username); //判断该用户是否存在 if (loginUser null) { return Result.error(用户名错误); } ​ //判断密码是否正确 loginUser对象中的password是密文 if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) { //登录成功 MapString, Object claims new HashMap(); claims.put(id, loginUser.getId()); claims.put(username, loginUser.getUsername()); String token JwtUtil.genToken(claims); //把token存储到redis中 ValueOperationsString, String operations stringRedisTemplate.opsForValue(); operations.set(token,token,1, TimeUnit.HOURS); return Result.success(token); } return Result.error(密码错误); }2.拦截器验证LoginInterceptor拦截器中需要验证浏览器携带的令牌并同时需要获取到redis中存储的与之相同的令牌核心代码LoginInterceptor拦截器中方法preHandle增加如下代码//从redis中获取相同的token ValueOperationsString, String operations stringRedisTemplate.opsForValue(); String redisToken operations.get(token); if (redisTokennull){ //token已经失效了 throw new RuntimeException(); }完整代码​ import com.itheima.pojo.Result; import com.itheima.utils.JwtUtil; import com.itheima.utils.ThreadLocalUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; ​ import java.util.Map; ​ Component public class LoginInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate stringRedisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //令牌验证 String token request.getHeader(Authorization); //验证token try { //从redis中获取相同的token ValueOperationsString, String operations stringRedisTemplate.opsForValue(); String redisToken operations.get(token); if (redisTokennull){ //token已经失效了 throw new RuntimeException(); } MapString, Object claims JwtUtil.parseToken(token); ​ //把业务数据存储到ThreadLocal中 ThreadLocalUtil.set(claims); //放行 return true; } catch (Exception e) { //http响应状态码为401 response.setStatus(401); //不放行 return false; } } ​ Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清空ThreadLocal中的数据 ThreadLocalUtil.remove(); } } ​3.修改密码删除旧就令牌当用户修改密码成功后删除redis中存储的旧令牌核心代码//删除redis中对应的token ValueOperationsString, String operations stringRedisTemplate.opsForValue(); operations.getOperations().delete(token);PatchMapping(/updatePwd) public Result updatePwd(RequestBody MapString, String params,RequestHeader(Authorization) String token) { //1.校验参数 String oldPwd params.get(old_pwd); String newPwd params.get(new_pwd); String rePwd params.get(re_pwd); ​ if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) { return Result.error(缺少必要的参数); } ​ //原密码是否正确 //调用userService根据用户名拿到原密码,再和old_pwd比对 MapString,Object map ThreadLocalUtil.get(); String username (String) map.get(username); User loginUser userService.findByUserName(username); if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){ return Result.error(原密码填写不正确); } ​ //newPwd和rePwd是否一样 if (!rePwd.equals(newPwd)){ return Result.error(两次填写的新密码不一样); } ​ //2.调用service完成密码更新 userService.updatePwd(newPwd); //删除redis中对应的token ValueOperationsString, String operations stringRedisTemplate.opsForValue(); operations.getOperations().delete(token); return Result.success(); }