简介
作为服务提供商,您希望人们使用您的服务,而不是滥用服务。

英文版:http://java.sun.com/developer/technicalArticles/J2EE/usingapikeys/ 

 

混搭(mashup) 正在以极快的速度在互联网上蔓延。据估计,每天互联网上都会出现三个新的混搭实现。作为服务提供商,您知道将服务公开以供混搭使用可以带多诸多好处。混搭可以为您的服务做宣传,而且混搭用户的数量也在不断增加。但是,将服务提供给混搭使用时应该考虑一些问题。其中之一便是公开服务所带来的安全问题。您希望人们使用您的服务,而不是滥用服务。

如果您是服务提供商,那么可以采取一些措施来为您的服务提供保护,比如说限制或约束访问。本文将主要介绍如何通过一些技巧来限制对服务的访问。然后将介绍基于 URL 的 API 密钥的使用,它可以为您提供细粒度的保护(优于本文所介绍的其他技巧)。您可以通过 API 密钥限制服务,只允许特定主机域中的用户使用。您还可以使用 API 密钥识别服务使用者的身份,或者测量特定时间段内的服务使用量。但是,本文的主要内容将是如何限制或约束对服务的访问。

您可以使用以下方法限制对服务的访问:

基于令牌的限制
在基于令牌的限制中,我们将使用标识字符串调用令牌以限制客户机对服务端资源的访问。

在这种方法中,我们将使用标识字符串调用令牌以限制客户机对服务端资源的访问。这些资源包括 URL、数据库、Web 和域对象。可以在文件中配置令牌,也可以通过服务器内建的安全特性配置令牌。

jMaki 是应用基于令牌的限制的地方之一。jMaki 是一个开放源码的框架,用于封装使用 JavaScript 编写的小部件。通过使用 jMaki 封装小部件,我们可以在支持 Ajax 的应用程序中使用这些小部件。jMaki 提供了一个库用于存储所封装的小部件,这些小部件来自各种流行客户端 JavaScript 技术库,比如说 Dojo toolkit 和 Script.aculo.us

jMaki 小部件需要通过服务或通过服务端代理加载数据。当 jMaki 小部件向某个服务发起请求时,包含此小部件的客户机页面将和其他任何 Ajax 客户机一样受到 来源服务器策略限制(server-of-origin policy constraint)。鉴于此,jMaki 提供了一个服务器端代理 XmlHttpProxy,用于在 Web 应用程序域的外部向服务发起请求。

作为安全保护,XmlHttpProxy 将使用令牌限制对服务的访问。可以通过代理访问的每个 Web 服务都配置了一个单独的令牌。封装在 jMaki 中的配置文件将令牌配置指定为 JavaScript Object Notation(JSON) 对象。代码示例 1 展示了一个令牌配置。注意,代码示例 1 的标题指明了该代码位于服务器上。本文中的代码示例将指出代码位于服务器上还是客户机上。

代码示例 1:在服务器上配置令牌

{"xhp": {
"version": "1.1",
"services": [
{"id": "yahoogeocoder",
"url":"http://api.local.yahoo.com/MapsService/V1/geocode",
"apiKey" : "appid=jmaki-key",
"xslStyleSheet": "yahoo-geocoder.xsl",
"defaultURLParams": "location=santa+clara,+ca"
},
]
}
}

在 代码示例 1 中,id 字段的值 yahoogeocoder 就是令牌。注意,令牌映射到了一组参数,这些参数包含服务的 URL、服务的 API 密钥(参见基于应用程序密钥的限制)、XSL 样式表,还映射到了传递给服务的默认参数和默认值。在本例中,服务为 Yahoo Maps Geocoding 服务。默认参数和它的值为location=santa+clara,+ca

要访问 Yahoo Maps Geocoding 服务,jMaki 小部件(如 Yahoo Geocoder 小部件)必须指定合适的令牌和位置,如 代码示例 2 所示

代码示例 2:通过在客户机中指定令牌访问服务

