- 本文基于之前幾個項目在部署在微信公眾號下的網(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ā)者成功,否則接入失敗。加密/校驗流程如下:
- 將token、timestamp、nonce三個參數(shù)進行字典序排序
- 將三個參數(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ū)別說明
- 以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)后獲取該用戶的基本信息。- 用戶管理類接口中的“獲取用戶基本信息接口”,是在用戶和公眾號產(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)登錄流程:

開發(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不變)
