背景: 在現在先後端分離開發的大環境中,咱們須要解決一些登錄,後期身份認證以及鑑權相關的事情,一般的方案就是採用請求頭攜帶token的方式進行實現。本篇文章主要分享下在Golang語言下使用jwt-go來實現後端的token認證邏輯。git
JSON Web Token(JWT)
是一個經常使用語HTTP的客戶端和服務端間進行身份認證和鑑權的標準規範,使用JWT能夠容許咱們在用戶和服務器之間傳遞安全可靠的信息。github
在開始學習JWT以前,咱們能夠先了解下早期的幾種方案。web
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
通常而言,用戶註冊登錄後會生成一個jwt token返回給瀏覽器,瀏覽器向服務端請求數據時攜帶token
,服務器端使用signature
中定義的方式進行解碼,進而對token進行解析和驗證。
header
{
"alg": "HS256",
"typ": "JWT"
}
複製代碼
對上面的json進行base64編碼便可獲得JWT的第一個部分
payload
{
"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的組成部分。
因此,基本上整個過程分爲兩個階段,第一個階段,客戶端向服務端獲取token,第二階段,客戶端帶着該token去請求相關的資源.
一般比較重要的是,服務端如何根據指定的規則進行token的生成。
在認證的時候,當用戶用他們的憑證成功登陸之後,一個JSON Web Token將會被返回。
此後,token就是用戶憑證了,你必須很是當心以防止出現安全問題。
通常而言,你保存令牌的時候不該該超過你所須要它的時間。
不管什麼時候用戶想要訪問受保護的路由或者資源的時候,用戶代理(一般是瀏覽器)都應該帶上JWT,典型的,一般放在Authorization header中,用Bearer schema: Authorization: Bearer <token>
服務器上的受保護的路由將會檢查Authorization header中的JWT是否有效,若是有效,則用戶能夠訪問受保護的資源。若是JWT包含足夠多的必需的數據,那麼就能夠減小對某些操做的數據庫查詢的須要,儘管可能並不老是如此。
若是token是在受權頭(Authorization header)中發送的,那麼跨源資源共享(CORS)將不會成爲問題,由於它不使用cookie.
1.基於服務器的認證
前面說到過session,cookie以及token的區別,在以前傳統的作法就是基於存儲在服務器上的session來作用戶的身份認證,可是一般會有以下問題:
2.Session和JWT Token的異同
均可以存儲用戶相關信息,可是session存儲在服務端,JWT存儲在客戶端
3.基於Token的身份認證如何工做
基於Token的身份認證是無狀態的,服務器或者session中不會存儲任何用戶信息.(很好的解決了共享session的問題)
注意:
Access-Control-Allow-Origin: *
4.用Token的好處
5.JWT和OAuth的區別
使用第三方帳號登陸的狀況
(好比使用weibo, qq, github登陸某個app),而
JWT是用在先後端分離
, 須要簡單的對後臺API進行保護時使用
在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-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")
複製代碼
# 運行項目
$ 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}%
複製代碼
本文使用 mdnice 排版