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

【博客系统】博客系统第四弹:令牌技术

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


令牌机制


为什么不能使用 Session 实现登录功能?


传统思路:

  • 登录页面把用户名密码提交给服务器。
  • 服务器端验证用户名密码是否正确,并返回校验结果给前端。
  • 如果密码正确,则在服务器端创建 Session。通过 Cookie 把 sessionId 返回给浏览器。

  • 问题:
    • 集群环境下无法直接使用 Session。

  • 原因分析:

    • 右边的三台服务器为一个集群,集群中的每一个服务器称为集群的节点

    • 我们开发的项目,在企业中很少会部署在一台机器上,容易发生单点故障(单点故障:一旦这台服务器挂了,整个应用都没法访问了)。

    • 所以通常情况下,一个 Web 应用会部署在多个服务器上,通过 Nginx 等进行负载均衡。此时,来自一个用户的请求就会被分发到不同的服务器上

image-20250422195216110


回忆 Session 机制:

Browser Server 首次请求(无Session) HTTP Request (无Cookie) 创建Session对象,生成唯一SessionID HTTP Response (Set-Cookie: JSESSIONID=abc123) 浏览器存储Cookie: JSESSIONID=abc123 后续请求(带SessionID) HTTP Request (Cookie: JSESSIONID=abc123) 通过SessionID查找对应Session HTTP Response (使用Session数据) Session维护过程 每个请求自动携带Cookie 通过SessionID获取用户数据 返回个性化响应 loop [会话期间] 会话终止 销毁Session对象 再次请求时携带无效SessionID 要求重新登录(返回新的Set-Cookie) opt [超时或注销] Browser Server

假设我们使用 Session 进行会话跟踪,我们来思考如下场景:

  1. 用户登录:用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,把 Session 存在了第一台服务器上。
  2. 查询操作:用户登录成功之后,携带 Cookie(里面包含 sessionId)继续执行查询操作,比如查询博客列表。此时请求转发到了第二台机器,第二台机器会先进行权限验证操作(通过 sessionId 验证用户是否登录),此时第二台机器上没有该用户的 Session,就会出现问题,提示用户登录,这是用户不能忍受的。

image-20250422195243295

Session 存储在内存中(耗费服务器资源),服务重启,Session 丢失,接下来我们介绍第三种方案:令牌技术。


令牌技术


令牌的运行机制


令牌其实就是用户身份的标识,名称起得很高端,其实本质就是一个字符串

image-20250422195300591

比如我们出行在外,会带着自己的身份证,需要验证身份时,就掏出身份证。

  • 身份证不能伪造,可以辨别真假。
  • 服务器具备生成令牌和验证令牌的能力。

我们使用令牌技术,继续思考上述场景:

  1. 用户登录:用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,生成一个令牌,并返回给客户端。

  2. 客户端收到令牌之后,把令牌存储起来。可以存储在 Cookie 中,也可以存储在其他的存储空间(比如 localStorage)。

  3. 查询操作:用户登录成功之后,携带令牌继续执行查询操作,比如查询博客列表。此时请求转发到了第二台机器,第二台机器会先进行权限验证操作服务器验证令牌是否有效,如果有效,就说明用户已经执行了登录操作;如果令牌是无效的,就说明用户之前未执行登录操作


令牌的优缺点


  • 优点:

    • 解决了集群环境下的认证问题(服务重启,Session 丢失)。
    • 令牌无需在服务器端存储,减轻服务器的存储压力,而 Session 存储在内存中,会耗费服务器资源。
  • 缺点:

    • 需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)。

当前企业开发中,解决会话跟踪使用最多的方案就是令牌技术


JWT 令牌介绍


JWT 全称:JSON Web Token

官网:https://jwt.io/

描述:JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),用于客户端和服务器之间传递安全可靠的信息。其本质是一个 token,是一种紧凑的 URL 安全方法

image-20250516222253059


JWT 令牌组成


JWT 由三部分组成,每部分中间使用点(.)分隔,比如:aaaaa.bbbbb.cccc

  • Header(头部)
    • 头部包括令牌的类型(即 JWT)及使用的哈希算法(如 HMAC SHA256 RSA)。

  • Payload(负载)

    • 负载部分是存放有效信息的地方,里面是一些自定义内容。比如:
    {"userId":"666","userName":"kunkun"
    }
    
    • 也可以存在 JWT 提供的内置字段,比如 exp(过期时间戳)等。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

  • Signature(签名)
    • 此部分用于防止 JWT 内容被篡改,确保安全性。防止被篡改,而不是防止被解析
    • JWT 之所以安全,就是因为最后的签名。JWT 当中任何一个字符被篡改,整个令牌都会校验失败。
    • 就好比我们的身份证,之所以能标识一个人的身份,是因为它不能被篡改,而不是因为内容加密(任何人可以看到身份证的信息,JWT 也是)。

