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(混淆码)是否已经被使用。

那么这样攻击者就没法自已生成 签名,或者偷你的签名来使用了。

关于开发文档

参考: 

相关文章
相关标签/搜索