微信公眾號開發(fā)-驗證微信服務(wù)器,授權(quán)登錄以及Token管理

  • 本文基于之前幾個項目在部署在微信公眾號下的網(wǎng)頁應(yīng)用,以此寫下微信公眾號開發(fā)的步驟以及踩過的坑

申請測試公眾號

  • 首先開發(fā)者可以在微信測試平臺申請測試公眾號微信測試號申請
    進入如下的界面
    TIM截圖20180305211259.png

    appid相當于公眾號的為唯一標識,appsecret相當于公眾號的密碼,用于獲取access_token等(access_token可以用于推送模板消息等)
  • 得到服務(wù)號后要部署到服務(wù)器上需要驗證服務(wù)器驗證(接口配置信息的URL),驗證規(guī)定使用80或443端口
  • 如果是本地主機測試沒有域名可以使用natapp進行內(nèi)網(wǎng)映射
  • 填寫服務(wù)器地址URL(可以使用主機域名)、Token(這里的token自定義,區(qū)別與之后的access_token)
  • 開發(fā)者提交信息后,微信服務(wù)器將發(fā)送GET請求到填寫的服務(wù)器地址URL上,GET請求攜帶參數(shù)如下表所示:
參數(shù) 描述
signature 微信加密簽名,signature結(jié)合了開發(fā)者填寫的token參數(shù)和請求中的>> timestamp參數(shù)、nonce參數(shù)。
timestamp 時間戳
nonce 隨機數(shù)
echostr 隨機字符串

若確認此次GET請求來自微信服務(wù)器,請原樣返回echostr參數(shù)內(nèi)容,則接入生效,成為開發(fā)者成功,否則接入失敗。加密/校驗流程如下:

  1. 將token、timestamp、nonce三個參數(shù)進行字典序排序
  2. 將三個參數(shù)字符串拼接成一個字符串進行sha1加密 3)開發(fā)者獲得加密后的字符串可與signature對比,標識該請求來源于微信

參考go語言

package main

import (
        "crypto/sha1"
        "fmt"
        "io"
        "log"
        "net/http"
        "sort"
        "strings"
)

const (
        token = "wechat"
)

func makeSignature(timestamp, nonce string) string {
        sl := []string{token, timestamp, nonce}
        sort.Strings(sl)
        s := sha1.New()
        io.WriteString(s, strings.Join(sl, ""))
        return fmt.Sprintf("%x", s.Sum(nil))
}

func validateUrl(w http.ResponseWriter, r *http.Request) bool {
        timestamp := strings.Join(r.Form["timestamp"], "")
        nonce := strings.Join(r.Form["nonce"], "")
        signatureGen := makeSignature(timestamp, nonce)

        signatureIn := strings.Join(r.Form["signature"], "")
        if signatureGen != signatureIn {
                return false
        }
        echostr := strings.Join(r.Form["echostr"], "")
        fmt.Fprintf(w, echostr)
        return true
}

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }
        log.Println("Wechat Service: validateUrl Ok!")
}

func main() {
        log.Println("Wechat Service: Start!")
        http.HandleFunc("/", procRequest)
        err := http.ListenAndServe(":80", nil)
        if err != nil {
                log.Fatal("Wechat Service: ListenAndServe failed, ", err)
        }
        log.Println("Wechat Service: Stop!")
}

注意微信服務(wù)器填寫的URL中的域名api對應(yīng)代碼的api


公眾號授權(quán)登錄

關(guān)于網(wǎng)頁授權(quán)的兩種scope的區(qū)別說明
  1. 以snsapi_base為scope發(fā)起的網(wǎng)頁授權(quán),是用來獲取進入頁面的用戶的openid的,并且是靜默授權(quán)并自動跳轉(zhuǎn)到回調(diào)頁的。用戶感知的就是直接進入了回調(diào)頁(往往是業(yè)務(wù)頁面)
    2.以snsapi_userinfo為scope發(fā)起的網(wǎng)頁授權(quán),是用來獲取用戶的基本信息的。但這種授權(quán)需要用戶手動同意,并且由于用戶同意過,所以無須關(guān)注,就可在授權(quán)后獲取該用戶的基本信息。
  2. 用戶管理類接口中的“獲取用戶基本信息接口”,是在用戶和公眾號產(chǎn)生消息交互或關(guān)注后事件推送后,才能根據(jù)用戶OpenID來獲取用戶基本信息。這個接口,包括其他微信接口,都是需要該用戶(即openid)關(guān)注了公眾號后,才能調(diào)用成功的。

