![](https://i-blog.csdnimg.cn/blog_migrate/7d12c98dbf430340e3ae64e72a87e741.gif)
Java系列文章目录
补充内容 Windows通过SSH连接Linux
第一章 Linux基本命令的学习与Linux历史
一、前言
- 学习JWT+Token的传输方式
- 流程图原作者 流程图来源 JWT的讲解也有可看这篇文章
- 本文以实操为主,部分以图片展示代码完整可自己敲加快掌握
- 本文自定义工具类仅仅是示例
二、学习内容:
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的工具类
这个只是示例
这个注释掉的在后面的方法比较中会讲.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配置
@Autowired
private 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);
// 构建并返回JWT
return 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.3 登入实现
4.3.1 登入步骤
密码比较不可以用equals,明码比较的话安全性低
- 前端输入账号密码
- 根据账号获得数据库加密的密码
- 前端密码跟数据库已加密的密码比较
- 比对成功后签Token返回
🌟 前端要获取Token
🌟 JWT不是很安全,用户的密码一定不能保存到JWT中
登入逻辑如下:
4.3.2 代码实现
后端控制层与逻辑层:
4.3.3 测试结果
- 测试登入
ApiPost模拟前端
登入后返回前端的数据
- 后台显示结果
4.4 配置拦截器
- 拦截器排除登入以外的路径
拦截器拦截判断请求头与Token
🌟 前端已经收到Token了接下来交互验证都通过这个Token
🌟 拦截后验证Token是否过期
拦截器逻辑如下:
拦截器代码如下:
4.5 通过Token获取数据
4.5.1 拦截器内部判断以及实现业务逻辑
- 判断有了Token才能继续进入否则直接被拦截
主要判断头部以及Token是否过期并顺便进行延期,登入后访问其他页面不需要再重新登入,因为已经持有Token且没有过期
🌟 通过拦截器后执行业务逻辑
获取数据逻辑:
- 这是在TokenInterceptor里面定义的🌟判断请求是否通过
经过拦截器成功后经过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));
// 检查Redis
if (!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 Claims
Claims 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令牌:解析JWT令牌中的claims,并从中提取用户ID。
4.6.2 设置JWT过期时间方法
.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))
- 工具类里面创建令牌时候的配置
4.6.3 比较
区别如下
-
存储位置:
-
JWT:令牌存储在客户端,每次请求时都携带。
-
Redis:令牌或相关状态存储在服务器端的Redis中。
-
-
过期时间管理:
- JWT:令牌本身的过期时间是在生成时就固定的,一旦过期就无法再使用。
- Redis:令牌或相关状态的过期时间可以动态调整。例如,你的代码中在过期时间接近时会自动续期。
-
复杂度:
- JWT:相对简单,无需服务器端额外的逻辑来管理过期时间。
- Redis:需要额外的逻辑来检查和续期过期时间,增加了服务器端的复杂度。
-
效果
- JWT过期时间:一旦过期,令牌就无效,客户端需要重新获取新的令牌。
- Redis过期时间:可以动态续期,使得令牌在一定条件下始终有效。
-
使用场景
- JWT:适用于需要简单、快速的身份验证场景,减少服务器负担。
- Redis:适用于需要更灵活的过期时间管理场景,如刷新令牌机制。
-
总结
- JWT:简单、无状态,适合轻量级的身份验证。
- Redis:灵活、状态化,适合需要动态管理过期时间的场景。
选择哪种方式取决于具体的应用需求和场景。
- 如果你需要更灵活的过期时间管理,可以选择Redis;
- 如果需要简单的无状态验证,可以选择JWT。
JWT与Session与Token与Cookie原理(后续补充)
本文暂以实操为主
五、总结:
JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它是一个轻量级的跨平台解决方案,可以在不同的系统之间安全地传输和验证信息。
JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。
- 头部包含了加密算法、令牌类型等信息,一般使用Base64编码进行编码。
- 负载是JWT的主要部分,包含了一些声明(claims),例如用户ID、角色等信息。负载也可以包含自定义的声明。负载也使用Base64编码进行编码。
- 签名用于保证令牌的完整性和真实性。签名由头部、负载、预先定义的密钥和指定的加密算法组成。一般使用HMAC或RSA算法进行签名。
使用JWT进行身份验证和授权的流程如下:
- 用户登录,服务器验证用户信息。
- 服务器生成JWT,将用户信息和其他必要的信息编码到负载中。
- 服务器使用密钥对JWT进行签名,生成签名。
- 服务器将JWT和签名返回给客户端。
- 客户端在后续请求中将JWT放入请求头、Cookie或其他合适的位置进行传输。
- 服务器验证JWT的签名,并根据负载中的信息进行权限控制和身份验证。
JWT的优点是简单、轻量级、跨平台、可扩展性强,并且不需要在服务器端保存用户的登录状态。
缺点是一旦签发的JWT被盗用,无法立即使其失效,除非附加一些额外的逻辑来实现JWT的撤销。
在使用JWT时需要注意以下几点:
- JWT中不应包含敏感信息,因为负载是经过Base64编码的,可能会被解码。
- 密钥的安全非常重要,因为密钥用于生成和验证签名。应该使用强密码来保护密钥,并定期更换密钥。
- JWT的过期时间应该适当设置,以免被盗用后长时间有效。
- 如果需要撤销JWT,可以使用黑名单或者额外的逻辑来实现。
(后续有遇到问题再添加)
声明:如本内容中存在错误或不准确之处,欢迎指正。转载时请注明原作者信息(麻辣香蝈蝈)。