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

【Java】基于JWT+Token实现完整登入功能(原理+实操图解)

Java系列文章目录

补充内容 Windows通过SSH连接Linux
第一章 Linux基本命令的学习与Linux历史


文章目录

  • Java系列文章目录
  • 一、前言
  • 二、学习内容:
  • 三、问题描述
  • 四、解决方案:
    • 4.1 认识相关依赖
      • 4.1.1 工具包依赖
      • 4.1.2 非空注解依赖
      • 4.1.3 Token相关依赖
      • 4.1.4 依赖文件参考
    • 4.2 使用JWT
      • 4.2.1 JwtConfig配置
      • 4.2.2 JWT的工具类
        • 4.2.2.1 代码内容
    • 4.2.3 工具类代码解析
      • 4.2.3.1 JWT结构
      • 4.2.3.2 JWT工作流程
    • 4.3 登入实现
      • 4.3.1 登入步骤
      • 4.3.2 代码实现
      • 4.3.3 测试结果
    • 4.4 配置拦截器
    • 4.5 通过Token获取数据
      • 4.5.1 拦截器内部判断以及实现业务逻辑
      • 4.5.2 测试结果
    • 4.6 JWT令牌的过期时间方法比较
      • 4.6.1 通过Redis方法
      • 4.6.2 设置JWT过期时间方法
      • 4.6.3 比较
    • 4.7 JWT与Session与Token与Cookie区别
  • 五、总结:

一、前言

  • 学习JWT+Token的传输方式
  • 流程图原作者 流程图来源 JWT的讲解也有可看这篇文章
  • 本文以实操为主,部分以图片展示代码完整可自己敲加快掌握
  • 本文于24年9月9日补充理论部分

二、学习内容:

  • Token+Redis 方法

一种结合JWT与Redis的解决方案

  • 服务器生成令牌并将器存储在Redis中,同时只有前端持有此令牌本身

流程图来源地址

实操如下流程:
在这里插入图片描述

实操项目结构与数据流程:
在这里插入图片描述


三、问题描述

  • 保证传输的安全性
  • 想了解JWT原理可看这篇文章 流程图来源

四、解决方案:

4.1 认识相关依赖

4.1.1 工具包依赖

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version>
</dependency>
  • 加密用的就是工具包的加密工具

4.1.2 非空注解依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • DTO使用的非空注解要引依赖

4.1.3 Token相关依赖

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version><scope>runtime</scope>
</dependency>
  • 自定义JWT工具类需要的依赖

4.1.4 依赖文件参考

  • pom.xml文件
    <dependencies>
<!--        提供加密工具包--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency><!--非空注解--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!--        token依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--MyBatis Plus 代码生成器--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.3.2</version></dependency><dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.2</version></dependency><!--mysql的连接--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></dependencies>

4.2 使用JWT

4.2.1 JwtConfig配置

JwtConfig通过properties文件配置JWT,properties文件自己配好数据库与Redis

  • properties文件里面加上这个
jwt.key=12345678901234567890123456789012
jwt.ttl=3600000
  • 通过前缀引入

在这里插入图片描述

4.2.2 JWT的工具类

4.2.2.1 代码内容

这个只是对称加密方法的示例

这个注释掉的在后面的方法比较中会讲.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))

代码如下:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;import org.example.learnjwt.config.JwtConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.security.Key;/*** JWT工具类,提供JWT的创建、解析和验证功能*/
@Component
public class JwtUtil {// 注入JWT配置@Autowiredprivate JwtConfig jwtConfig;// 用于签名的密钥private final Key key ;/*** 构造函数,初始化JWT配置和密钥* * @param jwtConfig JWT配置*/public JwtUtil(JwtConfig jwtConfig){this.jwtConfig=jwtConfig;key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes());}/*** 创建JWT令牌* * @param id 要包含在JWT中的标识符* @return 生成的JWT字符串*/public String createJwt(String id) {// 创建并设置声明Claims claims = Jwts.claims();claims.put("adminId",id);// 构建并返回JWTreturn Jwts.builder().setClaims(claims)//.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl())).signWith(key).compact();}/*** 解析JWT令牌,获取其中的声明* * @param token 待解析的JWT令牌* @return JWT中的声明*/public Claims parseJwt(String token) {// 使用密钥解析JWT,并返回其主体部分return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();}/*** 验证JWT令牌的有效性* * @param token 待验证的JWT令牌* @return 如果令牌有效则返回true,否则返回false*/public boolean validateToken(String token) {try {// 尝试解析JWT,如果成功则令牌有效Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);return true;} catch (Exception e) {// 如果解析异常,则令牌无效return false;}}
}
  • 补充前后端传输使用的DTO

