news 2026/6/10 19:35:02

SpringBoot整合JWT

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot整合JWT

一. JWT简介
1. 什么是JWT?

JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。

它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证;应用场景如用户登录。JWT详细讲解请见 github:https://github.com/jwtk/jjwt

2. 为什么使用JWT?

随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。

3. 传统Cookie+Session与JWT对比

① 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。

cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。

② JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。

4. JWT的组成(3部分)

第一部分为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。【中间用 . 分隔】

一个标准的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw

先来讲讲头部:

Jwt的头部承载两部分信息:

声明类型,这里是jwt

声明加密的算法,通常直接使用HMACSHA256,就是HS256了

{ "alg": "HS256", "typ": "JWT" }

然后将头部进行base64编码构成了第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

然后我们看第二部分:载荷

载荷,即承载的意思。也就是说这里是承载消息具体内容的地方。

内容又可以分为3种标准

标准中注册的声明

公共的声明

私有的声明

【标准声明】

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的.

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
【公共声明】

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

【私有声明】

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

对Payload进行Base64加密就得到了JWT第二部分的内容。

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

第三部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。

注意:
签证部分和Base64没啥关系,只是拼接了header与payload部分,然后i通过secret进行组合加密。secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。

5. JWT验证流程和特点

验证流程:

① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

6. JWT优缺点

优点:

①. 可扩展性好

应用程序分布式部署的情况下,Session需要做多机数据共享,通常可以存在数据库或者Redis里面。而JWT不需要。

②. 无状态

JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

缺点:

① 安全性:由于JWT的payload是使用Base64编码的,并没有加密,因此JWT中不能存储敏感数据。而Session的信息是存在服务端的,相对来说更安全。

② 性能:JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致JWT非常长,Cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在LocalStorage里面。并且用户在系统中的每一次Http请求都会把JWT携带在Header里面,Http请求的Header可能比Body还要大。而SessionId只是很短的一个字符串,因此使用JWT的Http请求比使用Session的开销大得多。

③ 一次性:无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。即缺陷是一旦下发,服务后台无法拒绝携带该jwt的请求(如踢除用户)

(1)无法废弃:通过JWT的验证机制可以看出来,一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的jwt还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。

(2)续签:如果你使用jwt做会话管理,传统的Cookie续签方案一般都是框架自带的,Session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在Redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。

可以看出想要破解JWT一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了JWT的初衷。而且这个方案和Session都差不多了。

二. Java实现JWT(SpringBoot方式整合)

1. Maven依赖与application.yml配置

<!-- JWT依赖 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> server: port: 8080 spring: application: name: springboot-jwt config: jwt: # 加密密钥 secret: abcdefg1234567 # token有效时长 expire: 3600 # header 名称 header: token

2. 编写JwtConfig

