OAuth无痛入门指南 –转载   Leave a comment

决心写个入门指南,理清思路。当然,如果你真的要仔细研究下oAuth,请看官方文档 http://oauth.net/core/1.0/ 。这里的东西最权威。也可以去oAuth邮件列表讨论。

一句话oAuth

oAuth涉及到3大块的交互和通信。1. 用户2. 拥有用户资料/资源的服务器A3. 求资源的服务器B,。

oAuth的典型应用场景(senario)

以前,用户在 拥有资源 的的网站A有一大堆东西;现在用户发现了一个新的网站B,比较好玩,但是这个新的网站B想调用 拥有资源的网站A的数据。

例如,

  • 在线打印服务的网站,需要调用我的私人Flickr图片。
  • xiaonei.com之类的SNS在线社交网站需要从我的Hotmail导入联系人

那么有以下几个方法:

  1. 需要转移的数据如果是一段文字的话,用户自己Ctrl+C, Ctrl+V。如果是图片呢?复制就麻烦了。
  2. 用户自己下载再上传。——麻烦
  3. 用户老老实实把自己账户ID和密码交上来,服务器自动去抓取。——这个等于把自己家的钥匙给别人来让别人借书一样。。。

oAuth的方式

用户在 求资源的网站B 上,点击一个URL,跳转到 拥有 资源的网站A。
拥有资源的网站A提示:你需要把资源分享给B网站吗?Yes/No。
用户点击 Yes拥有资源的网站A 给 求资源的网站B 临时/永久 开一个通道,然后 求资源的网站 就可以来 拥有资源的网站 抓取所需的信息了。

那么具体的说就有3个步骤:

  1. 首先是2个服务器之间的。求资源的网站B向拥有资源的网站A发送一个请求。术语叫Obtaining an Unauthorized Request Token。具体的作用,类似写了一个“资源远程调用申请书”给 拥有资源的A网站
  2. 第二步是,用户在拥有资源的网站A 通过/允许这个请求。术语叫Obtaining User Authorization。类比一下,就是用户给这个申请书盖章通过啦。
  3. 最后一步仍然是服务器之间的通信。回到B网站,求资源的网站B临时/永久拥有了在A网站远程调用资源的权限,术语又叫Obtaining an Access Token。就好比得到了一个“黄金招牌(Access Token)”。只要每次带上这个“黄金招牌”,求资源的网站B 就可以大摇大摆去 拥有资源的网站A 抓取任意有权限可以被抓取的资源了。具体的技术细节,就是拥有资源的服务器A返回了类似这样的一个application/x-www-form-urlencoded字符串:oauth_token_secret=ssssssssss&oauth_token=ttttttttttttttt&other_parameters=1331775, 这个时候,oauth_token 就相当于用户的密码,你可以用这个oauth_token做任意权限之内的事情了。oauth_token_secret是私钥。 other_parameters是服务器返回的其它可选参数。一般来说,你可以把这个oauth_token的值保存起来,下次接着用。而且一般来说一 个用户和一个oauth_token是一一对应的。

oAuth flow

传递oAuth参数

Technically。当然2个服务器之间传输数据的时候,这里面有一些秘密的oAuth参数以保障整套体系是安全可靠的。这些参数叫Consumer Request Parameters(在oAuth 1.0规范5.2里定义的)。具体的来说,传递oAuth参数有3种方法:

  1. 通过HTTP GET,在URL里面,? 后面的一堆参数。个人不推荐这种方法,因为用一个iframe或者img就可以被XSS了。
  2. HTTP POST方法,参数Content-Type格式为application/x-www-form-urlencoded
  3. 通过Authorization这个HTTP头。传递的东西有一定的格式要求,叫OAuth HTTP Authorization Scheme

这套体系有个比较龌龊的特性,对开发人员十分麻烦,但是对保密性做得比较好,叫“签名”。而且这个特性不是可选的,是必须的。

签名