非空注解记得引入相关依赖

@Data
public class AdminLoginDTO {@NotNull(message = "手机号不能为空")private String phone;@NotNull(message = "密码不能为空")private String password;
}

4.2.3 工具类代码解析

4.2.3.1 JWT结构

在JWT(JSON Web Token)中,令牌的结构由三部分组成:头部(Header)、有效载荷(Payload)和签名(Signature)

下面是对这三部分的详细解释,以及在你的代码中它们是如何被使用的:

  1. 标头(Header)

标头通常包含两部分信息:

  • 类型:通常是JWT。
  • 算法:用于签名的算法(如HMAC SHA256)。
  • 在你的代码中,标头没有显式地定义,因为Jwts.builder()会自动生成一个默认的标头。

可能结构如下:

{  "typ": "JWT",  "alg": "HS256"  
}
  1. 有效载荷(Payload)

有效载荷包含了JWT的主体信息,也就是你要传递的数据。它可以包含一些标准的声明(如iss、exp、sub等)和自定义声明。

在代码中,有效载荷是通过以下代码段创建的:

Claims claims = Jwts.claims();  
claims.put("adminId", id);  
  • 这里将adminId作为自定义声明放入有效载荷中。

可能结构如下:

{  "sub": "1234567890",  "name": "John Doe",  "adminId": "abc123",  "iat": 1516239022  
}
  1. 签名(Signature)

签名是通过将编码后的头部和有效载荷结合在一起,并用指定的算法和密钥进行加密生成的

签名的生成是在以下代码中完成的:

.signWith(key)  
  • 这里使用了前面定义的key来对JWT进行签名。

结构如下:

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload), secret)

4.2.3.2 JWT工作流程

整个JWT的工作流程如下:

  1. 构建头部和有效载荷

    • 经过Base64Url编码是为了方便传输所以没有加密能被看到
  2. 生成签名

    • 对于对称签名(如HMAC),使用共享密钥进行签名
    • 对于非对称签名(如RSA),使用私钥进行签名
    • 对标头和有效载荷进行编码后,使用指定的算法和密钥生成的对方没有密钥无法破解
  3. 将三部分合并

    • 将头部、有效载荷和签名用.连接起来,形成最终的JWT字符串(对编码连接)。就是Token

签名和加密是不同的:

JWT的签名机制(如HMAC或RSA)用于验证信息的完整性和真实性,而不是用来保护信息的机密性。

  • 即使攻击者知道签名算法,也无法伪造有效的JWT,因为没有密钥

Base64Url编码:

编码可以被任何人解码,因此有效载荷中不应该包含敏感信息(如密码、个人身份信息等)

JWT中的有效载荷部分(Payload)是经过Base64Url编码的,这只是编码,而不是加密。JWT的设计假设内容是公开的信息,且应该在有效载荷中加工数据,而不是保护数据。

4.3 登入实现

4.3.1 登入步骤

密码比较不可以用equals,明码比较的话安全性低

  1. 前端输入账号密码
  2. 根据账号获得数据库加密的密码
  3. 前端密码跟数据库已加密的密码比较
  4. 比对成功后签Token返回

🌟 前端要获取Token
🌟 JWT不是很安全,用户的密码一定不能保存到JWT中

登入逻辑如下:
在这里插入图片描述

4.3.2 代码实现

  • 登入成功后签发Token并放到Redis

放Redis里面并设置时间是为了方便后续验证与延期,验证成功方便获取ID

后端控制层与逻辑层:
在这里插入图片描述

4.3.3 测试结果

  • 测试登入

ApiPost模拟前端

登入后返回前端的数据
在这里插入图片描述

  • 后台显示结果

在这里插入图片描述

4.4 配置拦截器

  • 拦截器排除登入以外的路径

拦截器拦截判断请求头与Token

🌟 前端已经收到Token了接下来交互验证都通过这个Token
🌟 拦截后验证Token是否过期
🌟 TokenInterceptor用来判断请求是否通过
⭐️ 通过请求头携带Token

拦截器逻辑如下:
在这里插入图片描述

拦截器代码如下:
在这里插入图片描述

4.5 通过Token获取数据

4.5.1 拦截器内部判断以及实现业务逻辑

  • 判断有了Token才能继续进入否则直接被拦截

主要判断头部以及Token是否过期并顺便进行延期,登入后访问其他页面不需要再重新登入,因为已经持有Token且没有过期

🌟 通过拦截器后执行业务逻辑

经过拦截器成功后经过AdminController层最后到AdminServicelmpl执行获取数据方法

获取数据逻辑:
在这里插入图片描述