package com.example.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; /** * JWT的token,区分大小写 */ @ConfigurationProperties(prefix = "config.jwt") @Component public class JwtConfig { private String secret; private long expire; private String header; /** * 生成token * @param subject * @return */ public String createToken (String subject){ Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000);//过期时间 return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(subject) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 获取token中注册信息 * @param token * @return */ public Claims getTokenClaim (String token) { try { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); }catch (Exception e){ // e.printStackTrace(); return null; /* catch (ExpiredJwtException e){ return e.getClaims(); //防止jwt过期解析报错 } */ } } /** * 验证token是否过期失效 * @param expirationTime * @return */ public boolean isTokenExpired (Date expirationTime) { return expirationTime.before(new Date()); } /** * 获取token失效时间 * @param token * @return */ public Date getExpirationDateFromToken(String token) { return getTokenClaim(token).getExpiration(); } /** * 获取用户名从token中 */ public String getUsernameFromToken(String token) { return getTokenClaim(token).getSubject(); } /** * 获取jwt发布时间 */ public Date getIssuedAtDateFromToken(String token) { return getTokenClaim(token).getIssuedAt(); } // --------------------- getter & setter --------------------- public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public long getExpire() { return expire; } public void setExpire(long expire) { this.expire = expire; } public String getHeader() { return header; } public void setHeader(String header) { this.header = header; } }

3. 配置拦截器

package com.example.interceptor; import com.example.config.JwtConfig; import io.jsonwebtoken.Claims; import io.jsonwebtoken.SignatureException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class TokenInterceptor extends HandlerInterceptorAdapter { @Resource private JwtConfig jwtConfig ; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SignatureException { /** 地址过滤 */ String uri = request.getRequestURI() ; if (uri.contains("/login")){ return true ; } /** Token 验证 */ String token = request.getHeader(jwtConfig.getHeader()); if(StringUtils.isEmpty(token)){ token = request.getParameter(jwtConfig.getHeader()); } if(StringUtils.isEmpty(token)){ throw new SignatureException(jwtConfig.getHeader()+ "不能为空"); } Claims claims = null; try{ claims = jwtConfig.getTokenClaim(token); if(claims == null || jwtConfig.isTokenExpired(claims.getExpiration())){ throw new SignatureException(jwtConfig.getHeader() + "失效,请重新登录。"); } }catch (Exception e){ throw new SignatureException(jwtConfig.getHeader() + "失效,请重新登录。"); } /** 设置 identityId 用户身份ID */ request.setAttribute("identityId", claims.getSubject()); return true; } }

4. 编写统一异常处理类

package com.example.config; import io.jsonwebtoken.SignatureException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.yuyi.full.handler.exception.ExceptionInfoBO; import org.yuyi.full.handler.exception.ResultBO; import org.yuyi.full.handler.exception.ResultTool; @RestControllerAdvice public class PermissionHandler { @ExceptionHandler(value = { SignatureException.class }) @ResponseBody public ResultBO<?> authorizationException(SignatureException e){ return ResultTool.error(new ExceptionInfoBO(1008,e.getMessage())); } }

5.编写测试接口

package com.example.controller; import com.alibaba.fastjson.JSONObject; import com.example.config.JwtConfig; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.yuyi.full.handler.exception.ResultBO; import org.yuyi.full.handler.exception.ResultTool; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController public class TokenController { @Resource private JwtConfig jwtConfig ; /** * 用户登录接口 * @param userName * @param passWord * @return */ @PostMapping("/login") public ResultBO<?> login (@RequestParam("userName") String userName, @RequestParam("passWord") String passWord){ JSONObject json = new JSONObject(); /** 验证userName,passWord和数据库中是否一致,如不一致,直接return ResultTool.errer(); 【这里省略该步骤】*/ // 这里模拟通过用户名和密码,从数据库查询userId // 这里把userId转为String类型,实际开发中如果subject需要存userId,则可以JwtConfig的createToken方法的参数设置为Long类型 String userId = 5 + ""; String token = jwtConfig.createToken(userId) ; if (!StringUtils.isEmpty(token)) { json.put("token",token) ; } return ResultTool.success(json) ; } /** * 需要 Token 验证的接口 */ @PostMapping("/info") public ResultBO<?> info (){ return ResultTool.success("info") ; } /** * 根据请求头的token获取userId * @param request * @return */ @GetMapping("/getUserInfo") public ResultBO<?> getUserInfo(HttpServletRequest request){ String usernameFromToken = jwtConfig.getUsernameFromToken(request.getHeader("token")); return ResultTool.success(usernameFromToken) ; } /* 为什么项目重启后,带着之前的token还可以访问到需要info等需要token验证的接口? 答案:只要不过期,会一直存在,类似于redis */ }

用PostMan测试工具测试一下,访问登录接口,当对账号密码验证通过时,则返回一个token给客户端

当直接去访问info接口时,会返回token为空的自定义异常:

当在请求头加上正确token时,则拦截器验证通过,可以正常访问到接口

JWT总结
1. 基于JSON,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,Node.JS,PHP等很多语言都可以使用。

2. payload部分,需要时JWT可以存储一些其他业务逻辑所必要的非敏感信息。

3. 体积小巧,便于传输;JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 15:11:08

RePKG终极指南:解锁Wallpaper Engine资源处理全攻略

RePKG终极指南&#xff1a;解锁Wallpaper Engine资源处理全攻略 【免费下载链接】repkg Wallpaper engine PKG extractor/TEX to image converter 项目地址: https://gitcode.com/gh_mirrors/re/repkg 在Wallpaper Engine壁纸开发过程中&#xff0c;PKG文件解包和TEX纹理…

作者头像 李华
网站建设 2026/6/10 15:22:49

XUnity自动翻译器:Unity游戏本地化的终极解决方案

XUnity自动翻译器&#xff1a;Unity游戏本地化的终极解决方案 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为外语游戏中的生涩对话和复杂菜单烦恼吗&#xff1f;XUnity自动翻译器为你提供了一站式…

作者头像 李华
网站建设 2026/6/10 15:38:50

PDF-Extract-Kit模板匹配:处理固定格式文档的秘诀

PDF-Extract-Kit模板匹配&#xff1a;处理固定格式文档的秘诀 1. 引言&#xff1a;为何需要智能PDF提取工具&#xff1f; 在企业级文档处理、学术研究和自动化办公场景中&#xff0c;大量信息以PDF形式存在&#xff0c;尤其是发票、合同、报表等具有固定版式结构的文档。传统…

作者头像 李华
网站建设 2026/6/10 19:02:49

ROG笔记本性能优化全攻略:告别卡顿与高耗电的终极方案

ROG笔记本性能优化全攻略&#xff1a;告别卡顿与高耗电的终极方案 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目地址…

作者头像 李华
网站建设 2026/6/10 15:53:13

DLSS性能优化终极指南:3个免费工具让游戏帧率飙升200%

DLSS性能优化终极指南&#xff1a;3个免费工具让游戏帧率飙升200% 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper 想要在不升级硬件的情况下实现游戏性能质的飞跃&#xff1f;DLSS Swapper这款免费工具正是你需要的终极…

作者头像 李华
网站建设 2026/6/6 2:13:00

DLSS Swapper完整使用手册:轻松掌握游戏画质优化核心技术

DLSS Swapper完整使用手册&#xff1a;轻松掌握游戏画质优化核心技术 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper 想要在游戏中获得最佳画质表现&#xff0c;同时享受流畅的游戏体验吗&#xff1f;DLSS Swapper正是…

作者头像 李华