什么是 签名(Signature)
数字签名不是指将你的签名扫描成数字图像,或者用触摸板获取的签名,更不是你的落款。
数字签名在防止篡改,防止重放攻击,保证数据完整性,防止假冒签名,防止签名者抵赖方面更有效。在中国大陆,数字签名是具法律效力的。

目的:在保证安全(不泄漏密码,防止中间人攻击)的前提下,尽量简单的设计一套可靠“授权”体系。

oAuth官方定义了3种方式,分别是HMAC-SHA1RSA-SHA1PLAINTEXT。当然各个支持oAuth的服务器也可以自定定义一套体系。但是签名的大概过程都差不多。

需要签名的数据依次是:

  1. HTTP方法是GET, POST还是HEAD?
  2. 规范化表示的HTTP URL
  3. 编码合并过后的oAuth参数列表

这3部分需要用 & 符号连接起来,然后交给hmac或者RSA加密。当然我讨厌这么麻烦的方式,所以我用最后一种,也是最简单,各个oAuth服务基本都支持的“签名”方式:PLAINTEXT

具体的很简单,每次请求的时候,oAuth参数包括一个oauth_signature就行了。值就是拥有资源服务器提供的secret key

再次讨论oAuth参数:术语解释

oauth_consumer_key
求资源的网站B的一个标识符。可以认为世界上有许许多多类似 网站B 这样的需求。每一个服务都给了一个唯一的标识符。这个东西哪里来的?自己去问 拥有资源的网站A。例如douban就提供了一个Douban API Key,这里就填Douban API Key好啦。
oauth_consumer_secret
上面那个东西的私钥。如果是Douban,就填API Key的私钥。
oauth_token
oAuth进行到最后一步得到的一个“黄金招牌”。有了这个“黄金招牌”,求资源的网站B就可以大摇大摆去 拥有资源的网站A 抓取任意有权限可以被抓取的资源了。
oauth_token_secret
上面那个东西的私钥
oauth_signature_method
签名方法。三选一:SHA1, RSA-SHA1或者PLAINTEXT。哦。不对。不是三选一,oAuth服务商可以自定义的。
oauth_signature
签名值。这个得靠你自己把数据揉到一个加密函数里算出来。具体如何算看这里
oauth_timestamp
时间戳。timestamp。不懂的自己google。
oauth_nonce
这个就是一个临时值,只要保证每次请求,这个值都不一样就行了。一般随机生成一截数字。

实践

说了这么一大堆东西,还是应该弄一个library来完成这一系列工作。哈哈。不要重复发明轮子嘛。例如oauth-python。我个人觉得很可惜的是,如果你有把这个库弄懂,会用那个精力,还不如自己实现一个库算了。汗。

oAuth的其他

oAuth在安全领域里,是一套相当有Web特征的“授权(authorization)”体系。

oAuth有个好处,就是认证不一定是基于Web的,假如你写了个桌面程序,或者说widget,或者说javascript webapps,也可以直接采用oAuth。其实est个人在想的话,如果采用各个浏览器的cookiejar文件解析 + Flash的LSO,加上oAuth,基本可以做到一次登录永久授权了。

oAuth的诞生可以说是豪门云集啊。一出来的目标就是统一各个网络服务商的各自的登录/授权/认证API,例如Digg, Jaiku, Flickr, Ma.gnolia, Plaxo, Pownce, Twitter, Google, Yahoo, and others soon to follow。不服气的可以再去看看oAuth 1.0规范的作者列表,那些email地址都是牛B闪闪的大公司大巨头牛人啊。

国内的话,Douban采用的是oAuth,虽然用起来有点麻烦,但是支持oAuth应该算一个非常国际化标准化的举措了。

问题在哪里?

假如有一个网站,既扮演了求资源的服务器A这个角色,又扮演了用户角色。加上X技术Y方法Z漏洞。。。。。

 

OAuth 简介

OAuth 是由 Blaine Cook、Chris Messina、Larry Halff 及 David Recordon 共同发起的,目的在于为 API 访问授权提供一个安全、开放的标准。