image-20250422195329783

对上述部分的信息,使用 Base64Url 进行编码,合并在一起就是 JWT 令牌

Base64 是编码方式,而不是加密方式。


引入 JWT 令牌依赖


<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version>
</dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope>
</dependency>

引入依赖后,接下来,我们使用 Jar 包中提供的 API 来完成 JWT 令牌的生成和校验


生成 JWT 令牌


image-20250516222711324

public class JwtTest {@Testvoid genToken(){Map<String, Object> claims = new HashMap<>();claims.put("id", 666);claims.put("name", "kunkun");// 这个 Map 表示存放到 token 中的信息, 用户登录 id 为 666, 用户名为 kunkun// 设置 Jwts 令牌的载荷String compact = Jwts.builder().setClaims(claims).compact();// setClaims() 允许 Map 作为参数// compact() 可以将令牌转换成可以被打印的信息System.out.println(compact);}
}

运行测试方法,查看运行结果:

image-20250516224006916


将生成的令牌进行解码:

image-20250516224331119


接下来,我们要为该令牌设置签名

import java.security.Key;  // key 要导入的包public class JwtTest {@Testvoid genToken(){// Keys.hmacShaKeyFor() 用于生成安全密钥, 在这里是以 "123455556" 这个字符串的 getBytes() 进行对密钥的生成Key key = Keys.hmacShaKeyFor("123455556".getBytes(StandardCharsets.UTF_8));  // 这里设置的密钥长度没有达到要求, 运行程序会在这里报错Map<String, Object> claims = new HashMap<>();claims.put("id", 666);claims.put("name", "kunkun");String compact = Jwts.builder().setClaims(claims).signWith(key).compact();// signWith() 用于设置签名, 需要传一个 Key 类型的参数System.out.println(compact);}
}

Keys.hmacShaKeyFor(byte[] keyMaterial) 方法的作用是根据提供的字节数组生成一个适用于 HMAC-SHA 签名算法(如 HS256、HS384、HS512)的密钥对象Key)。这个方法是 JWT 工具类中用于生成密钥的常用方法之一。


运行测试方法 genToken() ,观察运行结果:

image-20250517202652678


报错中提到,可以考虑使用 secretKeyFor(SignatureAlgorithm)方法来创建一个 Key,接下来,我就来演示该方法如何使用:

image-20250517202857199

public class JwtTest {@Testvoid genToken(){// Key key = Keys.hmacShaKeyFor("123455556".getBytes(StandardCharsets.UTF_8));Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 使用 secretKeyFor(SignatureAlgorithm) 方法来创建一个 Key, 该方法每次生成的 Key 都是随机的Map<String, Object> claims = new HashMap<>();claims.put("id", 666);claims.put("name", "kunkun");String compact = Jwts.builder().setClaims(claims).signWith(key).compact();System.out.println(compact);}
}

运行测试方法,观察运行结果:

QQ_1747485218811

输出的内容,就是 JWT 令牌。因此,我们服务端生成令牌的方法就写好了;


固定令牌签名部分


每次调用 secretKeyFor() 方法,生成的密钥是随机的:

// 第一次调用生成的令牌
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoia3Vua3VuIiwiaWQiOjY2Nn0.iDb7jpsCG-EBSVNz6Ee4kPoRA5olz3ML6fZJ4ZddVMM// 第二次调用生成的令牌
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoia3Vua3VuIiwiaWQiOjY2Nn0.upxovdUjdxF4nXLFeG3rSTRH-Gkw2foz2CICN3kzWlE

观察到两次生成的令牌签名部分不一致,这表明每次调用 secretKeyFor() 方法时生成的密钥是随机的。


为了确保服务端的安全性和一致性,我们使用 secretKeyFor() 方法生成一个固定的密钥,并将其作为生成令牌的签名部分。

  • 密钥(Key):是用于生成和验证 JWT 签名的基础数据。
    在代码中,Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); 生成的是一个密钥,用于签名算法。

  • 签名信息:是 JWT 的一部分,由密钥和签名算法生成的哈希值,用于验证 JWT 的完整性和真实性。


前面是根据 secretKeyFor() 方法生成的密钥为基础,创建令牌。如果希望每次生成的 JWT 签名一致,需要使用固定的密钥

因此,我们需要先获取公共令牌的签名信息

