Gin框架中使用JWT進行接口認證

背景: 在現在先後端分離開發的大環境中,咱們須要解決一些登錄,後期身份認證以及鑑權相關的事情,一般的方案就是採用請求頭攜帶token的方式進行實現。本篇文章主要分享下在Golang語言下使用jwt-go來實現後端的token認證邏輯。git

JSON Web Token(JWT)是一個經常使用語HTTP的客戶端和服務端間進行身份認證和鑑權的標準規範,使用JWT能夠容許咱們在用戶和服務器之間傳遞安全可靠的信息。github

在開始學習JWT以前,咱們能夠先了解下早期的幾種方案。web

token、cookie、session的區別

Cookieredis

Cookie老是保存在客戶端中,按在客戶端中的存儲位置,可分爲內存Cookie硬盤Cookie算法

內存Cookie由瀏覽器維護,保存在內存中,瀏覽器關閉後就消失了,其存在時間是短暫的。硬盤Cookie保存在硬盤裏,有一個過時時間,除非用戶手工清理或到了過時時間,硬盤Cookie不會被刪除,其存在時間是長期的。因此,按存在時間,可分爲非持久Cookie和持久Cookie數據庫

cookie 是一個很是具體的東西,指的就是瀏覽器裏面能永久存儲的一種數據,僅僅是瀏覽器實現的一種數據存儲功能。json

cookie由服務器生成,發送給瀏覽器,瀏覽器把cookie以key-value形式保存到某個目錄下的文本文件內,下一次請求同一網站時會把該cookie發送給服務器。因爲cookie是存在客戶端上的,因此瀏覽器加入了一些限制確保cookie不會被惡意使用,同時不會佔據太多磁盤空間,因此每一個域的cookie數量是有限的。後端

Sessionapi

Session字面意思是會話,主要用來標識本身的身份。好比在無狀態的api服務在屢次請求數據庫時,如何知道是同一個用戶,這個就能夠經過session的機制,服務器要知道當前發請求給本身的是誰瀏覽器

爲了區分客戶端請求,服務端會給具體的客戶端生成身份標識session,而後客戶端每次向服務器發請求的時候,都帶上這個「身份標識」,服務器就知道這個請求來自於誰了。

至於客戶端如何保存該標識,能夠有不少方式,對於瀏覽器而言,通常都是使用cookie的方式

服務器使用session把用戶信息臨時保存了服務器上,用戶離開網站就會銷燬,這種憑證存儲方式相對於cookie來講更加安全,可是session會有一個缺陷: 若是web服務器作了負載均衡,那麼下一個操做請求到了另外一臺服務器的時候session會丟失。

所以,一般企業裏會使用redis,memcached緩存中間件來實現session的共享,此時web服務器就是一個徹底無狀態的存在,全部的用戶憑證能夠經過共享session的方式存取,當前session的過時和銷燬機制須要用戶作控制。

Token

token的意思是「令牌」,是用戶身份的驗證方式,最簡單的token組成: uid(用戶惟一標識)+time(當前時間戳)+sign(簽名,由token的前幾位+鹽以哈希算法壓縮成必定長度的十六進制字符串),同時還能夠將不變的參數也放進token

這裏咱們主要想講的就是Json Web Token,也就是本篇的主題:JWT

Json-Web-Token(JWT)介紹

通常而言,用戶註冊登錄後會生成一個jwt token返回給瀏覽器,瀏覽器向服務端請求數據時攜帶token,服務器端使用signature中定義的方式進行解碼,進而對token進行解析和驗證。

JWT Token組成部分

JWT-Token組成部分
JWT-Token組成部分
  • header: 用來指定使用的算法(HMAC SHA256 RSA)和token類型(如JWT)
  • payload: 包含聲明(要求),聲明一般是用戶信息或其餘數據的聲明,好比用戶id,名稱,郵箱等. 聲明可分爲三種: registered,public,private
  • signature: 用來保證JWT的真實性,可使用不一樣的算法

header

{
"alg": "HS256",
"typ": "JWT"
}
複製代碼

對上面的json進行base64編碼便可獲得JWT的第一個部分

payload

  • registered claims: 預約義的聲明,一般會放置一些預約義字段,好比過時時間,主題等(iss:issuer,exp:expiration time,sub:subject,aud:audience)
  • public claims: 能夠設置公開定義的字段
  • private claims: 用於統一使用他們的各方之間的共享信息
{
"sub": "xxx-api",
"name": "bgbiao.top",
"admin": true
}
複製代碼

