本文核心內容是利用jwt-go中間件來開發golang webapi用戶登陸模塊的token下發和驗證,小程序登陸功能只是一個切入點,這套邏輯同樣適用於其他客戶端的登陸處理。
小程序登陸邏輯
小程序的登陸邏輯在其他博主的文章中已經總結得非常詳盡,比如我參考的是這篇博文:微信小程序登錄邏輯整理,所以在這里不再贅述,只是大致歸納一下我的實現流程:
-
在小程序端調用
wx.login
方法,異步獲得到微信下發的 jscode ,然后將 jscode 發送到 golang 服務端(如果需要詳細用戶信息,見參考博文的實現邏輯,流程大致相似); -
服務端接收到 jscode 后,將其與 AppID 和 AppSecret 一起按官方文檔的格式,發送到微信接口( AppID 和 AppSecret 在小程序管理平台上進行查看),如果接口調用成功,會返回以下字段:
- openid:用戶信息唯一識別id
- session_key :解密用戶信息的key
- expires_in :key的有效期
-
根據 open_id 去數據庫查找對應的用戶信息,如果有,獲得其平台 uid ,否則新建用戶,返回新建信息 uid ;
-
將 uid 作為關鍵信息,生成 jwt 格式的 token 字符串,返回給小程序客戶端,小程序收到 token 判定登陸成功,並將 token 存入 localstorage ,以后的每次請求先讀取 localstorage 中的 token 放入請求頭部作為身份標識,如果 token 失效或者無法讀取,則重新執行登陸流程。
由於小程序段邏輯簡單,而且不是本文討論重點,代碼實現就不貼出了,后面應該也不會再補。
服務端處理流程
服務端實現是本文的重頭戲,由於 golang 語言的特性,我在實際開發中也踩了一些不大不小的坑,在這里進行詳細記錄,也作為一個經驗總結,同時加深印象。服務端流程可以大致分為以下幾個大步驟:
-
根據客戶端發送的 jscode 獲得用戶 open_id
-
利用 open_id 獲取平台 uid ,同時使用 jwt-go 中間件實現 token 的生成
-
封裝 token 驗證中間件,判斷請求是否合法
本文的代碼實現是在gin
框架基礎上完成的,gin
是一個非常輕量的 web http 處理框架,很符合 golang 輕框架的理念,但高度靈活也要求了一定的自主開發能力,比如請求數據庫讀寫、請求信息讀取和一些中間件的使用,這些都需要自己查找不同包的官方文檔,去檢索Api和查找對應的解決方案。雖然如此,但作為習慣了 .net平台 高度封裝和甜到發膩的語法糖的 .neter ,在 golang 開發過程中也體會到了不一樣的樂趣。廢話不多說,如果對 golang 開發感興趣的話,gin
是一個我十分推薦的上手框架。
獲得用戶 open_id
在這里簡單介紹一下路由、model和controller的一個分層開發實現:
- main.go 程序入口,在這里進行路由分發
- controllers/xx.go xx模塊的路由請求處理代碼相關
- models/xx.go xx模塊用到的結構體 struct (類似class)定和結構體相關函數定義
- middleware 存放封裝后的請求處理中間件
首先,按照gin框架的基礎路由處理,調用Controller
中的登陸函數,接收處理路由的 GET 請求。
//main.go
...
func main(){
r := gin.Default()
account := new(controllers.AccountController)
r.GET("/account/login", account.WxLogin)
}
然后在Controller
中利用c.Query
讀取參數 jscode ,再將 jscode 和其他信息一起發送給微信服務器,獲得官方返回的核心字段。
...
//接受請求參數后進行處理
func (ctrl AccountController) WxLogin(c *gin.Context) {
jscode := c.Query("jsCode")
//發送jscode,獲得用戶的open_id
wxSession, err := accountModel.WxLogin(jscode)
...
}
具體實現jscode發送和處理的邏輯在Model
中完成(順便吐槽一下golang的錯誤處理,寫了無數的if err!=nil
),還有golang 結構體中的tag十分好用,綁定數據庫讀寫實體、json序列化字段都能用一個結構體和不同tag靈活處理。
//WxSession 微信登陸接口返回session
type WxSession struct {
SessionKey string `json:"session_key"`
ExpireIn int `json:"expires_in"`
OpenID string `json:"openid"`
}
//WxLogin 微信用戶授權
func (m AccountModel) WxLogin(jscode string) (session WxSession, err error) {
client := &http.Client{}
//生成要訪問的url
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", "xxxYOUR APPIDxxx", "xxxYOUR SECRETxxx", jscode)
//提交請求
reqest, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
//處理返回結果
response, _ := client.Do(reqest)
body, err := ioutil.ReadAll(response.Body)
jsonStr := string(body)
//解析json
if err := json.Unmarshal(body, &session); err != nil {
session.SessionKey = jsonStr
return session, err
}
return session, err
}
返回的session中即包含open_id
利用jwt-go生成token
拿到open_id后就可以根據open_id去數據庫查詢所綁定的用戶id,得到用戶身份,同時也需要根據用戶信息來生成token。而我需要的只是uid,其他用戶信息的封裝原理類似,僅供參考。
jwt-go
是個功能強大的jwt生成包,封裝了很多定制化函數,可以根據實際需要靈活的配置信息來生成符合要求的token字符串,並且提供了token自動驗證的功能,詳細說明見GitHub主頁:jwt-to
//SignWxToken 生成token,uid用戶id,expireSec過期秒數
func (u Util) SignWxToken(uid int64, expireSec int) (tokenStr string, err error) {
// 帶權限創建令牌
claims := make(jwt.MapClaims)
claims["uid"] = uid
claims["admin"] = false
sec := time.Duration(expireSec)
claims["exp"] = time.Now().Add(time.Second * sec).Unix() //自定義有效期,過期需要重新登錄獲取token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用自定義字符串加密 and get the complete encoded token as a string
tokenStr, err = token.SignedString([]byte("xxxYOUR KEYxxx"))
return tokenStr, err
}
這里生成標准格式的jwt字符串,將token下發到小程序客戶端后,客戶端將在每次請求攜帶此token,來進行身份校驗。這里建議小程序端將所有的request進行集合封裝,以便於統一操作,這個項目開發完后也會對小程序端的一些操作進行總結概括,這里不多廢話,token需要存放在請求頭部的Authorization
中。
token驗證中間件
首先,在main.go
添加權限驗證路由組:
uAuth := r.Group("/xxx", jwtauth.WxAuth())
{
.......
這里是所有需要用戶身份識別的路由
.......
}
然后寫驗證中間件:
type MyCustomClaims struct {
UID int `json:"uid"`
jwt.StandardClaims
}
//WxAuth ...
func WxAuth() gin.HandlerFunc {
return func(c *gin.Context) {
authString := c.Request.Header.Get("Authorization")
kv := strings.Split(authString, " ")
if len(kv) != 2 || kv[0] != "Bearer" {
result := models.UnauthorizedResult()
c.JSON(200, result)
c.Abort()
return
}
tokenString := kv[1]
// Parse token
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("xxxYOUR KEYxxx"), nil
})
if err != nil {
result := models.UnauthorizedResult()
c.JSON(200, result)
c.Abort()
return
}
if !token.Valid {
result := models.UnauthorizedResult()
c.JSON(200, result)
c.Abort()
return
}
claims, ok := token.Claims.(*MyCustomClaims)
if !ok {
c.JSON(403, result)
c.Abort()
return
}
//將uid寫入請求參數
uid := claims.UID
c.Set("uid", uid)
}
}
其中result是自己封裝的一個返回值結構體,因為UID是自己額外封裝的參數,這里根據jwt-go的說明,封裝了一個結構體存放和解析參數,實際上如果用原生默認方法也是可以取到這個值,但是要添加額外的解析處理,這里還是推薦用封裝結構體的方法。最后token驗證成功的話,將uid寫入請求內部參數,供權限組內的路由函數使用,示例如下:
//List 列表
func (ctrl XXXController) List(c *gin.Context) {
uidVal, ok := c.Get("uid")
if ok {
uid := uidVal.(int)
....
根據UID進行其他操作
....
}
至此,一個簡單的從小程序登陸和api用戶認證的流程已經完成。
總結
寫到這里,發現這篇博文更像是一篇流水賬,把自己的實現邏輯和代碼記錄了一下,對詳細的實現原理和一些細節操作,沒有花很大篇幅去寫明白,原因之一是精力有限,業務功能還沒做完,不太可能花很多時間來介紹一個登陸模塊,這里只是簡單梳理進行經驗共享,主要原因還是自己功力不夠,對原理實現這一塊沒有深入了解過,目前更多關注的只是業務實現,對golang的運行機制和一些包的使用原理了解不夠,這些都需要慢慢加強,希望以后能做做一點學習分享吧。這篇簡單介紹就到此結束了,感謝閱讀。