微信公众号开发-验证微信服务器,授权登录以及Token管理

  • 本文基于之前几个项目在部署在微信公众号下的网页应用,以此写下微信公众号开发的步骤以及踩过的坑

申请测试公众号

  • 首先开发者可以在微信测试平台申请测试公众号微信测试号申请
    进入如下的界面
    TIM截图20180305211259.png

    appid相当于公众号的为唯一标识,appsecret相当于公众号的密码,用于获取access_token等(access_token可以用于推送模板消息等)
  • 得到服务号后要部署到服务器上需要验证服务器验证(接口配置信息的URL),验证规定使用80或443端口
  • 如果是本地主机测试没有域名可以使用natapp进行内网映射
  • 填写服务器地址URL(可以使用主机域名)、Token(这里的token自定义,区别与之后的access_token)
  • 开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
参数 描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的>> timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串

若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

  1. 将token、timestamp、nonce三个参数进行字典序排序
  2. 将三个参数字符串拼接成一个字符串进行sha1加密 3)开发者获得加密后的字符串可与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!")
}

注意微信服务器填写的URL中的域名api对应代码的api


公众号授权登录

关于网页授权的两种scope的区别说明
  1. 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
    2.以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
  2. 用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。

注:

  • 一个用户经过微信snsapi_base授权登录后,微信服务器会返回用户的唯一标识openid,该openid在本公众号中具有唯一性,但不同公众号同一用户拥有不同的openid
  • 开发者如果需要跨公众号识别同一用户,可以使用snsapi_userinfo方法拉取用户的UnionID
snsapi_base方法授权解释

遵循规则:
1、在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头,例如:gzhu.edu.cn
2、授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.htmlhttp://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.comhttp://music.qq.comhttp://qq.com无法进行OAuth2.0鉴权

授权登录流程:


微信授权.png

开发者开发时最好先判断用户信息是否过期,过期再重定向用户至微信授权登录
授权登录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, "系统错误")
        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, "系统错误")
        return
    }

    if sess==nil {
        wc.logger.Info("授权登陆:cookie 过期,重新授权")
        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("授权登陆成功:用户:%s 重定向至 %s",sess.UserID,redirectUrl))
        http.Redirect(w, r, redirectUrl, 302)
        return
    }
}

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

    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("微信授权:获取用户信息链接:" + url)

    data, err = http.Get(url)
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系统错误")
        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, "系统错误")
        return
    }

    userGrantInfo := &userGrantInfo{}
    err = json.Unmarshal(body, userGrantInfo)
    if err != nil {
        wc.logger.Error(fmt.Errorf("wechat grant fail: %v", err))
        responHtml(w, title, "系统错误")
        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, "系统错误")
        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("微信授权:用户%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个小时,需定时刷新,重复获取将导致上次获取的access_token失效,所以微信assess_token再服务器端应该有一个统一管理的模块

微信官方文档表述:

1、建议公众号开发者使用中控服务器统一获取和刷新Access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
2、目前Access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器可对外继续输出的老access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;
3、Access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,即appsecret

获取接口调用请求说明
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
参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

返回说明
正常情况下,微信会返回下述JSON数据包给公众号:
{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
返回码 说明
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
40002 请确保grant_type字段值为client_credential
40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置

{"errcode":40013,"errmsg":"invalid appid"}
返回码说明

返回码 说明
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
40002 请确保grant_type字段值为client_credential
40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置

注意,在生产环境中,access_token的获取需要在微信公众号后台设置服务器的IP地址,如果请求access_token的源IP不在微信公众号配置的ip白名单中,将会获取失败,因此保险起见要配置服务器对外IP所有可变情况,因为曾经微信服务器检测到我们学校的服务器是会跳动的(实际上我们的服务器对外IP不变)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容