var location = encodeURIComponent("location=sunnyvale ca");
var url = "xhp?id=yahoogeocoder&urlparams=" + location;

注意:url 参数指定了以下内容:

  • 令牌,即 yahoogeocoder
  • location 参数和它的值 location=sunnyvale ca,它们将传递给 Yahoo Maps Geocoding 服务。编写 location 时应尽可能使请求能够传递给该路径。
  • XMLHttpProxy 的 URL。在本例中,URL 映射到 xhp

需要注意,令牌配置存储在服务器上。客户机不能配置和修改令牌配置。这是有意的。如果允许 JavaScript 客户机直接配置参数(比如说代理访问的 URL),则会将服务公开给未授权的访问。

基于应用程序密钥的限制
在基于应用程序密钥的限制中,客户机将指定一个应用程序密钥以访问服务。客户机从服务提供商获取应用程序密钥。

在令牌方法中,客户机将提供一个标识符以访问资源。基于应用程序密钥的方法与此类似,只不过需要将资源换成服务,并将标识符换成服务提供的应用程序密钥。Yahoo 就是使用这种方法控制用户对其 Web 服务的访问。Yahoo 将密钥称为应用程序 ID。要访问 Yahoo Web 服务,客户机需要向 Yahoo 注册,并获取一个应用程序 ID,然后在请求各个 Web 服务时指定其应用程序 ID。比如说,jMaki 所获得的访问 Yahoo Web 服务的应用程序 ID 为 jmaki-key,则 jmaki-key 就是 jMaki 请求 Yahoo Web 服务时指定的应用程序 ID。代码示例 3 演示了 jMaki 请求 Yahoo Maps Geocoding 服务。

代码示例3:通过在客户机中指定应用程序密钥访问服务

http://api.local.yahoo.com/MapsService/V1/geocode?appid=jmaki-key&location=4140%20Network%20Circle,%20Santa%20Clara,%20CA,%2095054

注意,应用程序 ID 需要在 appid 参数中指定。还需注意,代码示例 1 中的令牌所映射的参数中也有一个 appid

使用应用程序密钥保护服务是一种便于开发人员操作的方法。与其他类型密钥中的长数值串不同,应用程序密钥可以很短并且易于读取。但是,该方法只能为服务访问提供粗粒度的保护。任何应用程序都可以通过提供一个有效的应用程序密钥来访问服务。实际上,访问 Yahoo 服务的所有 jMaki 小部件和 XmlHttpProxy 使用的都是相同的应用程序密钥。

基于会话的限制
在基于会话的限制中,只有在建立了会话的情况下,系统才许可对服务的访问。这种方法可以很好的防止跨域访问服务。

在这方法中,我们将测试是否已经为访问服务的客户机建立了一个会话。只有在建立了会话的情况下,系统才许可对服务的访问。这种方法可以很好的防止跨域访问服务。这种方法能够运行的原理在于:默认情况下,当用户导航到 Web 应用程序的任何页面时(包括访问服务的页面),servlet 或 Java EE Web 容器都会创建一个 HttpSession。如果客户机尝试从其他域直接访问某个服务,则不会建立 HttpSession代码示例 4 展示了此技巧。

代码示例 4:通过在服务器中测试会话来限制访问

HttpSession session = req.getSession(false);
if (session == null) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}

假设代码示例 4 中的代码是某个服务的一部分。当客户机请求该服务时,服务将查找是否存在 HttpSession。如果已经建立 HttpSession,则服务将允许来自客户机的访问。如果没有建立 HttpSession,则尝试访问服务(使用 JSON with Padding (JSONP) 之类的技巧)的客户机来自另一个域,服务将返回一个 HTTP 403 "Forbidden" 错误。

这种技巧具有一个限制:它并不能完全保证其他域中的客户机受到访问服务的限制。比如说,跨域的客户机可以使用电子欺骗技巧 或创建一个临时的 iFrame 来建立一个 HttpSession。而该技巧的另一个限制是:需要启用 HttpSession跟踪功能,也就是使用 HttpSession API 记录关于会话的数据,从而确保该方法能够奏效。当然,会话必须处于活动状态——如果会话在确定 HttpSession 是否存在之前就已经超时,则此技巧不起作用。

另外,还有一点需要考虑在内:实现此方法将强制用户登录。如果应用程序使用服务器端代理访问服务,则用户必须要登录服务器站点。如果应用程序使用动态脚本方法访问服务,则用户需要登录托管服务的第三方站点。强制用户登录可能会阻碍用户使用您的应用程序。

内容类型限制
在内容类型限制中,服务只返回 XML 格式的数据。这样可以防止直接访问服务的 JSONP 内容。

限制跨域 JavaScript 客户机直接访问服务的另一种方法是只返回 XML 格式的数据。为此,我们可以支持在请求服务时指定 XML 作为惟一的数据返回格式,并忽略其他返回格式,比如说 JSON 或 JSONP。另一种方法是使用 XML 使用默认格式。比如说,Yahoo Maps Geocoding 服务所支持的 output 参数允许用户指定服务所返回数据的格式。但是,它只支持 xml(默认)或 php(一种服务器端嵌入 HTML 的脚本语言)的 output 参数值。如果用户请求 Yahoo Maps Geocoding 服务并另外指定一个 output 参数值(如 jsonp),则服务将使用默认的 XML 格式返回数据,如 代码示例 5 所示。

代码示例 5:客户端请求 Yahoo Geocoding 服务

http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=jmaki&results=1&output=jsonp&callback=showResults

可以使用与 Yahoo Maps Geocoder 相同的方式限制对服务的访问,以防止直接 JSONP 访问。这种技巧将强制 JavaScript 客户机使用XmlHttpProxy 之类的代理访问服务。

如果希望将服务访问限制为一组很小的用户或域,一种可行的策略是限制服务返回内容的类型以强制用户使用代理。

基于身份验证的限制
HTTP 基本身份验证和 HTTP 摘要身份验证是限制资源访问的常见方法。

HTTP 基本身份验证 和 HTTP 摘要身份验证 是限制资源访问的常见方法。servlet API 内置支持通过这些身份验证方法为服务或其他 Web 资源提供保护。有关建立 HTTP 基本身份验证和 HTTP 摘要身份验证的详细信息,请参阅 Java EE 5 教程的Declaring Security Requirements in a Deployment Descriptor 一节。

如果希望允许用户更新数据,那么可以考虑使用基于身份验证的限制。这种技巧还允许我们跟踪更新并支持某种级别的帐目管理。与限制内容类型一样,如果希望将服务访问限制为一组很小的用户或域,则基于身份验证的限制是一种很好的方法。同样需要注意,限制内容类型和要求身份验证这两种方法都具有很强的限制性。因此,在使用这些技巧之前,请先考虑需要对服务访问施加多大的限制。

还需注意,在基于会话的限制中,实现这种方法将强制用户登录——这必然会阻碍用户使用您的应用程序。

基于 URL 的 API 密钥限制
在基于 URL 的 API 密钥限制中,客户机需要指定一个映射到客户机的域和目录的密钥。该域和目录的密钥需从服务提供商处获取。

此技巧类似于 基于应用程序密钥的限制,只不过这里密钥是 API 密钥,并且将映射到特定的域和目录。也就是说,要访问服务,客户机需要具备以下两个条件:

  1. 客户机在服务请求中指定正确的 API 密钥作为参数。
  2. 客户机位于 API 密钥所映射的域和目录中。

Google 就是使用这种方法保护它的 Google Maps 服务。首先,从 Google 获取一个 API 密钥。然后,在服务请求(比如 Google Maps 服务请求)中指定 API 密钥,如 代码示例 6 所示。

代码示例 6:Google Maps 服务的客户机请求

<script type="text/javascript" src="http://maps.google.com/maps?file=api&v=1& key=ABQIAAAAyEQwWkLnhtibmBGdNd7 ..."></script>

代码示例 6 并未完整显示 key 值所表示的 API 密钥,它是一个很长的字符串。假设这个 API 密钥只适用于与 URLhttp://jmaki.com/samples/ 对应的域和目录。http://jmaki.com/samples/ 中的客户机指定此 API 密钥便可以访问 Google Maps 服务。而其他 URL 中的客户机不能使用这个 API 密钥访问 Google Maps 服务。

我们可以使用相同的技巧为服务提供细粒度的访问。事实上,随 jMaki 发布的一个示例应用程序(API 密钥示例)就演示了这一技巧。应用程序可以访问 jMaki 所提供的服务。但是,在用户可以访问服务之前,应用程序将弹出提示要求用户输入将访问服务的主机 URL。用户提交 URL 并单击 Get Key 按钮之后,应用程序将针对该 URL 返回一个 API 密钥,如 图 1 所示。

图1. jMaki API 密钥示例应用程序返回的基于 URL 的 API 密钥
图1:jMaki API 密钥示例应用程序返回的基于 URL 的 API 密钥

此处需要注意,可以使用两种访问将个体用户映射到 API 密钥:1)采用 Google 的方式,要求用户创建一个帐户;2)采用 jMaki API 密钥示例的方式,要求用户注册使用基于身份验证的服务生成密钥。

我们可以使用 URL 的 API 密钥为服务提供细粒度的访问。该技巧还以实现以下目的:

  • 识别服务使用者的身份。
  • 定量提供服务,即控制使用服务的量。
  • 控制所提供服务的级别。

基于映射到 URL 的 API 密钥限制访问可允许您将服务访问限制为特定的域和目录。换句话说,您可以控制哪些主机能够访问服务。除此之外,此技巧还可以识别正在使用服务的用户的身份。您甚至还可以定量提供服务,即控制服务在特定时间段内可以使用的次数。此技巧还能够控制所提供服务的级别。您可以根据不同用户协议为他们建立服务水平协议,从而使交付的服务能够满足协议中所指定的响应时间、可靠性和可用性目标。

但是,基于 URL 的 API 密钥方法会使混搭开发人员访问服务时更加困难,因为它要求开发人员为混搭客户机所运行的每个 URL 都获取一个新的 API 密钥。

注意,这种方法并不能保证所有未许可请求都不能访问服务。如 限制 一节所述,可以使用电子欺骗和其他技巧绕过限制。但是,使用基于 URL 的密钥可以阻止大量潜在的服务滥用。

要实现这种方法,必须生成并分发 API 密钥。还需要验证访问服务的任何请求中的 API 密钥。接下来我们将研究如何实现。

生成和验证 API 密钥

本节将介绍如何生成和分发 API 密钥,并验证服务请求中所指定的 API 密钥。在这种方法中,服务将使用一个 散列函数 生成 API 密钥。同时,服务将验证服务请求中所指定的 API 密钥,方法是将它与服务为客户机的 HTTP Referer 生成的 API 密钥进行比较。HTTP Referer 标识了请求的源 URL。如果 API 密钥匹配,则允许客户机访问服务。

为演示这种方法,我们先查看一下 GenericService,它是 jMaki API 密钥示例应用程序所使用的一个服务。该服务是作为 servlet 实现的。要查看该服务的运行,请下载 示例应用程序包。需找到名称为 jmaki-java-x.x.x.x.zip 形式的程序包,比如说 jmaki-java-0.9.7.2.zip。然后编译与运行示例程序。

生成 API 密钥

代码示例 7 展示了创建 API 密钥的 GenericService 代码。

代码示例 7:在服务器上创建 API 密钥

private static String SERVER_TOKEN = "JMAKI_SERVICE";

md = MessageDigest.getInstance("MD5");

private String generateHash(String key) {
key += SERVER_TOKEN;
// start fresh
md.reset();
md.update(key.getBytes());
byte[] bytes = md.digest();
// buffer to write the md5 hash to
StringBuffer buff = new StringBuffer();
for (int l=0;l< bytes.length;l++) {
String hx = Integer.toHexString(0xFF & bytes[l]);
// make sure the hex string is correct if 1 character
if(hx.length() == 1) buff.append("0");
buff.append(hx);
}
return buff.toString().trim();
}

