OAuth認證協議原理分析及同步消息到Twitter和Facebook使用方法

OAuth有什麼用?爲何要使用OAuth?php

twitter或豆瓣用戶必定會發現,有時候,在別的網站,點登陸後轉到 twitter登陸,以後轉回原網站,你會發現你已經登陸此網站了,這種網站就是這個效果。其實這都是拜 OAuth所賜。html

OAuth(開放受權)是一個開放標準,容許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。程序員

OAuth 協議爲用戶資源的受權提供了一個安全的、開放而又簡易的標準。與以往的受權方式不一樣之處是OAUTH的受權不會使第三方觸及到用戶的賬號信息(如用戶名與密 碼),即第三方無需使用用戶的用戶名與密碼就能夠申請得到該用戶資源的受權,所以OAUTH是安全的。同時,任何第三方均可以使用OAUTH認證服務,任 何服務提供商均可以實現自身的OAUTH認證服務,於是OAUTH是開放的。業界提供了OAUTH的多種實現如PHP,JavaScript,Java,Ruby等各類語言開發包,大大節約了程序員的時間,於是OAUTH是簡易的。目前互聯網不少服務如Open API,不少大頭公司如Google,Yahoo,Microsoft等都提供了OAUTH認證服務,這些都足以說明OAUTH標準逐漸成爲開放資源受權的標準。web

認證和受權過程ajax

在認證和受權的過程當中涉及的三方包括:api

  • 服務提供方,用戶使用服務提供方來存儲受保護的資源,如照片,視頻,聯繫人列表(如twitter角色)
  • 用戶 ,存放在服務提供方的受保護的資源的擁有者。
  • Consumer ,要訪問服務提供方資源的第三方應用(中間商,相似上面的 twitterfeed 角色)。在認證過程以前,客戶端要向服務提供者申請客戶端標識。


 注意到用戶提交密碼是在第四步 而第三方Consumer從頭到尾沒有得到帳號信息 瀏覽器

Oauth的流程 官方的說明圖以下:安全

 使用OAuth進行認證和受權的過程以下所示:服務器

    1. 用戶訪問客戶端的網站,想操做本身存放在服務提供方的資源。
    2. 客戶端服務提供方請求一個臨時令牌。
    3. 服務提供方驗證客戶端的身份後,授予一個臨時令牌。
    4. 客戶端得到臨時令牌後,將用戶引導至服務提供方的受權頁面請求用戶受權。在這個過程當中將臨時令牌和客戶端的回調鏈接發送給服務提供方
    5. 用戶服務提供方的網頁上輸入用戶名和密碼,而後受權該客戶端訪問所請求的資源。
    6. 受權成功後,服務提供方引導用戶返回客戶端的網頁。
    7. 客戶端根據臨時令牌從服務提供方那裏獲取訪問令牌 。
    8. 服務提供方根據臨時令牌和用戶的受權狀況授予客戶端訪問令牌。
    9. 客戶端使用獲取的訪問令牌訪問存放在服務提供方上的受保護的資源。

一,Consumer 向 服務提供商 申請接入權限

可獲得:Consumer Key,Consumer Secret。twitter申請oauth的話,在 setting - connection - developer 裏面申請。 同時給出三個訪問網址:cookie

  1. request_token_url = 'http://twitter.com/oauth/request_token'
  2. access_token_url = 'http://twitter.com/oauth/access_token'
  3. authorize_url = 'http://twitter.com/oauth/authorize'

二,當Consumer接到用戶請求想要訪問第三方資源(如twitter)的時候

Consumer須要先取得 請求另牌(Request Token)。網址爲上面的 request_token_url,參數爲:

  1. oauth_consumer_key:Consumer Key
  2. oauth_signature_method:簽名加密方法
  3. oauth_signature:加密的簽名 (這個下面細說)
  4. oauth_timestamp:UNIX時間戳
  5. oauth_nonce:一個隨機的混淆字符串,隨機生成一個。
  6. oauth_version:OAuth版本,可選,若是設置的話,必定設置爲 1.0
  7. oauth_callback:返回網址連接。
  8. 及其它服務提供商定義的參數

這樣 Consumer就取得了 請求另牌(包括另牌名 oauth_token,另牌密鑰 oauth_token_secret。

三,瀏覽器自動轉向服務提供商的網站:

網址爲authorize_url?oauth_token=請求另牌名

四,用戶贊成 Consumer訪問 服務提供商資源

那麼會自動轉回上面的 oauth_callback 裏定義的網址。同時加上 oauth_token (就是請求另牌),及oauth_verifier(驗證碼)。

五,如今總能夠開始請求資源了吧?

NO。如今還須要再向 服務提供商 請求 訪問另牌(Access Token)。網址爲上面的access_token_url,參數爲: 

  1. oauth_consumer_key:Consumer Key
  2. oauth_token:上面取得的 請求另牌的名
  3. oauth_signature_method:簽名加密方法
  4. oauth_signature:加密的簽名 (這個下面細說)
  5. oauth_timestamp:UNIX時間戳
  6. oauth_nonce:一個隨機的混淆字符串,隨機生成一個。
  7. oauth_version:OAuth版本,可選,若是設置的話,必定設置爲 1.0
  8. oauth_verifier:上面返回的驗證碼。
  9. 請求 訪問另牌的時候,不能加其它參數。 

這樣就能夠取得 訪問另牌(包括Access Token 及 Access Token Secret)。這個就是須要保存在 Consumer上面的信息(沒有你的真實用戶名,密碼,安全吧!)

六,取得 訪問另牌 後

Consumer就能夠做爲用戶的身份訪問 服務提供商上被保護的資源了。提交的參數以下: 

    1. oauth_consumer_key:Consumer Key
    2. oauth_token:訪問另牌
    3. oauth_signature_method:簽名加密方法
    4. oauth_signature:加密的簽名 (這個下面細說)
    5. oauth_timestamp:UNIX時間戳
    6. oauth_nonce:一個隨機的混淆字符串,隨機生成一個。
    7. oauth_version:OAuth版本,可選,若是設置的話,必定設置爲 1.0
    8. 及其它服務提供商定義的參數

OAuth2.0 

OAuth2.0和OAuth1.0的區別仍是在於簡化了認證過程,不須要從未受權的Request Token轉化到受權Request Token,而是利用app key經過用戶受權生成access token可是,與1.0的不一樣之處是access token有自身的有效期,且不一樣平臺、不一樣級別的程序有着不一樣的有效期,在程序開發中必定記得判斷access token是否過時,對於過時以後的處理方法主要是利用access token和refresh token從新生成access token或者從新利用app key向服務器發送請求生成access token。因爲這個問題,與OAuth1.0基本一致不同,各個平臺OAuth2.0作了不同的選擇。

  OAuth2.0服務支持如下獲取Access Token的方式:

  a. Authorization Code:Web Server Flow,適用於全部有Server端配合的應用
  b. Implicit Grant:User-Agent Flow,適用於全部無Server端配合的應用

  由於demo是無服務器的程式,因此咱們採用Implicit Grant:User-Agent Flow的獲取方式。

                         示意圖(來自騰訊微博開發文檔)

   

  

獲取Access Token  

  爲了獲取Access Token,應用須要將用戶瀏覽器(或手機/桌面應用中的瀏覽器組件)到OAuth2.0受權服務的「http://xxxxxxxxx/authorize」地址上,並帶上如下參數:

參數名 必選 介紹
client_id true 申請組件時得到的API Key
response_type true 此值固定爲「token」
redirect_uri true 受權後要回調的URI,即接受code的URI。對於無Web Server的應用,其值能夠是「oob」。
scope false 以空格分隔的權限列表,若不傳遞此參數,表明請求默認的basic權限。如需調用擴展權限,必需傳遞此參數
state false 用於保持請求和回調的狀態,受權服務器在回調時(重定向用戶瀏覽器到「redirect_uri」時),會在Query Parameter中原樣回傳該參數
display false 登陸和受權頁面的展示樣式,默認爲「page」。手機訪問時,此參數無效
client false 是否爲手機訪問。手機訪問:client=1;不是手機,無需次參數

若用戶登陸並接受受權,受權服務將重定向用戶瀏覽器到「redirect_uri」,並在Fragment中追加以下參數:

參數名 介紹
access_token 要獲取的Access Token
expires_in Access Token的有效期,以秒爲單位
refresh_token

用於刷新Access Token 的 Refresh Token,一些平臺不返回這個參數,須要程序員進行判斷處理

scope

Access Token最終的訪問範圍,即用戶實際授予的權限列表

state 若是請求獲取Access Token時帶有state參數,則將該參數原樣返回

同步消息到Facebook

一、首先是到登陸地址 https://login.facebook.com/login.php?login_attempt=1 嘛,存儲好 cookies 而且從頁面解析出一個 lsd 的參數,而後再次向此地址提交登陸參數,包括 charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%8四、return_session=0、legacy_return=一、display=、session_key_only=0、trynum=一、emailpass、persistent=一、login=%E7%99%BB%E5%BD%9五、lsd

二、在登陸成功以後保存 cookies 轉向 http://www.facebook.com/home.php ,而後今後頁面解析出 profile_id、 composer_id、post_form_id 、fb_dtsg 4個參數出來。

三、最後一步向 http://www.facebook.com/ajax/updatestatus.php?__a=1 提交狀態信息,須要包括如下參數 action=HOME_UPDATE、home_tab_id=一、profile_idstatus、target_id=0、app_id、privacy_data[value]=80、privacy_data[friends]=0、privacy_data[list_anon]=0、privacy_data[list_x_anon]=0、composer_id、composer_id、hey_kid_im_a_composer=true、display_context=home、post_form_idfb_dtsglsd、_log_display_context=home、ajax_log=一、post_form_id_source=AsyncRequest 。

 如下的程序段呢,若是出問題還請參照一下之前的幾篇文章,好比使用代理地址,好比訪問 https 須要注意的地方等。

private void update_facebook(string user, string password, string message)
       {
          string service = "Facebook";
          this.BeginInvoke(new UpdateStatusDelegate(StateInfo), new object[] { "正在將消息發佈到 " + service });
          CookieContainer cc = new CookieContainer();
          string lsd = "";
          Uri uri = new Uri("https://login.facebook.com/login.php?login_attempt=1");
          HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
          request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
          request.ContentType = "application/x-www-form-urlencoded";
          request.Method = "GET";
          request.CookieContainer = cc;
          if (!string.IsNullOrEmpty(proxyserver))
          {
              request.Proxy = new WebProxy(proxyserver);
              ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);
          }
          try
          {
              using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
              {
                  foreach (Cookie cookie in response.Cookies)
                  {
                      cc.Add(cookie);
                  }
                  Stream responseStream = response.GetResponseStream();
                  StreamReader reader = new StreamReader(responseStream, Encoding.GetEncoding("UTF-8"));
                  string result = reader.ReadToEnd();
                  Match re = Regex.Match(result, @"name=\u0022lsd\u0022\svalue=\u0022([^\u0022]*)\u0022()");
                 lsd = re.Groups[1].ToString();
              }
           }
          catch (Exception ex)
          {
              ErrorMessage("獲取登陸到 "+ service +" 必要參數時出現意外:\r\n"+ex.Message);
              return;
          }
          StringBuilder postData = new StringBuilder();
          postData.Append("charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84");
          postData.Append("&lsd=" + lsd + "&return_session=0&legacy_return=1&display=&session_key_only=0&trynum=1");
          postData.Append("&email=" + Utility.UrlEncode(user) + "&pass=" + Utility.UrlEncode(password) + "&persistent=1&login=%E7%99%BB%E5%BD%95");
          byte[] bs = Encoding.UTF8.GetBytes(postData.ToString());
          request = (HttpWebRequest)HttpWebRequest.Create(uri);
          request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
          request.ContentType = "application/x-www-form-urlencoded";
          request.Method = "POST";
          request.ContentLength = bs.Length;
          request.CookieContainer = cc;
          request.AllowAutoRedirect = true;
          if (!string.IsNullOrEmpty(proxyserver))
          {
              request.Proxy = new WebProxy(proxyserver);
          }
          try
          {
              using (Stream requestStream = request.GetRequestStream())
              {
                  requestStream.Write(bs, 0, bs.Length);
                  requestStream.Close();
              }
              using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
              {
                  foreach (Cookie cookie in response.Cookies)
                  {
                      cc.Add(cookie);
                  }
                  response.Close();
              }
          }
          catch (Exception ex)
          {
              ErrorMessage("登陸到 " + service + " 時出現意外:\n"+ex.Message);
              return;
          }
          uri = new Uri("http://www.facebook.com/home.php");
          request = (HttpWebRequest)HttpWebRequest.Create(uri);
          request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
          request.ContentType = "application/x-www-form-urlencoded";
          request.Method = "GET";
          request.CookieContainer = cc;
          if (!string.IsNullOrEmpty(proxyserver))
          {
              request.Proxy = new WebProxy(proxyserver);
          }
          string profile_id = "", composer_id = "", post_form_id = "", fb_dtsg = "";
          try
          {
              using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
              {
                  foreach (Cookie cookie in response.Cookies)
                  {
                      cc.Add(cookie);
                  }   
                  Stream responseStream = response.GetResponseStream();
                  StreamReader reader = new StreamReader(responseStream, Encoding.GetEncoding("UTF-8"));
                  string result = reader.ReadToEnd();
                  Match re = Regex.Match(result, @"name=\u0022profile_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
                  profile_id = re.Groups[1].ToString();
                  re = Regex.Match(result, @"name=\u0022composer_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
                  composer_id = re.Groups[1].ToString();
                  re = Regex.Match(result, @"name=\u0022post_form_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
                  post_form_id = re.Groups[1].ToString();
                  re = Regex.Match(result, @"name=\u0022fb_dtsg\u0022\svalue=\u0022([^\u0022]*)\u0022()");
                  fb_dtsg = re.Groups[1].ToString();
                  response.Close();
              }
          }
          catch (Exception ex)
          {
              ErrorMessage("重定向facebook頁面並獲取發佈消息所需參數時出現意外:\n"+ex.Message);
              return;
          }
          postData = new StringBuilder();
           postData.Append("action=HOME_UPDATE&home_tab_id=1&profile_id=" + profile_id);
          postData.Append("&status=" + Utility.UrlEncode(message) + "&target_id=0&app_id=");
           postData.Append("&privacy_data[value]=80&privacy_data[friends]=0&&&&&privacy_data[list_anon]=0&&privacy_data[list_x_anon]=0");
          postData.Append("&&composer_id=" + composer_id + "&hey_kid_im_a_composer=true&display_context=home");
          postData.Append("&post_form_id=" + post_form_id + "&fb_dtsg=" + fb_dtsg+"&lsd="+lsd);
          postData.Append("&_log_display_context=home&ajax_log=1&post_form_id_source=AsyncRequest");
          bs = Encoding.UTF8.GetBytes(postData.ToString());
          uri = new Uri("http://www.facebook.com/ajax/updatestatus.php?__a=1");
          request = (HttpWebRequest)HttpWebRequest.Create(uri);
          request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
          request.ContentType = "application/x-www-form-urlencoded";
           request.Method = "POST";
          request.CookieContainer = cc;
          request.ContentLength = bs.Length;
          if (!string.IsNullOrEmpty(proxyserver))
          {
              request.Proxy = new WebProxy(proxyserver);
          }
          try
          {
              using (Stream requestStream = request.GetRequestStream())
              {
                  requestStream.Write(bs, 0, bs.Length);
                  requestStream.Close();
              }
           }
          catch (Exception ex)
          {
              ErrorMessage("發佈消息到 facebook 時出現意外:\n"+ex.Message);
              return;
          }
      }
Code

同步消息到Twitter

Twitter 更新消息時注意加入 postBody 參數就能夠叻,更多細節能夠對照 」簡版 OAuthr 認證 for C#

/// <summary>/// OAuth 認證更新twitter消息
/// </summary>
/// <param name="consumer_key">應用的consumer_key</param>
/// <param name="consumer_secret">應用的consumer_secret</param>
/// <param name="oauth_token">應用的access_key</param>
/// <param name="oauth_token_secret">應用的access_secret</param>
/// <param name="message">發送的消息</param>
/// <param name="request_path">請求的API</param>
private void update_twitter(
     string consumer_key,
     string consumer_secret,
     string oauth_token,
     string oauth_token_secret,
     string message,
     string request_path)
{
     string service = "推特";
     System.Net.ServicePointManager.Expect100Continue = false;
     this.BeginInvoke(new UpdateStatusDelegate(StateInfo), new object[] { "正在將消息發佈到 " + service });
     string postData = "status=" + Utility.UrlEncode(message);
     byte[] bs = Encoding.UTF8.GetBytes(postData);
     Dictionary<string, string> param = new Dictionary<string, string>();
     param = OAuth.RequestParams(
         consumer_key,
         consumer_secret,
         oauth_token,
         oauth_token_secret,
         request_path,
         "POST",
         postData,
         null);
     string head_string = OAuth.Dict2Header(param);
     try
     {
         HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(request_path);
         request.ContentType = "application/x-www-form-urlencoded";
         request.Method = "POST";
         request.ContentLength = bs.Length;
         if (!string.IsNullOrEmpty(proxyserver))
         {
             request.Proxy = new WebProxy(proxyserver);
             ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);
         }
         request.Headers.Add("Authorization", "OAuth realm=\"http://t.yunmengze.net\"," + head_string);
         using (Stream reqStream = request.GetRequestStream())
         {
             reqStream.Write(bs, 0, bs.Length);
             reqStream.Close();
         }
     }
     catch (Exception e)
     {
         ErrorMessage("將消息發佈到 " + service + " 時出現意外,建議暫時取消這一服務的同步:\r\n" + e.Message, 1);
         return;
     }
}
Code

若是使用代理,加上一段

public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
    //直接確認,不然打不開
    return true;
}
Code

OAuth安全機制是如何實現的?

OAuth 使用的簽名加密方法有 HMAC-SHA1,RSA-SHA1 (能夠自定義)。拿 HMAC-SHA1 來講吧,HMAC-SHA1這種加密碼方法,可使用 私鑰 來加密 要在網絡上傳輸的數據,而這個私鑰只有 Consumer及服務提供商知道,試圖攻擊的人即便獲得傳輸在網絡上的字符串,沒有 私鑰 也是白搭。

私鑰是:consumer secret&token secret  (哈兩個密碼加一塊兒)

要加密的字符串是:除 oauth_signature 外的其它要傳輸的數據。按參數名字符排列,若是同樣,則按 內容排。如:domain=kejibo.com&oauth_consumer_key=XYZ& word=welcome………………….

前面提的加密裏面都是固定的字符串,那麼攻擊者豈不是直接能夠偷取使用嗎?

不,oauth_timestamp,oauth_nonce。這兩個是變化的。並且服務器會驗證一個 nonce(混淆碼)是否已經被使用。

那麼這樣攻擊者就沒法自已生成 簽名,或者偷你的簽名來使用了。

關於開發文檔

參考: 

相關文章
相關標籤/搜索