Gin 框架之jwt 介绍与基本使用
- 一.JWT 介紹
-
二.JWT認證與session認證的區別
- 2.1 基于session認證流程圖
- 2.2 基于jwt認證流程圖
-
三. JWT 的構成
- 3.1 header : 頭部
-
3.2 payload : 負載
- 3.2.1 標準中注冊的聲明 (建議但不強制使用)
- 3.2.2 公共的聲明
- 3.2.3 私有的聲明
- 3.2.4 定義一個payload
- 3.3 signatrue : 簽名
- 3.4 得到 token
-
四.base64 編碼和解碼的使用
- 4.1 base64 編碼
- 4.2 base64 解碼
-
五.JWT 的本質原理
- 5.1 簽發
- 5.2 校驗
- 5.3 jwt認證開發流程(重點)
-
六、Gin 框架中使用jwt
- 6.1 安裝JWT庫
- 6.2 導入庫
-
6.3 使用JWT 鑒權認證
- 6.3.1 JWT中間件開發
- 6.3.2 使用JWT中間件
- 6.3.3 生成JWT token
- 6.3.4 訪問路由簽發token
- 6.3.5 通過 token 鑒權獲取用戶信息
一.JWT 介紹
-
Json web token(JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基于JSON的開放標準(RFC 7519) - 該token被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景
- JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源
- 也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用于認證,也可被加密
二.JWT認證與session認證的區別
2.1 基于session認證流程圖
服務器需要存儲用戶的token信息
2.2 基于jwt認證流程圖
服務端不需要存儲用戶token, 都存在客戶端
三. JWT 的構成
JWT就是一段字符串, 由三段信息構成, 三段信息文本使用.(點) 拼接就構成了JWT字符串 :
eyJhbGciOiJIUzI1sNiIsIn.eyJzdWIiOiIxMjRG9OnRydWV9.TJVArHDcEfxjoYZgeFONFh7HgQ- 第一部分我們稱它為頭部 :
header - 第二部分我們稱其為載荷 :
payload(類似于飛機上承載的物品) - 第三部分是簽證 :
signature
3.1 header : 頭部
頭部,JWT 的元數據,也就是描述這個 token 本身的數據,一個 JSON 對象。由兩部分組成 :
- 聲明類型(當前令牌名稱)
- 聲明加密算法
// 定義頭部信息
header := map[string]interface{}{
"alg": "HS256", // 聲明加密算法,可以根據需要修改
"typ": "JWT", // 聲明類型
}
將頭部使用base64編碼構成第一部分 (base64編碼方法, 該編碼可以對稱解碼)
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定義頭部信息
header := map[string]interface{}{
"alg": "HS256", // 聲明加密算法,可以根據需要修改
"typ": "JWT", // 聲明類型
}
// 將頭部信息序列化為JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
fmt.Println(headerStr)
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
}
3.2 payload : 負載
存放用戶有效信息的地方,一個 JSON 對象, 這些有效信息包含三個部分:
- 標準中注冊的聲明
- 公共的聲明
- 私有的聲明
3.2.1 標準中注冊的聲明 (建議但不強制使用)
-
iss: JWT簽發者 -
sub: JWT所面向的用戶 -
aud: 接收JWT的一方 -
exp: JWT的過期時間,這個過期時間必須要大于簽發時間 -
nbf: 定義在什么時間之前,該JWT都是不可用的 -
iat: JWT的簽發時間 -
jti: JWT的唯一身份標識,主要用來作為一次性token,從而回避時序攻擊
3.2.2 公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息。但不建議添加敏感信息,因為該部分在客戶端可解密。
3.2.3 私有的聲明
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味著該部分信息可以歸類為明文信息。
3.2.4 定義一個payload
除了上面的字段, 你自己也可以添加自己想要的字段, 需要注意的是:這些信息是不加密的, 所以最好不要存敏感信息
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定義Payload信息
payload := map[string]interface{}{
"sub": "1234567890", // 主題,表示該JWT的所有者
"name": "John Doe", // 自定義聲明,可以根據需要添加其他聲明
"iat": 1516239022, // 簽發時間,表示JWT的簽發時間,一般為當前時間的時間戳
"exp": 1516239022 + 3600, // 過期時間,表示JWT的過期時間,一般為簽發時間加上有效期,以秒為單位
"roles": []string{"admin", "user"}, // 自定義聲明,可以存儲用戶角色等信息
}
// 將Payload信息序列化為JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
fmt.Println(payloadStr) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
}
然后將其進行base64加密,得到JWT的第二部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
3.3 signatrue : 簽名
signature 是根據 header 和 token 生成, 由三部分構成 :
- base64 編碼后的 header
- base64 編碼后的 payload
- secret : 秘鑰 (只有服務端知道)
這個部分需要將base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了JWT的第三部分。
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定義頭部信息
header := map[string]interface{}{
"alg": "HS256",
"typ": "JWT",
}
// 定義Payload信息
payload := map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239022 + 3600,
"roles": []string{"admin", "user"},
}
// 將頭部信息序列化為JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
// 將Payload信息序列化為JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
// 定義秘鑰
secret := "your-secret-key" // 替換為實際的秘鑰
// 生成簽名
signature := generateSignature(headerStr, payloadStr, secret)
fmt.Println(signature) // C-94Wc6olGK6CEbkA9Xj0ogDQIFdPsEefZKCZrz_fvA
// 生成的簽名字符串
}
func generateSignature(headerStr, payloadStr, secret string) string {
// 構造要簽名的數據
dataToSign := headerStr + "." + payloadStr
// 使用HMAC-SHA256算法生成簽名
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(dataToSign))
signatureBytes := h.Sum(nil)
// 對簽名進行base64編碼
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
return signature
}
3.4 得到 token
算出簽名之后, 把 header、payload、signatrue 三部分使用 .(點) 拼接成一個大字符串, 然后返回給客戶端讓其存儲
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
// 定義頭部信息
header := map[string]interface{}{
"alg": "HS256",
"typ": "JWT",
}
// 定義Payload信息
payload := map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239022 + 3600,
"roles": []string{"admin", "user"},
}
// 將頭部信息序列化為JSON格式字符串
headerBytes, err := json.Marshal(header)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
headerStr := base64.RawURLEncoding.EncodeToString(headerBytes)
// 將Payload信息序列化為JSON格式字符串
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
payloadStr := base64.RawURLEncoding.EncodeToString(payloadBytes)
// 將base64加密后的header和payload拼接起來
dataToSign := headerStr + "." + payloadStr
// 定義秘鑰
secret := "your-secret-key" // 替換為實際的秘鑰
// 生成簽名
signature := generateSignature(dataToSign, secret)
// 最終的JWT字符串
jwtToken := dataToSign + "." + signature
fmt.Println(jwtToken)
// 最終生成的JWT字符串
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDI2MjIsImlhdCI6MTUxNjIzOTAyMiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sInN1YiI6IjEyMzQ1Njc4OTAifQ.C-94Wc6olGK6CEbkA9Xj0ogDQIFdPsEefZKCZrz_fvA
}
func generateSignature(dataToSign, secret string) string {
// 使用HMAC-SHA256算法生成簽名
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(dataToSign))
signatureBytes := h.Sum(nil)
// 對簽名進行base64編碼
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
return signature
}
注意:secret 是保存在服務器端的,JWT的簽發生成也是在服務器端的,secret 就是用來進行JWT的簽發和JWT的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個 secret,那就意味著客戶端是可以自我簽發JWT了。
四.base64 編碼和解碼的使用
首先 base64 是一種編碼方式, 并非加密方式; 它跟語言無關, 任何語言都能使用 base64 編碼&解碼
4.1 base64 編碼
// 定義一個信息字段
dic := map[string]interface{}{"id": 1, "name": "jarvis", "age": "male"}
// 將其序列化成json格式字符串
jsonBytes, err := json.Marshal(dic)
if err != nil {
fmt.Println("JSON encoding error:", err)
return
}
jsonStr := string(jsonBytes)
// 將json格式字符串encode再使用base64編碼成一串Bytes格式編碼
base64Str := base64.StdEncoding.EncodeToString([]byte(jsonStr))
fmt.Println([]byte(base64Str))
// [101 121 74 112 90 67 73 54 73 68 69 115 73 67 50 70 109 90 121 66 67 74 112 73 106 111 103 73 109 70 48 105 71 108 112 77 97 86 120 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 109 116 65 87 120 108 73 106 111 103 73 61]
fmt.Println(base64Str)
// eyJhZ2UiOiJtYWxlIiwiaWQiOjEsIm5hbWUiOiJqYXJ2aXMifQ==
4.2 base64 解碼
// 替換為你的 base64 編碼字符串
base64Str := "eyJhZ2UiOiJtYWxlIiwiaWQiOjEsIm5hbWUiOiJqYXJ2aXMifQ=="
// base64 解碼
decodedBytes, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
fmt.Println("Base64 decoding error:", err)
return
}
// JSON 反序列化
var dic map[string]interface{}
err = json.Unmarshal(decodedBytes, &dic)
if err != nil {
fmt.Println("JSON decoding error:", err)
return
}
fmt.Println(dic)
// map[age:male id:1 name:jarvis]
五.JWT 的本質原理
/*
1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓服務器可以反解出user對象;簽名是不可逆加密,保證整個token的安全性的
3)頭體簽名三部分,都是采用json格式的字符串,進行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)頭中的內容是基本信息:公司信息、項目組信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)體中的內容是關鍵信息:用戶主鍵、用戶名、簽發時客戶端信息(設備號、地址)、過期時間
{
"user_id": 1,
...
}
6)簽名中的內容時安全信息:頭的加密結果 + 體的加密結果 + 服務器不對外公開的安全碼 進行md5加密
{
"head": "頭的加密字符串",
"payload": "體的加密字符串",
"secret_key": "安全碼"
}
*/
5.1 簽發
根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 token
- 用基本信息存儲 json 字典, 采用 base64 編碼得到頭字符串
- 用關鍵信息存儲 json 字典,采用 base64 編碼得到體字符串
- 用頭、體編碼的字符串再加安全碼信息(secret)存儲 json 字典, 采用 header 中指定的算法加密得到簽名字符串
- 最后形成的三段字符串用 . 拼接成
token字符串返回給前臺
5.2 校驗
根據客戶端帶 token 的請求 反解出 user 對象
- 將 token 按
.(點) 拆分為三段字符串, 第一段編碼后的頭字符串一般不需要做任何處理 - 第二段編碼后的體字符串, 要解碼出用戶主鍵, 通過主鍵從 User 表中就能得到登錄用戶, 過期時間和設備信息都是安全信息, 確保 token 沒過期, 且是同一設備來的
- 再將第一段 + 第二段 + 服務器安全碼使用header中指定的不可逆算法加密, 與第三段 簽名字符串進行對比校驗, 通過后才能代表第二段校驗得到的 user 對象就是合法的登錄用戶
5.3 jwt認證開發流程(重點)
-
用賬號密碼訪問登錄接口,登錄接口邏輯中調用簽發
token算法,得到token,返回給客戶端,客戶端自己存到cookies中。 -
校驗
token的算法應該寫在中間件中,所有請求都會進行認證校驗,所以請求帶了token,就會反解出用戶信息。
六、Gin 框架中使用jwt
6.1 安裝JWT庫
使用Gin框架時,你可以選擇一個適用于Go語言的JWT庫。一個流行的選擇是github.com/dgrijalva/jwt-go庫。
go get -u github.com/golang-jwt/jwt/v5
6.2 導入庫
在你的Go代碼中導入github.com/golang-jwt/jwt/v5和github.com/gin-gonic/gin。
import (
"github.com/golang-jwt/jwt/v5"
"github.com/gin-gonic/gin"
)
6.3 使用JWT 鑒權認證
6.3.1 JWT中間件開發
JWT中間件: 創建一個JWT中間件,它將用于保護需要身份驗證的路由。
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"net/http"
"strings"
"webook/internal/web"
)
// LoginJWTMiddlewareBuilder JWT 登錄校驗
type LoginJWTMiddlewareBuilder struct {
paths []string
}
func NewLoginJWTMiddlewareBuilder() *LoginJWTMiddlewareBuilder {
return &LoginJWTMiddlewareBuilder{}
}
// IgnorePaths 忽略的路徑
func (l *LoginJWTMiddlewareBuilder) IgnorePaths(path string) *LoginJWTMiddlewareBuilder {
l.paths = append(l.paths, path)
return l
}
func (l *LoginJWTMiddlewareBuilder) Build() gin.HandlerFunc {
// 用 Go 的方式編碼解碼
return func(ctx *gin.Context) {
// 不需要登錄校驗的
for _, path := range l.paths {
if ctx.Request.URL.Path == path {
return
}
}
// 用 JWT 來校驗
tokenHeader := ctx.GetHeader("Authorization")
if tokenHeader == "" {
// 沒登錄
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
segs := strings.Split(tokenHeader, " ")
if len(segs) != 2 {
// 沒登錄,有人瞎搞
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
tokenStr := segs[1]
claims := &web.UserClaims{}
// ParseWithClaims 里面,一定要傳入指針
// 這里的95osj3fUD7fo0mlYdDbncXz4VD2igvf0 代表的是簽發的時候的key,并且key 要和簽發的時候一樣
token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"), nil
})
if err != nil {
// 沒登錄
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
// token 驗證不通過
if token == nil || !token.Valid {
// 沒登錄
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
// 將用戶信息存儲到上下文中
ctx.Set("claims", claims)
}
}
6.3.2 使用JWT中間件
使用JWT中間件: 在需要身份驗證的路由上使用JWT中間件。
func initWebServer() *gin.Engine {
ser := gin.Default()
ser.Use(cors.New(cors.Config{
//AllowOrigins: []string{"*"},
//AllowMethods: []string{"POST", "GET"},
AllowHeaders: []string{"Content-Type", "Authorization"},
// 允許跨域訪問的響應頭,不加這個前端拿不到token響應頭
ExposeHeaders: []string{"x-jwt-token"},
// 是否允許你帶 cookie 之類的東西
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
if strings.HasPrefix(origin, "http://localhost") {
// 你的開發環境
return true
}
return strings.Contains(origin, "http://你的公司域名.com")
},
MaxAge: 12 * time.Hour,
}))
// 注冊登錄校驗中間件以及不要登錄校驗的路徑
ser.Use(middleware.NewLoginJWTMiddlewareBuilder().
IgnorePaths("/users/signup").
IgnorePaths("/users/login").Build())
return ser
}
6.3.3 生成JWT token
生成JWT token: 在用戶登錄成功后,你可以生成JWT并將其返回給客戶端。
// UserClaims 自定義的聲明結構體并內嵌 jwt.StandardClaims
type UserClaims struct {
jwt.RegisteredClaims
// 聲明你自己的要放進去 token 里面的數據
Uid int64
// 后續需要什么字段,就在這里添加
}
func (u *UserHandler) LoginJWT(ctx *gin.Context) {
type LoginReq struct {
Email string `json:"email"`
Password string `json:"password"`
}
var req LoginReq
if err := ctx.Bind(&req); err != nil {
return
}
user, err := u.svc.Login(ctx, req.Email, req.Password)
if err == service.ErrInvalidUserOrPassword {
ctx.String(http.StatusOK, "用戶名或密碼不對")
return
}
if err != nil {
ctx.String(http.StatusOK, "系統錯誤")
return
}
// 步驟2
// 在這里用 JWT 設置登錄態
// 生成一個 JWT token
// 將用戶信息存儲到token中
claims := UserClaims{
Uid: user.Id,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
tokenStr, err := token.SignedString([]byte("95osj3fUD7fo0mlYdDbncXz4VD2igvf0"))
if err != nil {
ctx.String(http.StatusInternalServerError, "系統錯誤")
return
}
ctx.Header("x-jwt-token", tokenStr)
fmt.Println(user)
ctx.String(http.StatusOK, "登錄成功")
return
}
6.3.4 訪問路由簽發token
我們通過接口調試工具訪問路由127.0.0.1:8080/users/login 簽發用戶token,header 中就會有X-Jwt-Token這個字段以及生成的token 對應值。
6.3.5 通過 token 鑒權獲取用戶信息
在平時開發中,我們一般不會直接傳user_id 過來,一般是通過token來獲取用戶信息,比如我們需要查詢用戶信息,之前我們已經將用戶ID放入到token中了,直接通過c, _ := ctx.Get("claims")來獲取我們存放的用戶信息,以下是具體代碼;
func (u *UserHandler) ProfileJWT(ctx *gin.Context) {
c, _ := ctx.Get("claims")
// 你可以斷定,必然有 claims
//if !ok {
// // 你可以考慮監控住這里
// ctx.String(http.StatusOK, "系統錯誤")
// return
//}
// ok 代表是不是 *UserClaims
claims, ok := c.(*UserClaims)
if !ok {
// 你可以考慮監控住這里
ctx.String(http.StatusOK, "系統錯誤")
return
}
fmt.Println("當前用戶ID為:", claims.Uid)
ctx.String(http.StatusOK, "查詢成功")
}
最后我們只需要訪問路由:127.0.0.1:8080/users/profile,在header中加入token 即可。
總結
以上是生活随笔為你收集整理的Gin 框架之jwt 介绍与基本使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Go - 基本数据类型和其字符串表示之间
- 下一篇: 一文总结现代 C++ 中的初始化