主要代码:
在这里插入图片描述

存储存储和获取当前线程的上下文信息代码:

/*** BaseContext类是用于存储和获取当前线程的上下文信息* 主要用于在多线程环境下,为每个线程提供独立的存储空间*/
public class BaseContext {// 使用ThreadLocal为每个线程提供独立的存储空间,避免数据共享带来的问题private static ThreadLocal<String> threadLocal = new ThreadLocal<>();/*** 设置当前线程的上下文信息* 通常用于标识当前操作的用户ID,以便在日志记录或数据权限控制中使用** @param adminId 管理员ID,用于标识当前操作的用户*/public static void set(String adminId){threadLocal.set(adminId);}/*** 获取当前线程的上下文信息** @return 当前线程存储的管理员ID,如果未设置则返回null*/public static String get(){return threadLocal.get();}}
  • 如果Token存储时间不够会进行延期处理

如果长时间没使用就需要重新登入

在这里插入图片描述

4.5.2 测试结果

  • 测试成功

ApiPost模拟前端

在这里插入图片描述

  • 测试失败

此时Token在Redis里面已经过期

在这里插入图片描述

4.6 JWT令牌的过期时间方法比较

4.6.1 通过Redis方法

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("Authorization");log.info("token还剩下{}秒", redisTemplate.getExpire("adminLogin:token", TimeUnit.SECONDS));// 检查Redisif (!redisTemplate.hasKey("adminLogin:token")) {throw new RuntimeException("redis里面token不存在");}Long expire = redisTemplate.getExpire("adminLogin:token", TimeUnit.SECONDS);if (expire < 30) {redisTemplate.expire("adminLogin:token", 40, TimeUnit.SECONDS);}// 获取JWT ClaimsClaims claims = jwtUtil.parseJwt(token);String id = claims.get("adminId", String.class);log.info("id:{}", id);BaseContext.set(id);return true;
}

这段代码主要做了以下几件事:

  • 检查JWT令牌的存在:从请求头中获取JWT令牌。
  • 检查Redis中的过期时间:获取Redis中存储的令牌的过期时间。
  • 自动续期:如果过期时间小于30秒,则将其延长到40秒。
  • 解析JWT令牌:通过Token解析JWT令牌中的claims,并从中提取用户ID对应有效载荷里的ID

4.6.2 设置JWT过期时间方法

.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))
  • 工具类里面创建令牌时候的配置

4.6.3 比较

区别如下

  1. 存储位置:

    • JWT:令牌存储在客户端,每次请求时都携带。

    • Redis:令牌或相关状态存储在服务器端的Redis中。

  2. 过期时间管理:

    • JWT:令牌本身的过期时间是在生成时就固定的,一旦过期就无法再使用。
    • Redis:令牌或相关状态的过期时间可以动态调整。例如,你的代码中在过期时间接近时会自动续期。
  3. 复杂度:

    • JWT:相对简单,无需服务器端额外的逻辑来管理过期时间。
    • Redis:需要额外的逻辑来检查和续期过期时间,增加了服务器端的复杂度。
  4. 效果

    • JWT过期时间:一旦过期,令牌就无效,客户端需要重新获取新的令牌。
    • Redis过期时间:可以动态续期,使得令牌在一定条件下始终有效。
  5. 使用场景

    • JWT:适用于需要简单、快速的身份验证场景,减少服务器负担。
    • Redis:适用于需要更灵活的过期时间管理场景,如刷新令牌机制。
  6. 总结

    • JWT:简单、无状态,适合轻量级的身份验证。
    • Redis:灵活、状态化,适合需要动态管理过期时间的场景。

选择哪种方式取决于具体的应用需求和场景。

  • 如果你需要更灵活的过期时间管理,可以选择Redis;
  • 如果需要简单的无状态验证,可以选择JWT。

4.7 JWT与Session与Token与Cookie区别

网上文章很详细,这里记录关键

  • JWT是一种用于身份验证的开放标准,而Token是一种用于身份验证和访问控制的令牌
  • Session是在服务器端存储用户信息的机制,而Cookie是在客户端存储用户信息的机制;
  • JWT和Token都可以用于跨域身份验证和授权,而Session和Cookie通常用于同域身份验证和授权;
  • JWT和Token都使用数字签名来验证其有效性,而Session和Cookie通过唯一标识来验证用户。

五、总结:

JWT的优点是简单、轻量级、跨平台、可扩展性强,并且不需要在服务器端保存用户的登录状态。
缺点是一旦签发的JWT被盗用,无法立即使其失效,除非附加一些额外的逻辑来实现JWT的撤销。

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。

JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。

  • 头部包含了加密算法、令牌类型等信息,一般使用Base64编码进行编码。
  • 负载是JWT的主要部分,包含了一些声明(claims),例如用户ID、角色等信息。负载也可以包含自定义的声明。负载也使用Base64编码进行编码。
  • 签名用于保证令牌的完整性和真实性。签名由头部、负载、预先定义的密钥和指定的加密算法组成。一般使用HMAC或RSA算法进行签名。

使用JWT进行身份验证和授权的流程如下:

  • 用户登录,服务器验证用户信息。
  • 服务器生成JWT,将用户信息和其他必要的信息编码到负载中
  • 服务器使用密钥对JWT进行签名,生成签名。
  • 服务器将JWT和签名返回给客户端。
  • 客户端在后续请求中将JWT放入请求头、Cookie或其他合适的位置进行传输。
  • 服务器验证JWT的签名,并根据负载中的信息进行权限控制和身份验证。

在使用JWT时需要注意以下几点:

  • JWT中不应包含敏感信息,因为负载是经过Base64编码的,可能会被解码。
  • 密钥的安全非常重要,因为密钥用于生成和验证签名。应该使用强密码来保护密钥,并定期更换密钥。
  • JWT的过期时间应该适当设置,以免被盗用后长时间有效。
  • 如果需要撤销JWT,可以使用黑名单或者额外的逻辑来实现。

(后续有遇到问题再添加)


声明:如本内容中存在错误或不准确之处,欢迎指正。转载时请注明原作者信息(麻辣香蝈蝈)。

在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 鸿蒙开发5.0【帧率】解析
  • 宠物毛发对人体有什么危害?宠物空气净化器小米、希喂、352对比实测
  • Nginx的配置性能优化
  • Docker方式部署ProxySQL和Keepalived组合实现MGR的高可用访问
  • Java面试篇基础部分-Java反射机制是什么
  • 使用Jenkins扩展钉钉消息通知
  • python操作腾讯企业邮箱读取邮件内容
  • 人工智能(AI)正在以前所未有的速度融入我们生活的方方面面
  • IntelliJ IDEA 中实现 Spring Boot 项目 的自动编译
  • 【PyTorch】使用容器(Containers)进行网络层管理(Module)
  • 峟思投入式水位计的安全操作指南
  • AD元器件库中参数的设计
  • Java Spring Boot 项目中的密码加密与验证开发案例手册
  • FPGA技术赋能云数据中心:提高性能与效率
  • 数据治理与数据管理的区别:深入剖析与理解
  • python大佬养成计划----difflib模块
  • Redis在Web项目中的应用与实践
  • 给新手的新浪微博 SDK 集成教程【一】
  • 推荐一款sublime text 3 支持JSX和es201x 代码格式化的插件
  • 一份游戏开发学习路线
  • NLPIR智能语义技术让大数据挖掘更简单
  • # 再次尝试 连接失败_无线WiFi无法连接到网络怎么办【解决方法】
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • #162 (Div. 2)
  • %check_box% in rails :coditions={:has_many , :through}
  • (1)SpringCloud 整合Python
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (CVPRW,2024)可学习的提示:遥感领域小样本语义分割
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (二十三)Flask之高频面试点
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (源码分析)springsecurity认证授权
  • (转)创业家杂志:UCWEB天使第一步
  • (转)利用ant在Mac 下自动化打包签名Android程序
  • (转)项目管理杂谈-我所期望的新人
  • (转载)深入super,看Python如何解决钻石继承难题
  • .gitignore
  • .NET Framework与.NET Framework SDK有什么不同?
  • .Net Remoting常用部署结构
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • .NET 将混合了多个不同平台(Windows Mac Linux)的文件 目录的路径格式化成同一个平台下的路径
  • .NET 中创建支持集合初始化器的类型
  • .Net(C#)常用转换byte转uint32、byte转float等
  • .Net面试题4
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .NET序列化 serializable,反序列化
  • @Autowired 和 @Resource 区别的补充说明与示例
  • [ARM]ldr 和 adr 伪指令的区别
  • [C/C++]_[初级]_[关于编译时出现有符号-无符号不匹配的警告-sizeof使用注意事项]
  • [E链表] lc83. 删除排序链表中的重复元素(单链表+模拟)
  • [Fri 26 Jun 2015 ~ Thu 2 Jul 2015] Deep Learning in arxiv
  • [HeMIM]Cl,[AeMIM]Br,[CeEIM]Cl,([HO-PECH-MIM]Cl,[HOOC-PECH-MIM]Cl改性酚醛树脂
  • [i.MX]飞思卡尔IMX6处理器的GPIO-IOMUX_PAD说明