那些年,我們開發的介面之:QQ登入(OAuth2.0)
吳劍 2013-06-14
原創文章,轉載必須註明出處:http://www.cnblogs.com/wu-jian
前言
開發這些年,做過很多型別的介面。有對接保險公司的;有對接電信運營商的;有對接支付平臺的;還有對接各個大小公司五花八門的介面。
最早大家用URL引數(當然現在也一直在用,因為這個最方便最輕量,並且是HTTP協議的一部分,具有高通用性);後來很多公司選擇用XML來封裝大一點的資料,封裝資料邏輯;再後來通過介面傳遞的資料越來越複雜,於是有了在XML之上封裝的SOAP;直到近些年,隨著前端技術佔據越來越重要的地位,JSON甚至脫離了JAVASCRIPT滲透到伺服器端;最後,還有像GOOGLE這種追求極限的公司願意在看似毫無技術含量的介面技術上花費人力財力,好比他們開源的Google Protocol Buffers,在傳輸和解析效能上比XML提升了一個數量級。
想起星爺電影裡有句臺詞:能力越大責任也就越大。希望中國網際網路的幾位大佬不要光想著掙光中國網民的錢,而更應該為中國網際網路的基礎設施、中國網際網路的生態環境儘自己該盡的責任。如果你們實在不願意為中國網際網路貢獻啥,那也請你們也不要破壞啥吧。
OK,扯遠了,《那些年,我們開發的介面》會是一個系列文章,後面也會陸續補充和完善,目前包含的目錄如下:
那些年,我們開發的介面之:微信
那些年,我們開發的介面之:新浪微博(OAuth2.0)
QQ登入演示地址:www.paotiao.com
關於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/
本文版權歸作者和部落格園共有,歡迎轉載,但必需註明出處,並且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。