@Test
void getKey() {Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 使用 HS256 算法生成一个随机的密钥String encodedKey = Encoders.BASE64.encode(key.getEncoded());// 将密钥的字节数组转换为 Base64 编码的字符串// getEncoded():获取密钥的字节数组表示// Encoders.BASE64.encode():将字节数组转换为 Base64 编码的字符串,便于存储和传输System.out.println(encodedKey);// 打印 Base64 编码的密钥字符串}

程序运行结果:

sYAN5HvB8HQRzX1QTEFRhseSsgXIDJsggPhC1gNLa0Y=Process finished with exit code 0

因此,我们就获取到了公共令牌的密钥;


使用hmacShaKeyFor(),根据刚刚生成的密钥字符串来创建密钥对象:

@Test
void genToken(){//        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);Key key = Keys.hmacShaKeyFor("sYAN5HvB8HQRzX1QTEFRhseSsgXIDJsggPhC1gNLa0Y=".getBytes(StandardCharsets.UTF_8));// 使用刚刚获取到的密钥字符串, 来创建密钥对象Map<String, Object> claims = new HashMap<>();claims.put("id", 666);claims.put("name", "kunkun");String compact = Jwts.builder().setClaims(claims).signWith(key).compact();// signWith(key) 表示设置令牌签名, 会根据传入的密钥和密钥算法, 转化为签名部分System.out.println(compact);
}

运行两次方法,输出的内容,就是 JWT 令牌,我们查看一下,生成令牌对应的签名是否相同:

第一次调用:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoia3Vua3VuIiwiaWQiOjY2Nn0.kSCNNN-_b3aPZRkCaTiAlZ1Jqt5lizfxW0HtPNdcP-Y第二次调用:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoia3Vua3VuIiwiaWQiOjY2Nn0.kSCNNN-_b3aPZRkCaTiAlZ1Jqt5lizfxW0HtPNdcP-Y

此时,两次调用 genToken()方法生成的令牌,其中签名的部分就被固定好了’


通过点(.)对三个部分进行分割,我们把生成的令牌通过官网进行解析,就可以看到我们存储的信息了。

image-20250422195412716

  1. HEADER 部分:可以看到使用的算法为 HS256
  2. PAYLOAD 部分:是我们自定义的内容exp 表示过期时间
  3. VERIFY SIGNATURE 部分:是经过签名算法计算出来的,所以不会解析

校验 JWT 令牌


服务端完成了令牌的生成,我们需要根据令牌,来校验令牌的合法性(以防客户端伪造)。


令牌解析


public class JwtTest {@Testvoid genToken(){Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);Map<String, Object> claims = new HashMap<>();claims.put("id", 666);claims.put("name", "kunkun");String compact = Jwts.builder().setClaims(claims).signWith(key).compact();System.out.println(compact);// 创建解析器,设置签名密钥JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 解析 token 并打印解析结果System.out.println(build.parse(compact).getBody());// parse() 的参数是一个字符串, 表示要解析的令牌// getBody() 表示获取解析结果, 进而可以打印除解析的结果}
}

测试方法运行结果:

image-20250517205258866

  • 令牌解析后,我们可以看到里面存储的信息。如果在解析的过程中没有报错,就说明解析成功了。
  • 令牌解析时,也会进行时间有效性的校验。如果令牌过期了,解析也会失败。

令牌是可以被解析的,那么令牌是否可以被修改呢?

public class JwtTest {@Testvoid genToken(){// .....JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 原来的令牌也是字符串, 现在解析(原来的令牌+多余字符串)System.out.println(build.parse(compact + "kunkun666").getBody());}
}

运行测试方法,程序运行结果:

image-20250517205638728

因此,修改令牌中的任何一个字符,都会校验失败,所以令牌无法篡改。


在这里插入图片描述

在这里插入图片描述

相关文章:

  • 【python深度学习】Day34 GPU训练及类的call方法
  • 智能指针
  • 科研经验贴:AI领域的研究方向总结
  • DAO模式
  • Java转Go日记(五十六):gin 渲染
  • 提高 Maven 项目的编译效率
  • 大厂技术大神远程 3 年,凌晨 1 点到 6 点竟开会 77 次。同事一脸震惊,网友:身体还扛得住吗?
  • matlab时间反转镜算法
  • Appium+python自动化(四)- 如何查看程序所占端口号和IP
  • 动态防御体系实战:AI如何重构DDoS攻防逻辑
  • 交安安全员:交通工程安全领域的关键角色
  • DB-GPT扩展自定义Agent配置说明
  • 同为科技领军智能电源分配单元技术,助力物联网与计量高质量发展
  • Linux安装Nginx并配置转发
  • WPF性能优化之延迟加载(解决页面卡顿问题)
  • 园区/小区执法仪部署指南:ZeroNews低成本+高带宽方案”
  • 实时操作系统革命:实时Linux驱动的智能时代底层重构
  • EasyExcel使用
  • Git全流程操作指南
  • OS面试篇
  • 建设厅职业资格中心网站/免费b2b推广网站
  • 六安网站制作/制作app平台需要多少钱
  • 邹城做网站/百度下载安装2019
  • 电商排名/seo关键词优化推广外包
  • 网站建设报告实训步骤/seo专业技术培训
  • 旅游商业网站策划书/360竞价推广客服电话