当前位置: 首页 > news >正文

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的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。

http://www.jsqmd.com/news/227881/

相关文章:

  • Blender3mfFormat:重塑3D打印工作流的智能插件解决方案
  • Switch手柄电脑连接新姿势:从游戏操控到创意玩法的全面解锁
  • TranslucentTB透明任务栏深度配置指南:从基础应用到高级定制全解析
  • G-Helper完整指南:华硕ROG笔记本的终极轻量化控制方案
  • DLSS Swapper完全指南:解锁游戏画质与性能的终极解决方案
  • RePKG终极指南:轻松解包Wallpaper Engine资源与纹理转换
  • PDF-Extract-Kit部署教程:Docker容器化部署完整步骤
  • SpringBoot整合Email 邮件发送详解
  • PDF-Extract-Kit进阶教程:多模型协同工作流设计
  • 终极网盘下载工具:3分钟掌握6大云盘极速下载技巧
  • BetterGI原神自动化助手:3分钟掌握智能游戏体验
  • 网盘直链下载助手:告别限速困扰的完整使用指南
  • 全面掌握LeaguePrank:LOL游戏界面个性化定制权威指南
  • STM32下ST7789屏幕旋转功能实现解析
  • 猫抓浏览器扩展:告别视频下载难题的终极解决方案
  • 嵌入式C项目实战:keil5编译器5.06下载与工程创建
  • 如何快速掌握LeagueAkari:英雄联盟自动化助手的完整使用指南
  • 游戏画面升级利器:5个步骤快速掌握DLSS版本切换技巧
  • 游戏性能优化新境界:DLSS版本管理的艺术与科学
  • PDF-Extract-Kit WebUI使用指南:从安装到高级功能详解
  • PDF-Extract-Kit保姆级教程:解决中文PDF识别难题
  • DownKyi视频下载工具:解锁B站内容离线收藏新体验
  • 精通XUnity Auto Translator:游戏文本本地化深度解析与实战进阶
  • LeagueAkari英雄联盟辅助工具:5分钟快速上手的智能游戏管家
  • PDF-Extract-Kit表格识别进阶:合并单元格处理技巧
  • 高效年会抽奖系统实战手册:从零配置到专业应用
  • League Akari智能辅助:让英雄联盟操作效率提升300%的秘密武器
  • 5分钟掌握League Akari:英雄联盟玩家的终极自动化解决方案
  • Switch手柄PC连接全攻略:轻松实现跨平台游戏操控
  • Ryujinx Switch模拟器终极配置指南:从新手到高手的快速上手教程