一、JWT
JWT全稱JSON Web Token是一種跨域認證解決方案,屬于一個開放的標準,它規定了一種Token實現方式,目前多用于前后端分離項目和OAuth2.0業務場景下。
二、為什么要用在你的Gin中使用JWT
傳統的Cookie-Sesson模式占用服務器內存, 拓展性不好,遇到集群或者跨服務驗證的場景的話, 要支持Sesson復制或者sesson持久化
1.JWT的基本原理
在服務器驗證之后, 得到用戶信息JSON
1 2 3 4 5 | { ????? "user_id" : "xxxxxx" , ???? "role" : "xxxxxx" , ???? "refresh_token" : "xxxxx" } |
(1)JWT TOKEN怎么組成
JWT是一個很長的字符串
eyJhbGciOiJI123afasrwrqqewrcCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
它由三部分組成, 每部分用點(.)分隔, 三個部分依次如下
?
- Header(頭部)
- Payload(負載)
- Signature(簽名)
1)Header
Header是一個經過BASE64URL算法加密過的JSON對象, 解密后如下
1 2 3 4 | { ?? "alg" : "HS256" , ?? "typ" : "JWT" } |
其中,?alg屬性表示簽名所用的算法,默認是HS256;
typ則表示當前token的類型, 而JWT的類型則為jwt
Base64URL
與BASE64類似, 由于+、/、=這幾個符號在URL中有特殊含義, 因此BASE64RUL算法, 把這幾個符號進行了替換
+?->?-
=?-> 被忽略
/?->?_
2)Payload
Payload部分也是JSON對象經過BASE64URL算法轉成字符串的, Payload部分包含7個基本字段
- iss (issuer):簽發人
- exp (expiration time):過期時間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號
也可以往里面塞入自定義的業務字段, 如下
user_id
role
3)Signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(
? base64UrlEncode(header) + "." +
? base64UrlEncode(payload),
? secret)
(2)解密過程
當系統接收到TOKEN時, 拿出Header和Payload的字符串用.拼接在一起之后, 用Header里面指定的哈希方法通過公式
HMACSHA256(
? base64UrlEncode(header) + "." +
? base64UrlEncode(payload),
? secret)
進行加密得出密文
然后用剛剛得出的密文與TOKEN傳過來的密文對比, 如果相等則表明密文沒有更改.
三、JWT一些特點(優點與缺點)
- JWT 默認是不加密,但也是可以加密的。生成原始 Token 以后,可以用密鑰再加密一次。
- JWT 不加密的情況下,不能將秘密數據寫入 JWT。
- JWT 不僅可以用于認證,也可以用于交換信息。有效使用 JWT,可以降低服務器查詢數據庫的次數。
- JWT 的最大缺點是,由于服務器不保存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。
- JWT 本身包含了認證信息,一旦泄露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT 的有效期應該設置得比較短。對于一些比較重要的權限,使用時應該再次對用戶進行認證。
- 為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
1.GIN整合JWT
1 2 | go get -u github.com/dgrijalva/jwt- go go get github.com/gin-gonic/gin |
編寫jwtutil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | var Secret = [] byte ( "whatasecret" ) // jwt過期時間, 按照實際環境設置 const expiration = 2 * time.Minute type Claims struct { ???? // 自定義字段, 可以存在用戶名, 用戶ID, 用戶角色等等 ???? Username string ???? // jwt.StandardClaims包含了官方定義的字段 ???? jwt.StandardClaims } func GenToken(username string ) ( string , error ) { ???? // 創建聲明 ???? a := &Claims{ ???????? Username: username, ???????? StandardClaims: jwt.StandardClaims{ ???????????? ExpiresAt: time.Now().Add(expiration).Unix(), // 過期時間 ???????????? IssuedAt:? time.Now().Unix(),???????????????? // 簽發時間 ???????????? Issuer:??? "gin-jwt-demo" ,??????????????????? // 簽發者 ???????????? Id:??????? "" ,??????????????????????????????? // 按需求選這個, 有些實現中, 會控制這個ID是不是在黑/白名單來判斷是否還有效 ???????????? NotBefore: 0 ,???????????????????????????????? // 生效起始時間 ???????????? Subject:?? "" ,??????????????????????????????? // 主題 ???????? }, ???? } ???? // 用指定的哈希方法創建簽名對象 ???? tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a) ???? // 用上面的聲明和簽名對象簽名字符串token ???? // 1. 先對Header和PayLoad進行Base64URL轉換 ???? // 2. Header和PayLoadBase64URL轉換后的字符串用.拼接在一起 ???? // 3. 用secret對拼接在一起之后的字符串進行HASH加密 ???? // 4. 連在一起返回 ???? return tt.SignedString(Secret) } func ParseToken(tokenStr string ) (*Claims, error ) { ???? // 第三個參數: 提供一個回調函數用于提供要選擇的秘鑰, 回調函數里面的token參數,是已經解析但未驗證的,可以根據token里面的值做一些邏輯, 如`kid`的判斷 ???? token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func (token *jwt.Token) ( interface {}, error ) { ???????? return Secret, nil ???? }) ???? if err != nil { ???????? return nil , err ???? } ???? // 校驗token ???? if claims, ok := token.Claims.(*Claims); ok && token.Valid { ???????? return claims, nil ???? } ???? return nil , errors. New ( "invalid token" ) } |
- Secret是秘鑰, 用于加密簽名
- expiration是TOKEN過期時間
- Claims是簽名聲明對象, 包含自定義的字段和JWT規定的字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | type Claims struct { ???? // 自定義字段, 可以存在用戶名, 用戶ID, 用戶角色等等 ???? Username string ???? // jwt.StandardClaims包含了官方定義的字段 ???? jwt.StandardClaims } type StandardClaims struct { ???? Audience? string `json: "aud,omitempty" ` ???? ExpiresAt int64 ? `json: "exp,omitempty" ` ???? Id??????? string `json: "jti,omitempty" ` ???? IssuedAt? int64 ? `json: "iat,omitempty" ` ???? Issuer??? string `json: "iss,omitempty" ` ???? NotBefore int64 ? `json: "nbf,omitempty" ` ???? Subject?? string `json: "sub,omitempty" ` } |
(1)GenToken方法
GenToken方法為某個username生成一個token, 每次生成都不一樣
jwt.NewWithClaims(jwt.SigningMethodHS256, a)聲明了一個簽名對象, 并且指定了HS256的哈希算法
token.SignedString(Secret)表明用剛剛的聲明對象和SECRET利用指定的哈希算法去加密,包含下面流程
- 先對Header和PayLoad進行Base64URL轉換
- Header和PayLoadBase64URL轉換后的字符串用.拼接在一起
- 用secret對拼接在一起之后的字符串進行HASH加密
- BASE64URL(Header).BASE64URL(Payload).signature連在一起的字符串返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | func GenToken(username string ) ( string , error ) { ???? // 創建聲明 ???? a := &Claims{ ???????? Username: username, ???????? StandardClaims: jwt.StandardClaims{ ???????????? ExpiresAt: time.Now().Add(expiration).Unix(), // 過期時間 ???????????? IssuedAt:? time.Now().Unix(),???????????????? // 簽發時間 ???????????? Issuer:??? "gin-jwt-demo" ,??????????????????? // 簽發者 ???????????? Id:??????? "" ,??????????????????????????????? // 按需求選這個, 有些實現中, 會控制這個ID是不是在黑/白名單來判斷是否還有效 ???????????? NotBefore: 0 ,???????????????????????????????? // 生效起始時間 ???????????? Subject:?? "" ,??????????????????????????????? // 主題 ???????? }, ???? } ???? // 用指定的哈希方法創建簽名對象 ???? tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a) ???? // 用上面的聲明和簽名對象簽名字符串token ???? // 1. 先對Header和PayLoad進行Base64URL轉換 ???? // 2. Header和PayLoadBase64URL轉換后的字符串用.拼接在一起 ???? // 3. 用secret對拼接在一起之后的字符串進行HASH加密 ???? // 4. 連在一起返回 ???? return tt.SignedString(Secret) } |
(2)ParseToken方法
ParseToken方法解析一個Token, 并驗證Token是否生效
jwt.ParseWithClaims方法, 用于解析Token, 其第三個參數:
提供一個回調函數用于提供要選擇的秘鑰, 回調函數里面的token參數,是已經解析但未驗證的,可以根據token里面的值做一些邏輯, 如判斷kid來選用不同的secret
KID(可選): 代表秘鑰序號。開發人員可以用它標識認證token的某一秘鑰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func ParseToken(tokenStr string ) (*Claims, error ) { ???? // 第三個參數: 提供一個回調函數用于提供要選擇的秘鑰, 回調函數里面的token參數,是已經解析但未驗證的,可以根據token里面的值做一些邏輯, 如`kid`的判斷 ???? token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func (token *jwt.Token) ( interface {}, error ) { ???????? return Secret, nil ???? }) ???? if err != nil { ???????? return nil , err ???? } ???? // 校驗token ???? if claims, ok := token.Claims.(*Claims); ok && token.Valid { ???????? return claims, nil ???? } ???? return nil , errors. New ( "invalid token" ) } |
編寫中間件
從Header中取出Authorization并拿去解析jwt.ParseToken,
驗證token是否被串改, 是否過期
從token取出有效信息并設置到上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | func JWTAuthMiddleware() func (ctx *gin.Context) { ???? return func (ctx *gin.Context) { ???????? // 根據實際情況取TOKEN, 這里從request header取 ???????? tokenStr := ctx.Request.Header.Get( "Authorization" ) ???????? if tokenStr == "" { ???????????? ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ ???????????????? "code" : code.ERR_AUTH_NULL, ???????????????? "msg" :? code.GetMsg(code.ERR_AUTH_NULL), ???????????? }) ???????????? return ???????? } ???????? claims, err := jwt.ParseToken(tokenStr) ???????? if err != nil { ???????????? ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ ???????????????? "code" : code.ERR_AUTH_INVALID, ???????????????? "msg" :? code.GetMsg(code.ERR_AUTH_INVALID), ???????????? }) ???????????? return ???????? } else if time.Now().Unix() > claims.ExpiresAt { ???????????? ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ ???????????????? "code" : code.ERR_AUTH_EXPIRED, ???????????????? "msg" :? code.GetMsg(code.ERR_AUTH_EXPIRED), ???????????? }) ???????????? return ???????? } ???????? // 此處已經通過了, 可以把Claims中的有效信息拿出來放入上下文使用 ???????? ctx.Set( "username" , claims.Username) ???????? ctx.Next() ???? } } |
使用中間件
/login不用中間件
中間件指定在authorizedrouter, 因此authorized下的所有路由都會使用此中間件
1 2 3 4 5 6 7 8 9 10 | func main() { ???? r := gin. Default () ???? r.POST( "/login" , router.Login) ???? authorized := r.Group( "/auth" ) ???? authorized.Use(jwt.JWTAuthMiddleware()) ???? { ???????? authorized.GET( "/getUserInfo" , router.GetUserInfo) ???? } ???? r.Run( ":8082" ) } |
測試
login請求獲取token
POST?http://localhost:8082/login
?
把token放入getUserInfo請求
GET? http://localhost:8082/auth/getUserInfo
?
其他
完整的JWT登錄還應該包括
- 使TOKEN失效(過期或者黑名單等功能)
- refresh token
- …