對payload部分的json進行base64編碼後便可獲得JWT的第二個部分

注意: 不要在header和payload中放置敏感信息,除非信息自己已經作過脫敏處理

signature

爲了獲得簽名部分,必須有編碼過的header和payload,以及一個祕鑰,簽名算法使用header中指定的那個,而後對其進行簽名便可

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

簽名是用於驗證消息在傳遞過程當中有沒有被更改,而且,對於使用私鑰簽名的token,它還能夠驗證JWT的發送方是否爲它所稱的發送方。

jwt.io網站中,提供了一些JWT token的編碼,驗證以及生成jwt的工具。

下圖就是一個典型的jwt-token的組成部分。

jwt官方簽名結構
jwt官方簽名結構

何時用JWT

  • Authorization(受權): 典型場景,用戶請求的token中包含了該令牌容許的路由,服務和資源。單點登陸其實就是如今普遍使用JWT的一個特性
  • Information Exchange(信息交換): 對於安全的在各方之間傳輸信息而言,JSON Web Tokens無疑是一種很好的方式.由於JWTs能夠被簽名,例如,用公鑰/私鑰對,你能夠肯定發送人就是它們所說的那我的。另外,因爲簽名是使用頭和有效負載計算的,您還能夠驗證內容沒有被篡改

JWT(Json Web Tokens)是如何工做的

JWT認證過程
JWT認證過程

因此,基本上整個過程分爲兩個階段,第一個階段,客戶端向服務端獲取token,第二階段,客戶端帶着該token去請求相關的資源.

一般比較重要的是,服務端如何根據指定的規則進行token的生成。

在認證的時候,當用戶用他們的憑證成功登陸之後,一個JSON Web Token將會被返回。

此後,token就是用戶憑證了,你必須很是當心以防止出現安全問題。

通常而言,你保存令牌的時候不該該超過你所須要它的時間。

不管什麼時候用戶想要訪問受保護的路由或者資源的時候,用戶代理(一般是瀏覽器)都應該帶上JWT,典型的,一般放在Authorization header中,用Bearer schema: Authorization: Bearer <token>

服務器上的受保護的路由將會檢查Authorization header中的JWT是否有效,若是有效,則用戶能夠訪問受保護的資源。若是JWT包含足夠多的必需的數據,那麼就能夠減小對某些操做的數據庫查詢的須要,儘管可能並不老是如此。

若是token是在受權頭(Authorization header)中發送的,那麼跨源資源共享(CORS)將不會成爲問題,由於它不使用cookie.

獲取JWT以及訪問APIs以及資源
獲取JWT以及訪問APIs以及資源
  • 客戶端向受權接口請求受權
  • 服務端受權後返回一個access token給客戶端
  • 客戶端使用access token訪問受保護的資源

基於Token的身份認證和基於服務器的身份認證

1.基於服務器的認證

前面說到過session,cookie以及token的區別,在以前傳統的作法就是基於存儲在服務器上的session來作用戶的身份認證,可是一般會有以下問題:

  • Sessions: 認證經過後須要將用戶的session數據保存在內存中,隨着認證用戶的增長,內存開銷會大
  • 擴展性: 因爲session存儲在內存中,擴展性會受限,雖而後期可使用redis,memcached來緩存數據
  • CORS: 當多個終端訪問同一份數據時,可能會遇到禁止請求的問題
  • CSRF: 用戶容易受到CSRF攻擊

2.Session和JWT Token的異同

均可以存儲用戶相關信息,可是session存儲在服務端,JWT存儲在客戶端

session和jwt數據存儲位置
session和jwt數據存儲位置

3.基於Token的身份認證如何工做

基於Token的身份認證是無狀態的,服務器或者session中不會存儲任何用戶信息.(很好的解決了共享session的問題)

  • 用戶攜帶用戶名和密碼請求獲取token(接口數據中可以使用appId,appKey)
  • 服務端校驗用戶憑證,並返回用戶或客戶端一個Token
  • 客戶端存儲token,並在請求頭中攜帶Token
  • 服務端校驗token並返回數據

注意:

  • 隨後客戶端的每次請求都須要使用token
  • token應該放在header中
  • 須要將服務器設置爲接收全部域的請求: Access-Control-Allow-Origin: *

