赞
踩
80/443端口能被联网访问:内网穿透、云服务器(测试号无需域名,ip即可)
申请测试号:微信公众平台 (qq.com)
Token
随便写入,用于判断请求是否来自微信服务器。
URl
绑定需要编写一个接口用来接收微信的信息:
接收微信GET请求传来四个参数 (后续事件推送也会POST到这个接口)
返回参数中的Echostr参数
(gin框架)
wxgroup := r.Group("/wechat")
{
wxgroup.Any("/message",controller.WxMessage)
}
r.Run(":80") // 80 端口启动服务
(wxgo
是博主学习过程写的微信登陆小工具包: go get github.com/beropero/mysdk/wxgo
)
func WxMessage(ctx *gin.Context) {
vp := &wxgo.VerifyParams{
Signature: ctx.Query("signature"),
Echostr: ctx.Query("echostr"),
Timestamp: ctx.Query("timestamp"),
Nonce: ctx.Query("nonce"),
}
// 判断请求是否来自微信服务器。
if flag, _ := wechat.Wx.VerifySignature(*vp); flag {
log.Println(vp)
// 返回echostr字符串
ctx.String(http.StatusOK, vp.Echostr)
}
}
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
参数 | 描述 |
---|---|
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
检验signature的Golang示例代码:
// 签名验证 func (w *Wechat) VerifySignature(vp VerifyParams) (res bool, err error) { // 获取 token 字段, 为接口配置设置的token参数 token, err := w.Cfg.GetToken() if err != nil { return false, err } // 构造匹配字段 strs := []string{vp.Timestamp, vp.Nonce, token} // 按字典序排列后拼接成一个字符串 sort.Strings(strs) str := strings.Join(strs, "") // 对拼接后的字符串进行 SHA1 加密 hash := sha1.New() hash.Write([]byte(str)) hashed := fmt.Sprintf("%x", hash.Sum(nil)) // 加密结果与 signature 比较 if hashed != vp.Signature { return false, errors.New("error: Signature mismatch") } return true, nil }
将接口地址http://your_ip:80/wechat/message写入配置信息中的URL即可配置成功
流程:
开始开发 / 获取 Access token (qq.com)
在测试号信息下获取:appID、appsecret
用http client发送请求获取access token
接口调用请求说明
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
grant_type | 是 | 获取access_token填写client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,即appsecret |
返回说明
正常情况下,微信会返回下述JSON数据包给公众号:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
参数说明
参数 | 说明 |
---|---|
access_token | 获取到的凭证 |
expires_in | 凭证有效时间,单位:秒 |
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
{"errcode":40013,"errmsg":"invalid appid"}
// 获取普通access token func (w *Wechat) GetATReq() error { // 获取appID与appsecret appid, err := w.Cfg.GetAppid() if err != nil { return err } appsecret, err := w.Cfg.GetAppsecret() if err != nil { return err } // 构造请求地址 // ReqUrl.ATUrl: "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", url := fmt.Sprintf(ReqUrl.ATUrl, appid, appsecret) // 发送 GET 请求获取响应 client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return err } // 如果响应结果包含错误错误码,返回错误信息 if strings.Contains(string(body), "errcode") { return fmt.Errorf("wechat response error: %s", string(body)) } // 解析字符串 err = json.Unmarshal(body, &w.LatestAT) if err != nil { return errors.New("json Unmarshal fail") } // 设置成功获取时间 w.LatestAT.Time = time.Now() return nil }
// wechat 用于保存at与配置信息 type Wechat struct { Cfg *WechatCfg LatestAT *AT } // access token type AT struct { AccessToken string `json:"access_token"` ExpiresTime int `json:"expires_in"` Time time.Time // access token获取时间 } // 配置 type WechatCfg struct { Token string Appid string Appsecret string ExpiresTime int } // 获取 accesstoken func (w *Wechat) GetAccessToken() (at string, err error) { w.RefreshAT() return w.LatestAT.AccessToken, nil } // 刷新 LatestAT func (w *Wechat) RefreshAT() error { // 先判断上次获取的是否超时 duration := time.Since(w.LatestAT.Time) durationInSeconds := int(duration.Seconds()) if durationInSeconds < (w.LatestAT.ExpiresTime - 600) { return nil } // 发送请求获取access token err := w.GetATReq() return err }
创建二维码ticket
每次创建二维码ticket需要提供一个开发者自行设定的参数(scene_id),分别介绍临时二维码和永久二维码的创建二维码ticket过程。
临时二维码请求说明
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式:json POST数据例子:{“expire_seconds”: 604800, “action_name”: “QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{“expire_seconds”: 604800, “action_name”: “QR_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}
参数说明
参数 | 说明 |
---|---|
expire_seconds | 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为60秒。 |
action_name | 二维码类型,QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值 |
action_info | 二维码详细信息 |
scene_id | 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1–100000) |
scene_str | 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64 |
func (w *Wechat) GetQRTicketReq(codetype string, sceneId int) (string, error) { // 获取 access token at, err := w.GetAccessToken() if err != nil { return "", err } // 拼接请求地址 url := fmt.Sprintf(ReqUrl.TicketUrl, at) // 构造请求数据 data := &QRCodeReq{ ExpireSeconds: w.Cfg.GetExpiresTime(), ActionName: codetype, // QR码 类型 ActionInfo: ActionInfo{ Scene: Scene{ SceneId: sceneId, }, }, } // 发送 post 请求获取响应 client := &http.Client{} Jsondata, err := json.Marshal(&data) if err != nil { return "", err } reader := bytes.NewReader(Jsondata) req, err := http.NewRequest("POST", url, reader) if err != nil { return "", err } // 设置请求头 json格式 req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var respData = QRCodeRes{} // 解析字符串 err = json.Unmarshal(body, &respData) if err != nil { return "", errors.New("json unmarsha fail") } return respData.Ticket, nil }
通过ticket换取二维码
获取二维码ticket后,开发者可用ticket换取二维码图片。请注意,本接口无须登录态即可调用。
请求说明
HTTP GET请求(请使用https协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode
返回说明
ticket正确情况下,http 返回码是200,是一张图片,可以直接展示或者下载。
HTTP头(示例)如下: Accept-Ranges:bytes Cache-control:max-age=604800 Connection:keep-alive Content-Length:28026 Content-Type:image/jpg Date:Wed, 16 Oct 2013 06:37:10 GMT Expires:Wed, 23 Oct 2013 14:37:10 +0800 Server:nginx/1.4.1
错误情况下(如ticket非法)返回HTTP错误码404。
func (w *Wechat) GetQrImageUrl(ticket string) string {
ticket = url.QueryEscape(ticket) // 进行UrlEncode
// ReqUrl.QRImgUrl: "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s"
url := fmt.Sprintf(ReqUrl.QRImgUrl,ticket)
return url
}
wxgroup.GET("/getloginqr",controller.GetGZQrUrl)
// 获取公众号登陆二维码
func GetGZQrUrl(ctx *gin.Context) {
// 获取ticket
ticket, _ := wechat.Wx.GetQRTicketReq("QR_STR_SCENE", 123)
// 拼装二维码url
qrUrl := wechat.Wx.GetQrImageUrl(ticket)
ctx.JSON(http.StatusOK, gin.H{
"ticket": ticket,
"qrUrl": qrUrl,
})
}
基础消息能力 / 接收事件推送 (qq.com)基础消息能力 / 接收事件推送 (qq.com)
用户扫描带场景值二维码时,可能推送以下两种事件:
推送XML数据包示例:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>
参数说明:
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方账号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,event |
Event | 事件类型,subscribe(关注) / SCAN(之前已关注) |
EventKey | 事件KEY值,qrscene_为前缀,后面为二维码的参数值 |
Ticket | 二维码的ticket,可用来换取二维码图片 |
在最开始时的消息接口添加消息处理逻辑,接收到扫描带参数二维码事件推送后将ticket和openid放入redis
func WxMessage(ctx *gin.Context) { vp := &wxgo.VerifyParams{ Signature: ctx.Query("signature"), Echostr: ctx.Query("echostr"), Timestamp: ctx.Query("timestamp"), Nonce: ctx.Query("nonce"), } if flag, _ := wechat.Wx.VerifySignature(*vp); flag { log.Println(vp) ctx.String(http.StatusOK, vp.Echostr) } // 添加消息处理逻辑 uEvent := &wxgo.UserEvent{} ctx.ShouldBindXML(&uEvent) fmt.Println(uEvent) if uEvent.Ticket != "" && (uEvent.Event == "SCAN" || uEvent.Event == "subscribe") { openid := uEvent.FromUserName log.Printf("ticket:%s, openid:%s", uEvent.Ticket, openid) // 将ticket和openid存入redis err := redis.RedisClient.Set(uEvent.Ticket, openid, 60*time.Second).Err() if err != nil { log.Println(err.Error()) } } }
// 用户事件
type UserEvent struct {
ToUserName string `xml:"ToUserName"` // 开发者微信号
FromUserName string `xml:"FromUserName"` // 发送方账号(一个OpenID)
CreateTime int `xml:"CreateTime"` // 消息创建时间(整型)
MsgType string `xml:"MsgType"` // 消息类型,event,
Event string `xml:"Event"` // 事件类型,subscribe(关注), SCAN(已关注)
EventKey string `xml:"EventKey"` // 事件KEY值,qrscene_为前缀,后面为二维码的参数值
Ticket string `xml:"Ticket"` // 二维码的ticket,可用来换取二维码图片
}
前端需要传递参数ticket询问是否登陆
// 登陆轮训接口 func CheckLogin(ctx *gin.Context) { ticket := ctx.Query("ticket") // 从redis中读取ticket openid, err := redis.RedisClient.Get(ticket).Result() if err != nil { ctx.JSON(http.StatusOK, gin.H{ "login": false, }) } else { ctx.JSON(http.StatusOK, gin.H{ "login": true, "openid": openid, }) } }
wxgroup.GET("/checklogin",controller.CheckLogin)
修改两处请求地址
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录页面</title> <style> #qr-code { width: 200px; height: 200px; } </style> </head> <body> <div id="qr-code-container"> <img id="qr-code" src="#" alt="QR Code"> </div> <h1 id="login-status"></h1> <h3 id="login-user"></h3> <script> // 请求后端获取二维码图片地址 function getQRCode() { // 修改请求地址 fetch('http://your_ip/wechat/getloginqr') // 假设'/get_qr_code'是后端提供的接口地址 .then(response => response.json()) .then(data => { console.log(data.qrUrl) document.getElementById('qr-code').src = data.qrUrl; // 开始轮询检查登录状态 // 根据问号分割URL,获取问号后面的部分 var queryString = data.qrUrl.split('?')[1]; // 根据等号分割查询字符串,获取ticket参数的值 var ticket = queryString.split('=')[1]; console.log(ticket); checkLoginStatus(ticket); }) .catch(error => console.error('Error:', error)); } // 轮询检查登录状态 function checkLoginStatus(ticket) { // 修改请求地址 url = 'http://your_ip/wechat/checklogin?ticket='+ ticket console.log(url) var checkLoginInterval = setInterval(() => { fetch(url) // 假设'/check_login_status'是后端提供的接口地址 .then(response => response.json()) .then(data => { console.log(data) if (data.login) { document.getElementById('login-status').innerText = '登录成功'; document.getElementById('login-user').innerText = 'Openid: ' + data.openid; clearInterval(checkLoginInterval); // 登录成功后停止轮询 } }) .catch(error => console.error('Error:', error)); }, 2000); // 每隔2秒轮询一次 } // 页面加载完成后立即获取二维码 window.onload = function() { getQRCode(); }; </script> </body> </html>
微信公众测试号不支持PC端网页登陆,但是可以模拟带参数二维码的登陆流程来实现类似的功能
流程:
ticket
作为授权地址的state参数生成随机字符串
func GenerateRandomTicket(length int) string {
rand.Seed(time.Now().UnixNano())
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, length)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(已认证服务号,默认拥有scope参数中的snsapi_base和snsapi_userinfo 权限),引导关注者打开如下页面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 公众号的唯一标识 |
redirect_uri | 是 | 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理 |
response_type | 是 | 返回类型,请填写code |
scope | 是 | 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 ) |
state | 否 | 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节 |
#wechat_redirect | 是 | 无论直接打开还是做页面302重定向时候,必须带此参数 |
forcePopup | 否 | 强制此次授权需要用户弹窗确认;默认为false;需要注意的是,若用户命中了特殊场景下的静默授权逻辑,则此参数不生效 |
// 生成ticket
ticket := wxgo.GenerateRandomTicket(20)
// 生成授权地址
redirect_url := "http://your_ip/wechat/accessusercode" // 微信授权后重定向地址,用于接收用户code
scope := "snsapi_base" //授权权限
oauthUrl := wechat.Wx.GetOauth2CodeUrl(redirect_url, scope, ticket) // 授权地址
// Oauth2CodeUrl: "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
func (w *Wechat) GetOauth2CodeUrl(redirectUrl string, scope string, state string) string {
encodeUrl := url.QueryEscape(redirectUrl)
url := fmt.Sprintf(ReqUrl.Oauth2CodeUrl, w.Cfg.Appid, encodeUrl, scope, state)
return url
}
qr码保存到服务器中,需要联网访问到
r.Static("/static","resource")
wxgroup.GET("/getauthqr",controller.GetAuthQrUrl)
// 获取网页授权登陆二维码 func GetAuthQrUrl(ctx *gin.Context) { // 生成ticket ticket := wxgo.GenerateRandomTicket(20) // 生成授权地址 redirect_url := "http://your_ip/wechat/accessusercode" // 微信授权后重定向地址,用于接收用户code scope := "snsapi_base" //授权权限 oauthUrl := wechat.Wx.GetOauth2CodeUrl(redirect_url, scope, ticket) // 将授权地址生成QR码 savePath := "./resource/image" err := wxgo.GenerateQrCode(oauthUrl, savePath, ticket) if err != nil { log.Fatal(err.Error()) return } qrUrl := fmt.Sprintf("http://your_ip/static/image/%s.png", ticket) ctx.JSON(http.StatusOK, gin.H{ "ticket": ticket, "qrUrl": qrUrl, }) }
GenerateQrCode:
func GenerateQrCode(url string, savedir string, fname string) error {
// "github.com/skip2/go-qrcode" 包
qrcode, err := qrcode.New(url, qrcode.Highest)
if err != nil {
return err
}
qrcode.DisableBorder = true
//保存成文件
savepath := fmt.Sprintf("%s/%s.png", savedir, fname)
err = qrcode.WriteFile(256, savepath)
return err
}
如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。
code说明:code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
// 网页授权接收code
func AccessUserCode(ctx *gin.Context) {
code := ctx.Query("code")
ticke := ctx.Query("state")
// 用code获取用户access token
uat, _ := wechat.Wx.GetUserATReq(code)
// 用access token获取用户信息
uInfo, _ := wechat.Wx.GetUserInfoReq(uat)
log.Printf(uInfo.NickName, uInfo.Headimgurl, uInfo.City)
// 将ticket和openid放入redis
redis.RedisClient.Set(ticke, uInfo.OpenId, 60*time.Second)
// 重定向到成功/失败页面
ctx.Redirect(http.StatusTemporaryRedirect, "http://your_ip/static/html/loginsucceed.html")
}
复用带参数二维码的登陆轮训接口
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录页面</title> <style> #qr-code { width: 200px; height: 200px; } </style> </head> <body> <div id="qr-code-container"> <img id="qr-code" src="#" alt="QR Code"> </div> <h1 id="login-status"></h1> <h3 id="login-user"></h3> <script> // 请求后端获取二维码图片地址 function getQRCode() { // 修改ip fetch('http://your_ip/wechat/getauthqr') .then(response => response.json()) .then(data => { console.log(data.qrUrl) document.getElementById('qr-code').src = data.qrUrl; // 开始轮询检查登录状态 var ticket = data.ticket; console.log(ticket); checkLoginStatus(ticket); }) .catch(error => console.error('Error:', error)); } // 轮询检查登录状态 function checkLoginStatus(ticket) { // 修改ip url = 'http://your_ip/wechat/checklogin?ticket='+ ticket console.log(url) var checkLoginInterval = setInterval(() => { fetch(url) // 假设'/check_login_status'是后端提供的接口地址 .then(response => response.json()) .then(data => { console.log(data) if (data.login) { document.getElementById('login-status').innerText = '登录成功'; document.getElementById('login-user').innerText = 'Openid: ' + data.openid; clearInterval(checkLoginInterval); // 登录成功后停止轮询 } }) .catch(error => console.error('Error:', error)); }, 2000); // 每隔2秒轮询一次 } // 页面加载完成后立即获取二维码 window.onload = function() { getQRCode(); }; </script> </body> </html>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。