那些年,咱們開發的接口之:QQ登陸(OAuth2.0)php
吳劍 2013-06-14html
原創文章,轉載必須註明出處:http://www.cnblogs.com/wu-jian前端
前言web
開發這些年,作過不少類型的接口。有對接保險公司的;有對接電信運營商的;有對接支付平臺的;還有對接各個大小公司五花八門的接口。json
最先你們用URL參數(固然如今也一直在用,由於這個最方便最輕量,而且是HTTP協議的一部分,具備高通用性);後來不少公司選擇用XML來封裝大一點的數據,封裝數據邏輯;再後來經過接口傳遞的數據愈來愈複雜,因而有了在XML之上封裝的SOAP;直到近些年,隨着前端技術佔據愈來愈重要的地位,JSON甚至脫離了JAVASCRIPT滲透到服務器端;最後,還有像GOOGLE這種追求極限的公司願意在看似毫無技術含量的接口技術上花費人力財力,比如他們開源的Google Protocol Buffers,在傳輸和解析性能上比XML提高了一個數量級。安全
想起星爺電影裏有句臺詞:能力越大責任也就越大。但願中國互聯網的幾位大佬不要光想着掙光中國網民的錢,而更應該爲中國互聯網的基礎設施、中國互聯網的生態環境盡本身該盡的責任。若是大家實在不肯意爲中國互聯網貢獻啥,那也請大家也不要破壞啥吧。服務器
OK,扯遠了,《那些年,咱們開發的接口》會是一個系列文章,後面也會陸續補充和完善,目前包含的目錄以下:微信
那些年,咱們開發的接口之:QQ登陸(OAuth2.0)app
那些年,咱們開發的接口之:微信異步
那些年,咱們開發的接口之:新浪微博(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/本文版權歸做者和博客園共有,歡迎轉載,但必需註明出處,而且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。