【博客系統】博客系統第四彈:令牌技術

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述


令牌機制


為什么不能使用 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
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/81959.shtml
英文地址,請注明出處:http://en.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; -- 注釋內容 或 # 注釋內容&#…