OAuth有什么用?为什么要使用OAuth?
twitter或豆瓣用户一定会发现,有时候,在别的网站,点登录后转到 twitter登录,之后转回原网站,你会发现你已经登录此网站了,这种网站就是这个效果。其实这都是拜 OAuth所赐。
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密 码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。目前互联网很多服务如Open API,很多大头公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
认证和授权过程
在认证和授权的过程中涉及的三方包括:
- 服务提供方,用户使用服务提供方来存储受保护的资源,如照片,视频,联系人列表(如twitter角色)。
- 用户 ,存放在服务提供方的受保护的资源的拥有者。
- Consumer ,要访问服务提供方资源的第三方应用(中间商,类似上面的 twitterfeed 角色)。在认证过程之前,客户端要向服务提供者申请客户端标识。
注意到用户提交密码是在第四步 而第三方Consumer从头至尾没有获得账号信息
Oauth的流程 官方的说明图如下:
使用OAuth进行认证和授权的过程如下所示:
- 用户访问客户端的网站,想操作自己存放在服务提供方的资源。
- 客户端向服务提供方请求一个临时令牌。
- 服务提供方验证客户端的身份后,授予一个临时令牌。
- 客户端获得临时令牌后,将用户引导至服务提供方的授权页面请求用户授权。在这个过程中将临时令牌和客户端的回调连接发送给服务提供方。
- 用户在服务提供方的网页上输入用户名和密码,然后授权该客户端访问所请求的资源。
- 授权成功后,服务提供方引导用户返回客户端的网页。
- 客户端根据临时令牌从服务提供方那里获取访问令牌 。
- 服务提供方根据临时令牌和用户的授权情况授予客户端访问令牌。
- 客户端使用获取的访问令牌访问存放在服务提供方上的受保护的资源。
一,Consumer 向 服务提供商 申请接入权限
可得到:Consumer Key,Consumer Secret。twitter申请oauth的话,在 setting - connection - developer 里面申请。 同时给出三个访问网址:
- request_token_url = 'http://twitter.com/oauth/request_token'
- access_token_url = 'http://twitter.com/oauth/access_token'
- authorize_url = 'http://twitter.com/oauth/authorize'
二,当Consumer接到用户请求想要访问第三方资源(如twitter)的时候
Consumer需要先取得 请求另牌(Request Token)。网址为上面的 request_token_url,参数为:
- oauth_consumer_key:Consumer Key
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- oauth_callback:返回网址链接。
- 及其它服务提供商定义的参数
这样 Consumer就取得了 请求另牌(包括另牌名 oauth_token,另牌密钥 oauth_token_secret。
三,浏览器自动转向服务提供商的网站:
网址为authorize_url?oauth_token=请求另牌名
四,用户同意 Consumer访问 服务提供商资源
那么会自动转回上面的 oauth_callback 里定义的网址。同时加上 oauth_token (就是请求另牌),及oauth_verifier(验证码)。
五,现在总可以开始请求资源了吧?
NO。现在还需要再向 服务提供商 请求 访问另牌(Access Token)。网址为上面的access_token_url,参数为:
- oauth_consumer_key:Consumer Key
- oauth_token:上面取得的 请求另牌的名
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- oauth_verifier:上面返回的验证码。
- 请求 访问另牌的时候,不能加其它参数。
这样就可以取得 访问另牌(包括Access Token 及 Access Token Secret)。这个就是需要保存在 Consumer上面的信息(没有你的真实用户名,密码,安全吧!)
六,取得 访问另牌 后
Consumer就可以作为用户的身份访问 服务提供商上被保护的资源了。提交的参数如下:
- oauth_consumer_key:Consumer Key
- oauth_token:访问另牌
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- 及其它服务提供商定义的参数
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参数,则将该参数原样返回 |
1、首先是到登录地址 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%84、return_session=0、legacy_return=1、display=、session_key_only=0、trynum=1、email、pass、persistent=1、login=%E7%99%BB%E5%BD%95、lsd。
2、在登录成功之后保存 cookies 转向 http://www.facebook.com/home.php ,然后从此页面解析出 profile_id、 composer_id、post_form_id 、fb_dtsg 4个参数出来。
3、最后一步向 http://www.facebook.com/ajax/updatestatus.php?__a=1 提交状态信息,需要包括以下参数 action=HOME_UPDATE、home_tab_id=1、profile_id、status、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_id、fb_dtsg、lsd、_log_display_context=home、ajax_log=1、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=u0022lsdu0022svalue=u0022([^u0022]*)u0022()"); lsd = re.Groups[1].ToString(); } } catch (Exception ex) { ErrorMessage("获取登录到 "+ service +" 必要参数时出现意外: "+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 + " 时出现意外: "+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_idu0022svalue=u0022([^u0022]*)u0022()"); profile_id = re.Groups[1].ToString(); re = Regex.Match(result, @"name=u0022composer_idu0022svalue=u0022([^u0022]*)u0022()"); composer_id = re.Groups[1].ToString(); re = Regex.Match(result, @"name=u0022post_form_idu0022svalue=u0022([^u0022]*)u0022()"); post_form_id = re.Groups[1].ToString(); re = Regex.Match(result, @"name=u0022fb_dtsgu0022svalue=u0022([^u0022]*)u0022()"); fb_dtsg = re.Groups[1].ToString(); response.Close(); } } catch (Exception ex) { ErrorMessage("重定向facebook页面并获取发布消息所需参数时出现意外: "+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 时出现意外: "+ex.Message); return; } }
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 + " 时出现意外,建议暂时取消这一服务的同步: " + e.Message, 1); return; } }
如果使用代理,加上一段
public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { //直接确认,否则打不开 return true; }
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(混淆码)是否已经被使用。
那么这样攻击者就无法自已生成 签名,或者偷你的签名来使用了。
关于开发文档
- 新浪:http://open.weibo.com/wiki/%E9%A6%96%E9%A1%B5
- 空间:http://wiki.opensns.qq.com/wiki/%E3%80%90QQ%E7%99%BB%E5%BD%95%E3%80%91%E6%96%87%E6%A1%A3%E8%B5%84%E6%BA%90
- 腾讯:http://wiki.open.t.qq.com/index.php/%E9%A6%96%E9%A1%B5
- 人人:http://wiki.dev.renren.com/wiki/%E9%A6%96%E9%A1%B5
- 开心:http://open.kaixin001.com/document.php
- 豆瓣:http://www.douban.com/service/apidoc/
- 搜狐:http://open.t.sohu.com/en/%E9%A6%96%E9%A1%B5
- 网易:http://open.t.163.com/wiki/index.php?title=%E9%A6%96%E9%A1%B5
参考: