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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


令牌机制


为什么不能使用 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

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


在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/81959.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【瑞数3代】药监评审中心逆向分析 | 后缀MmEwMD参数

1.目标 目标网址&#xff1a;https://www.cde.org.cn/main/news/listpage/545cf855a50574699b46b26bcb165f32 import requestscookies {FSSBBIl1UgzbN7N80S: 8sYeMWaC_IHoNl8Ckfx2y9MLiueMCkPr2V3MIoZkrMPUfzMMaXKzAoxpNPvyw4lt,Path: /,FSSBBIl1UgzbN7N80T: 3js3ygV.St6BvO20…

【漫话机器学习系列】274.基尼指数(Gini Index)

决策树中的基尼指数&#xff08;Gini Index&#xff09;详解 —— 从公式理解到实际应用 在构建决策树模型时&#xff0c;一个核心问题是&#xff1a;如何选择最优的特征来进行节点划分&#xff1f; 这就涉及到了“划分准则”的问题。常见的准则有信息增益、信息增益率以及本文…

R语言学习--Day07--T分布与T检验

昨天我们介绍了R中用于对数据进行分类的聚类分析的方法&#xff0c;接下来我们来看T分布。 T分布 T分布适用于帮我们估计整组数据&#xff08;较小的数据量&#xff0c;一般小于30&#xff09;的真实值在哪一个区间&#xff0c;具体是计算置信区间&#xff08;一般为95%&#…

数据结构与算法-线性表-双向链表(Double Linked List)

1 线性表 1.4 双向链表&#xff08;Double Linked List&#xff09; 双向链表的结点中有两个指针域&#xff0c;一个指向直接后继&#xff0c;另一个指向直接前驱&#xff0c;主要是为了解决前向查找的问题。 双向链表结构&#xff1a; 书籍和视频教程都只讲解了插入和删除的…

甘特图实例 dhtmlxGantt.js

本文介绍了如何使用dhtmlxGantt库创建一个基础的甘特图示例&#xff0c;并对其进行汉化和自定义配置。首先&#xff0c;通过引入dhtmlxgantt.css和dhtmlxgantt.js文件初始化甘特图。接着&#xff0c;通过设置gantt.i18n.setLocale("cn")实现核心文本的汉化&#xff0…

C++23 新增扁平化关联容器详解

文章目录 一、引言已有关联容器回顾新容器的引入原因 二、std::flat_set定义与特性代码示例适用场景 三、std::flat_multiset定义与特性代码示例适用场景 四、std::flat_map定义与特性代码示例适用场景 五、std::flat_multimap定义与特性代码示例适用场景 六、与其他容器的比较…

使用zap,对web应用/API接口 做安全检测

https://www.zaproxy.org/getting-started/ 检测方法 docker pull ghcr.io/zaproxy/zaproxy:stable# 执行baseline测试 docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \ -t https://baseline.yeshen.org# 执行api测试 docker run -t ghcr.io/zaproxy/zaproxy…

Qt—模态与非模态对话框

Qt—模态与非模态对话框 核心概念 ​模态对话框​​&#xff1a;强制用户优先处理当前窗口&#xff0c;阻塞指定范围的用户交互。​非模态对话框​​&#xff1a;允许用户自由切换窗口&#xff0c;无交互限制。 一、模态对话框类型与行为 1. 应用级模态&#xff08;Applica…

Axure高保真CRM客户关系管理系统原型

一套出色的CRM&#xff08;客户关系管理&#xff09;系统&#xff0c;无疑是企业管理者掌控客户动态、提升销售业绩的得力助手。今天&#xff0c;就为大家介绍一款精心打造的Axure高保真CRM客户关系管理系统原型模板&#xff0c;助你轻松开启高效客户管理之旅。 这款CRM原型模…

【羊圈——状压 + DP / 记忆化搜索DP】

题目 一般DP代码&#xff08;注意&#xff0c;这里只能向外推(起始状态是f(1,0)&#xff0c;不能向内推&#xff08;不然会导致之前的羊圈被割裂&#xff09;&#xff09; #include <bits/stdc.h> using namespace std;const int MAX_N 210; const int MAX_M 16;int n…

