RESTful网络服务 - 如何验证来自其他服务的请求?

119

我正在设计一个RESTful Web服务,需要用户以及其他Web服务和应用程序访问。所有传入的请求都需要进行身份验证。所有通信都通过HTTPS进行。用户身份验证将基于身份验证令牌工作,通过将用户名和密码(通过SSL连接)POST到服务提供的/session资源来获取。

对于Web服务客户端,客户端服务后面没有终端用户。请求是由计划任务、事件或其他计算机操作发起的。连接服务的列表是预先知道的(显然,我想)。我应该如何验证来自其他(Web)服务的这些请求?我希望身份验证过程对这些服务的实现越简单越好,但不能以安全为代价。像这样的情况有哪些标准和最佳实践呢?

我能想到的选项(或者别人建议给我的):

  1. 让客户端服务采用“虚假”的用户名和密码,并以与用户相同的方式进行身份验证。我不喜欢这个选项——它感觉不对。

  2. 为客户端服务分配永久的应用程序ID,可能还包括应用程序密钥。就我所理解的而言,这与具有用户名+密码的情况完全相同。通过此ID和密钥,我可以验证每个请求,或者创建身份验证令牌来验证进一步的请求。但问题是,任何能够获取应用程序ID和密钥的人都可以冒充客户端。

  3. 我可以在前面的选项中添加IP地址检查。这将使虚假请求更难实现。

  4. 客户端证书。设置自己的证书颁发机构,创建根证书,并为客户端服务创建客户端证书。然而,有几个问题需要考虑:a)如何使用户无需使用证书进行身份验证?b)从客户端服务的角度来看,这种情况有多复杂?

  5. 其他解决方案——肯定还有其他解决方案吧?

我的服务将在Java上运行,但我故意省略了它将建立在哪个具体框架上的信息,因为我更关注基本原则而不是具体实现细节 - 我认为无论底层框架如何,都可以实现最佳解决方案。然而,我对这个主题有些缺乏经验,因此实际实现方面的具体提示和示例(例如有用的第三方库、文章等)也将非常感激。


如果我可以建议的话,熟悉大型网站服务并挑选自己喜欢的部分。您的用户也会发现与其他RESTful服务的最佳实践相似性非常有用。 - Yzmir Ramirez
发现另一个几乎两年前涉及类似主题的问题:http://stackoverflow.com/questions/1138831/how-to-authenticate-client-application-for-trust-of-messages-sent-from-it - Tommi
这些服务(包括网络和其他服务)运行在什么操作系统上?它们是否运行在相同基础设施的服务器上? - Anders Abel
操作系统可能会有所不同:Win,*nix等。客户端服务可能与我的服务不在同一基础设施中。 - Tommi
9个回答

36

阅读了你的问题后,我认为需要生成一个特殊的令牌来进行所需的请求。该令牌将在特定时间内有效(例如一天)。

以下是生成身份验证令牌的示例:

(day * 10) + (month * 100) + (year (last 2 digits) * 1000)

例如: 2011年6月3日

(3 * 10) + (6 * 100) + (11 * 1000) = 
30 + 600 + 11000 = 11630

然后将其与用户密码连接起来,例如 "my4wesomeP4ssword!"

11630my4wesomeP4ssword!

然后对该字符串进行MD5哈希:

05a9d022d621b64096160683f3afe804

当您调用请求时,始终使用此令牌。

https://mywebservice.com/?token=05a9d022d621b64096160683f3afe804&op=getdata

这个token每天都是唯一的,所以我猜这种保护措施已经足够保护你的服务了。

希望能有所帮助

:)


1
我真的很喜欢你在每个请求中添加安全令牌的方式,但是当程序员已经创建了数百个JSP页面并且之后必须在先前创建的100个页面以及即将创建的页面中实现安全性时,该怎么办?在这种情况下,在每个请求中附加令牌不是正确的选择。无论如何,对于你的技术,我给一个赞。 :) - Ankur Verma
4
如果时钟没有同步,会发生什么?在这种情况下,客户端不会生成错误的令牌吗?即使两者都使用UTC生成日期时间,它们的时钟仍可能存在差异,导致每天有一段时间窗口,令牌将无法使用? - NickG
但我仍然可以使用这个令牌在一天内调用您的 Web 服务,对吧?我不知道这如何有所帮助。 - Mina Gabriel
@MinaGabriel 你可以在令牌生成中添加更多的时间框架。(分钟10)+(小时100)+(天1000)+(月10000)+(年(最后2位数字)*100000) - kororo
关于您的问题,一旦客户端和服务器通过电话/短信/其他方式知道了秘密密码“my4wesomeP4ssword”,仍然应该是安全的,因为您在HTTPS通道后面。当然,客户端和服务器的安全性不在讨论范围内,例如攻击者是否可以入侵系统。 - kororo
显示剩余3条评论

34
任何解决此问题的方案都归结为共享密钥。我也不喜欢硬编码的用户名和密码选项,但它确实具有相当简单的优点。客户端证书也很好,但它真的有区别吗?服务器上有一个证书,客户端上也有一个证书。它的主要优势在于更难以被暴力破解。希望您已经采取了其他保护措施来防止这种情况发生。
我认为基于客户端证书的解决方案的A点并不难解决。您只需要使用分支。if(客户端证书){检查它} else {http基本身份验证} 我不是Java专家,也没有使用过它来进行客户端证书。不过,快速搜索可以找到这个教程,看起来非常适合您。
尽管有关“最佳方案”的所有讨论,让我指出还有另一种哲学观点,即“代码越少,越不 clever,越好”。 (我个人持有这种哲学观点)。 基于客户端证书的解决方案听起来像是很多代码。
我知道您对OAuth提出了问题,但OAuth2建议确实包括一个名为“bearer tokens”的解决方案,必须与SSL一起使用。 出于简单起见,我会选择硬编码的用户/密码(每个应用程序一个,因此可以单独撤销)或非常相似的令牌方案。

28
客户端证书不是共享密钥。这就是它们存在的原因。客户端拥有私钥,服务器拥有公钥。客户端从不分享其私密,而公钥不是秘密。 - Tim
6
该教程链接并非指向教程文章,而是指向Oracle网站上的某个Java索引页面。 - Marjan Venema
2
@MarjanVenema 嗯,那是因为你在 newz2000 回答之后两年才尝试链接,但你可以尝试 WayBack Machine:http://web.archive.org/web/20110826004236/http://java.sun.com/developer/technicalArticles/Security/secureinternet2/ - Fábio Duque Silva
@FábioSilva:这就是所谓的链接失效,这也是我为什么要引起注意的原因。感谢提供链接和找到缓存版本,但我更希望该链接能够更新,指向此内容的新位置或更加更新的版本。 - Marjan Venema
1
@MarjanVenema:非常抱歉,链接失效后你还期望newz2000前来更新吗?就像你所说的,链接腐烂总会发生,不管是早是晚。你可以尝试访问存档以查看作者当时的内容,或者找到新的链接做出积极贡献。我不明白你的评论如何帮助任何人。但是,点击这个链接:http://www.oracle.com/technetwork/articles/javase/secureinternet2-137010.html(请注意,它最终也会失效)。 - Fábio Duque Silva
2
@FábioSilva:不,我并不“期望”他这样做。我确实阅读了存档,但我没有时间去寻找新链接,所以我采取了下一个最好的办法:在评论中注明它已经失效,以便社区中的其他人可以找到新位置并更新帖子。既然你显然有时间找到新链接,为什么不在帖子中更新链接,而是在评论中抱怨我呢? - Marjan Venema

11

有几种不同的方法可供选择。

  1. RESTful纯粹主义者希望你使用基本身份验证,并在每个请求中发送凭据。他们的理由是没有人存储任何状态。

  2. 客户端服务可以存储cookie,维护会话ID。我个人觉得这并不像我听到的一些纯粹主义者那样令人反感——重复认证可能很昂贵。虽然您似乎对这个想法不太感冒。

  3. 从您的描述来看,您可能对OAuth2感兴趣。就我目前所见,它有点混淆和前沿。虽然存在实现,但它们寥寥无几。在Java中,我了解到它已经集成到Spring3的安全模块中。(他们的教程写得很好。)我一直在等看看是否会有一个在Restlet中的扩展,但迄今为止,尽管它已经被提出,可能在孵化器中,但仍未完全纳入。


我对选项2没有任何意见 - 我认为在RESTful应用程序中这是一个很好的解决方案 - 但客户端服务首先从哪里获取令牌呢?他们第一次如何进行身份验证?也许我想错了,但似乎客户端服务需要拥有自己的用户名和密码才能实现这一点。 - Tommi
如果最终用户是您的用户,则中介服务可以在第一次请求时传递他们的凭据给您,您可以返回一个Cookie或其他令牌。 - jwismar
同样,在OAuth场景中,最终用户将其对您的Web服务的访问权限委托给了中介服务。 - jwismar
似乎有一个误解 - 在客户端服务背后根本没有最终用户。我已经更新了我的问题以更好地解释情况。 - Tommi
我发现每次进行身份验证的方法很好,因为它使编写客户端代码变得轻而易举。它的一个变体(仅在我们谈论受信任的任务并且当然假设它是通过HTTPS完成)是使用自定义HTTP头来包含简单的身份验证信息(例如,预配置的UUID),并在服务器端使用自定义SpringSec处理程序来理解它。 - Donal Fellows
1
我只想补充一点,上面列出的第一选项应该只在HTTPS上完成。 - mr-sk

3
我认为这种方法:
  1. 首先请求,客户端发送id/passcode
  2. 交换id/pass以获取唯一令牌
  3. 在每个后续请求中验证令牌直到其过期