基于 OAuth 认证授权具有以下特点:

  • 安全。OAuth 与别的授权方式不同之处在于:OAuth 的授权不会使消费方(Consumer)触及到用户的帐号信息(如用户名与密码),也是是说,消费方无需使用用户的用户名与密码就可以申请获得该用户资源的授权。
  • 开放。任何消费方都可以使用 OAuth 认证服务,任何服务提供方 (Service Provider) 都可以实现自身的 OAuth 认证服务。
  • 简单。不管是消费方还是服务提供方,都很容易于理解与使用。

OAuth 的解决方案如下图所示。
图 1. OAuth Solution
图 1. OAuth Solution

如图 1 所示 OAuth 解决方案中用户、消费方及其服务提供方之间的三角关系:当用户需要 Consumer 为其提供某种服务时,该服务涉及到需要从服务提供方那里获取该用户的保护资源。OAuth 保证:只有在用户显式授权的情况下(步骤 4),消费方才可以获取该用户的资源,并用来服务于该用户。

从宏观层次来看,OAuth 按以下方式工作:

  1. 消费方与不同的服务提供方建立了关系。
  2. 消费方共享一个密码短语或者是公钥给服务提供方,服务提供方使用该公钥来确认消费方的身份。
  3. 消费方根据服务提供方将用户重定向到登录页面。
  4. 该用户登录后告诉服务提供方该消费方访问他的保护资源是没问题的。

回页首

OAuth 认证授权流程

在了解 OAuth 认证流程之前,我们先来了解一下 OAuth 协议的一些基本术语定义:

  • Consumer Key:消费方对于服务提供方的身份唯一标识。
  • Consumer Secret:用来确认消费方对于 Consumer Key 的拥有关系。
  • Request Token:获得用户授权的请求令牌,用于交换 Access Token。
  • Access Token:用于获得用户在服务提供方的受保护资源。
  • Token Secret:用来确认消费方对于令牌(Request Token 和 Access Token)的拥有关系。

图 2. OAuth 授权流程(摘自 OAuth 规范)
图 2. OAuth 授权流程(摘自 OAuth 规范)

对于图 2 具体每一执行步骤,解释如下:

  • 消费方向 OAuth 服务提供方请求未授权的 Request Token。
  • OAuth 服务提供方在验证了消费方的合法请求后,向其颁发未经用户授权的 Request Token 及其相对应的 Token Secret。
  • 消费方使用得到的 Request Token,通过 URL 引导用户到服务提供方那里,这一步应该是浏览器的行为。接下来,用户可以通过输入在服务提供方的用户名 / 密码信息,授权该请求。一旦授权成功,转到下一步。
  • 服务提供方通过 URL 引导用户重新回到消费方那里,这一步也是浏览器的行为。
  • 在获得授权的 Request Token 后,消费方使用授权的 Request Token 从服务提供方那里换取 Access Token。
  • OAuth 服务提供方同意消费方的请求,并向其颁发 Access Token 及其对应的 Token Secret。
  • 消费方使用上一步返回的 Access Token 访问用户授权的资源。

总的来讲,在 OAuth 的技术体系里,服务提供方需要提供如下基本的功能:

  • 第 1、实现三个 Service endpoints,即:提供用于获取未授权的 Request Token 服务地址,获取用户授权的 Request Token 服务地址,以及使用授权的 Request Token 换取 Access Token 的服务地址。
  • 第 2、提供基于 Form 的用户认证,以便于用户可以登录服务提供方做出授权。
  • 第 3、授权的管理,比如用户可以在任何时候撤销已经做出的授权。

而对于消费方而言,需要如下的基本功能:

  • 第 1、从服务提供方获取 Customer Key/Customer Secret。
  • 第 2、提供与服务提供方之间基于 HTTP 的通信机制,以换取相关的令牌。

我们具体来看一个使用 OAuth 认证的例子。

在传统的网站应用中,如果您想在网站 A 导入网站 B 的联系人列表,需要在网站 A 输入您网站 B 的用户名、密码信息。例如,您登陆 Plaxo (https://www.plaxo.com ),一个联系人管理网站,当您想把 GMail 的联系人列表导入到 Plaxo,您需要输入您的 GMail 用户名 / 密码,如图 3 所示:
图 3. 在 Plaxo 获得 GMail 联系人
图 3. 在 Plaxo 获得 GMail 联系人

在这里,Plaxo 承诺不会保存您在 Gmail 的密码。

如果使用 OAuth 认证,情况是不同的,您不需要向网站 A(扮演 Consumer 角色)暴露您网站 B(扮演 Service Provider 角色)的用户名、密码信息。例如,您登录 http://lab.madgex.com/oauth-net/googlecontacts/default.aspx 网站, 如图 4 所示:
图 4. 在 lab.madgex.com 获得 GMail 联系人
图 4. 在 lab.madgex.com 获得 GMail 联系人

点击“Get my Google Contacts”,浏览器将会重定向到 Google,引导您登录 Google,如图 5 所示:
图 5. 登录 Google
图 5. 登录 Google

登录成功后,将会看到图 6 的信息:
图 6. Google 对 lab.madgex.com 网站授权
图 6. Google 对 lab.madgex.com 网站授权

在您登录 Google,点击“Grant access”,授权 lab.madgex.com 后,lab.madgex.com 就能获得您在 Google 的联系人列表。

在上面的的例子中,网站 lab.madgex.com 扮演着 Consumer 的角色,而 Google 是 Service Provider,lab.madgex.com 使用基于 OAuth 的认证方式从 Google 获得联系人列表。

下一节,本文会给出一个消费方实现的例子,通过 OAuth 机制请求 Google Service Provider 的 OAuth Access Token,并使用该 Access Token 访问用户的在 Google 上的日历信息 (Calendar)。

回页首

示例

准备工作

作为消费方,首先需要访问 https://www.google.com/accounts/ManageDomains,从 Google 那里获得标志我们身份的 Customer Key 及其 Customer Secret。另外,您可以生成自己的自签名 X509 数字证书,并且把证书上传给 Google,Google 将会使用证书的公钥来验证任何来自您的请求。

具体的操作步骤,请读者参考 Google 的说明文档:http://code.google.com/apis/gdata/articles/oauth.html。

在您完成这些工作,您将会得到 OAuth Consumer Key 及其 OAuth Consumer Secret,用于我们下面的开发工作。

如何获得 OAuth Access Token

以下的代码是基于 Google Code 上提供的 OAuth Java 库进行开发的,读者可以从 http://oauth.googlecode.com/svn/code/java/core/ 下载获得。

  • 指定 Request Token URL,User Authorization URL,以及 Access Token URL,构造 OAuthServiceProvider 对象:
    OAuthServiceProvider serviceProvider = new OAuthServiceProvider( 
        "https://www.google.com/accounts/OAuthGetRequestToken", 
        "https://www.google.com/accounts/OAuthAuthorizeToken", 
        "https://www.google.com/accounts/OAuthGetAccessToken");
  • 指定 Customer Key,Customer Secret 以及 OAuthServiceProvider,构造 OAuthConsumer 对象:
    OAuthConsumer oauthConsumer = new OAuthConsumer(null 
        , "www.example.com" 
        , "hIsGkM+T4+90fKNesTtJq8Gs"
        , serviceProvider);
  • 为 OAuthConsumer 指定签名方法,以及提供您自签名 X509 数字证书的 private key。
    oauthConsumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
    oauthConsumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey);
  • 由 OAuthConsumer 对象生成相应的 OAuthAccessor 对象:
     accessor = new OAuthAccessor(consumer);
  • 指定您想要访问的 Google 服务,在这里我们使用的是 Calendar 服务:
     Collection<? extends Map.Entry> parameters 
        = OAuth.newList("scope","http://www.google.com/calendar/feeds/");
  • 通过 OAuthClient 获得 Request Token:
    OAuthMessage response = getOAuthClient().getRequestTokenResponse( 
        accessor, null, parameters);

    使用 Request Token, 将用户重定向到授权页面,如图 7 所示:

    图 7. OAuth User Authorization
    图 7. OAuth User Authorization

  • 当用户点击“Grant access”按钮,完成授权后,再次通过 OAuthClient 获得 Access Token:
    oauthClient.getAccessToken(accessor, null, null);