GenericService 服务将使用 generateHash() 方法生成密钥。该方法是一个单向函数,它将生成一个简单的值,即一个散列。单向 的意思是方法将根据一些数据生成散列,并且无法通过该散列轻易计算出原始值。也就是说,不能轻易地通过散列值重新得出输入数据。此处的输入数据为客户机的 URL,即 HTTP Referer。

关于 generateHash() 方法,应该注意以下两点:

  • 密钥后面将附加一个 SERVER_TOKEN。此处的密钥为 HTTP Referer。在密钥后面附加一个 SERVER_TOKEN 将使散列更加难以反向计算。
  • 将使用 MD5消息摘要算法 生成散列。Java 平台含有一个 MessageDigest 类,可轻易在应用程序中实现消息摘要算法。在 generateHash() 方法中,md 是一个 MessageDigest 对象,它实现了 MD5 算法。

生成密钥之后,服务会将密钥返回给客户机,标识出密钥的主机域。然后,客户机可以使用密钥请求服务,前提是 HTTP Referer 为适当的域。

验证 API 密钥

代码示例8 展示了验证服务请求API密钥的GenericService代码。

代码示例 8:在服务器中验证 API 密钥

private boolean testAPIKey(HttpServletRequest req, String apiKey) {
String referer = req.getHeader("Referer");
// check if it's a relative URL and make sure we end with a slash
if (!referer.startsWith("http") &&
!referer.endsWith("/") ) referer = referer + "/";
// get the key for the host used by the request
String refererKey = generateHash(referer + SERVER_TOKEN);
if (apiKey != null) return refererKey.equals(apiKey);
else return false;
}

GenericService 服务将使用 testAPIKey() 方法验证 API 密钥。为此,方法将为客户机的 HTTP Referer 生成一个 API 密钥——它使用generateHash 方法生成密钥。然后,testAPIKey() 方法将为 HTTP Referer 生成的 API 密钥与客户机在服务请求中指定的 API 密钥进行比较。这种方法的好处之一便是无需将 API 密钥存储于服务中。如果 API 密钥匹配,客户机将可以访问服务,只要服务支持请求所指定的响应格式。GenericService 服务支持三种响应格式:JSONP、JSON 和 XML。

比如说,假设 GenericService 的 URL 为 http://localhost:8080/gs 并且针对 URL 为 http://localhost:8084/ 的客户机所生成的 API 密钥为 123ABC。如果客户机提交请求访问 GenericService(如 代码示例9 所示),并通过 http://localhost:8084/ 提交请求,那么服务将准许该请求。

代码示例 9:来自客户机的有效 GenericService 服务请求

<script src="http://localhost:8080/gs?apikey=123ABC&format=jsonp"></script>

局限性

使用 API 密钥限制访问具有一些限制。可以通过许多工具和技巧来伪造 HTTP Referer,从而实现电子欺骗。同样,虽然这种方法所使用的单向函数难于进行反向计算,但是并非完全不可能。即便使用 MD5 算法并附加一个 SERVER_TOKEN来生成 API 密钥也是如此。

最后,这种方法要求客户机浏览器启用 JavaScript。如果计划使用 API 密钥限制访问,则需要考虑包含代码检查浏览器是否启用了 JavaScript。如果禁用了 JavaScript,那么您的代码将无功而返。也就是说,需要实现一个替代解决方案或显示错误消息。

结束语

可以采用各种方法为基于 Ajax 的服务提供保护。基于应用程序密钥限制之类的方法可以提供粗粒度的保护,但是对于开发人员却相对易于使用。而其他方法(如基于 URL 的 API 密钥限制)可以提供细粒度的访问控制,但是开发人员会比较难以使用。因此,比较这些方法的好处和限制并选择最合适的方法是很重要的。

更多信息
posted on 2011-11-24 13:28  懒懒的呐喊  阅读(381)  评论(0编辑  收藏  举报