无论您如何实现和其他具体技术细节,都是相当标准的。
如果您真的想要推动极限,也许您可以将客户端的https密钥视为暂时无效状态,直到验证凭据,如果未验证,则限制信息,如果已验证,则授予访问权限,再次基于过期时间。
希望这有所帮助。

3
就客户端证书的方法而言,实现起来并不是非常困难,而且仍然可以允许没有客户端证书的用户。如果您确实创建了自己的自签名证书颁发机构,并向每个客户端服务发放了客户端证书,则可以轻松地验证这些服务。根据您使用的Web服务器,应该有一种指定客户端身份验证的方法,该方法将接受客户端证书但不要求客户端证书。例如,在Tomcat中指定https连接器时,您可以设置“clientAuth = want”,而不是'true'或'false'。然后,您需要将自签名CA证书添加到您的信任商店中(默认情况下为JRE中的cacerts文件,除非您在Web服务器配置中指定了其他文件),因此,唯一可信任的证书是由您的自签名CA颁发的那些证书。在服务器端,如果您能够从请求中检索到客户端证书(不为空)并通过任何DN检查(如果您需要额外的安全性),则只允许访问您希望保护的服务。对于没有客户端证书的用户,他们仍然可以访问您的服务,但请求中将没有证书。在我看来,这是最“安全”的方法,但它肯定有其学习曲线和开销,因此可能并不是您需求的最佳解决方案。

3

5. 其他解决方案 - 必须还有其他的解决方案吧?

没错,还有一个解决方案!它叫做 JWT(JSON Web Tokens)。

JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于安全地在各方之间传输信息,该信息以 JSON 对象的形式存在。这些信息可以得到验证和信任,因为它们是数字签名的。JWT 可以使用密钥(HMAC 算法)或公钥/私钥对(RSA)进行签名。

我强烈建议您研究一下 JWT。与替代解决方案相比,它们是一个更简单的解决方案。

https://jwt.io/introduction/


1
您可以在服务器上创建会话,并在每个REST调用之间共享sessionId,以便客户端和服务器之间进行通信。
首先进行身份验证REST请求:/authenticate。返回响应(按您的客户端格式)与sessionId: ABCDXXXXXXXXXXXXXX;将此sessionId存储在具有实际会话的Map中。使用Map.put(sessionid, session)或使用SessionListener为您创建和销毁密钥;对于每个REST调用,如URL?jsessionid=ABCDXXXXXXXXXXXXXX(或其他方式),获取sessionid;使用sessionId从map中检索HttpSession;如果会话处于活动状态,请验证该会话的请求;发送回响应或错误消息。

0
我会建议使用一个应用程序重定向用户到您的网站,并带有一个应用程序ID参数,一旦用户批准请求,生成一个唯一的令牌,由其他应用程序用于身份验证。这样,其他应用程序就不需要处理用户凭据,用户可以添加、删除和管理其他应用程序。Foursquare和其他一些网站都是以这种方式进行身份验证的,而且非常容易实现。

嗯,我不确定我是否能够理解这个解释。我们正在谈论哪个用户?我正在谈论与另一个应用程序通信的应用程序。我想你已经明白了,但我仍然不能完全理解。例如,当这个“token”过期时会发生什么? - Tommi
你生成并发送回另一个应用程序的令牌是持久性令牌,它与用户和应用程序绑定。这里有一个链接到Foursquare文档https://developer.foursquare.com/docs/oauth.html,它基本上是oauth2,因此可以研究一下以获得良好的身份验证解决方案。 - Devin M
只需为应用程序生成密钥,如果您所做的只是允许应用程序访问,则简单的application_id和application_key应该适用于身份验证。如果您想要使用令牌进行身份验证,请查看使用devise的令牌身份验证选项,因为它只是通过url请求传递的参数。 - Devin M
但这不是与用户名+密码=会话身份验证令牌方案完全相同吗?application_id和application_key难道不只是用户名和密码的同义词吗? :) 如果这确实是像这样情况下的标准做法,那就没问题了——正如我所说,我在这方面经验不足——但我认为可能还有其他选择... - Tommi
它并不依赖于会话,如果您不想创建会话,令牌也不会创建会话。 - Devin M
显示剩余3条评论

-3
除了身份验证,我建议您考虑整体的大局。可以考虑让您的后端RESTful服务不需要任何身份验证;然后在终端用户和后端服务之间放置一些非常简单的身份验证中间层服务。

在终端用户和后端服务之间?那不会完全使客户端服务未经身份验证吗?这不是我想要的。当然,我可以在我的Web服务和客户端服务之间放置中间层,但仍然存在一个问题:实际的身份验证模型将是什么? - Tommi
中间层可以是像Nginx这样的Web服务器,您可以在那里进行身份验证。身份验证模型可以基于会话。 - Dagang
正如我在问题中尝试解释的那样,我不想为这些客户端服务使用基于会话的身份验证方案。(请查看我的问题更新。) - Tommi
我建议您使用白名单IP列表或IP范围,而不是用户名和密码。通常,客户服务IP是稳定的。 - Dagang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接