使用 Go 添加 JWT 認證

在典型的業務場景中, 認證與鑑權是十分基礎的.html

對於 API 接口, 一般是在第一次驗證以後生成一個帶有時效的 token. 接下來的一系列請求都攜帶這個 token, 服務器會對這個 token 進行驗證.git

介紹 jwt

JSON Web Tokens(jwt) 是一種用於在兩個主體間傳遞認證消息的方式. 注意, 消息是經過數字簽名的, 所以能夠被驗證和信任, 但卻不是加密的.github

jwt 格式

一個 jwt 由三部分組成:web

  • Header
  • Payload
  • Signature

Header 部分一般只有兩個字段, 分別定義了簽名算法和 token 類型.算法

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

Payload 部分是實際負載, 用於聲明. 一般存儲一些用戶 ID 之類的索引數據, 也能夠放一些其餘有用的信息. 注意, 不要存儲機密數據.json

jwt 規範也在 Payload 中預約義了推薦字段, 但非強制的, 但不少庫都會遵守着實現. 好比 iss 字段定義發佈者, exp 定義 token 的過時時間. 更多字段能夠在 rfc7519 規範 中查看.bash

Signature 就是簽名了, 大體樣式以下:服務器

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
複製代碼

將 header 和 payload 分別使用 base64UrlEncode 編碼, 中間加上點號., 而後使用 header 中指定的簽名算法.函數

簽名用於發佈者驗證消息沒有被篡改, 若是使用非對稱加密, 還能夠驗證發佈者身份.ui

jwt 一般用在請求頭的 Authorization 字段中, 形如:

Authorization: Bearer <token>
複製代碼

更多內容能夠參考 Introduction to JSON Web Tokens.

實踐

對於如何使用 jwt, 應該是很是清晰了. 首先, 咱們要定義一個接口簽發 jwt. 獲取到 token 以後, 就能夠在請求其餘資源時帶上這個 token.

驗證環節能夠用到上一節中講到的中間件技術.

jwt.io 頁面上列出了不少 Go 庫, 這裏選擇功能最全的 github.com/gbrlsnchs/jwt/v3.

go get -u github.com/gbrlsnchs/jwt/v3
複製代碼

定義功能

對於 jwt 有兩個必須實現的功能, 簽發 token 和驗證 token.

首先, 首先定義 Payload 內容, 這裏保持用戶的 ID 和暱稱.

// 記錄登陸信息的 JWT
type LoginToken struct {
	jwt.Payload
	ID       uint   `json:"id"`
	Username string `json:"username"`
}
複製代碼

而後選擇簽名方法.

// 簽名算法, 隨機, 不保存密鑰, 每次都是隨機的
var privateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var publicKey = &privateKey.PublicKey
var hs = jwt.NewES256(
	jwt.ECDSAPublicKey(publicKey),
	jwt.ECDSAPrivateKey(privateKey),
)
複製代碼

編寫簽發和驗證函數.

// 簽名
func Sign(id uint, username string) (string, error) {
	now := time.Now()
	pl := LoginToken{
		Payload: jwt.Payload{
			Issuer:         "coolcat",
			Subject:        "login",
			Audience:       jwt.Audience{},
			ExpirationTime: jwt.NumericDate(now.Add(7 * 24 * time.Hour)),
			NotBefore:      jwt.NumericDate(now.Add(30 * time.Minute)),
			IssuedAt:       jwt.NumericDate(now),
			JWTID:          uuid.NewV4().String(),
		},
		ID:       id,
		Username: username,
	}
	token, err := jwt.Sign(pl, hs)
	return string(token), err
}

// 驗證
func Verify(token []byte) (*LoginToken, error) {
	pl := &LoginToken{}
	_, err := jwt.Verify(token, hs, pl)
	return pl, err
}
複製代碼

簽發接口

首先, 構建一個接口簽發 jwt.

func Login(ctx *gin.Context) {
	var u model.UserModel
	// 應該使用 ShouldBindJSON, 以便使用自定義的 handler.SendResponse
	if err := ctx.ShouldBindJSON(&u); err != nil {
		handler.SendResponse(ctx, errno.New(errno.ErrBind, err), nil)
		return
	}

	user, err := model.GetUserByName(u.Username)
	if err != nil {
		handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
		return
	}

	if err := user.Compare(u.Password); err != nil {
		handler.SendResponse(ctx, errno.New(errno.ErrPasswordIncorrect, err), nil)
		return
	}

	// 簽發 token
	t, err := token.Sign(user.ID, user.Username)
	if err != nil {
		handler.SendResponse(ctx, errno.New(errno.ErrTokenSign, err), nil)
		return
	}
	handler.SendResponse(ctx, nil, model.Token{Token: t})
}
複製代碼

用戶傳遞用戶名和密碼, 經過驗證後返回 jwt.

驗證中間件

由於驗證可能有不少接口都用獲得, 因此寫成中間件是最天然的方式.

前面介紹過標準的傳遞 jwt 的方式是存儲在 Authorization 請求頭中,

Authorization: Bearer <token>
複製代碼

因此, 這裏也依據這種規範來驗證 jwt.

// AuthJWT 驗證 JWT 的中間件
func AuthJWT() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		header := ctx.GetHeader("Authorization")
		headerList := strings.Split(header, " ")
		if len(headerList) != 2 {
			err := errors.New("沒法解析 Authorization 字段")
			handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
			ctx.Abort()
			return
		}
		t := headerList[0]
		content := headerList[1]
		if t != "Bearer" {
			err := errors.New("認證類型錯誤, 當前只支持 Bearer")
			handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
			ctx.Abort()
			return
		}
		if _, err := token.Verify([]byte(content)); err != nil {
			handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
			ctx.Abort()
			return
		}

		ctx.Next()
	}
}
複製代碼

使用

定義好中間件後, 就能夠在 router 中使用了.

g.POST("/v1/create", user.Create) // 爲了方便建立用戶, 無需認證

u := g.Group("/v1/user")
u.Use(middleware.AuthJWT()) // 添加認證
{
  u.GET("", user.List)
  u.POST("", user.Create)
  u.GET("/:id", user.Get)
  u.PUT("/:id", user.Save)
  u.PATCH("/:id", user.Update)
  u.DELETE("/:id", user.Delete)
}
複製代碼

這裏爲了方便, 有個建立用戶的接口放在了外邊, 逃避了 jwt 驗證. 否則一開始沒有用戶又沒法建立挺尷尬的.

總結

認證與鑑權是 API 接口比不可少的一部分, 這裏介紹了 jwt. 更復雜強大的受權協議是 OAuth 2.0, OAuth 2.0 更多用在協做共享資源上, 對於簡單的 API 服務器, jwt 就足夠了. jwt 也能夠做爲 OAuth 2.0 的一部分, 用於承載內容.

當前部分的代碼

做爲版本 v0.9.0

相關文章
相關標籤/搜索