OAuth是Open Authorization的缩写,即开放授权。OAuth协议为用户资源的授权提供了一个安全的、开放而又简单的标准,即在第三方无需使用用户名和密码的情况下就可以申请到该用户的资源。常见的OAuth2.0授权场景有:QQ、微信、今日头条、抖音、微博、GitHub等。比如,使用QQ授权登录第三方网站的场景,这样就免去了用户注册账号的过程,提高了用户访问体验。.
一.OAuth2.0原理和网页授权
1.OAuth2.0原理
根据授权码模式,第一步获取code,第二步获取access_token,第三步携带access_token获取资源。每一步都是通过API实现的:
OAuth相关方法都在Senparc.Weixin.MP.AdvancedAPIs命名空间下的OAuthApi类中:
2.微信公众号网页授权
网页授权流程分为四步:
(1)获取code:引导用户进入授权页面同意授权
(2)获取access_token:通过code换取网页授权access_token
(3)刷新access_token:如果需要,开发者刷新网页授权来避免过期
(4)请求资源:通过网页授权access_token和openid获取用户基本信息
3.对UnionID的理解
当开发者拥有多个移动应用、网站应用和公众帐号时,用户账号是不同的。微信官方是通过微信开放平台来解决这个问题的,对同一个微信开放平台下的不同应用[比如,移动应用、网站应用和公众帐号等],UnionID是相同的,即是同一个账号。
说明:自己想要的另外一种方式是通过手机号码来识别同一个用户账号。
二.用Senparc实现OAuth2.0的过程
1.snsapi_userinfo和snsapi_base
snsapi_userinfo需要用户确认同意的,而snsapi_base是静态获取,用户无感知。需要说明的是特殊场景下的静默授权:对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使scope为snsapi_userinfo,也是静默授权,用户无感知。
2.登录入口
登录入口位于WeiXinMPSDK/Samples/MP/Senparc.Weixin.Sample.MP/Controllers/OAuth2Controller.cs文件中的Index()方法:
// 用户尝试进入的需要登录的页面
public ActionResult Index(string returnUrl)
{
ViewData["returnUrl"] = returnUrl;
//此页面引导用户点击授权
ViewData["UrlUserInfo"] = OAuthApi.GetAuthorizeUrl(appId, "http://fengling.nat300.top/oauth2/UserInfoCallback?returnUrl=" + returnUrl.UrlEncode(), null, OAuthScope.snsapi_userinfo);//snsapi_userinfo方式回调地址
ViewData["UrlBase"] = OAuthApi.GetAuthorizeUrl(appId, "http://fengling.nat300.top/oauth2/BaseCallback?returnUrl=" + returnUrl.UrlEncode(), null, OAuthScope.snsapi_base);//snsapi_base方式回调地址
return View();
}
其中,OAuthApi.GetAuthorizeUrl()方法的具体实现如下所示:
/// <summary>
/// 获取验证地址
/// </summary>
/// <param name="appId">公众号的唯一标识</param>
/// <param name="redirectUrl">授权后重定向的回调链接地址,请使用urlencode对链接进行处理</param>
/// <param name="state">重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节</param>
/// <param name="scope">应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)</param>
/// <param name="responseType">返回类型,请填写code(或保留默认)</param>
/// <param name="addConnectRedirect">加上后可以解决40029-invalid code的问题(测试中)</param>
/// <returns></returns>
public static string GetAuthorizeUrl(string appId, string redirectUrl, string state, OAuthScope scope, string responseType = "code", bool addConnectRedirect = true)
{
var url = string.Format("https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type={2}&scope={3}&state={4}{5}#wechat_redirect",
appId.AsUrlData(), redirectUrl.AsUrlData(), responseType.AsUrlData(), scope.ToString("g").AsUrlData(), state.AsUrlData(), addConnectRedirect ? "&connect_redirect=1" : "");
/* 这一步发送之后,客户会得到授权页面,无论同意或拒绝,都会返回redirectUrl页面。
* 如果用户同意授权,页面将跳转至redirect_uri/?code=CODE&state=STATE。这里的code用于换取access_token(和通用接口的access_token不通用)
* 若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数redirect_uri?state=STATE
*/
return url;
}
微信开发者工具中打开的页面:
3.OAuthScope.snsapi_userinfo方式回调
该方式位于WeiXinMPSDK/Samples/MP/Senparc.Weixin.Sample.MP/Controllers/OAuth2Controller.cs文件中的UserInfoCallback()方法:
/// <summary>
/// OAuthScope.snsapi_userinfo方式回调
/// </summary>
/// <param name="code"></param>
/// <param name="returnUrl">用户最初尝试进入的页面</param>
/// <returns></returns>
public ActionResult UserInfoCallback(string code, string returnUrl)
{
if (string.IsNullOrEmpty(code)) { return Content("您拒绝了授权!"); }
OAuthAccessTokenResult result = null;
//通过,用code换取access_token
try
{
result = OAuthApi.GetAccessToken(appId, appSecret, code);
}
catch (Exception ex)
{
return Content(ex.Message);
}
if (result.errcode != ReturnCode.请求成功)
{
return Content("错误:" + result.errmsg);
}
//下面2个数据也可以自己封装成一个类,储存在数据库中(建议结合缓存)
//如果可以确保安全,可以将access_token存入用户的cookie中,每一个人的access_token是不一样的
// HttpContext.Session.SetString("OAuthAccessTokenStartTime", SystemTime.Now.ToString());
// HttpContext.Session.SetString("OAuthAccessToken", result.ToJson());
//因为第一步选择的是OAuthScope.snsapi_userinfo,这里可以进一步获取用户详细信息
try
{
if (!string.IsNullOrEmpty(returnUrl)) { return Redirect(returnUrl); }
OAuthUserInfo userInfo = OAuthApi.GetUserInfo(result.access_token, result.openid);
return View(userInfo);
}
catch (ErrorJsonResultException ex)
{
return Content(ex.Message);
}
}
首先是获取到access_token,然后如果returnUrl为空,那么返回用户信息页面。获取access_token代码为OAuthApi.GetAccessToken(appId, appSecret, code);
:
/// <summary>
/// 获取AccessToken(OAuth专用)
/// </summary>
/// <param name="appId">公众号的唯一标识</param>
/// <param name="secret">公众号的appsecret</param>
/// <param name="code">code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。</param>
/// <param name="grantType">填写为authorization_code(请保持默认参数)</param>
/// <returns></returns>
public static OAuthAccessTokenResult GetAccessToken(string appId, string secret, string code, string grantType = "authorization_code")
{
var url = string.Format(Config.ApiMpHost + "/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type={3}", appId.AsUrlData(), secret.AsUrlData(), code.AsUrlData(), grantType.AsUrlData());
return CommonJsonSend.Send<OAuthAccessTokenResult>(null, url, null, CommonJsonSendType.GET);
}
返回用户信息页面代码为OAuthApi.GetUserInfo(result.access_token, result.openid)
:
/// <summary>
/// 获取用户基本信息
/// </summary>
/// <param name="oauthAccessToken">调用接口凭证(OAuth专用)</param>
/// <param name="openId">普通用户的标识,对当前公众号唯一</param>
/// <param name="lang">返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语</param>
/// <returns></returns>
public static OAuthUserInfo GetUserInfo(string oauthAccessToken, string openId, Language lang = Language.zh_CN)
{
var url = string.Format(Config.ApiMpHost + "/sns/userinfo?access_token={0}&openid={1}&lang={2}", oauthAccessToken.AsUrlData(), openId.AsUrlData(), lang.ToString("g").AsUrlData());
return CommonJsonSend.Send<OAuthUserInfo>(null, url, null, CommonJsonSendType.GET);
}
当点击"点击这里测试snsapi_userinfo"链接时:
这时显示弹出授权页面,点击"同意"按钮:
4.OAuthScope.snsapi_base方式回调
该方式位于WeiXinMPSDK/Samples/MP/Senparc.Weixin.Sample.MP/Controllers/OAuth2Controller.cs文件中的BaseCallback()方法:
/// <summary>
/// OAuthScope.snsapi_base方式回调
/// </summary>
/// <param name="code"></param>
/// <param name="returnUrl">用户最初尝试进入的页面</param>
/// <returns></returns>
public ActionResult BaseCallback(string code, string returnUrl)
{
try
{
if (string.IsNullOrEmpty(code)) { return Content("您拒绝了授权!"); }
// 通过,用code换取access_token
var result = OAuthApi.GetAccessToken(appId, appSecret, code);
if (result.errcode != ReturnCode.请求成功) { return Content("错误:" + result.errmsg); }
//下面2个数据也可以自己封装成一个类,储存在数据库中(建议结合缓存)
//如果可以确保安全,可以将access_token存入用户的cookie中,每一个人的access_token是不一样的
// HttpContext.Session.SetString("OAuthAccessTokenStartTime", SystemTime.Now.ToString());
// HttpContext.Session.SetString("OAuthAccessToken", result.ToJson());
//因为这里还不确定用户是否关注本微信,所以只能试探性地获取一下
OAuthUserInfo userInfo = null;
try
{
//已关注,可以得到详细信息
userInfo = OAuthApi.GetUserInfo(result.access_token, result.openid);
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
ViewData["ByBase"] = true;
return View("UserInfoCallback", userInfo);
}
catch (ErrorJsonResultException ex)
{
//未关注,只能授权,无法得到详细信息
//这里的ex.JsonResult可能为:"{\"errcode\":40003,\"errmsg\":\"invalid openid\"}"
return Content("用户已授权,授权Token:" + result, "text/html", Encoding.UTF8);
}
}
catch (Exception ex)
{
WeixinTrace.SendCustomLog("BaseCallback发生错误", ex.ToString());
return Content("发生错误:" + ex.ToString());
}
}
获取access_token和用户信息的方法与OAuthScope.snsapi_userinfo方式回调完全相同,不再介绍。当点击"点击这里测试snsapi_base"链接时:可见OAuthScope.snsapi_base方式回调是静默授权,并不需要显示弹出授权页面。