4.用Token的好處

  • 無狀態和可擴展性
  • 安全: 防止CSRF攻擊;token過時從新認證

5.JWT和OAuth的區別

  • 1.OAuth2是一種受權框架 ,JWT是一種認證協議
  • 2.不管使用哪一種方式切記用HTTPS來保證數據的安全性
  • 3.OAuth2用在 使用第三方帳號登陸的狀況(好比使用weibo, qq, github登陸某個app),而 JWT是用在先後端分離, 須要簡單的對後臺API進行保護時使用

使用Gin框架集成JWT

在Golang語言中,jwt-go庫提供了一些jwt編碼和驗證的工具,所以咱們很容易使用該庫來實現token認證。

另外,咱們也知道gin框架中支持用戶自定義middleware,咱們能夠很好的將jwt相關的邏輯封裝在middleware中,而後對具體的接口進行認證。

自定義中間件

在gin框架中,自定義中間件比較容易,只要返回一個gin.HandlerFunc即完成一箇中間件定義。

接下來,咱們先定義一個用於jwt認證的中間件.

// 定義一個JWTAuth的中間件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 經過http header中的token解析來認證
token := c.Request.Header.Get("token")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "請求未攜帶token,無權限訪問",
"data": nil,
})
c.Abort()
return
}

log.Print("get token: ", token)

// 初始化一個JWT對象實例,並根據結構體方法來解析token
j := NewJWT()
// 解析token中包含的相關信息(有效載荷)
claims, err := j.ParserToken(token)

if err != nil {
// token過時
if err == TokenExpired {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "token受權已過時,請從新申請受權",
"data": nil,
})
c.Abort()
return
}
// 其餘錯誤
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
c.Abort()
return
}

// 將解析後的有效載荷claims從新寫入gin.Context引用對象中
c.Set("claims", claims)

}
}


複製代碼

定義jwt編碼和解碼邏輯

根據前面提到的jwt-token的組成部分,以及jwt-go中相關的定義,咱們可使用以下方法進行生成token.

// 定義一個jwt對象
type JWT struct {
// 聲明簽名信息
SigningKey []byte
}

// 初始化jwt對象
func NewJWT() *JWT {
return &JWT{
[]byte("bgbiao.top"),
}
}

// 自定義有效載荷(這裏採用自定義的Name和Email做爲有效載荷的一部分)
type CustomClaims struct {
Name string `json:"name"`
Email string `json:"email"`
// StandardClaims結構體實現了Claims接口(Valid()函數)
jwt.StandardClaims
}


// 調用jwt-go庫生成token
// 指定編碼的算法爲jwt.SigningMethodHS256
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#Token
// 返回一個token的結構體指針
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}


// token解碼
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims
// 輸入用戶自定義的Claims結構體對象,token,以及自定義函數來解析token字符串爲jwt的Token結構體指針
// Keyfunc是匿名函數類型: type Keyfunc func(*Token) (interface{}, error)
// func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})

if err != nil {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError
// jwt.ValidationError 是一個無效token的錯誤結構
if ve, ok := err.(*jwt.ValidationError); ok {
// ValidationErrorMalformed是一個uint常量,表示token不可用
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token不可用")
// ValidationErrorExpired表示Token過時
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, fmt.Errorf("token過時")
// ValidationErrorNotValidYet表示無效token
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, fmt.Errorf("無效的token")
} else {
return nil, fmt.Errorf("token不可用")
}

}
}

// 將token中的claims信息解析出來並斷言成用戶自定義的有效載荷結構
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}

return nil, fmt.Errorf("token無效")

}

複製代碼

定義登錄驗證邏輯

接下來的部分就是普通api的具體邏輯了,好比能夠在登錄時進行用戶校驗,成功後未該次認證請求生成token。

// 定義登錄邏輯
// model.LoginReq中定義了登錄的請求體(name,passwd)
func Login(c *gin.Context) {
var loginReq model.LoginReq
if c.BindJSON(&loginReq) == nil {
// 登錄邏輯校驗(查庫,驗證用戶是否存在以及登錄信息是否正確)
isPass, user, err := model.LoginCheck(loginReq)
// 驗證經過後爲該次請求生成token
if isPass {
generateToken(c, user)
} else {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "驗證失敗" + err.Error(),
"data": nil,
})
}

} else {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": "用戶數據解析失敗",
"data": nil,
})
}
}