注:

  • 一個用戶經(jīng)過微信snsapi_base授權(quán)登錄后,微信服務(wù)器會返回用戶的唯一標識openid,該openid在本公眾號中具有唯一性,但不同公眾號同一用戶擁有不同的openid
  • 開發(fā)者如果需要跨公眾號識別同一用戶,可以使用snsapi_userinfo方法拉取用戶的UnionID
snsapi_base方法授權(quán)解釋

遵循規(guī)則:
1、在微信公眾號請求用戶網(wǎng)頁授權(quán)之前,開發(fā)者需要先到公眾平臺官網(wǎng)中的“開發(fā) - 接口權(quán)限 - 網(wǎng)頁服務(wù) - 網(wǎng)頁帳號 - 網(wǎng)頁授權(quán)獲取用戶基本信息”的配置選項中,修改授權(quán)回調(diào)域名。請注意,這里填寫的是域名(是一個字符串),而不是URL,因此請勿加 http:// 等協(xié)議頭,例如:gzhu.edu.cn
2、授權(quán)回調(diào)域名配置規(guī)范為全域名,比如需要網(wǎng)頁授權(quán)的域名為:www.qq.com,配置以后此域名下面的頁面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以進行OAuth2.0鑒權(quán)。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com無法進行OAuth2.0鑒權(quán)

授權(quán)登錄流程:


微信授權(quán).png

開發(fā)者開發(fā)時最好先判斷用戶信息是否過期,過期再重定向用戶至微信授權(quán)登錄
授權(quán)登錄go演示代碼

func AuthLogin(w http.ResponseWriter, r *http.Request) {

    title := "登錄"

    if r.Method != "GET" {
        responHtml(w, title, "請求方式錯誤!")
        return
    }

    err:=r.ParseForm()
    if err!=nil{
        wc.logger.Error(fmt.Errorf("auth login fail: %v",err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }


    route := r.FormValue("route")
    if route == "" {
        wc.logger.Warning("auth login fail: route is nil")
        responHtml(w, title, "路由為空")
        return
    }

    sess,err := wc.CheckSession(r)
    if err!=nil{
        wc.logger.Error(fmt.Errorf("auth login fail: %v",err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }

    if sess==nil {
        wc.logger.Info("授權(quán)登陸:cookie 過期,重新授權(quán)")
        url := wc.conf.UserOAuthUrl + "appid=" + wc.conf.AppID + "&redirect_uri=" + wc.conf.ServerHost+wc.conf.ServerGrantAPI +
            "&response_type=code&scope=snsapi_base&state=" + route + "#wechat_redirect"
        http.Redirect(w, r, url, 302)
        return
    }else {
        if wc.route[route] == "" {
            wc.logger.Warning(fmt.Errorf("auth login fail: route %s doesn't exist ", route))
            responHtml(w, title, "路由 "+route+" 不存在")
            return
        }

        redirectUrl:=wc.conf.ServerHost+wc.route[route]
        wc.logger.Info(fmt.Sprintf("授權(quán)登陸成功:用戶:%s 重定向至 %s",sess.UserID,redirectUrl))
        http.Redirect(w, r, redirectUrl, 302)
        return
    }
}

//微信授權(quán)
func WechatGrant(w http.ResponseWriter, r *http.Request) {
    title := "授權(quán)"

    r.ParseForm()
    code := r.FormValue("code")
    state := r.FormValue("state")

    if len(code) == 0 {
        wc.logger.Warning("wechat grant fail: code is invalid")
        responHtml(w, title, "code is invalid")
        return
    }
    if state == "" {
        wc.logger.Warning("wechat grant fail: state is invalid")
        responHtml(w, title, "state is invalid")
        return
    }

    var data *http.Response
    var err error
    url := wc.conf.GetOpenIDUrl + "appid=" + wc.conf.AppID + "&secret=" + wc.conf.AppSecret +
        "&code=" + code + "&grant_type=authorization_code"

    wc.logger.Info("微信授權(quán):獲取用戶信息鏈接:" + url)

    data, err = http.Get(url)
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }

    body, err := ioutil.ReadAll(data.Body)
    defer data.Body.Close()
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }

    userGrantInfo := &userGrantInfo{}
    err = json.Unmarshal(body, userGrantInfo)
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }

    if len(userGrantInfo.OpenID) == 0 {
        wc.logger.Error("wechat grant fail: openid nil")
        responHtml(w, title, "openid 為空")
        return
    }

    err = wc.GrantCallBack(w, r, userGrantInfo.OpenID)
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系統(tǒng)錯誤")
        return
    }

    if wc.route[state] == "" {
        wc.logger.Warning(fmt.Errorf("wechat grant fail: route %s doesn't exist", state))
        responHtml(w, title, "路由錯誤:"+state)
        return
    }

    wc.logger.Info("微信授權(quán):用戶%s登錄成功,重定向至%s",userGrantInfo.OpenID,wc.conf.ServerHost+wc.route[state])

    http.Redirect(w, r, wc.conf.ServerHost+wc.route[state], 302)

}