在上述步骤成功完成后,Access Token 将保存在 accessor 对象的 accessToken 成员变量里。查看您的 Google Account 安全管理页面,可以看到您授权的所有消费方,如图 8 所示。
图 8. Authorized OAuth Access to your Google Account
图 8. Authorized OAuth Access to your Google Account

使用 OAuth Access Token 访问 Google 服务

接下来,我们使用上一节获得的 Access Token 设置 Google Service 的 OAuth 认证参数,然后从 Google Service 获取该用户的 Calendar 信息:

OAuthParameters para = new OAuthParameters(); 
para.setOAuthConsumerKey("www.example.com"); 
para.setOAuthToken(accessToken); 
googleService.setOAuthCredentials(para, signer);

 

清单 1 是完整的示例代码,供读者参考。
清单 1. 基于 OAuth 认证的 Google Service 消费方实现

				
import java.util.Collection; 
import java.util.Map; 
import net.oauth.OAuth; 
import net.oauth.OAuthAccessor; 
import net.oauth.OAuthConsumer; 
import net.oauth.client.OAuthClient; 

 public class DesktopClient { 
    private final OAuthAccessor accessor; 
    private OAuthClient oauthClient = null; 
    public DesktopClient(OAuthConsumer consumer) { 
        accessor = new OAuthAccessor(consumer); 
    } 

    public OAuthClient getOAuthClient() { 
        return oauthClient; 
    } 

    public void setOAuthClient(OAuthClient client) { 
        this.oauthClient = client; 
    } 

    //get the OAuth access token. 
    public String getAccessToken(String httpMethod, 	
	    Collection<? extends Map.Entry> parameters) throws Exception { 
        getOAuthClient().getRequestTokenResponse(accessor, null,parameters); 

        String authorizationURL = OAuth.addParameters( 
		    accessor.consumer.serviceProvider.userAuthorizationURL, 
			 OAuth.OAUTH_TOKEN, accessor.requestToken); 

        //Launch the browser and redirects user to authorization URL 
        Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " 
		    + authorizationURL); 

        //wait for user's authorization 
        System.out.println("Please authorize your OAuth request token. " 
		    + "Once that is complete, press any key to continue..."); 
        System.in.read(); 
        oauthClient.getAccessToken(accessor, null, null); 
        return accessor.accessToken; 
    } 
 } 

 import java.net.URL; 
 import java.security.KeyFactory; 
 import java.security.PrivateKey; 
 import java.security.spec.EncodedKeySpec; 
 import java.security.spec.PKCS8EncodedKeySpec; 
 import java.util.Collection; 
 import java.util.Map; 
 import com.google.gdata.client.GoogleService; 
 import com.google.gdata.client.authn.oauth.OAuthParameters; 
 import com.google.gdata.client.authn.oauth.OAuthRsaSha1Signer; 
 import com.google.gdata.client.authn.oauth.OAuthSigner; 
 import com.google.gdata.data.BaseEntry; 
 import com.google.gdata.data.BaseFeed; 
 import com.google.gdata.data.Feed; 
 import net.oauth.OAuth; 
 import net.oauth.OAuthConsumer; 
 import net.oauth.OAuthMessage; 
 import net.oauth.OAuthServiceProvider; 
 import net.oauth.client.OAuthClient; 
 import net.oauth.client.httpclient4.HttpClient4; 
 import net.oauth.example.desktop.MyGoogleService; 
 import net.oauth.signature.OAuthSignatureMethod; 
 import net.oauth.signature.RSA_SHA1; 

 public class GoogleOAuthExample { 
    //Note, use the private key of your self-signed X509 certificate. 
    private static final String PRIVATE_KEY = "XXXXXXXX"; 

    public static void main(String[] args) throws Exception { 
        KeyFactory fac = KeyFactory.getInstance("RSA"); 
        //PRIVATE_KEY is the private key of your self-signed X509 certificate. 
        EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec( 
		    OAuthSignatureMethod.decodeBase64(PRIVATE_KEY)); 
        fac = KeyFactory.getInstance("RSA"); 
        PrivateKey privateKey = fac.generatePrivate(privKeySpec); 
        OAuthServiceProvider serviceProvider = new OAuthServiceProvider( 
            //used for obtaining a request token 
			 //"https://www.google.com/accounts/OAuthGetRequestToken", 
	        //used for authorizing the request token 
            "https://www.google.com/accounts/OAuthAuthorizeToken", 
             //used for upgrading to an access token 
            "https://www.google.com/accounts/OAuthGetAccessToken"); 

        OAuthConsumer oauthConsumer = new OAuthConsumer(null 
            , "lszhy.weebly.com" //consumer key 
            , "hIsGnM+T4+86fKNesUtJq7Gs" //consumer secret 
            , serviceProvider); 

        oauthConsumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1); 
        oauthConsumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey); 

        DesktopClient client = new DesktopClient(oauthConsumer); 
        client.setOAuthClient(new OAuthClient(new HttpClient4())); 

        Collection<? extends Map.Entry> parameters = 
		    OAuth.newList("scope","http://www.google.com/calendar/feeds/");

        String accessToken = client.getAccessToken(OAuthMessage.GET,parameters); 

        //Make an OAuth authorized request to Google 

        // Initialize the variables needed to make the request 
        URL feedUrl = new URL( 
		    "http://www.google.com/calendar/feeds/default/allcalendars/full");

        System.out.println("Sending request to " + feedUrl.toString()); 
        System.out.println(); 

        GoogleService googleService = new GoogleService("cl", "oauth-sample-app"); 

        OAuthSigner signer = new OAuthRsaSha1Signer(MyGoogleService.PRIVATE_KEY); 

        // Set the OAuth credentials which were obtained from the step above. 
        OAuthParameters para = new OAuthParameters(); 
        para.setOAuthConsumerKey("lszhy.weebly.com"); 
        para.setOAuthToken(accessToken); 
        googleService.setOAuthCredentials(para, signer); 

        // Make the request to Google 
        BaseFeed resultFeed = googleService.getFeed(feedUrl, Feed.class); 
        System.out.println("Response Data:");               
        System.out.println("=========================================="); 

        System.out.println("|TITLE: " + resultFeed.getTitle().getPlainText()); 
        if (resultFeed.getEntries().size() == 0) { 
           System.out.println("|\tNo entries found."); 
        } else { 
            for (int i = 0; i < resultFeed.getEntries().size(); i++) { 
               BaseEntry entry = (BaseEntry) resultFeed.getEntries().get(i); 
               System.out.println("|\t" + (i + 1) + ": "
                    + entry.getTitle().getPlainText()); 
            } 
        } 
        System.out.println("=========================================="); 	
    } 
 }

 

回页首

小结

OAuth 协议作为一种开放的,基于用户登录的授权认证方式,目前互联网很多 Open API 都对 OAuth 提供了支持,这包括 Google, Yahoo,Twitter 等。本文以 Google 为例子,介绍了 Java 桌面程序如何开发 OAuth 认证应用。在开发桌面应用访问 Web 资源这样一类程序时,一般通行的步骤是:使用 OAuth 做认证,然后使用获得的 OAuth Access Token,通过 REST API 访问用户在服务提供方的资源。

事实上,目前 OAuth 正通过许多实现(包括针对 Java、C#、Objective-C、Perl、PHP 及 Ruby 语言的实现)获得巨大的动力。大部分实现都由 OAuth 项目维护并放在 Google 代码库 (http://oauth.googlecode.com/svn/) 上。开发者可以利用这些 OAuth 类库编写自己需要的 OAuth 应用。

 

Posted 2010年10月7日 by gw8310 in 未分类

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s

%d 博主赞过: