首页 热点资讯 义务教育 高等教育 出国留学 考研考公

如何使用 Oauth 实现一个安全的 REST API 服务

发布网友 发布时间:2022-04-22 11:08

我来回答

2个回答

懂视网 时间:2022-05-04 05:19

上篇了解了如何调用 OAuth 授权来获取数据,本篇介绍如何开放OAuth授权,并控制服务端数据访问。[源码下载] 先看一下图: 这两天事太多,文章整理的断断续续 OK,步入正题,这里还是要借力:DevDefined.OAuth 框架。它提供了客户端访问,服务端管理Token的基

上篇了解了如何调用 OAuth 授权来获取数据,本篇介绍如何开放OAuth授权,并控制服务端数据访问。[源码下载]
先看一下图:



这两天事太多,文章整理的断断续续

OK,步入正题,这里还是要借力: DevDefined.OAuth 框架。它提供了客户端访问,服务端管理Token的基础功能。

1. OAuthChannel
定义了服务端用户模型,OAuth的,OAuthWebServiceHostFactory(继承于WebServiceHostFactory,用于添加),以及 RequestToken 和 AccessToken 保持在内存里的容器及存取类 (InMemoryTokenRepository,InMemoryTokenStore)

OAuthWebServiceHostFactory 添加,使用了 WebServiceHost2 (Microsoft.ServiceModel.Web.dll 里,是 Microsoft 发布的WCF REST Starter Kit的一部分)
WebServiceHost2 重写了 ServiceHost 里 OnOpening 方法添加。WebServiceHost2的源代码猛击这里
OAuthWebServiceHostFactory:

using System;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using Microsoft.ServiceModel.Web;
using DevDefined.OAuth.Provider;
using OAuthChannel.Repositories;

namespace OAuthChannel
{
 public class OAuthWebServiceHostFactory : WebServiceHostFactory
 {
 public IOAuthProvider OAuthProvider { get; set; }
 public ITokenRepository AccessTokenRepository { get; set; }

 protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
 {
  var serviceHost = new WebServiceHost2(serviceType, true, baseAddresses);
  var interceptor = new OAuthChannel.OAuthInterceptor(OAuthProvider, AccessTokenRepository);
  serviceHost.Interceptors.Add(interceptor);
  return serviceHost;
 }
 }
}

(OAuthInterceptor.cs)将请求的 OAuth (Request Header中) 转换成 OAuthChannel.Models.AccessToken

public class AccessToken : TokenBase
{
	public string UserName { get; set; }
	public string[] Roles { get; set; }
	public DateTime ExpireyDate { get; set; }
}



2. OAuth WCF Rest Service
首先创建一个 WCF Rest Service:

定义一个基础数据模型,供Sample访问:
namespace OAuthWcfRestService
{
 public class Contact
 {
 public int Id { get; set; }
 public string Name { get; set; }
 public string Email { get; set; }
 public string Owner { get; set; }
 }

 public class DataModel
 {
 public static List Contacts;

 static DataModel()
 {
  Contacts = new List {
  new Contact(){ Id=0, Name="Felix", Email="Felix@test.com", Owner = "jane" },
  new Contact(){ Id=1, Name="Wendy", Email="Wendy@test.com", Owner = "jane"},
  new Contact(){ Id=2, Name="John", Email="John@test.com", Owner = "john"},
  new Contact(){ Id=3, Name="Philip", Email="Philip@mail.com", Owner = "john"}
  };
 }
 }
}
Contacts 中的数据只有属于 Owner 的“用户”才可以访问,因此 OAuthService 中实现如下:
namespace OAuthWcfRestService
{
 [ServiceContract]
 [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
 public class OAuthService
 {
 [WebGet(UriTemplate = "Contacts")]
 public List Contacts()
 {
  var name = Thread.CurrentPrincipal.Identity.Name;
  return DataModel.Contacts.Where(c => c.Owner == name).ToList();
 } 
 }
}
上面的 name 从 Thread.CurrentPrincipal.Identity.Name 而来,即访问当前服务的客户端ID。这个ID是由OAuth服务的(Interceptor)实现由 AccessToken(String) 转换成服务端用户模型。

在 web.config 中,利用 WCF 对 ASP.NET 的兼容机制,使用 Form 认证:定义了两个用户:john 和 jane

 
 
 
 
 
 
 
 
 
 
 
 

并修改 Global.asax 的 WebServiceHostFactory,改为 OAuthWebServiceHostFactory
public class Global : HttpApplication
{
 void Application_Start(object sender, EventArgs e)
 {
 RegisterRoutes();
 }

