网站服务部小组文档

#微信授权项目开发流程详解

1.微信授权的交互过程

整个微信授权流程的目的是为了取到access_token,平台通过access_token去取用户的微信信息,整个流程会用到以下概念

  • code:微信授权后跳转到项目页面时url上带的临时票据;例如:http://game.163.com//weixin/ubee/?code=xxxxx
  • access_token:平台通过code调用微信API获取最终的授权凭证,access_token有超时时间,超时后用户进入页面需要重新授权
  • weixin_userid:用户的微信ID

微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有server端的应用授权。按照我们的业务一般流程为:

  • 用户在微信中打开页面A;
  • 页面A会跳转到微信授权页面发起微信授权登录请求,微信用户允许授权第三方应用后,微信会重定向回到A页面,并且带上授权临时票据code参数
  • 页面A调用接口将code传给平台,平台通过code参数加上AppID和AppSecret等,通过API换取access_token,并通过access_token进行接口调用,获取用户基本数据并一起返回给A页面。

从网上找的一张获取access_token时序图,其中第三方应用可以等同于前端 +平台:

Alt text

2.微信授权项目模型

由于我们用于微信授权的公众号绑定了 http://game.163.com 域名,以及微信授权时会限制访问来源为公众号绑定的唯一域名(即game.163.com)的机制,因此调用微信授权的页面需要放到 http://game.163.com/ 下。这意味着建立GIT项目时需要选择GROUP为NIE,不能建立到项目所属产品的GROUP下,而且目前大多数项目都是这么做的。但这种方式存在三个缺点:

  • 1.不利于项目管理,所有的产品的H5项目都集中到game Group中。
  • 2.不便于测试,只能发布到上线地址测试,或者在预发布环境中测试。
  • 3.不便于上线后迭代,迭代时只能发布到临时地址测试,测试完成后再发布回原地址覆盖。

为了便于项目管理与测试,同时绕过,我们选择将项目建立在产品Group下,并发布到产品域名下,同时在项目页面与微信授权跳转中间添加一个game.163.com域名的授权中转页面,以绕过微信授权页面的来源检测。

这种方式可以解决上面3个缺点,使H5项目归入各自产品group,同时可以使用本地地址调试以及使用测试地址供QA测试

但同时也存在着一个缺点,就是每一次授权需要多跳转一个页面。而项目实施中,当用户是第一次授权并且项目需要获取用户详细信息时实际上是会跑两次授权(第一次自动授权取不到用户详细信息,再进行第二次手动授权)。

综上所述,我们的项目页面模型为:

  • 项目页面A
  • GAME域名下的授权中转页面B
  • 微信授权页面C

页面流程为:

  • 当用户访问页面A时,拼接微信授权页面的URL参数UrlParam(将本页面URL设置为微信授权后的重定向链接),跳转到中转页面B:http://B?+UrlParam (当项目上线域名为game.163.com时不使用中转页面直接跳转到微信授权页面C:http://C?+UrlParam)
  • 中转页面B将A传过来的UrlParam中的redirect_uri参数 escape()编码后以source_uri参数形式拼接到当前页面B的url并替换UrlParam中的redirect_uri后,跳转到微信授权页面C
  • 微信授权页面C完成授权后在redirect_uri后拼接code参数并跳转回页面redirect_uri设置的页面,即中转页面B
    • 中转页面B将UrlParam中的source_uri unescape()解码后拼接上code参数后跳转到source_uri.即A页面

此处应有示例:http://test.nie.163.com/test10/test_weixin/

Alt text

3.公用中转页面

URL:http://game.163.com/weixin/redirector/

<!DOCTYPE HTML>
<html>
<head>
<title></title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" >
    var params = function(u, paras){
        var url = u;
        var paraString = url.substring(url.indexOf("?")+1,url.length).split("&");
        var paraObj = {}
        for (i=0; j=paraString[i]; i++){
            paraObj[j.substring(0,j.indexOf("=")).toLowerCase()] = j.substring(j.indexOf("=")+1,j.length);
        }
        var ret = paraObj[paras.toLowerCase()];
        if(typeof(ret)=="undefined"){
            return "";
        }else{
            return ret;
        }
    }
    if(location.search){
        //alert(location.href);
        var url=location.href;
        var code=params(url,"code")
        if(code){//如果url上带有code参数则说明是从微信授权成功返回
            var source_uri= unescape(params(url,"source_uri"));
            //alert(source_uri);
            location.href=source_uri+"&code="+code;
        }else{//如果url上没有code就是从项目页面跳转过来需要请求微信授权
            var appid=params(url,"appid");
            var source_uri=escape(decodeURIComponent(params(url,"redirect_uri")));
            var response_type=params(url,"response_type");
            var scope=params(url,"scope");
            var state=params(url,"state");
            var hash=location.hash;
            var redirect_uri=encodeURIComponent(location.origin+location.pathname+"?source_uri="+source_uri);
            location.href="https://open.weixin.qq.com/connect/oauth2/authorize?appid="+appid+"&redirect_uri="+redirect_uri+"&response_type="+response_type+"&scope="+scope+"&state="+state+hash;
        }
    }
</script>
</head>
    <body>
    </body>
</html>

4.授权接口示例

一般平台会提供如下类似的授权接口,具体参数数量及返回字段的数量根据项目需求会有所变化

URL /login?code=&page_key=&need_userinfo=&callback=

调用方式 GET

输入参数 code:微信授权代码,字符串,必传 page_key:访问页面的key,字符串,选传 need_userinfo:是否需要获取用户详细信息,0/1,必传 callback:JSONP回调函数名,字符串,选传

返回值 格式:JSONP

  callback({
         "success": true/false, // 是否成功
        "msg": str, // 具体错误信息(中文,unicode),success为false时返回
        "user_id": int, // 用户id
        "weixin_token": str, // 用户token,用作session id
        "nickname": str, // 用户昵称
        "headimgurl": str, // 用户头像URL
        "page_key": str, // 自己的页面key
        "view_user_info": { // 查看其他人的信息,若没有传page_key或传自己的page_key时,返回null,
            "user_id": int, // 用户id
            "nickname": str, // 用户昵称
            "headimgurl": str, // 用户头像URL
    }
})

注:以上参数及返回结果根据项目需求会有所不同。

5.授权模块Authorize()示例


//公共函数

var params = function(u, paras){//获取URL参数的方法
    var url = u;
    var paraString = url.substring(url.indexOf("?")+1,url.length).split("&");
    var paraObj = {}
    for (i=0; j=paraString[i]; i++){
        paraObj[j.substring(0,j.indexOf("=")).toLowerCase()] = j.substring(j.indexOf("=")+1,j.length);
    }
    var ret = paraObj[paras.toLowerCase()];
    if(typeof(ret)=="undefined"){
        return "";
    }else{
        return ret;
    }
}
//Authorize授权模块
nie.define("Authorize",function(){
    var defPic="";//当用户没有设置微信头像时取此作为默认头像;
    var redirectorURL="http://game.163.com/weixin/redirector";//微信授权中转URL
    if(location.host=="game.163.com"){
        redirectorURL="https://open.weixin.qq.com/connect/oauth2/authorize";//当项目上线地址域名为game.163.com时,可以直接跳转授权,不需要中转。
    }
    var options={//定义参数格式
        pageUrl:'',//微信授权后自动跳转向此url
        serverHost:"",//授权接口地址
        callback:null//获取到用户微信信息后回调函数
    };
    function dealHeadImg(img) {//对返回的微信头像url做处理
        if(img) {
            return img.substr(0,img.length-1)+'132';
        } else {
            return defPic;
        }
    }
    function getAuthCode(type) {//页面打开时先跳转到微信oauth2授权页面,参数type表示获取微信授权类型,决定后面是否需要取用户微信详细信息(type=="user")还是单取微信ID(type!="user")
        var box = "?nie="+Math.random();//用于拼接微信授权后回跳的URL
        var pagekey=params(location.search, "pagekey");//点击好友分享出来的页面时页面URL参数里的好友页面唯一ID,UrlParam key由分享时的url决定,这里以pagekey为例,部分案例使用的是bid;
        var channel = params(location.search, "c");//进入此页面的渠道,作章鱼统计用,必须要加
        var mbshare = params(location.search, "mbshare");//分享组件统计参数-分享类型,必须要加
        var spreadtimes= params(location.search, "spreadtimes");//分享组件统计参数-传播深度,必须要加
        if(mbshare) {
            box = box+'&mbshare ='+mbshare ;
        }
        if(spreadtimes) {
            box = box+'&spreadtimes='+spreadtimes;
        }
        if(channel) {
            box = box+'&c='+channel;
        }
        if(pagekey){
            box = box+'&pagekey='+pagekey;
        }
        var toLink = '';
        var jurl = options.pageUrl;
        if(type=='user') {//需要获取微信用户的所有详细信息(包括微信id,昵称,头像,性别,城市,国家等),会弹出绿色的授权提示框,以微信授权链接中的&scope=snsapi_userinfo给微信APP区分是否需要用户手动授权,并在用户手动授权跳转回页面后通过uinfo=1告诉平台取用户全部微信信息[getUserInfo()]
            box = box+'&uinfo=1';
            jurl = encodeURIComponent(jurl+box);
            toLink=redirectorURL+"?appid=wx85f583832dbd07e9&redirect_uri=" + jurl + "&response_type=code&scope=snsapi_userinfo&state=163#wechat_redirect";
        } else {//仅获取用户的微信ID,不会出现绿色同意授权框,以授权链接中的&scope=snsapi_base给微信APP区分是否需要用户手动授权,并在自动授权跳转回页面后通过uinfo=0告诉平台仅取用户微信ID[getUserInfo()]
            box = box+'&uinfo=0';//注:有些案例中此处为uinfo=2,具体需要根据平台login接口的参数need_userinfo说明设置
            jurl = encodeURIComponent(jurl+box);
            toLink=redirectorURL+"?appid=wx85f583832dbd07e9&redirect_uri=" + jurl + "&response_type=code&scope=snsapi_base&state=163#wechat_redirect";
        }
        location.href=toLink;
    }
    function getUserInfo(callback) {
        var code = params(location.search, "code");//微信授权成功后的生成的临时微信code
        if(!code) {//如果没有微信code,跳转到微信author进行授权
            getAuthCode();
            return;
        }
        var pagekey = params(location.search, "pagekey");
        var uinfo = params(location.search, "uinfo");
        $.ajax({
            url:options.serverHost+'login',
            data:{
                'code':code,//微信授权后的临时凭据code
                'need_userinfo':uinfo,//uinfo=1获取微信用户的所有详细信息(包括微信id,昵称,头像,性别,城市,国家等),uinfo=0仅获取微信ID,
                'page_key':pagekey//点击好友分享出来的页面进入此页面时url上带着的pagekey参数,代表好友个人页id
            },
            async: false,
            dataType:"jsonp",
            success:function(data){
                //alert("ajax_sec:"+JSON.stringify(data));
                if(data.success==true) {
                    data.headimgurl = dealHeadImg(data.headimgurl);//微信头像
                    if(!data.nickname&&!data.headimgurl) {//当用户的微信头像和微信昵称都不存在时,再次执行getAuthCode("user")跳转到到微信oauth2授权页面进行授权,仅在项目需要获取用户微信头像和昵称时才需要做这一步
                        setTimeout(function(){
                            getAuthCode('user');
                        },100);
                        return;
                    }
                    if(typeof options.callback=="function"){//执行参数中的回调函数,并把取到的用户信息传入回调函数
                        options.callback(data);
                    }
                } else {
                    if(data.msg.indexOf('invalid code')!=-1) {//平台接口出错时重新授权
                        setTimeout(function(){
                            getAuthCode();
                        }, 100);
                        return;
                    }
                }
            },
            error:function(){
                alert('网络信号不好,请刷新再试');
            }
        });
    }    
    function init(param){
        options=$.extend(options,param||{})
        getUserInfo(function(data){
            if(options.callback){
                options.callback(data);
            }
        })
    }
    return{

        init:init
    }
})

Authorize()的大概逻辑如下:

  • 1.打开页面时,由于此时URL上并没有微信授权生成的code,会执行getAuthCode(),跳转去微信授权页面获取code;

  • 2.此时调用getAuthCode(type)时不传type=user,该函数执行时会拼接微信授权URL参数UrlParam,包括设置微信授权成功后重定向链接参数 redirect_uri=jurl,其中jurl中的参数uinfo=0,并且微信授权类型参数 scope=snsapi_base(即只取微信ID,不取用户详细信息,这是避免已经授权的用户进入本页面需要重复手动授权,即使现在微信已经做了优化,在上次授权后的一段时间内再请求手动授权会自动授权,但是中间还是有个页面提示显示)。

  • 3.跳转到微信授权中转页面:http://game.163.com/weixin/redirector?+UrlParam 【步骤4】(若当前域名为game.163.com,则直接跳转到https://open.weixin.qq.com/connect/oauth2/authorize?+UrlParam 【步骤5】),此时UrlParam为: appid=wx85f583832dbd07e9&redirect_uri=" + jurl + "&response_type=code&scope=snsapi_base&state=163#wechat_redirect"; 注:redirect_uri=jurl为当前页面url设置为授权后的重定向页面,并将当前页面URL中的有效参数都带上,包括但是不限于uinfo(用于调用login接口时告知平台是否需要向微信获取用户详细信息)、mbshare(二次访问来源)、spreadtimes(传播深度)、c(页面渠道)、pagekey(用户个人页ID);

  • 4.跳转到中转页面http://game.163.com/weixin/redirector时重新组合 redirect_uri,再跳转到微信授权页面

  • 5.跳转到微信授权页面后,由于使用的是scope=snsapi_base的授权链接,会自动授权,不显示绿色授权界面,无需用户手动同意授权按钮,也不会有已经授权过的提示。自动授权成功后,会跳转到redirect_uri指定的uri(根据步骤3的情况跳转到中转页面【步骤6】或者项目页面【步骤7】)并在URL上加上生成的 微信授权临时凭据code参数;

  • 6.中转页面从url参数中拿到code和source_uri,并unescape(source_uri)后,跳转到source_uri并带上code参数

  • 7.微信授权成功重定向回原页面,此时是第二次访问页面,此时URL上已经带有code参数,同时获取URL上的uinfo参数,然后调用平台授权接口login,平台会先去微信获取access_token和用户的微信ID,并在后台数据库中根据取到的微信ID查询用户的相关数据(包括了用户手动授权后平台存储起来的昵称和头像),并返回给页面。若用户无平台数据则仅会返回用户的微信ID和access_token

  • 8.页面通过login接口拿到微信ID和access_token后,若login同时返回了用户平台数据,页面可根据项目需求显示当前用户的个人数据或个人页面等其他页面逻辑,授权流程结束。 若用户平台数据为空,则表示用户未手动授权。此时需要根据项目需求分两种情况处理:

    • A:页面需要获取用户详细的微信信息(头像,昵称等): 此时平台数据没有储存用户的微信信息,需要调用getAuthCode("user"),跳转到微信授权链接进行第二次授权,此时jurl中包含参数uinfo=1,并且微信授权类型参数 scope=snsapi_userinfo(即会跳转到有绿色的授权界面,需要用户手动点击同意授权)。此时的UrlParam参数为 "appid=wx85f583832dbd07e9&redirect_uri=" + jurl + "&response_type=code&scope=snsapi_userinfo&state=163#wechat_redirect"; 授权成功后,平台会去微信获取用户的详细微信信息并存储。然后重复第3-7步流程;
    • B:页面不需要用户详细的微信信息(头像,昵称等),授权流程结束
  • 9.授权结束后,会将平台返用的用户数据(微信信息及平台数据)传入回调函数callback (user);

注意:此处的access_token同时作为平台其他接口的session id。 例如下面接口中的token: /create_home?user_id=&token=&init_num=&callback= 输入参数 user_id:用户id,整数,必传 token:用户token,字符串,必传 init_num:文案ID,整数,必传 callback:JSONP回调函数名,字符串,选传

  • 7.拿到user信息后执行项目业务逻辑,不再一一详述。

6.调用示例

var _friendPagekey;//全局变量,用于存储用好友的个人页ID
var _interfaceHost="";//平台接口服务器
var _user;//全局变量,用于存储用户信息(微信信息及平台数据),例如:

//    {
//        mybid:'',//微信ID
//        nickname:'',//微信昵称
//        headimgurl:'',//微信头像
//        token:'',//微信access_token
//        page_key:''//用户参与页面之后生成的个人页key;
//    }
nie.define("Index",function(){
    var Authorize=nie.require("Authorize");
    Authorize.init({
        pageUrl:'http://game.163.com/weixin/test_ubee/',
        serverHost:_interfaceHost,
        callback:function(data){
            _user=data;
            $("#lb_nickname").html(_user.nickname);//显示昵称
            _friendPagekey= params(location.search, "pagekey");
        }
    });
})

7.微信授权项目的调试

由于授权只能在微信中运行,无法在浏览器中调试,所以授权项目的调试稍显繁琐。

  • 针对开发中需要在浏览器中调试业务逻辑可以使用以下方法

微信访问:http://game.163.com/weixin/test_ubee

Alt text

在页面中会显示授权成功后取到的当前用户数据,其中包括access_token和你的weixin_userid等微信信息,其中access_token在一段时间内固定不变且有效的。 直接复制data数据,在你的代码中跳过授权,直接用 data数据调试其他平台接口。 若项目有其他特殊数据需要调用,也可以参照此页面,单独将你的授权代码直接发到测试地址后在微信中浏览去获取你的用户数据。 调用代码示例:

var _friendPagekey;//全局变量,用于存储用好友的个人页ID
var _user;//全局变量,用于存储用户信息(微信信息及平台数据),例如:
var _test=true;//是否使用本地数据测试接口
var _interfaceHost="";//平台接口服务器
nie.define("Index",function(){
    var Authorize=nie.require("Authorize");
    function FreindCallback(data){
            _user=data;
            $("#lb_nickname").html(_user.nickname);//显示昵称
            _friendPagekey= params(location.search, "pagekey");
            ......
    }
    if(_test){//使用静态用户数据调试接口
        var data={//test_ubee页面拿到的用户信息
            mybid:'1234567896',//微信ID
            nickname:'我的微信名',//微信昵称
            headimgurl:'',//微信头像
            token:'',//微信access_token
            page_key:''//用户参与页面之后生成的个人页key;
        }
        FreindCallback(data)
    }else{//执行微信授权
        Authorize.init({
        pageUrl:'http://game.163.com/weixin/gfxm_yy/',
        serverHost:_interfaceHost,
        callback:function(data){
            FreindCallback(data)
        }
        });
    }

})
  • 在微信中调试、供QA、编辑、营销测试的方法 使用中转授权页的开发模式可以直接发布到测试地址测试。 若使用直接跳转微信授权的开发模式,可使用下面的方法进行测试: 在webpub发布工具中,将项目发布到预发布环境,并在手机网络设置代理。 代理服务器地址:webproxy.nie.netease.com 端口:8899 设置完后,用微信访问正式地址即可以在微信中像正式环境中一般浏览页面。 具体设置方式可以查看CC姐提供的note笔记: http://note.youdao.com/share/web/file.html?id=dcc0feed254abecd469b959ad0de7383&type=note

写在最后

此份文档在苏苏(苏秋宏)写的《移动端网站微信授权的处理方法》基础上,综合小军(黎军)提出的授权中转页模式的意见,整理得出。目的是为了降低大家上手授权项目的难度,降低开发成本。希望大家能仔细查阅并按照此文档开发授权项目,如在项目实施中有任何可完善的建议欢迎提出。