thinkingmik / api-proxy-laravel
JavaScript API 调用的 Laravel 代理包
Requires
- php: >=5.4.0
- guzzlehttp/guzzle: ~5.0
This package is not auto-updated.
Last update: 2024-09-28 17:49:10 UTC
README
摘要
简介
此包是针对由 Alex Bilbie 提出的问题的解决方案。他说
假设你已经开发了一个闪亮的 Angular/Ember/Backbone 等单页网络应用程序,它通过 AJAX 调用从你编写的 API 获取所有数据。你还选择了使用 OAuth 保护 API,并且你还使用 SSL 保护 API 端点(因为 OAuth 规范要求这样做)。
POST /auth HTTP/1.1
Host: api.example.com
grant_type=password
&client_id=webapp
&client_secret=abc123
&username=admin
&password=mypassword
服务器将响应
{
"access_token": "DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF",
"refresh_token": "24QmIt2aV1ubaenB2D6G0se5pFRk4W05",
"token_type": "Bearer",
"expires": 1415741799
}
已经存在一些主要问题。
首先,在我们的应用程序请求中,我们发送了客户端 ID 和密钥,API 使用这些信息来确保请求来自已知来源。由于没有后端支持这个网络应用程序,这些信息将不得不存储在前端代码中,并且它们不能在代码中加密,因为你不能在 JavaScript 中安全地执行加密。所以,我们已经有一个问题,那就是唯一能够识别网络应用程序的方法——使用凭证——已经泄露到公共代码中,这会让攻击者尝试独立于应用程序进行身份验证请求。你也不能使用 referrer 头来锁定请求,因为它们很容易被伪造。你也不能在 cookie 中以加密形式存储凭证,因为攻击者可以像抓取嵌入到源代码中的客户端凭证一样轻松地抓取那个 cookie。
继续前进,在请求的响应中,服务器给了我们一个访问令牌,用于对 API 的请求进行身份验证,以及一个刷新令牌,用于在它过期时获取新的访问令牌。
首先,我们有一个问题,即访问令牌现在对攻击者可用。他不需要其他任何东西就可以向你的 API 发送请求,并疯狂地抓取所有用户的私有数据,并执行 API 允许的任何操作。服务器无法知道这些请求不是由网络应用程序发出的。
因此,因为这是一个你编写的应用程序,并且它与你的后端进行通信,所以你决定使用 资源所有者密码凭证授权(也称为 密码授权)来获取访问令牌。然后可以使用访问令牌对 API 请求进行身份验证。
网络应用程序将向 API 发送 AJAX 请求以在捕获其凭证后登录用户(在此添加换行以方便阅读)。这是有效的 OAuth 2.0 密码授权访问令牌请求应该看起来像
来自网络应用程序的有效请求
GET /resource/123 HTTP/1.1
Authorization: Bearer DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF
Host: api.example.com
来自攻击者的有效请求
GET /resource/123 HTTP/1.1
Authorization: Bearer DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF
Host: api.example.com
即使你的 API 有短暂的访问令牌,刷新令牌也包含在浏览器响应中,因此攻击者可以在原始令牌过期时使用它来获取新的访问令牌。
简单的故事是,你无法在前端安全地保持事情的安全。所以不要这样做。
那么,如何在单页网络应用程序中安全地使用 OAuth 呢?
很简单;通过一个轻量级的后端组件proxy来代理所有API调用。这个组件(从现在开始我们称之为proxy)将验证来自用户会话的ajax请求。访问令牌和刷新令牌可以以加密形式存储在cookie中,只有proxy可以解密。应用程序客户端凭证也将硬编码到proxy中,因此它们也不公开可用。
首先,为了验证用户,Web应用程序将只带用户凭证和客户端ID向proxy发送请求,不包括客户端密钥!
POST /ajax/auth HTTP/1.1
Host: example.com
grant_type=password
&username=admin
&password=mypassword
&client_id=webapp
然后proxy将添加只有它知道的客户端密钥,并将请求转发到API
POST /auth HTTP/1.1
Host: api.example.com
grant_type=password
&username=admin
&password=mypassword
&client_id=webapp
&client_secret=abc123
服务器将响应
{
"access_token": "DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF",
"refresh_token": "24QmIt2aV1ubaenB2D6G0se5pFRk4W05",
"token_type": "Bearer",
"expires": 1415741799
}
proxy将令牌加密存储在cookie中,并向用户返回成功消息。
当Web应用程序向API端点发送请求时,它将调用proxy而不是直接调用API
GET /ajax/resource/123 HTTP/1.1
Cookie: <encrypted cookie with tokens>
Host: example.com
proxy将解密cookie,向请求中添加授权头,并将其转发到API
GET /resource/123 HTTP/1.1
Authorization: Bearer DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF
Host: api.example.com
proxy将直接将响应传递回浏览器。
在这种配置下,没有公开可见或明文形式的客户端凭证或令牌,这意味着攻击者不会向API发送伪造的请求。此外,因为浏览器不再直接与API通信,您可以将其从公共互联网中移除,并锁定防火墙规则,以便只有来自Web服务器的请求才被允许。
为了防止攻击者仅窃取cookie,您可以采取CSRF保护措施。
感谢Alex Bilbie提出的问题: http://alexbilbie.com/2014/11/oauth-and-javascript
安装
将以下行添加到composer.json的require部分
{
"require": {
"thinkingmik/api-proxy-laravel": "1.x"
}
}
设置
- 将
'ThinKingMik\ApiProxy\ApiProxyServiceProvider',添加到app/config/app.php中的服务提供者列表。 - 将
'Proxify' => 'ThinKingMik\ApiProxy\Facades\ApiProxyFacade',添加到app/config/app.php中的别名列表。
配置
为了使用Api Proxy,首先发布其配置
php artisan config:publish thinkingmik/api-proxy-laravel
然后编辑文件app/config/packages/thinkingmik/api-proxy-laravel/proxy.php以适应您的需求。
使用
在app/config/routes.php中添加一个新的端点
Route::match(array('GET', 'POST'), '/proxify', function() { return Proxify::makeRequest(Request::method(), Input::all()); });
这是您的代理端点,然后您可以调用proxy以获取访问令牌(client_id参数是可选的)
POST public/proxify HTTP/1.1
Host: example.com
uri=http://example.com/public/oauth/access_token
&grant_type=password
[&client_id=webapp]
&username=admin
&password=mypassword
然后您就可以调用受保护的资源
POST public/proxify HTTP/1.1
Host: example.com
uri=http://example.com/public/protected_resource
如果access_token过期并且您有一个refresh_token,ApiProxy将代表您调用OAuth服务器并使用新的access_token刷新它。之后,它将再次调用受保护的资源。
这个ApiProxy包与oauth2-server-laravel配合得很好,由Luca Degasperi编写。我已经为我的测试使用了这个包。
外观
ApiProxy通过Proxify Facade或通过IOC容器中的代理服务提供。可用的方法是
/** * Use this method in the laravel route file * @param $method * @param array $inputs * @return Response * @throws ProxyMissingParamException */ Proxify::makeRequest(Request::method(), Input::all());
许可
此包采用MIT许可证发布。