 private void RegisterRoutes()
 {
 var oauthWebServiceHostFactory = new OAuthChannel.OAuthWebServiceHostFactory 
 { 
  AccessTokenRepository = OAuthServicesLocator.AccessTokenRepository,
  OAuthProvider = OAuthServicesLocator.Provider 
 };
 RouteTable.Routes.Add(new ServiceRoute("OAuthService", oauthWebServiceHostFactory, typeof(OAuthService)));
 }
}

作为一个基本的OAuth授权服务,我们还需要提供:
1. 获取 RequestToken 的服务
2. 获取 AccessToken 的服务
RequestToken.ashx :返回 RequestToken

using System;
using System.Web.UI;
using DevDefined.OAuth.Framework;
using DevDefined.OAuth.Provider;

namespace OAuthWcfRestService
{
 public partial class RequestToken : System.Web.IHttpHandler
 {
 public bool IsReusable
 {
  get { return true; }
 }

 public void ProcessRequest(System.Web.HttpContext context)
 {
  IOAuthContext oauthContext = new OAuthContextBuilder().FromHttpRequest(context.Request);
  IOAuthProvider provider = OAuthManager.Provider;
  IToken token = provider.GrantRequestToken(oauthContext);
  context.Response.Write(token);
  context.Response.End();
 }
 }
}
AccessToken.ashx :交换 RequestToken 返回 AccessToken

using System;
using System.Web.UI;
using DevDefined.OAuth.Framework;
using DevDefined.OAuth.Provider;

namespace OAuthWcfRestService
{
 public partial class AccessToken : System.Web.IHttpHandler
 {
 public bool IsReusable
 {
  get { return true; }
 }

 public void ProcessRequest(System.Web.HttpContext context)
 {
  IOAuthContext oauthContext = new OAuthContextBuilder().FromHttpRequest(context.Request);
  IOAuthProvider provider = OAuthManager.Provider;
  IToken accessToken = provider.ExchangeRequestTokenForAccessToken(oauthContext);
  context.Response.Write(accessToken);
  context.Response.End();
 }
 }
}
当然我们还需要提供用户登录和授权的页面:Login.aspx 和 UserAuthorize.aspx Form登录就不累述了, UserAuthorize.aspx 中实现授权的方法如下:
private void ApproveRequestForAccess(string tokenString)
{  
 OAuthChannel.Models.RequestToken requestToken = RequestTokenRepository.GetToken(tokenString);
 var accessToken = new OAuthChannel.Models.AccessToken
    {
    ConsumerKey = requestToken.ConsumerKey,
    Realm = requestToken.Realm,
    Token = Guid.NewGuid().ToString(),
    TokenSecret = Guid.NewGuid().ToString(),
    UserName = HttpContext.Current.User.Identity.Name,
    ExpireyDate = DateTime.Now.AddMinutes(1),
    Roles = new string[] { }
    };
 AccessTokenRepository.SaveToken(accessToken);
 requestToken.AccessToken = accessToken;
 RequestTokenRepository.SaveToken(requestToken);
}

3. 应用

Default.aspx 发起请求获取RequestToken,授权成功后回调 Callback.ashx

namespace OAuthConsumerSample
{
 public partial class _Default : Page
 {
 protected void oauthRequest_Click(object sender, EventArgs e)
 {
	 OAuthSession session = OAuthSessionFactory.CreateSession();
  IToken requestToken = session.GetRequestToken();
  if (string.IsNullOrEmpty(requestToken.Token))
  {
  throw new Exception("The request token was null or empty");
  }
  Session[requestToken.Token] = requestToken;
  string callBackUrl = "http://localhost:" + HttpContext.Current.Request.Url.Port + "/Callback.ashx";
  string authorizationUrl = session.GetUserAuthorizationUrlForToken(requestToken, callBackUrl);
  Response.Redirect(authorizationUrl, true);
 }
 }
}
Callback.ashx
namespace OAuthConsumerSample
{
 public partial class Callback : System.Web.IHttpHandler, System.Web.SessionState.IRequiresSessionState
 {
 public void ProcessRequest(System.Web.HttpContext context)
 {
  var session = OAuthSessionFactory.CreateSession();
  string requestTokenString = context.Request["oauth_token"];
  var requestToken = (IToken)context.Session[requestTokenString];
  IToken accessToken = session.ExchangeRequestTokenForAccessToken(requestToken);
  context.Session[requestTokenString] = null;
  context.Session[accessToken.Token] = accessToken;
  context.Response.Redirect("ViewData.ashx?oauth_token=" + accessToken.Token);
 }