讲解Mysql InnoDB的MVCC

1. 定义 MVCC是多版本并发控制&#xff08;Multi - Version Concurrency Control&#xff09;的缩写。它是InnoDB存储引擎实现高并发控制的一种机制。在数据库系统中&#xff0c;多个事务可能会同时对数据进行读写操作&#xff0c;而MVCC通过为数据行保存多个版本来解决并发事务…

ZeroMQ Sockets介绍及应用示例

1. 概念解释 ZeroMQ Sockets提供了一种类标准套接字&#xff08;socket-like&#xff09;的 API&#xff0c;是消息导向的通信机制&#xff0c;基于 TCP/UDP 等传输层协议&#xff0c;但封装了底层细节&#xff08;如连接管理、消息路由、缓冲区等&#xff09;&#xff0c;提供…

语音合成之十五 语音合成(TTS)分句生成拼接时的响度一致性问题:现状、成因与对策

语音合成&#xff08;TTS&#xff09;分句生成拼接时的响度一致性问题&#xff1a;现状、成因与对策 引言&#xff1a;分段式文本转语音中的响度一致性挑战业界对响度差异问题的认知拼接语音片段中响度变化的根本原因分段拼接的固有挑战各片段预测韵律特征的差异文本特征和模型…

Android中Binder驱动作用?

Binder驱动的作用与核心功能 Binder驱动是Android系统中实现进程间通信&#xff08;IPC&#xff09;的核心底层组件&#xff0c;它工作于Linux内核层&#xff0c;负责管理跨进程通信的建立、数据传输、资源同步等关键任务。以下是其核心作用及实现细节&#xff1a; 1. ​​进程…

网络学习-TCP协议(七)

一、TCP协议 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议。 1、三次握手 客户端&#xff1a; 1、先发起连接&#xff0c;发送SYN置1&#xff0c;seqnum12345(随机值)----半连接…

【Python 基础与实战】从基础语法到项目应用的全流程解析

&#xff08;1&#xff09;列表和元组的区别是什么?如何从列表创建元组?如何从元组创建列表? 列表和元组的区别&#xff1a; 可变性&#xff1a;列表是可变的&#xff0c;即可以对列表进行元素的增、删、改操作。例如&#xff0c;可以使用append()方法添加元素&#xff0c;r…

Docker部署Zookeeper集群

简介 ZooKeeper 是一个开源的分布式协调服务&#xff0c;由 Apache 软件基金会开发和维护。它主要用于管理和协调分布式系统中的多个节点&#xff0c;以解决分布式环境下的常见问题&#xff0c;如配置管理、服务发现、分布式锁等。ZooKeeper 提供了一种可靠的机制&#xff0c;…

【学习笔记】Sophus (Python) 使用文档

以下是一份针对 Sophus 库的 Python 使用文档&#xff0c;涵盖基础概念、安装方法、核心功能及代码示例。内容围绕 SO3&#xff08;3D旋转群&#xff09;和 SE3&#xff08;3D刚体变换群&#xff09;展开&#xff0c;适合机器人学、SLAM、三维几何等领域。 Sophus (Python) 使用…

计算机图形学:(三)MVP变换扩展

Three.js WebGL允许把JavaScript和OpenGL 结合在一起运用&#xff0c;但使用WebGL原生的API来写3D程序非常的复杂&#xff0c;同时需要相对较多的数学知识&#xff0c;对于前端开发者来说学习成本非常高。 Three.js是基于webGL的封装的一个易于使用且轻量级的3D库&#xff0c;T…

MySQL数据库操作合集

一、SQL通用语法 ①SQL语句可以单行或多行书写&#xff0c;以分号结尾。 ②SQL语句可以使用空格/缩进来增强语句可读性。 ③MySQL数据库的SQL语句不区分大小写&#xff0c;关键字建议使用大写。 ④注释&#xff1a; 单行注释&#xff1a; -- 注释内容 或 # 注释内容&#…