本文已同步發(fā)布到我的個(gè)人博客:https://glorin.xyz/2019/11/23/Golang-jwt-simple-auth/
前言
在開發(fā)app的時(shí)候,難免會(huì)需要有后臺(tái)API丢氢,Golang是一門非常適合開發(fā)后臺(tái)服務(wù)的高性能的語(yǔ)言傅联。在使用Golang開發(fā)后臺(tái)API的時(shí)候,經(jīng)常需要有用戶注冊(cè)疚察、登錄的功能蒸走,例如為了保存用戶數(shù)據(jù)、為了給不同用戶提供不同服務(wù)等貌嫡。本文便是介紹一種基于jwt的Golang的用戶認(rèn)證系統(tǒng)比驻。
目標(biāo)
我們的模板是實(shí)現(xiàn)三個(gè)API接口:
- /api/account: 支持Post方法,用來注冊(cè)一個(gè)用戶
- /api/account/accesstoken: 登錄功能
- /api/account/me: 獲取用戶信息岛抄,具有認(rèn)證功能别惦,如果沒有用戶認(rèn)證信息(token),則獲取失敗夫椭,否則返回當(dāng)前用戶的信息步咪。
工具原料
- Golang + Goland IDE(非必須,VSCode等等也可以)
- jwt-go:一個(gè)go語(yǔ)言的jwt實(shí)現(xiàn)
- github.com/gorilla/mux: go語(yǔ)言的一個(gè)路由組件益楼,用來提供http路由服務(wù)
實(shí)現(xiàn)步驟
搭建http服務(wù)猾漫,提供API
- 首先我們?cè)谧约旱膅o語(yǔ)言目錄下建立項(xiàng)目:golang-jwt-simple-auth(名字可自由決定),golang項(xiàng)目一般遵循golang的約定感凤,放在GOPATH(默認(rèn)是用戶目錄下的go文件夾)下面悯周,比如我的項(xiàng)目放在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()
// 注冊(cè)jwt認(rèn)證的中間件
router.Use(app.JwtAuthentication)
// 注冊(cè)路由
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)
// 獲取端口號(hào)
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庫(kù)的使用族跛,可以參考 http://www.gorillatoolkit.org/pkg/mux, 這里我們只要知道它起到一個(gè)路由器的作用闰挡,負(fù)責(zé)把一個(gè)api請(qǐng)求映射到一個(gè)方法上。
關(guān)鍵就在于
router.Use(app.JwtAuthentication)
這相當(dāng)與注冊(cè)了一個(gè)中間件礁哄,也可以理解為攔截器长酗,就是說所有的請(qǐng)求都會(huì)先經(jīng)過這個(gè)中間件攔截處理,于是我們便可以在里面處理認(rèn)證相關(guān) 的邏輯了桐绒,接下來就來說說這個(gè)JwtAuthentication夺脾。
JWT認(rèn)證實(shí)現(xiàn)
首先還是貼上JwtAuthentication的代碼:
package app
import (
... 省略
)
var JwtAuthentication = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 只針對(duì)這個(gè)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)
}
這個(gè)函數(shù)的作用就是解析客戶端傳遞過來的token咧叭,將其解析為一個(gè)Token對(duì)象,Token對(duì)象的格式如下:
/*
JWT claims struct
*/
type Token struct {
UserId uint
jwt.StandardClaims
}
可以看到烁竭,除了jwt標(biāo)準(zhǔn)的數(shù)據(jù)菲茬,我們還添加了一個(gè)UserId字段,這是為了方便從Token確定用戶的id派撕。從這里我們也可以看出婉弹,Jwt Token是可以包含額外信息的。
關(guān)于這個(gè)Token是如何生程的腥刹,我們下面分析马胧。
注冊(cè)功能
返回去看main.go,我們發(fā)現(xiàn)注冊(cè)接口被綁定到一個(gè)函數(shù)上:
router.HandleFunc("/api/account", controllers.CreateUser).Methods(http.MethodPost)
來看這個(gè)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())
}
最終的實(shí)現(xiàn)是在account.Create()函數(shù)佩脊,位于account.go中:
func (account *Account) Create() map[string]interface{} {
// 校驗(yàn) 省略
// 將密碼做一個(gè)加密
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
account.Password = string(hashedPassword)
// 創(chuàng)建數(shù)據(jù)庫(kù)數(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ù)有兩個(gè)威彰,第一個(gè)是簽名方法,采用HS256穴肘,第二個(gè)就是一個(gè)Token對(duì)象歇盼,這個(gè)對(duì)象即將被編碼到Token中,這也是我們?cè)谶M(jìn)行認(rèn)證的時(shí)候评抚,從Token解析出來的對(duì)象豹缀。
登錄功能
登錄功能與注冊(cè)功能類似伯复,只是把創(chuàng)建數(shù)據(jù)改為校驗(yàn)用戶名密碼。
運(yùn)行部署
我們可以直接在Goland中運(yùn)行程序邢笙,默認(rèn)會(huì)運(yùn)行在8081端口啸如,然后便可以用Postman或者curl調(diào)試相應(yīng)接口,這部分內(nèi)容請(qǐng)讀者自行研究氮惯。
注:筆者在mac os 10.15上叮雳,發(fā)現(xiàn)編譯時(shí)需要添加-ldflags "-w"參數(shù),否則會(huì)運(yùn)行失敗妇汗。
小結(jié)
本文介紹了如何使用Golang + jwt構(gòu)建一個(gè)建議的認(rèn)證系統(tǒng)帘不,可以讓大家對(duì)用戶認(rèn)證有一個(gè)基本的概念,詳細(xì)的代碼也已經(jīng)同步到github上杨箭,讀者們可以clone下來參考:https://github.com/glorinli/go-jwt-simple-auth