 public bool IsReusable
 {
  get { return true; }
 }
 }
}

热心网友 时间:2022-05-04 02:27

连续2天的Peyote实验后(你可能会找到更好的放松办法),结论终于呈现在你眼前:Amazon是拥有最大的、使用最多的在线网络API的网络服务之一,并且根本不支持OAuth!
经过一个下午长时间的狂想之后,你最终败下阵来,并看到Amazon是如何保持API请求安全的。你不清楚为什么,但读完整页关于如何为Amazon网络服务装配一个请求后,你依然觉得不完全合理。这个“签名”和什么连在一起?代码示例中的“data”参数是什么?这样,你会继续查找关于“安全API设计”的文章。。。
当遇到其他人问同样的问题时,你看到一些指出"HMAC"或其他事物的优秀回复,但还是不太确定。

你找到其他鼓励你使用“HMAC”的文章并且你正H-FINE地使用它,如果有人将“HMAC”解释成简明的H_ENGLISH的话。
你的确偶遇了一个有道理的蒸馏的基本概念,它是这样一简明的英语描述的:
一个服务器和客户端知道一个公钥和一个私钥;只有服务器和客户端知道私钥,但每个人都知道公钥。。。但不关心别人所知道的。
一个客户端生成一个唯一的HMAC(哈希)表示它到服务器的请求。通过把请求数据(参数和值或XML/JSON或任何它计划发送的数据)以及请求数据的散列blob和私钥结合来实现。
客户端随后将这个HASH以及所有它将要发送的参数和值一并发给服务器。
服务器接到请求,并使用与客户端相同的方式重新生成自己独有的基于提交值的HMAC(哈希)。
然后,服务器比较这两个HMAC,如果相同,服务器就信任这个客户端并执行请求。

这似乎很直截了当。最初让你困惑的是,你以为原始请求是经过加密传送的,但实际上,HMAC方法所做的一切只是使用只有客户端和服务器才知道的私钥将参数生成为一些独特的校验和(哈希)。
随后,客户端将这个校验和及原始参数和值发给服务器,然后服务器复核校验和(哈希)以确定它接受客户端所发的请求。
因为根据假设,只有在客户端和服务器知道私钥,我们假设如果他们的哈希匹配,那么它们会互相信任,以至服务器随即正常处理这个请求。
你知道在现实中,这就相当于某人过来对你说:“Jimmy让我告诉你把钱给Johnny”,但你不知道这个人是谁,所以你要伸出手去试探他,看看他是否知道这个秘密握手。

如果三次握手证明无误,则通讯继续进行,否则中断通讯。.

你明白了大概是怎么回事,但还是想会不会还有更好的方法呢?还好,有tarsnap网站 tarsnap帮你答疑解惑。看看亚马逊是如何解决签名认证问题的Amazon screwed this up with Signature Version 1.

看完了亚马逊的web service是如何鉴权的,re-read how Amazon Web Services does authentication 讲的确实有道理,整个流程如下:
[客户端]在调用REST API之前,首先将待发送消息体打包, combine a bunch of unique data together(websevice端将要接收的数据)

[客户端]用系统分派的密钥使用哈希(最好是HMAC-SHA1 or SHA256 ) 加密(第一步的数据).

[客户端]向服务器发送数据:

用户身份认证信息例如,用户ID,客户ID或是其他能别用户身份的信息。这是公共API,大家都能访问的到(自然也包括了那些居心叵测的访问者)系统仅仅需要这部分信息来区分发信人而不考虑可靠与否(当然可以通过HMAC来判断可靠性).

发送生成的HMAC码.
发送消息体(属性名和属性值),如果是私有信息需要加密,像是(“mode=start&number=4&order=desc”或其他不重要的信息)直接发送即可.
(可选项)避免重放攻击 “replay attacks” o的唯一办法就是加上时间戳。在使用HMAC算法时加入时间戳,这样系统就能依据一定的条件去验证是否有重放的请求并拒绝.

[服务器端]接收客户端发来的消息.
[服务器端] (参看可选项)检查接收时间和发送时间的间隔是否在允许范围内(5-15分)以避免重放攻击replay attacks.
提示: 确保待检对象的时区无误daylight savings time
更新: 最近得到的结论就是直接使用UTC时区而无需考虑DST的问题 use UTC time .
[服务器端]使用发送请求中用户信息(比如.API值)从数据库检索出对应的私匙.

[服务器端]

跟客户端相同,先将消息体打包然后用刚得到的私匙加密(生成HMAC)消息体.
(参看可选项) 如果你使用了加入时间戳的方式避免重放攻击,请确保服务端生成的加密信息中拥有和客户端相同的时间戳信息以避免中间人攻击man-in-the-middle attack.
[服务器端] 就像在客户端一样,使用HMAC哈希加密刚才的信息体.

[服务器端]

将服务器端刚生成的哈希与客户端的对比。如果一致,则通讯继续;否则,拒绝请求!

提示: 在打包消息体的时候一定要考虑清楚,如果像亚马逊进行签名版本1中信息识别那样会面临哈希冲突的问题 open yourself up to hash-collisions! (建议:将整个包含URL的请求加密即可!)
特别提示:私匙绝对不能在通讯过程中传递,它仅仅用来生成HMAC,服务器端会自动查询出它的私匙并重新生成自己的HMAC.我来翻译公匙仅仅用来区分不同的用户,即使被破解也无所谓。因为此时的消息无需判断其可靠性,服务端和客户端还是要通过私匙来加密(比如,前缀、后缀,倍数等等)

10/13/11更新:Chris最近发现 pointed out 如果在HMAC计算中加入了URI或是HTTP请求/回复,攻击者更易通过更改末端或是HTTP方法来搞破坏。比如,在HTTP POST方法中将/issue/create改成/user/delete。多谢Chris的提醒!

总结

经过几天的煎熬,你终于设计出了一个安全的API访问机制,感到很骄傲吧。更值得骄傲的是,通过这种设计方式很好的避开了另一种常见的API访问危机:劫持 side-jacking.
会话劫持是通过嗅探出的会话ID来破解短时效的操作数(比如,1小时内).但采用上面的设计就很好的避免了这一点,因为通讯过程中往来操作均被校验过,根本无需生成会话ID.

兴奋啊.
但是你慢慢的意识到在某些时候确实还是得使用OAuth,have to implement OAuth, 也许就是还未成熟的 isn’t quite ready yet OAuth2.0吧 OAuth 2.0 support
我也刚接触RESTful架构没多长时间,只是关注了一下客户端的包文件部分 client-side libraries.
如果上文中遗漏了什么,请帮忙之处,我会尽快修订。如果您对上文中有任何的意见和建议,请留下您的宝贵意见。
或者,也可以给我发邮件,一起讨论一下!

留观室(待解决问题)

<此处已删除,只要你使用UTC时区 using UTC time 就不会有时差问题了,所以我的提议也没什么意义了.>

题外话

如果是为Twitter这种模式开发API呢,那会有成千上万台手机上安装被植入公匙私匙的应用private keys embedded in the app。
在设备上,用户很可能破解出应用中的私匙,这不是很危险么?
是的,是很危险。
那该怎么办呢?
按照Twitter的说法,这种情况无法避免。应用需要它的私匙(它们称之为密匙),那也意味着在安全方面要做出妥协。
目前能做的就是生成应用级别的私匙而非用户级别的。那样的话,如果应用被黑了可以禁用直到发布包含新生成私匙的新版本应用。

如果刚更新的私匙又被黑了怎么办?
对,这很有可能。你可以自己再加密原有的私匙,或是祈求自己的应用不会再被黑掉.
无所谓,你甚至可以再加密一次私匙但至少这样你就能在凶险的网络环境中使用自己的新应用了。这总比账户被锁,应用无法使用要好得多.

更新#1:在评论中有很多很好的建议和观点,我在这引述几条:
使用nonce “nonce”来避开重放攻击 (每次都采用不同的token)并实现idempotentcy implement idempotentcy in your API.
上面的算法跟双数据模式的OAutho1.0有95%的相似“95% similar to ‘two-legged’ OAuth 1.0“,所以还是看一下OAuth的说明为妙.
通过SSL发送消息可以减少复杂的安全性访问设计sending all traffic to go over SSL (HTTPS)!
更新 #2: 我又看了一下双数据模式的OAuth,正如大家指出的,确实跟上面的过程很像。使用OAuth的唯一好处就是有很多现成的客户端库文件 OAuth client libraries

关于OAuth需要注意的地方:
OAuth的说明文档super-specific 指出了如何在HMAC计算中完成转码,排序并打包(在OAuth叫“方法标识“)
在OAuth使用HMAC-SHA1加密时,会要求发送端提供一个简述随机数。并以此简述随机数和时间戳来确保请求的唯一性(潜在的“重放攻击” )原理上,你可以在数据存储后失效这些数据,但最好还是把它们保存在文件中以便后用.
简述随机数可以公开。它就是在关联某个时间戳时附加的一些字段。它们的关系就好像一个指纹来确保“12:22pm 收到一条包含HdjS872djas83的消息请求 “这就杜绝了使用中间人重放攻击的可能,因为所有HMAC加密算法中都同时含有简述随机数和时间戳。如果受到攻击,它会告诉你”这条消息两小时前处理过了,你想干什么呢?!”
这些所有的数据都以逗号分隔并塞入很大的鉴权HTTP包头中,而并非通过GET方法传值.
这确实是双数据模式的OAuth的过人之处。HMAC计算时需要的请求数据无需改变就原封不动的放在那里,发送过程中的简述随机数和时间戳也一样就连发送请求中的参数也一样.

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com