本文已同步發(fā)布到我的個人博客:https://glorin.xyz/2019/11/23/Golang-jwt-simple-auth/
前言
在開發(fā)app的時候,難免會需要有后臺API,Golang是一門非常適合開發(fā)后臺服務(wù)的高性能的語言。在使用Golang開發(fā)后臺API的時候,經(jīng)常需要有用戶注冊、登錄的功能,例如為了保存用戶數(shù)據(jù)、為了給不同用戶提供不同服務(wù)等。本文便是介紹一種基于jwt的Golang的用戶認(rèn)證系統(tǒng)。
目標(biāo)
我們的模板是實現(xiàn)三個API接口:
- /api/account: 支持Post方法,用來注冊一個用戶
- /api/account/accesstoken: 登錄功能
- /api/account/me: 獲取用戶信息,具有認(rèn)證功能,如果沒有用戶認(rèn)證信息(token),則獲取失敗,否則返回當(dāng)前用戶的信息。
工具原料
- Golang + Goland IDE(非必須,VSCode等等也可以)
- jwt-go:一個go語言的jwt實現(xiàn)
- github.com/gorilla/mux: go語言的一個路由組件,用來提供http路由服務(wù)
實現(xiàn)步驟
搭建http服務(wù),提供API
- 首先我們在自己的go語言目錄下建立項目:golang-jwt-simple-auth(名字可自由決定),golang項目一般遵循golang的約定,放在GOPATH(默認(rèn)是用戶目錄下的go文件夾)下面,比如我的項目放在github上,那目錄就是
~/go/src/github.com/glorinli/golang-jwt-simple-auth
- 新建main.go問,作為程序的主入口,main.go內(nèi)容如下:
package main
import (
"fmt"
"github.com/glorinli/go-jwt-simple-auth/app"
"github.com/glorinli/go-jwt-simple-auth/controllers"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
func init() {
log.SetPrefix("simple-auth")
}
func main() {
// 新建路由器
router := mux.NewRouter()
// 注冊jwt認(rèn)證的中間件
router.Use(app.JwtAuthentication)
// 注冊路由
router.HandleFunc("/api/account", controllers.CreateUser).Methods(http.MethodPost)
router.HandleFunc("/api/account/accesstoken", controllers.Login).Methods(http.MethodGet)
router.HandleFunc("/api/account/me", controllers.Me).Methods(http.MethodGet)
// 獲取端口號
port := os.Getenv("golang-jwt-simple-auth-port")
if port == "" {
port = "8001"
}
fmt.Println("Port is:", port)
// 開始服務(wù)
err := http.ListenAndServe(":"+port, router)
if err != nil {
fmt.Print("Fail to start server", err)
}
}
代碼的作用在注釋中已經(jīng)說明了,關(guān)于mux庫的使用,可以參考 http://www.gorillatoolkit.org/pkg/mux, 這里我們只要知道它起到一個路由器的作用,負(fù)責(zé)把一個api請求映射到一個方法上。
關(guān)鍵就在于
router.Use(app.JwtAuthentication)
這相當(dāng)與注冊了一個中間件,也可以理解為攔截器,就是說所有的請求都會先經(jīng)過這個中間件攔截處理,于是我們便可以在里面處理認(rèn)證相關(guān) 的邏輯了,接下來就來說說這個JwtAuthentication。
JWT認(rèn)證實現(xiàn)
首先還是貼上JwtAuthentication的代碼:
package app
import (
... 省略
)
var JwtAuthentication = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 只針對這個me接口開啟認(rèn)證
needAuthPaths := []string{"/api/account/me"}
requestPath := r.URL.Path
var needAuth = false
// 判斷是否是需要認(rèn)證的api,省略
// 不需要認(rèn)證,直接走下一步
if !needAuth {
next.ServeHTTP(w, r)
return
}
// 從Header讀取Token
tokenHeader := r.Header.Get("Authorization")
// Token is missing
if tokenHeader == "" {
sendInvalidTokenResponse(w, "Missing auth token")
return
}
tk := &models.Token{}
token, err := jwt.ParseWithClaims(tokenHeader, tk, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("token_password")), nil
})
fmt.Println("Parse token error:", err)
if err != nil {
sendInvalidTokenResponse(w, "Invalid auth token: "+err.Error())
return
}
// Token is invalid
if !token.Valid {
sendInvalidTokenResponse(w, "Token is not valid")
return
}
// Auth ok
fmt.Println("User:", tk.UserId)
ctx := context.WithValue(r.Context(), "user", tk.UserId)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func sendInvalidTokenResponse(w http.ResponseWriter, message string) {
response := u.Message(false, message)
w.WriteHeader(http.StatusForbidden)
w.Header().Set("Content-Type", "application/json")
u.Respond(w, response)
}
這個函數(shù)的作用就是解析客戶端傳遞過來的token,將其解析為一個Token對象,Token對象的格式如下:
/*
JWT claims struct
*/
type Token struct {
UserId uint
jwt.StandardClaims
}
可以看到,除了jwt標(biāo)準(zhǔn)的數(shù)據(jù),我們還添加了一個UserId字段,這是為了方便從Token確定用戶的id。從這里我們也可以看出,Jwt Token是可以包含額外信息的。
關(guān)于這個Token是如何生程的,我們下面分析。
注冊功能
返回去看main.go,我們發(fā)現(xiàn)注冊接口被綁定到一個函數(shù)上:
router.HandleFunc("/api/account", controllers.CreateUser).Methods(http.MethodPost)
來看這個CreateUser函數(shù),位于authController.go中:
var CreateUser = func(w http.ResponseWriter, r *http.Request) {
account := &models.Account{}
err := json.NewDecoder(r.Body).Decode(account)
if err != nil {
utils.Respond(w, utils.Message(false, "Invalid info: "+err.Error()))
return
}
utils.Respond(w, account.Create())
}
最終的實現(xiàn)是在account.Create()函數(shù),位于account.go中:
func (account *Account) Create() map[string]interface{} {
// 校驗 省略
// 將密碼做一個加密
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
account.Password = string(hashedPassword)
// 創(chuàng)建數(shù)據(jù)庫數(shù)據(jù)
err := GetDB().Create(account).Error
// ...
// 創(chuàng)建Token
tk := &Token{UserId: account.ID}
token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
account.Token = tokenString
account.Password = ""
response := u.MessageWithData(true, "Account has been created", account)
return response
}
關(guān)鍵就在于創(chuàng)建Token這一步,我們調(diào)用jwt.NewWithClaims來生成Token,參數(shù)有兩個,第一個是簽名方法,采用HS256,第二個就是一個Token對象,這個對象即將被編碼到Token中,這也是我們在進(jìn)行認(rèn)證的時候,從Token解析出來的對象。
登錄功能
登錄功能與注冊功能類似,只是把創(chuàng)建數(shù)據(jù)改為校驗用戶名密碼。
運(yùn)行部署
我們可以直接在Goland中運(yùn)行程序,默認(rèn)會運(yùn)行在8081端口,然后便可以用Postman或者curl調(diào)試相應(yīng)接口,這部分內(nèi)容請讀者自行研究。
注:筆者在mac os 10.15上,發(fā)現(xiàn)編譯時需要添加-ldflags "-w"參數(shù),否則會運(yùn)行失敗。
小結(jié)
本文介紹了如何使用Golang + jwt構(gòu)建一個建議的認(rèn)證系統(tǒng),可以讓大家對用戶認(rèn)證有一個基本的概念,詳細(xì)的代碼也已經(jīng)同步到github上,讀者們可以clone下來參考:https://github.com/glorinli/go-jwt-simple-auth