manukn / oauth2-server-proxify-laravel
SPA JavaScript API 调用的 Laravel 代理包
Requires
- php: >=5.5.0
- guzzlehttp/guzzle: ~5.2
- laravel/framework: 5.2.*
README
摘要
简介
此包最初由 Michele Andreoli 创建,但该包已过时,无法在 Laravel 版本 5 中使用。我已经将其修改为与 Laravel 5 兼容。
此包将是解决Alex Bilbie 提出的问题的方案。他说
假设你刚刚创建了一个Angular/Ember/Backbone 等单页网络应用,该应用通过ajax调用从你编写的API获取所有数据。你还选择通过OAuth来保护API,并且也通过SSL(如OAuth规范所要求的)来保护API端点。
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使用这些来确保请求来自已知的来源。由于没有后端支持这个web应用,这些将不得不存储在前端代码中,并且它们不能在代码中加密,因为你不能在JavaScript中安全地执行加密。因此,我们已经有了这样一个问题:唯一识别web应用的方法——通过使用凭证——已经泄露在公开的代码中,这将允许攻击者尝试进行认证请求,而与app无关。你也不能使用引用头来锁定请求,因为它们很容易被伪造。你也不能在cookie中以加密形式存储凭证,因为这个cookie可以像源代码中嵌入的客户端凭证一样轻易地被攻击者抓取。
继续前进,在请求的响应中,服务器已经给了我们一个访问令牌,用于认证对API的请求,以及一个刷新令牌,用于在它过期时获取一个新的访问令牌。
首先,我们有这个问题:访问令牌现在对攻击者可用。他现在不需要任何其他东西来向你的API发出请求,并且可以疯狂地抓取所有用户的私有数据并执行API允许的任何操作。服务器没有任何方式知道这不是web应用发出的请求。
所以,因为这个是你编写的应用,并且它与你的后端通信,所以你决定使用资源所有者密码凭证授权(也称为密码授权)来获取访问令牌。访问令牌然后可以用来认证API请求。
web应用将向API发出ajax请求以在捕获到凭证后登录用户(添加了换行以提高可读性)。这是一个有效的OAuth 2密码授权访问令牌请求的示例
来自web应用的请求
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有短期访问令牌,刷新令牌也包含在浏览器响应中,因此攻击者可以使用它来在原始令牌过期时获取新的访问令牌。
简单的说法是,你无法在前端保持安全。所以不要这样做。
那么,如何在单页web应用中安全地使用OAuth呢?
这很简单;通过一个轻量级的后端组件代理所有的API调用。这个组件(我们不妨称之为代理)将验证来自用户会话的Ajax请求。访问令牌和刷新令牌可以以加密的形式存储在cookie中,只有代理能够解密。应用客户端凭证也将硬编码到代理中,因此也不公开可访问。
首先,为了验证用户,Web应用将仅携带用户的凭证和客户端ID,向代理发送请求,不包括客户端密钥!
POST /ajax/auth HTTP/1.1
Host: example.com
grant_type=password
&username=admin
&password=mypassword
&client_id=webapp
然后,代理将添加只有它知道的客户端密钥,并将请求转发到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
}
代理将令牌加密存储在cookie中,并向用户返回成功消息。
当Web应用向API端点发送请求时,它将调用代理而不是直接调用API
GET /ajax/resource/123 HTTP/1.1
Cookie: <encrypted cookie with tokens>
Host: example.com
代理将解密cookie,向请求中添加授权头,并将请求转发到API
GET /resource/123 HTTP/1.1
Authorization: Bearer DDSHs55zpG51Mtxnt6H8vwn5fVJ230dF
Host: api.example.com
代理将直接将响应传递给浏览器。
在这种设置下,没有公开可见的或明文客户端凭证或令牌,这意味着攻击者不会向API发送伪造的请求。此外,因为浏览器不再直接与API通信,您可以将其从公共互联网上移除,并锁定防火墙规则,仅允许来自Web服务器的请求。
为了防止攻击者仅窃取cookie,您可以使用CSRF保护措施。
感谢Alex Bilbie提出问题:[http://alexbilbie.com/2014/11/oauth-and-javascript](http://alexbilbie.com/2014/11/oauth-and-javascript)
安装
运行以下composer命令
composer require manukn/oauth2-server-proxify-laravel
设置
- 将
Manukn\LaravelProxify\ApiProxyServiceProvider::class,添加到app/config/app.php中的服务提供者列表。 - 将
Proxify' => Manukn\LaravelProxify\Facades\ApiProxyFacade',添加到app/config/app.php中的别名列表。
配置
为了使用Api代理,首先发布其配置
php artisan vendor:publish
然后,编辑文件app/config/proxy.php以适应您的需求。
使用
在app/config/routes.php中添加一个新的端点
Route::any('proxify/{url?}', function ($url) { return Proxify::makeRequest(Request::method(), Request::all(), $url); })->where('url', '(.*)');
这是您的代理端点,然后您可以调用代理来获取访问令牌(client_id参数是可选的)
POST proxify/example.com/oauth/access_token HTTP/1.1
Host: example.com
&grant_type=password
[&client_id=webapp]
&username=admin
&password=mypassword
然后您可以调用受保护的资源
POST proxify/example.com/protected_resource HTTP/1.1
Host: example.com
如果access_token过期并且您有refresh_token,ApiProxy将代表您调用OAuth服务器并使用新的一个刷新access_token。之后,它将对受保护的资源进行新的调用。
这个ApiProxy包与oauth2-server-laravel配合得很好,这是由Luca Degasperi编写的。我已经使用了这个包进行测试。
门面
ApiProxy通过Facade Proxify或通过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许可证发布。