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)。
下面是对这三部分的详细解释,以及在你的代码中它们是如何被使用的:
- 标头(Header)
标头通常包含两部分信息:
- 类型:通常是JWT。
- 算法:用于签名的算法(如HMAC SHA256)。
- 在你的代码中,标头没有显式地定义,因为Jwts.builder()会自动生成一个默认的标头。
可能结构如下:
{ "typ": "JWT", "alg": "HS256"
}
- 有效载荷(Payload)
有效载荷包含了JWT的主体信息,也就是你要传递的数据。它可以包含一些标准的声明(如iss、exp、sub等)和自定义声明。
在代码中,有效载荷是通过以下代码段创建的:
Claims claims = Jwts.claims();
claims.put("adminId", id);
- 这里将adminId作为自定义声明放入有效载荷中。
可能结构如下:
{ "sub": "1234567890", "name": "John Doe", "adminId": "abc123", "iat": 1516239022
}
- 签名(Signature)
签名是通过将编码后的头部和有效载荷结合在一起,并用指定的算法和密钥进行加密生成的
签名的生成是在以下代码中完成的:
.signWith(key)
- 这里使用了前面定义的key来对JWT进行签名。
结构如下:
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload), secret)。
4.2.3.2 JWT工作流程
整个JWT的工作流程如下:
-
构建头部和有效载荷
- 经过Base64Url编码是为了方便传输
所以没有加密能被看到
- 经过Base64Url编码是为了方便传输
-
生成签名
- 对于对称签名(如HMAC),使用共享密钥进行签名
- 对于非对称签名(如RSA),使用私钥进行签名
- 对标头和有效载荷进行编码后,使用指定的算法和密钥生成的
对方没有密钥无法破解
-
将三部分合并
- 将头部、有效载荷和签名用.连接起来,形成最终的JWT字符串(对编码连接)。
就是Token
- 将头部、有效载荷和签名用.连接起来,形成最终的JWT字符串(对编码连接)。
签名和加密是不同的:
JWT的签名机制(如HMAC或RSA)用于验证信息的完整性和真实性,而不是用来保护信息的机密性。
- 即使攻击者知道签名算法,也无法伪造有效的JWT,因为没有密钥
Base64Url编码:
编码可以被任何人解码,因此有效载荷中不应该包含敏感信息(如密码、个人身份信息等)。
JWT中的有效载荷部分(Payload)是经过Base64Url编码的,这只是编码,而不是加密。JWT的设计假设内容是公开的信息,且应该在有效载荷中加工数据,而不是保护数据。
4.3 登入实现
4.3.1 登入步骤
密码比较不可以用equals,明码比较的话安全性低
- 前端输入账号密码
- 根据账号获得数据库加密的密码
- 前端密码跟数据库已加密的密码比较
- 比对成功后签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 比较
区别如下
-
存储位置:
-
JWT:令牌存储在客户端,每次请求时都携带。
-
Redis:令牌或相关状态存储在服务器端的Redis中。
-
-
过期时间管理:
- JWT:令牌本身的过期时间是在生成时就固定的,一旦过期就无法再使用。
- Redis:令牌或相关状态的过期时间可以动态调整。例如,你的代码中在过期时间接近时会自动续期。
-
复杂度:
- JWT:相对简单,无需服务器端额外的逻辑来管理过期时间。
- Redis:需要额外的逻辑来检查和续期过期时间,增加了服务器端的复杂度。
-
效果
- JWT过期时间:一旦过期,令牌就无效,客户端需要重新获取新的令牌。
- Redis过期时间:可以动态续期,使得令牌在一定条件下始终有效。
-
使用场景
- JWT:适用于需要简单、快速的身份验证场景,减少服务器负担。
- Redis:适用于需要更灵活的过期时间管理场景,如刷新令牌机制。
-
总结
- 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,可以使用黑名单或者额外的逻辑来实现。
(后续有遇到问题再添加)
声明:如本内容中存在错误或不准确之处,欢迎指正。转载时请注明原作者信息(麻辣香蝈蝈)。