基於gin框架和jwt-go中間件實現小程序用戶登錄和token驗證

本文核心內容是利用jwt-go中間件來開發golang webapi用戶登錄模塊的token下發和驗證,小程序登錄功能只是一個切入點,這套邏輯一樣適用於其餘客戶端的登錄處理。git

小程序登錄邏輯

小程序的登錄邏輯在其餘博主的文章中已經總結得很是詳盡,好比我參考的是這篇博文:微信小程序登陸邏輯整理,因此在這裏再也不贅述,只是大體概括一下個人實現流程:github

  1. 在小程序端調用wx.login方法,異步得到到微信下發的 jscode ,而後將 jscode 發送到 golang 服務端(若是須要詳細用戶信息,見參考博文的實現邏輯,流程大體類似);golang

  2. 服務端接收到 jscode 後,將其與 AppID 和 AppSecret 一塊兒按官方文檔的格式,發送到微信接口( AppID 和 AppSecret 在小程序管理平臺上進行查看),若是接口調用成功,會返回如下字段:
    • openid:用戶信息惟一識別id
    • session_key :解密用戶信息的key
    • expires_in :key的有效期
  3. 根據 open_id 去數據庫查找對應的用戶信息,若是有,得到其平臺 uid ,不然新建用戶,返回新建信息 uid ;
  4. 將 uid 做爲關鍵信息,生成 jwt 格式的 token 字符串,返回給小程序客戶端,小程序收到 token 斷定登錄成功,並將 token 存入 localstorage ,之後的每次請求先讀取 localstorage 中的 token 放入請求頭部做爲身份標識,若是 token 失效或者沒法讀取,則從新執行登錄流程。web

因爲小程序段邏輯簡單,並且不是本文討論重點,代碼實現就不貼出了,後面應該也不會再補。數據庫

服務端處理流程

服務端實現是本文的重頭戲,因爲 golang 語言的特性,我在實際開發中也踩了一些不大不小的坑,在這裏進行詳細記錄,也做爲一個經驗總結,同時加深印象。服務端流程能夠大體分爲如下幾個大步驟:json

  1. 根據客戶端發送的 jscode 得到用戶 open_id小程序

  2. 利用 open_id 獲取平臺 uid ,同時使用 jwt-go 中間件實現 token 的生成微信小程序

  3. 封裝 token 驗證中間件,判斷請求是否合法api

本文的代碼實現是在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的運行機制和一些包的使用原理了解不夠,這些都須要慢慢增強,但願之後能作作一點學習分享吧。這篇簡單介紹就到此結束了,感謝閱讀。

相關文章
相關標籤/搜索