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


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

小程序登陸邏輯

小程序的登陸邏輯在其他博主的文章中已經總結得非常詳盡,比如我參考的是這篇博文:微信小程序登錄邏輯整理,所以在這里不再贅述,只是大致歸納一下我的實現流程:

  1. 在小程序端調用wx.login方法,異步獲得到微信下發的 jscode ,然后將 jscode 發送到 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 失效或者無法讀取,則重新執行登陸流程。

由於小程序段邏輯簡單,而且不是本文討論重點,代碼實現就不貼出了,后面應該也不會再補。

服務端處理流程

服務端實現是本文的重頭戲,由於 golang 語言的特性,我在實際開發中也踩了一些不大不小的坑,在這里進行詳細記錄,也作為一個經驗總結,同時加深印象。服務端流程可以大致分為以下幾個大步驟:

  1. 根據客戶端發送的 jscode 獲得用戶 open_id

  2. 利用 open_id 獲取平台 uid ,同時使用 jwt-go 中間件實現 token 的生成

  3. 封裝 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的運行機制和一些包的使用原理了解不夠,這些都需要慢慢加強,希望以后能做做一點學習分享吧。這篇簡單介紹就到此結束了,感謝閱讀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM