目录
微信网页授权能力调整造成的问题
能力调整的内容和理由
原有运行方案
is_snapshotuser字段
改造原有方案
如何复现测试场景
小结
微信网页授权能力调整造成的问题
依附于第三方的开发,做为开发者经常会遇到第三方进行规范和开发的调整,如开发腾讯微信的相关应用。我所经历的如小程序隐私政策调整、信息备案调整、微信授权获取个人信息限制调整等。
最近我们的一些项目因为微信页面授权能力的调整出现了一些问题,对于新用户未经授权前,微信开发团队给出的输出是快照页,该页内所获取的openId等均为虚拟账号数据,并在屏幕下方非常不明显的显示“使用完整服务”,如下图所示:
此图即是微信给出的授权提示,也是我们折中的解决方案,图中所示的提示框源自己于我们通过携带的参数反馈给用户的提示,以引导用户点击下方的“使用完整服务”链接,并进行授权。
能力调整的内容和理由
微信团队给出的解释是当开发者在网页中在不规范使用发起 snsapi_userinfo 网页授权时,微信将默认打开网页快照页模式进行基础浏览。
微信网页授权规范
- 授权流程需引导清晰、准确:在申请获取用户信息的弹窗出现前,应该清晰、准确地告知用户获取信息的范围及获取信息的目的;
- 必要场景申请:在必须获取用户信息时才申请,而不是用户尚未了解服务前就强制弹窗。如使用医院挂号时才需要获取用户信息;
- 不强制登录:提供游客模式,供用户了解网页提供的基础服务,不强制用户允许网页获取用户信息后才能使用网页服务。
常见的微信网页授权不规范使用案例
- 强制登录:在用户打开网页时立即要求用户授权,用户拒绝后无法使用网页提供的服务;
- 违规收集个人信息:未在网页提前告知使用个人信息的目的、方式和范围;
- 非必要收集:非必要获取用户信息的网页,如文章、视频等,要求用户在浏览内容前登录;
- 差别对待微信用户:同样的网页在浏览器内可以无需登录直接访问,在微信内却要求用户先登录才可访问。
原有运行方案
微信OA2授权访问地址如下(示例url为C#字符串):
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx7964497eb8bad783&redirect_uri=https%3A//www.leadihr.com/weixin/oa2.aspx%3F&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect&connect_redirect = 1
重定向接收地址 OA2.ASPX程序 (C#版本)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml;
using System.Collections;
using System.Net;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LitJson;
using System.Data;
using System.Data.SqlClient;
using CosysJaneCommonAPI;
using System.Web.Script.Serialization;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
public partial class oa2 : System.Web.UI.Page
{
string Appid = "";
string appsecret = "";
string domain = "";
public class OAuth_Token
{
public OAuth_Token()
{
}
//access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
//expires_in access_token接口调用凭证超时时间,单位(秒)
//refresh_token 用户刷新access_token
//openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
//scope 用户授权的作用域,使用逗号(,)分隔
public string access_token { get; set; }
public string expires_in { get; set; }
public string refresh_token { get; set; }
public string openid { get; set; }
public string scope { get; set; }
}
public class OAuthUser
{
public OAuthUser()
{ }
#region 数据库字段
private string _openID;
private string _searchText;
private string _unionid;
private string _nickname;
private string _sex;
private string _province;
private string _city;
private string _country;
private string _headimgUrl;
// private string _privilege;
#endregion
#region 字段属性
/// <summary>
/// 用户的唯一标识
/// </summary>
public string openid
{
set { _openID = value; }
get { return _openID; }
}
public string SearchText
{
set { _searchText = value; }
get { return _searchText; }
}
/// <summary>
/// 用户昵称
/// </summary>
public string nickname
{
set { _nickname = value; }
get { return _nickname; }
}
public string unionid
{
set { _unionid = value; }
get { return _unionid; }
}
/// <summary>
/// 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
/// </summary>
public string sex
{
set { _sex = value; }
get { return _sex; }
}
/// <summary>
/// 用户个人资料填写的省份
/// </summary>
public string province
{
set { _province = value; }
get { return _province; }
}
/// <summary>
/// 普通用户个人资料填写的城市
/// </summary>
public string city
{
set { _city = value; }
get { return _city; }
}
/// <summary>
/// 国家,如中国为CN
/// </summary>
public string country
{
set { _country = value; }
get { return _country; }
}
/// <summary>
/// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
/// </summary>
public string headimgurl
{
set { _headimgUrl = value; }
get { return _headimgUrl; }
}
/// <summary>
/// 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)其实这个格式称不上JSON,只是个单纯数组
/// </summary>
//public string privilege
//{
// set { _privilege = value; }
// get { return _privilege; }
//}
#endregion
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
if (!string.IsNullOrEmpty(Request.QueryString["code"]))
{
string Code = Request.QueryString["code"].ToString();
string State = Request.QueryString["state"].ToString();
//获得Token
OAuth_Token Model = Get_token(Code);
OAuthUser OAuthUser_Model = Get_UserInfo(Model.access_token, Model.openid);
string content=Model.access_token+ "用户OPENID:" + OAuthUser_Model.openid + "<br>用户昵称:" + OAuthUser_Model.nickname + "<br>性别:" + OAuthUser_Model.sex + "<br>所在省:" + OAuthUser_Model.province + "<br>所在市:" + OAuthUser_Model.city + "<br>所在国家:" + OAuthUser_Model.country + "<br>头像地址:" + OAuthUser_Model.headimgurl + "<br>用户特权信息:";
Response.Redirect("https://x.x.com/index.aspx?&oid=" + OAuthUser_Model.openid);
}
}
}
public class JsonHelper
{
/// <summary>
/// 生成Json格式
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static string GetJson<T>(T obj)
{
DataContractJsonSerializer json = new DataContractJsonSerializer(obj.GetType());
using (MemoryStream stream = new MemoryStream())
{
json.WriteObject(stream, obj);
string szJson = Encoding.UTF8.GetString(stream.ToArray()); return szJson;
}
}
/// <summary>
/// 获取Json的Model
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="szJson"></param>
/// <returns></returns>
public static T ParseFromJson<T>(string szJson)
{
T obj = Activator.CreateInstance<T>();
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(szJson)))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
return (T)serializer.ReadObject(ms);
}
}
}
//获得Token
protected OAuth_Token Get_token(string Code)
{
string Str = GetJson("https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + Appid + "&secret=" + appsecret + "&code=" + Code + "&grant_type=authorization_code");
OAuth_Token Oauth_Token_Model = JsonHelper.ParseFromJson<OAuth_Token>(Str);
return Oauth_Token_Model;
}
//刷新Token
protected OAuth_Token refresh_token(string REFRESH_TOKEN)
{
string Str = GetJson("https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=" + Appid + "&grant_type=refresh_token&refresh_token=" + REFRESH_TOKEN);
OAuth_Token Oauth_Token_Model = JsonHelper.ParseFromJson<OAuth_Token>(Str);
return Oauth_Token_Model;
}
//获得用户信息
protected OAuthUser Get_UserInfo(string REFRESH_TOKEN, string OPENID)
{
// Response.Write("获得用户信息REFRESH_TOKEN:" + REFRESH_TOKEN + "||OPENID:" + OPENID);
string Str = GetJson("https://api.weixin.qq.com/sns/userinfo?access_token=" + REFRESH_TOKEN + "&openid=" + OPENID + "&lang=zh_CN");
OAuthUser OAuthUser_Model = JsonHelper.ParseFromJson<OAuthUser>(Str);
return OAuthUser_Model;
}
protected string GetJson(string url)
{
WebClient wc = new WebClient();
wc.Credentials = CredentialCache.DefaultCredentials;
wc.Encoding = Encoding.UTF8;
string returnText = "";
try
{
returnText = wc.DownloadString(url);
}
catch (Exception e)
{
Response.Write(e.Message);
Response.End();
}
if (returnText.Contains("errcode"))
{
//可能发生错误
}
//Response.Write(returnText);
return returnText;
}
}
is_snapshotuser字段
通过code换取网页授权access_token
请求方法是获取code后,请求以下链接获取access_token:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
正确时会返回具有如下图所示的JSON数据包:
因此可能通过判断 is_snapshotuser 字段是否为1,判断是否快照页模式
改造原有方案
主要是增加 string is_snapshotuser = "0" 和后续对JSON返回值的判断,并返回回调的url并携带此参数。示例代码如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml;
using System.Collections;
using System.Net;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LitJson;
using System.Data;
using System.Data.SqlClient;
using CosysJaneCommonAPI;
using System.Web.Script.Serialization;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
public partial class oa2 : System.Web.UI.Page
{
string Appid = "";
string appsecret = "";
string domain = "";
string is_snapshotuser = "0";
public class OAuth_Token
{
public OAuth_Token()
{
}
//access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
//expires_in access_token接口调用凭证超时时间,单位(秒)
//refresh_token 用户刷新access_token
//openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
//scope 用户授权的作用域,使用逗号(,)分隔
public string access_token { get; set; }
public string expires_in { get; set; }
public string refresh_token { get; set; }
public string openid { get; set; }
public string scope { get; set; }
}
public class OAuthUser
{
public OAuthUser()
{ }
#region 数据库字段
private string _openID;
private string _searchText;
private string _unionid;
private string _nickname;
private string _sex;
private string _province;
private string _city;
private string _country;
private string _headimgUrl;
// private string _privilege;
#endregion
#region 字段属性
/// <summary>
/// 用户的唯一标识
/// </summary>
public string openid
{
set { _openID = value; }
get { return _openID; }
}
public string SearchText
{
set { _searchText = value; }
get { return _searchText; }
}
/// <summary>
/// 用户昵称
/// </summary>
public string nickname
{
set { _nickname = value; }
get { return _nickname; }
}
public string unionid
{
set { _unionid = value; }
get { return _unionid; }
}
/// <summary>
/// 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
/// </summary>
public string sex
{
set { _sex = value; }
get { return _sex; }
}
/// <summary>
/// 用户个人资料填写的省份
/// </summary>
public string province
{
set { _province = value; }
get { return _province; }
}
/// <summary>
/// 普通用户个人资料填写的城市
/// </summary>
public string city
{
set { _city = value; }
get { return _city; }
}
/// <summary>
/// 国家,如中国为CN
/// </summary>
public string country
{
set { _country = value; }
get { return _country; }
}
/// <summary>
/// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
/// </summary>
public string headimgurl
{
set { _headimgUrl = value; }
get { return _headimgUrl; }
}
/// <summary>
/// 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)其实这个格式称不上JSON,只是个单纯数组
/// </summary>
//public string privilege
//{
// set { _privilege = value; }
// get { return _privilege; }
//}
#endregion
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
if (!string.IsNullOrEmpty(Request.QueryString["code"]))
{
string Code = Request.QueryString["code"].ToString();
string State = Request.QueryString["state"].ToString();
//获得Token
OAuth_Token Model = Get_token(Code);
OAuthUser OAuthUser_Model = Get_UserInfo(Model.access_token, Model.openid);
string content=Model.access_token+ "用户OPENID:" + OAuthUser_Model.openid + "<br>用户昵称:" + OAuthUser_Model.nickname + "<br>性别:" + OAuthUser_Model.sex + "<br>所在省:" + OAuthUser_Model.province + "<br>所在市:" + OAuthUser_Model.city + "<br>所在国家:" + OAuthUser_Model.country + "<br>头像地址:" + OAuthUser_Model.headimgurl + "<br>用户特权信息:";
Response.Redirect("https://x.x.com/index.aspx?&oid=" + OAuthUser_Model.openid+"&is_snapshotuser="+is_snapshotuser);
}
}
}
public class JsonHelper
{
/// <summary>
/// 生成Json格式
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static string GetJson<T>(T obj)
{
DataContractJsonSerializer json = new DataContractJsonSerializer(obj.GetType());
using (MemoryStream stream = new MemoryStream())
{
json.WriteObject(stream, obj);
string szJson = Encoding.UTF8.GetString(stream.ToArray()); return szJson;
}
}
/// <summary>
/// 获取Json的Model
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="szJson"></param>
/// <returns></returns>
public static T ParseFromJson<T>(string szJson)
{
T obj = Activator.CreateInstance<T>();
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(szJson)))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
return (T)serializer.ReadObject(ms);
}
}
}
//获得Token
protected OAuth_Token Get_token(string Code)
{
string Str = GetJson("https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + Appid + "&secret=" + appsecret + "&code=" + Code + "&grant_type=authorization_code");
if (Str.IndexOf("\"is_snapshotuser\":1") != -1||Str.IndexOf("\"is_snapshotuser\": 1")!=-1)
{
is_snapshotuser = "1";
}
OAuth_Token Oauth_Token_Model = JsonHelper.ParseFromJson<OAuth_Token>(Str);
return Oauth_Token_Model;
}
//刷新Token
protected OAuth_Token refresh_token(string REFRESH_TOKEN)
{
string Str = GetJson("https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=" + Appid + "&grant_type=refresh_token&refresh_token=" + REFRESH_TOKEN);
OAuth_Token Oauth_Token_Model = JsonHelper.ParseFromJson<OAuth_Token>(Str);
return Oauth_Token_Model;
}
//获得用户信息
protected OAuthUser Get_UserInfo(string REFRESH_TOKEN, string OPENID)
{
// Response.Write("获得用户信息REFRESH_TOKEN:" + REFRESH_TOKEN + "||OPENID:" + OPENID);
string Str = GetJson("https://api.weixin.qq.com/sns/userinfo?access_token=" + REFRESH_TOKEN + "&openid=" + OPENID + "&lang=zh_CN");
OAuthUser OAuthUser_Model = JsonHelper.ParseFromJson<OAuthUser>(Str);
return OAuthUser_Model;
}
protected string GetJson(string url)
{
WebClient wc = new WebClient();
wc.Credentials = CredentialCache.DefaultCredentials;
wc.Encoding = Encoding.UTF8;
string returnText = "";
try
{
returnText = wc.DownloadString(url);
}
catch (Exception e)
{
Response.Write(e.Message);
Response.End();
}
if (returnText.Contains("errcode"))
{
//可能发生错误
}
//Response.Write(returnText);
return returnText;
}
}
这样可以在业务页面,如上述代码中的index.aspx进行如下判断:
if (Request.QueryString["is_snapshotuser"] == "1")
{
Layer.open("使用前微信要求您的授权,请点击下方使用完整服务后继续...", "'确定'", "info");
return;
}
如何复现测试场景
已经授权的用户,如果想测试重新授权的场景,请打开微信,依如下步骤进行设置:
小结
以上示例是一种较小改动的解决方案,个人比较习惯于应用程序稳定性第一的思路。如果已经使用新规则设计方案则可仅供参考。
另外在此介绍一下关于网页授权的两种scope的区别:
1、以snsapi_base为scope发起的网页授权,可以直接获取进入页面的用户的openid,且是静默授权并自动跳转到业务页面。
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。需要用户手动同意,无须关注,就可在授权后获取该用户的基本信息。
以上是个人的一些观点和解决方案,感谢阅读,并提出指正。