1 JWT介紹
在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
2 JWT的構成
JWT就是一段字符串,由三段信息構成的,將這三段信息文本用.
鏈接一起就構成了Jwt字符串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).
header
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HS256
完整的頭部就像下面這樣的JSON:
{"alg":"HS256","typ":"JWT"}
然后將頭部進行base64編碼構成了第一部分.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload
載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分
- 標准中注冊的聲明
- 公共的聲明
- 私有的聲明
標准中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避時序攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
定義一個payload:
{"foo":"bar","nbf":1444478400}
然后將其進行base64加密,得到JWT的第二部分。
eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9
signature
JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64后的)
- payload (base64后的)
- secret
這個部分需要base64加密后的header和base64加密后的payload使用.
連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret
組合加密,然后就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
// u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
將這三部分用.
連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
3 本質原理
/*
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": "安全碼"
}
*/
簽發
根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 token
/*
1)用基本信息存儲json字典,采用base64算法加密得到 頭字符串
2)用關鍵信息存儲json字典,采用base64算法加密得到 體字符串
3)用頭、體加密字符串再加安全碼信息存儲json字典,采用hash md5算法加密得到 簽名字符串
賬號密碼就能根據User表得到user對象,形成的三段字符串用 . 拼接成token返回給前台
*/
校驗
根據客戶端帶token的請求 反解出 user 對象
/*
1)將token按 . 拆分為三段字符串,第一段 頭加密字符串 一般不需要做任何處理
2)第二段 體加密字符串,要反解出用戶主鍵,通過主鍵從User表中就能得到登錄用戶,過期時間和設備信息都是安全信息,確保token沒過期,且時同一設備來的
3)再用 第一段 + 第二段 + 服務器安全碼 不可逆md5加密,與第三段 簽名字符串 進行碰撞校驗,通過后才能代表第二段校驗得到的user對象就是合法的登錄用戶
*/
jwt認證開發流程(重點)
/*
1)用賬號密碼訪問登錄接口,登錄接口邏輯中調用 簽發token 算法,得到token,返回給客戶端,客戶端自己存到cookies中
2)校驗token的算法應該寫在中間件中,所有請求,都會進行認證校驗,所以請求帶了token,就會反解出用戶信息
*/
4 base64編碼解碼
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// 1 編碼
res:=base64.StdEncoding.EncodeToString([]byte("lqz is nb"))
fmt.Println(res)
//2 解碼
res,err:=base64.StdEncoding.DecodeString("eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9")
if err != nil {
fmt.Println("解碼出錯,",err)
return
}
fmt.Println(string(res))
}
5 Gin中使用jwt
github地址:https://github.com/golang-jwt/jwt
文檔地址:https://pkg.go.dev/github.com/golang-jwt/jwt
下載:go get github.com/golang-jwt/jwt
5.1 簽發token和驗證token
package main
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt"
"time"
)
//func main() {
// // 秘鑰
// mySigningKey := []byte("lqzisnb")
// type MyCustomClaims struct {
// Id int `json:"id"`
// Username string `json:"username"`
// jwt.StandardClaims
// }
//
// // Create the Claims
// claims := MyCustomClaims{
// 1,
// "lqz",
// jwt.StandardClaims{
// ExpiresAt: 15000, // 過期時間
// Issuer: "lqz", // 簽發人
// },
// }
// // 使用HS256加密方式
// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// signToken, err := token.SignedString(mySigningKey)
// fmt.Printf("%v,%v %v", token, signToken, err)
//}
// 第一步:定義結構體
// MyClaims 定義結構體並繼承jwt.StandardClaims
// jwt包自帶的jwt.StandardClaims只包含了官方字段
// 我們需要額外記錄一個username和id字段,所以要自定義結構體
// 如果想要保存更多信息,都可以添加到這個結構體中
type MyCustomClaims struct {
Id int `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
// 定義加密秘鑰
var mySigningKey = []byte("lqzisnb")
func genToken(claims MyCustomClaims) (string, error) {
// 使用HS256加密方式
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signToken, err := token.SignedString(mySigningKey)
if err != nil {
return "", err
}
return signToken, nil
}
func parserToken(signToken string) (*MyCustomClaims, error) {
var claims MyCustomClaims
token, err := jwt.ParseWithClaims(signToken, &claims, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if token.Valid {
return &claims, nil
} else {
return nil, err
}
//else if ve, ok := err.(*jwt.ValidationError); ok {
// if ve.Errors&jwt.ValidationErrorMalformed != 0 {
// return nil, errors.New("不是一個合法的token")
// } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
// return nil, errors.New("token過期了")
// } else {
// fmt.Println("Couldn't handle this token:", err)
// return nil, errors.New("無法處理這個token")
// }
//} else {
// return nil, errors.New("無法處理這個token")
//}
}
// 帶詳細錯誤的解析
func parserTokenWithError(signToken string) (*MyCustomClaims, error) {
var claims MyCustomClaims
token, err := jwt.ParseWithClaims(signToken, &claims, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if token.Valid {
return &claims, nil
} else if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.New("不是一個合法的token")
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
return nil, errors.New("token過期了")
} else {
fmt.Println("Couldn't handle this token:", err)
return nil, errors.New("無法處理這個token")
}
} else {
return nil, errors.New("無法處理這個token")
}
}
func main() {
claims := MyCustomClaims{
1,
"lqz",
jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * time.Hour).Unix(), // 過期時間7小時
Issuer: "lqz", // 簽發人
},
}
signToken, _ := genToken(claims)
fmt.Println(signToken)
//signToken="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJscXoiLCJleHAiOjE2NTAxNTA3NzMsImlzcyI6ImxxeiJ9.ZkoH79u6UEeTURnNaI_X6M4KpqzFgIcBCoMtF11AxF"
c, err := parserToken(signToken)
fmt.Println(c, err)
}
5.2 Gin框架中集成
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"net/http"
"time"
)
// 第一步:定義結構體
// MyClaims 定義結構體並繼承jwt.StandardClaims
// jwt包自帶的jwt.StandardClaims只包含了官方字段
// 我們需要額外記錄一個username和id字段,所以要自定義結構體
// 如果想要保存更多信息,都可以添加到這個結構體中
type MyCustomClaims struct {
Id int `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
// 定義加密秘鑰
var mySigningKey = []byte("lqzisnb")
func genToken(claims MyCustomClaims) (string, error) {
// 使用HS256加密方式
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signToken, err := token.SignedString(mySigningKey)
if err != nil {
return "", err
}
return signToken, nil
}
func parserToken(signToken string) (*MyCustomClaims, error) {
var claims MyCustomClaims
token, err := jwt.ParseWithClaims(signToken, &claims, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if token.Valid {
return &claims, nil
} else {
return nil, err
}
}
// 基於JWT的認證中間件
func JWTAuthMiddleware(c *gin.Context) {
// 從請求頭中取出
signToken := c.Request.Header.Get("Authorization")
if signToken == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1002,
"msg": "token為空",
})
c.Abort()
return
}
// 校驗token
myclaims, err := parserToken(signToken)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusOK, gin.H{
"code": 1003,
"msg": "token校驗失敗",
})
c.Abort()
return
}
// 將用戶的id放在到請求的上下文c上
c.Set("userid", myclaims.Id)
c.Next() // 后續的處理函數可以用過c.Get("userid")來獲取當前請求的id
}
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
if username == "lqz" && password == "123" {
token, _ := genToken(MyCustomClaims{1, "lqz", jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * time.Hour).Unix(), // 過期時間
Issuer: "lqz", // 簽發人
}})
c.JSON(200, gin.H{"code": "100", "msg": "登陸成功", "token": token})
} else {
c.JSON(200, gin.H{"code": "101", "msg": "用戶名或密碼錯誤"})
}
})
// 該接口登陸后才能訪問,加中間件
r.GET("/home", JWTAuthMiddleware, func(c *gin.Context) {
fmt.Println(c.Get("userid"))
c.JSON(200, gin.H{"code": 100, "msg": "home"})
})
r.Run(":8080")
}