微信assess_token管理

access_token的有效期目前為2個小時,需定時刷新,重復(fù)獲取將導(dǎo)致上次獲取的access_token失效,所以微信assess_token再服務(wù)器端應(yīng)該有一個統(tǒng)一管理的模塊

微信官方文檔表述:

1、建議公眾號開發(fā)者使用中控服務(wù)器統(tǒng)一獲取和刷新Access_token,其他業(yè)務(wù)邏輯服務(wù)器所使用的access_token均來自于該中控服務(wù)器,不應(yīng)該各自去刷新,否則容易造成沖突,導(dǎo)致access_token覆蓋而影響業(yè)務(wù);
2、目前Access_token的有效期通過返回的expire_in來傳達,目前是7200秒之內(nèi)的值。中控服務(wù)器需要根據(jù)這個有效時間提前去刷新新access_token。在刷新過程中,中控服務(wù)器可對外繼續(xù)輸出的老access_token,此時公眾平臺后臺會保證在5分鐘內(nèi),新老access_token都可用,這保證了第三方業(yè)務(wù)的平滑過渡;
3、Access_token的有效時間可能會在未來有調(diào)整,所以中控服務(wù)器不僅需要內(nèi)部定時主動刷新,還需要提供被動刷新access_token的接口,這樣便于業(yè)務(wù)服務(wù)器在API調(diào)用獲知access_token已超時的情況下,可以觸發(fā)access_token的刷新流程。

參數(shù) 是否必須 說明
grant_type 獲取access_token填寫client_credential
appid 第三方用戶唯一憑證
secret 第三方用戶唯一憑證密鑰,即appsecret

獲取接口調(diào)用請求說明
https請求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
參數(shù)說明

參數(shù) 是否必須 說明
grant_type 獲取access_token填寫client_credential
appid 第三方用戶唯一憑證
secret 第三方用戶唯一憑證密鑰,即appsecret
參數(shù) 說明
access_token 獲取到的憑證
expires_in 憑證有效時間,單位:秒
錯誤時微信會返回錯誤碼等信息,JSON數(shù)據(jù)包示例如下(該示例為AppID無效錯誤):

返回說明
正常情況下,微信會返回下述JSON數(shù)據(jù)包給公眾號:
{"access_token":"ACCESS_TOKEN","expires_in":7200}

參數(shù) 說明
access_token 獲取到的憑證
expires_in 憑證有效時間,單位:秒
錯誤時微信會返回錯誤碼等信息,JSON數(shù)據(jù)包示例如下(該示例為AppID無效錯誤):
返回碼 說明
-1 系統(tǒng)繁忙,此時請開發(fā)者稍候再試
0 請求成功
40001 AppSecret錯誤或者AppSecret不屬于這個公眾號,請開發(fā)者確認AppSecret的正確性
40002 請確保grant_type字段值為client_credential
40164 調(diào)用接口的IP地址不在白名單中,請在接口IP白名單中進行設(shè)置

{"errcode":40013,"errmsg":"invalid appid"}
返回碼說明

返回碼 說明
-1 系統(tǒng)繁忙,此時請開發(fā)者稍候再試
0 請求成功
40001 AppSecret錯誤或者AppSecret不屬于這個公眾號,請開發(fā)者確認AppSecret的正確性
40002 請確保grant_type字段值為client_credential
40164 調(diào)用接口的IP地址不在白名單中,請在接口IP白名單中進行設(shè)置

注意,在生產(chǎn)環(huán)境中,access_token的獲取需要在微信公眾號后臺設(shè)置服務(wù)器的IP地址,如果請求access_token的源IP不在微信公眾號配置的ip白名單中,將會獲取失敗,因此保險起見要配置服務(wù)器對外IP所有可變情況,因為曾經(jīng)微信服務(wù)器檢測到我們學校的服務(wù)器是會跳動的(實際上我們的服務(wù)器對外IP不變)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容