那些年,我们开发的接口之:QQ登录(OAuth2.0)
吴剑 2013-06-14
原创文章,转载必须注明出处:http://www.cnblogs.com/wujian
前言
开发这些年,做过很多类型的接口。有对接保险公司的;有对接电信运营商的;有对接支付平台的;还有对接各个大小公司五花八门的接口。
最早大家用URL参数(当然现在也一直在用,因为这个最方便最轻量,并且是HTTP协议的一部分,具有高通用性);后来很多公司选择用XML来封装大一点的数据,封装数据逻辑;再后来通过接口传递的数据越来越复杂,于是有了在XML之上封装的SOAP;直到近些年,随着前端技术占据越来越重要的地位,JSON甚至脱离了JAVASCRIPT渗透到服务器端;最后,还有像GOOGLE这种追求极限的公司愿意在看似毫无技术含量的接口技术上花费人力财力,好比他们开源的Google Protocol Buffers,在传输和解析性能上比XML提升了一个数量级。
想起星爷电影里有句台词:能力越大责任也就越大。希望中国互联网的几位大佬不要光想着挣光中国网民的钱,而更应该为中国互联网的基础设施、中国互联网的生态环境尽自己该尽的责任。如果你们实在不愿意为中国互联网贡献啥,那也请你们也不要破坏啥吧。
OK,扯远了,《那些年,我们开发的接口》会是一个系列文章,后面也会陆续补充和完善,目前包含的目录如下:
那些年,我们开发的接口之:微信
那些年,我们开发的接口之:新浪微博(OAuth2.0)
QQ登录演示地址:www.paotiao.com
关于QQ登录
3Q大战之后,腾讯终于摆出了姿态,现在脱口而出的就是Open、开放、合作、戴三块表、协和社会......
网站要用QQ登录,OK完全没问题!
什么?你们用了QQ登录还要自己登录?有我QQ登录你们的登录就免了吧,用户数据你们都可以不用存,我先给你们一部分凑合着用,以后也许我会给你们更多,但以后也许我也会停用QQ登录,你们自己看着办吧。
OAuth
OAuth(开放授权)是一个开放标准,由Twitter于2006年最早提出,得到各大网站广泛支持,如Google、Facebook、Microsoft等等。
它允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
关于OAuth的介绍:http://zh.wikipedia.org/wiki/OAuth
逻辑概述
准备工作
首先需要在QQ开放平台(http://open.qq.com/)上注册你的网站然后申请到APP ID与APP KEY,如下图所示:
临时令牌
临时令牌为一次性令牌,每次QQ登录第一步即申请一个临时令牌,同时它是一个异步接口(狭义),它的流程很单一,携带一堆参数,获取一个令牌。
请求:
using System; using System.IO; using System.Web; using System.Collections.Generic; namespace WuJian.OAuth.Qq.Authorization { /// <summary> /// 临时令牌(Authorization)请求 /// 注:临时令牌为一次性使用 /// 参考:http://wiki.open.qq.com/wiki/website/%E4%BD%BF%E7%94%A8Authorization_Code%E8%8E%B7%E5%8F%96Access_Token /// </summary> public class Request { #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="state"> /// client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。 /// 请务必严格按照流程检查用户与state参数状态的绑定。 /// </param> /// <param name="scopeArray"> /// 请求用户授权时向用户显示的可进行授权的列表。 /// 可填写的值是API文档中列出的接口,以及一些动作型的授权(目前仅有:do_like) /// 不传则默认请求对接口get_user_info进行授权。 /// 建议控制授权项的数量,只传入必要的接口名称,因为授权项越多,用户越可能拒绝进行任何授权。 ///</param> ///<param name="backURL">在回调地址中嵌入的backurl参数(传入前请进行UrlEncode)</param> public Request(string state, string[] scopeArray = null, string backURL = "") { this.mUrl = Config.AuthorizationURI; this.mClientID = Config.AppID; this.mRedirectURI = Config.RedirectURI + "?backurl=" + backURL; this.mState = state; //默认为 get_user_info if (scopeArray == null || scopeArray.Length == 0) this.Scope = new string[] { APIs.get_user_info.ToString() }; else this.mScope = scopeArray; } #endregion #region 请求参数 private string mUrl; private readonly string mResponseType = "code"; private string mClientID; private string mRedirectURI; private string mState; private string[] mScope; /// <summary> /// 接口请求地址(不包含参数) /// 如:https://graph.qq.com/oauth2.0/authorize /// </summary> public string Url { get { return this.mUrl; } set { this.mUrl = value; } } /// <summary> /// 授权类型,此值固定为“code”。 /// </summary> public string ResponseType { get { return this.mResponseType; } } /// <summary> /// 申请QQ登录成功后,分配给应用的appid。 /// </summary> public string ClientID { get { return this.mClientID; } set { this.mClientID = value; } } /// <summary> /// 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。 /// 注意需要将url进行URLEncode。 /// </summary> public string RedirectURI { get { return this.mRedirectURI; } set { this.mRedirectURI = value; } } /// <summary> /// client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。 /// 请务必严格按照流程检查用户与state参数状态的绑定。 /// </summary> public string State { get { return this.mState; } set { this.mState = value; } } /// <summary> /// 请求用户授权时向用户显示的可进行授权的列表。 /// 可填写的值是API文档中列出的接口,以及一些动作型的授权(目前仅有:do_like),如果要填写多个接口名称,请用逗号隔开。 /// 例如:scope=get_user_info,list_album,upload_pic,do_like /// 不传则默认请求对接口get_user_info进行授权。 /// 建议控制授权项的数量,只传入必要的接口名称,因为授权项越多,用户越可能拒绝进行任何授权。 /// </summary> public string[] Scope { get { return this.mScope; } set { this.mScope = value; } } #endregion #region 方法 /// <summary> /// 获取请求URL(包含参数) /// </summary> public string GetUrl() { return string.Format("{0}?response_type={1}&client_id={2}&redirect_uri={3}&state={4}&scope={5}", this.mUrl, this.mResponseType, this.mClientID, System.Web.HttpUtility.UrlEncode(this.mRedirectURI), this.mState, string.Join(",", this.mScope)); } /// <summary> /// 根据HttpContext获取Authorization.Response对象 /// 在回调中获取 /// </summary> /// <param name="context">HttpContext</param> /// <returns></returns> public static Authorization.Response GetResponse(HttpContext context) { Authorization.Response obj = null; if (context != null && context.Request.Params["code"] != null && context.Request.Params["state"] != null) { obj = new Authorization.Response(); obj.Code = context.Request.Params["code"]; obj.State = context.Request.Params["state"]; obj.BackURL = ""; if (context.Request.Params["backurl"] != null) obj.BackURL = WuJian.Common.Security.UrlDecode(context.Request.Params["backurl"]); //临时令牌日志 if (WuJian.OAuth.Qq.Config.Log) { string path = Path.Combine(WuJian.OAuth.Qq.Config.LogDir, "auth", DateTime.Now.ToString("yyyyMMdd") + ".txt"); Log.Write(path, "response", "code:" + obj.Code + "\r\nstate:" + obj.State + "\r\nbackurl:" + obj.BackURL); } } return obj; } #endregion }//end class }
响应:
using System; using System.Web; using System.Collections.Generic; namespace WuJian.OAuth.Qq.Authorization { /// <summary> /// 临时令牌(Authorization)响应 /// 注:成功时通过callback地址响应,失败时在QQ登录窗提示 /// 参考:http://wiki.open.qq.com/wiki/website/%E4%BD%BF%E7%94%A8Authorization_Code%E8%8E%B7%E5%8F%96Access_Token /// </summary> public class Response { private string mCode; private string mState; private string mBackURL; /// <summary> /// 临时令牌 /// 此code会在10分钟内过期 /// </summary> public string Code { get { return this.mCode; } set { this.mCode = value; } } /// <summary> /// 防止CSRF攻击,成功授权后原样带回Request中的state值 /// </summary> public string State { get { return this.mState; } set { this.mState = value; } } /// <summary> /// 非接口参数 /// UrlDecode /// </summary> public string BackURL { get { return this.mBackURL; } set { this.mBackURL = value; } } } }
访问令牌
打个简单的比喻,好比现在我们的大多数小区,一般有个由保安看守的大门,这像临时令牌。然后进了小区拿钥匙打开自家房间门,这是访问令牌。
请求:
using System; using System.IO; using System.Collections.Generic; namespace WuJian.OAuth.Qq.AccessToken { /// <summary> /// 访问令牌(Access Token)请求 /// 参考:http://wiki.open.qq.com/wiki/website/%E4%BD%BF%E7%94%A8Authorization_Code%E8%8E%B7%E5%8F%96Access_Token /// </summary> public class Request { #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="code">临时令牌:Authorization Code</param> public Request(string code) { this.mUrl = Config.TokenURI; this.mClientID = Config.AppID; this.mClientSecret = Config.AppKey; this.mCode = code; this.mRedirectURI = Config.RedirectURI; } #endregion #region 请求参数 private string mUrl; private readonly string mGrantType = "authorization_code"; private string mClientID; private string mClientSecret; private string mCode; private string mRedirectURI; /// <summary> /// 接口请求地址(不包含参数) /// 如:https://graph.qq.com/oauth2.0/token /// </summary> public string Url { get { return this.mUrl; } set { this.mUrl = value; } } /// <summary> /// 授权类型,此值固定为“authorization_code”。 /// </summary> public string GrantType { get { return this.mGrantType; } } /// <summary> /// 申请QQ登录成功后,分配给网站的appid。 /// </summary> public string ClientID { get { return this.mClientID; } set { this.mClientID = value; } } /// <summary> /// 申请QQ登录成功后,分配给网站的appkey。 /// </summary> public string ClientSecret { get { return this.mClientSecret; } set { this.mClientSecret = value; } } /// <summary> /// 上一步返回的authorization code。 /// 如果用户成功登录并授权,则会跳转到指定的回调地址,并在URL中带上Authorization Code。 /// 例如,回调地址为www.qq.com/my.php,则跳转到: /// http://www.qq.com/my.php?code=520DD95263C1CFEA087****** /// 注意此code会在10分钟内过期。 /// </summary> public string Code { get { return this.mCode; } set { this.mCode = value; } } /// <summary> /// 与上一步中传入的redirect_uri保持一致。 /// </summary> public string RedirectURI { get { return this.mRedirectURI; } set { this.mRedirectURI = value; } } #endregion #region 方法 /// <summary> /// 获取请求URL(包含参数) /// </summary> public string GetUrl() { return string.Format("{0}?grant_type={1}&client_id={2}&client_secret={3}&code={4}&redirect_uri={5}", this.mUrl, this.mGrantType, this.mClientID, this.mClientSecret, this.mCode, System.Web.HttpUtility.UrlEncode(this.mRedirectURI)); } /// <summary> /// 获取响应 /// </summary> public WuJian.OAuth.Qq.AccessToken.Response GetResponse() { WuJian.OAuth.Qq.AccessToken.Response response = null; //如:access_token=FE04************************CCE2&expires_in=7776000 string responseText = WuJian.Common.Http.Get(GetUrl()); //日志 if (WuJian.OAuth.Qq.Config.Log) { string path = Path.Combine(WuJian.OAuth.Qq.Config.LogDir, "token", DateTime.Now.ToString("yyyyMMdd") + ".txt"); //请求日志 Log.Write(path, "request", GetUrl()); //响应日志 Log.Write(path, "response", responseText); } //获取所有参数键值对 var parameterArray = WuJian.Common.Http.RequestQuery(responseText); if (parameterArray.Count > 0) { string accessToken = ""; string expiresIn = ""; foreach (var para in parameterArray) { if (para.Key == "access_token") { accessToken = para.Value; continue; } if (para.Key == "expires_in") { expiresIn = para.Value; continue; } } if (accessToken != "" && expiresIn != "") { response = new Response(); response.AccessToken = accessToken; response.ExpiresIn = int.Parse(expiresIn); } } return response; } #endregion }//end class }
响应:
using System; using System.Collections.Generic; namespace WuJian.OAuth.Qq.AccessToken { /// <summary> /// 访问令牌(Access Token)响应 /// 参考:http://wiki.open.qq.com/wiki/website/%E4%BD%BF%E7%94%A8Authorization_Code%E8%8E%B7%E5%8F%96Access_Token /// </summary> public class Response { private string mAccessToken; private int mExpiresIn; /// <summary> /// 授权令牌 /// </summary> public string AccessToken { get { return this.mAccessToken; } set { this.mAccessToken = value; } } /// <summary> /// 令牌有效期(单位:秒) /// </summary> public int ExpiresIn { get { return this.mExpiresIn; } set { this.mExpiresIn = value; } } }//end class }
OPENID
出于安全考虑,腾讯不会给第三方QQ号,而它给了一个与QQ号唯一相对应OPENID,在QQ登录中,这个OPENID用于识别唯一的QQ用户。
OPENID是OAUTH的核心思想,不管是QQ、新浪微博,还是Fackbook、Twitter,只要基于OAUTH,都会存在OPENID。
请求:
using System; using System.IO; using System.Collections.Generic; using LitJson; namespace WuJian.OAuth.Qq.OpenID { /// <summary> /// OPEN_ID请求 /// 参考:http://wiki.open.qq.com/wiki/website/%E8%8E%B7%E5%8F%96%E7%94%A8%E6%88%B7OpenID_OAuth2.0 /// </summary> public class Request { #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="accessToken">授权令牌</param> public Request(string accessToken) { this.mUrl = Config.OpenIdURI; this.mAccessToken = accessToken; } #endregion #region 请求参数 private string mUrl; private string mAccessToken; /// <summary> /// 接口请求地址(不包含参数) /// 如:https://graph.qq.com/oauth2.0/me /// </summary> public string Url { get { return this.mUrl; } set { this.mUrl = value; } } /// <summary> /// 授权令牌 /// </summary> public string AccessToken { get { return this.mAccessToken; } set { this.mAccessToken = value; } } #endregion /// <summary> /// 获取WEB请求URL /// </summary> public string GetUrl() { return string.Format("{0}?access_token={1}", this.mUrl, this.mAccessToken); } /// <summary> /// 获取响应 /// </summary> /// <returns></returns> public WuJian.OAuth.Qq.OpenID.Response GetResponse() { WuJian.OAuth.Qq.OpenID.Response response = null; string responseText = WuJian.Common.Http.Get(GetUrl()); //日志 if (WuJian.OAuth.Qq.Config.Log) { string path = Path.Combine(WuJian.OAuth.Qq.Config.LogDir, "openid", DateTime.Now.ToString("yyyyMMdd") + ".txt"); //请求日志 Log.Write(path, "request", GetUrl()); //响应日志 Log.Write(path, "response", responseText); } try { //过滤 int begin = responseText.IndexOf("{"); int end = responseText.LastIndexOf("}"); responseText = responseText.Substring(begin, end - begin + 1); //构造JSON对象 JsonData jd = JsonMapper.ToObject(responseText); if (jd != null) { response = new Response(); response.ClientID = (string)jd["client_id"]; response.OpenID = (string)jd["openid"]; } } catch { return null; } return response; } }//end class }
响应:
using System; using System.Collections.Generic; namespace WuJian.OAuth.Qq.OpenID { /// <summary> /// OpenID 响应 /// 参考:http://wiki.open.qq.com/wiki/website/%E8%8E%B7%E5%8F%96%E7%94%A8%E6%88%B7OpenID_OAuth2.0 /// </summary> public class Response { private string mClientID; private string mOpenID; public string ClientID { get { return this.mClientID; } set { this.mClientID = value; } } public string OpenID { get { return this.mOpenID; } set { this.mOpenID = value; } } }//end class }
API调用
拿到访问令牌和OPENID,我们就可以调用QQ的系列API了,此处列举了get_user_info,其它接口调用模式也完全相同,如下代码所示。
请求与响应:
using System; using System.IO; using System.Collections.Generic; using LitJson; namespace WuJian.OAuth.Qq.API { /// <summary> /// 请求 /// 参考:http://wiki.open.qq.com/wiki/website/get_user_info /// </summary> public class GetUserInfoRequest : APIRequestBase { #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="accessToken">授权凭证</param> /// <param name="openID">OPENID</param> public GetUserInfoRequest(string accessToken, string openID) { this.mUrl = Config.ApiGetUserInfoURI; this.mAccessToken = accessToken; this.mOauthConsumerKey = Config.AppID; this.mOpenID = openID; } #endregion #region 请求参数 private string mUrl; private string mAccessToken; private string mOauthConsumerKey; private string mOpenID; private readonly string mFormat = "json"; /// <summary> /// 接口请求地址(不包含参数) /// 如:https://graph.qq.com/user/get_user_info /// </summary> public override string Url { get { return this.mUrl; } set { this.mUrl = value; } } /// <summary> /// 授权凭证 /// 可通过使用Authorization_Code获取Access_Token 或来获取。 /// access_token有3个月有效期。 /// </summary> public override string AccessToken { get { return this.mAccessToken; } set { this.mAccessToken = value; } } /// <summary> /// 申请QQ登录成功后,分配给应用的appid /// </summary> public override string OauthConsumerKey { get { return this.mOauthConsumerKey; } set { this.mOauthConsumerKey = value; } } /// <summary> /// 用户的ID,与QQ号码一一对应。 /// 可通过调用https://graph.qq.com/oauth2.0/me?access_token=YOUR_ACCESS_TOKEN 来获取。 /// </summary> public override string OpenID { get { return this.mOpenID; } set { this.mOpenID = value; } } /// <summary> /// 固定值json /// </summary> public string Format { get { return this.mFormat; } } #endregion /// <summary> /// 获取请求URL(包含参数) /// 如:https://graph.qq.com/user/get_user_info?access_token=*************&oauth_consumer_key=12345&openid=****************&format=json /// </summary> public override string GetUrl() { return string.Format("{0}?access_token={1}&oauth_consumer_key={2}&openid={3}&format={4}", this.mUrl, this.mAccessToken, this.mOauthConsumerKey, this.mOpenID, this.mFormat); } /// <summary> /// 获取响应 /// </summary> /// <param name="errorCode">返回错误信息(暂返回null,未处理)</param> /// <returns></returns> public override APIResponseBase GetResponse(out ErrorCode errorCode) { string logPath = Path.Combine(WuJian.OAuth.Qq.Config.LogDir, "get_user_info", DateTime.Now.ToString("yyyyMMdd") + ".txt"); WuJian.OAuth.Qq.API.GetUserInfoResponse response = null; errorCode = null; //向接口发送请求并获取响应字符串 string responseText = WuJian.Common.Http.Get(GetUrl()); //日志 if (WuJian.OAuth.Qq.Config.Log) { //请求日志 Log.Write(logPath, "request", GetUrl()); //响应日志 Log.Write(logPath, "response", responseText); } try { //过滤 int begin = responseText.IndexOf("{"); int end = responseText.LastIndexOf("}"); responseText = responseText.Substring(begin, end - begin + 1); responseText = responseText.Replace("\\", ""); //构造JSON对象 JsonData jd = JsonMapper.ToObject(responseText); if (jd != null) { response = new GetUserInfoResponse(); response.Ret = (int)jd["ret"]; response.Msg = (string)jd["msg"]; response.Nickname = (string)jd["nickname"]; response.Figureurl = (string)jd["figureurl"]; response.Figureurl1 = (string)jd["figureurl_1"]; response.Figureurl2 = (string)jd["figureurl_2"]; response.FigureurlQQ1 = (string)jd["figureurl_qq_1"]; response.FigureurlQQ2 = (string)jd["figureurl_qq_2"]; response.Gender = (string)jd["gender"]; response.IsYellowVip = (string)jd["is_yellow_vip"]; response.Vip = (string)jd["vip"]; response.YellowVipLevel = (string)jd["yellow_vip_level"]; response.Level = (string)jd["level"]; response.IsYellowYearVip = (string)jd["is_yellow_year_vip"]; } } catch(Exception error) { //结果转换失败日志 Log.Write(logPath, "response convert", error.ToString()); return null; } return response; } } /// <summary> /// 响应 /// </summary> public class GetUserInfoResponse : APIResponseBase { private int mRet; private string mMsg; private string mNickname; private string mFigureurl; private string mFigureurl1; private string mFigureurl2; private string mFigureurlQQ1; private string mFigureurlQQ2; private string mGender; private string mIsYellowVip; private string mVip; private string mYellowVipLevel; private string mLevel; private string mIsYellowYearVip; /// <summary> /// 返回码 /// </summary> public int Ret { get { return this.mRet; } set { this.mRet = value; } } /// <summary> /// 如果ret小于0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 /// </summary> public string Msg{ get { return this.mMsg; } set { this.mMsg = value; } } /// <summary> /// 用户在QQ空间的昵称 /// </summary> public string Nickname { get { return this.mNickname; } set { this.mNickname = value; } } /// <summary> /// 大小为30×30像素的QQ空间头像URL。 /// </summary> public string Figureurl { get { return this.mFigureurl; } set { this.mFigureurl = value; } } /// <summary> /// 大小为50×50像素的QQ空间头像URL。 /// </summary> public string Figureurl1 { get { return this.mFigureurl1; } set { this.mFigureurl1 = value; } } /// <summary> /// 大小为100×100像素的QQ空间头像URL。 /// </summary> public string Figureurl2 { get { return this.mFigureurl2; } set { this.mFigureurl2 = value; } } /// <summary> /// 大小为40×40像素的QQ头像URL。 /// </summary> public string FigureurlQQ1 { get { return this.mFigureurlQQ1; } set { this.mFigureurlQQ1 = value; } } /// <summary> /// 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100x100的头像,但40x40像素则是一定会有。 /// </summary> public string FigureurlQQ2 { get { return this.mFigureurlQQ2; } set { this.mFigureurlQQ2 = value; } } /// <summary> /// 性别。 如果获取不到则默认返回"男" /// </summary> public string Gender { get { return this.mGender; } set { this.mGender = value; } } /// <summary> /// 标识用户是否为黄钻用户(0:不是;1:是)。 /// </summary> public string IsYellowVip { get { return this.mIsYellowVip; } set { this.mIsYellowVip = value; } } /// <summary> /// 标识用户是否为黄钻用户(0:不是;1:是) /// </summary> public string Vip { get { return this.mVip; } set { this.mVip = value; } } /// <summary> /// 黄钻等级 /// </summary> public string YellowVipLevel { get { return this.mYellowVipLevel; } set { this.mYellowVipLevel = value; } } /// <summary> /// 黄钻等级 /// </summary> public string Level { get { return this.mLevel; } set { this.mLevel = value; } } /// <summary> /// 标识是否为年费黄钻用户(0:不是; 1:是) /// </summary> public string IsYellowYearVip { get { return this.mIsYellowYearVip; } set { this.mIsYellowYearVip = value; } } } }
后记
本文详细介绍了基于OAUTH2.0的QQ登录原理和过程。同时将整个过程拆分为每个独立的单元并用代码进行了演示。可前往:www.paotiao.com 体验,希望给像我一样的小站站长带来便捷和帮助。
作者:吴剑
出处:http://www.cnblogs.com/wu-jian/
本文版权归作者和博客园共有,欢迎转载,但必需注明出处,并且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。