// token生成器
// md 爲上面定義好的middleware中間件
func generateToken(c *gin.Context, user model.User) {
// 構造SignKey: 簽名和解簽名須要使用一個值
j := md.NewJWT()

// 構造用戶claims信息(負荷)
claims := md.CustomClaims{
user.Name,
user.Email,
jwtgo.StandardClaims{
NotBefore: int64(time.Now().Unix() - 1000), // 簽名生效時間
ExpiresAt: int64(time.Now().Unix() + 3600), // 簽名過時時間
Issuer: "bgbiao.top", // 簽名頒發者
},
}

// 根據claims生成token對象
token, err := j.CreateToken(claims)

if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": -1,
"msg": err.Error(),
"data": nil,
})
}

log.Println(token)
// 封裝一個響應數據,返回用戶名和token
data := LoginResult{
Name: user.Name,
Token: token,
}

c.JSON(http.StatusOK, gin.H{
"status": 0,
"msg": "登錄成功",
"data": data,
})
return

}
複製代碼

定義普通待驗證接口

// 定義一個普通controller函數,做爲一個驗證接口邏輯
func GetDataByTime(c *gin.Context) {
// 上面咱們在JWTAuth()中間中將'claims'寫入到gin.Context的指針對象中,所以在這裏能夠將之解析出來
claims := c.MustGet("claims").(*md.CustomClaims)
if claims != nil {
c.JSON(http.StatusOK, gin.H{
"status": 0,
"msg": "token有效",
"data": claims,
})
}
}


// 在主函數中定義路由規則
router := gin.Default()
v1 := router.Group("/apis/v1/")
{
v1.POST("/register", controller.RegisterUser)
v1.POST("/login", controller.Login)
}

// secure v1
sv1 := router.Group("/apis/v1/auth/")
// 加載自定義的JWTAuth()中間件,在整個sv1的路由組中都生效
sv1.Use(md.JWTAuth())
{
sv1.GET("/time", controller.GetDataByTime)

}
router.Run(":8081")

複製代碼

驗證使用JWT後的接口

# 運行項目
$ go run main.go
127.0.0.1
13306
root:bgbiao.top@tcp(127.0.0.1:13306)/test_api?charset=utf8mb4&parseTime=True&loc=Local
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST /apis/v1/register --> warnning-trigger/controller.RegisterUser (3 handlers)
[GIN-debug] POST /apis/v1/login --> warnning-trigger/controller.Login (3 handlers)
[GIN-debug] GET /apis/v1/auth/time --> warnning-trigger/controller.GetDataByTime (4 handlers)
[GIN-debug] Listening and serving HTTP on :8081

# 註冊用戶
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"name": "hahaha1",
"password": "hahaha1",
"email": "hahaha1@bgbiao.top",
"phone": 10000000000
}' \
'http://localhost:8081/apis/v1/register'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:09:28 GMT
Content-Length: 41

{"data":null,"msg":"success ","status":0}%


# 登錄用戶以獲取token
$ curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{
"name":"hahaha1",
"password":"hahaha1"
}' \
'http://localhost:8081/apis/v1/login'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 15 Mar 2020 07:10:41 GMT
Content-Length: 290

{"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw","name":"hahaha1"},"msg":"登錄成功","status":0}


# 訪問須要認證的接口
# 由於咱們對/apis/v1/auth/的分組路由中加載了jwt的middleware,所以該分組下的api都須要使用jwt-token認證
$ curl http://localhost:8081/apis/v1/auth/time
{"data":null,"msg":"請求未攜帶token,無權限訪問","status":-1}%

# 使用token認證
$ curl http://localhost:8081/apis/v1/auth/time -H 'token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImhhaGFoYTEiLCJlbWFpbCI6ImhhaGFoYTFAYmdiaWFvLnRvcCIsImV4cCI6MTU4NDI1OTg0MSwiaXNzIjoiYmdiaWFvLnRvcCIsIm5iZiI6MTU4NDI1NTI0MX0.HNXSKISZTqzjKd705BOSARmgI8FGGe4Sv-Ma3_iK1Xw'
{"data":{"userName":"hahaha1","email":"hahaha1@bgbiao.top","exp":1584259841,"iss":"bgbiao.top","nbf":1584255241},"msg":"token有效","status":0}%

複製代碼

gin-jwt-go源碼


知識星球
知識星球
公衆號
公衆號

本文使用 mdnice 排版

相關文章
相關標籤/搜索