cellcote/laravel-proxify

用于SPA JavaScript API调用的Laravel代理包

v0.1.2 2016-05-24 09:18 UTC

This package is not auto-updated.

Last update: 2024-09-26 00:21:26 UTC


README

Software License Build Status Code Quality Total Downloads

摘要

简介

此包最初由Michele Andreoli创建,但该包已过时,不支持Laravel版本5。我已经将其修改以支持Laravel 5。

此包将解决由Alex Bilbie提出的问题。他说

假设你已经开发了一个单页Web应用,例如Angular/Ember/Backbone,它通过Ajax调用从API获取所有数据。你还将API通过OAuth进行安全保护,并且使用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应用的方式——通过使用凭据——已经在公开代码中泄露,这将允许攻击者尝试独立于应用进行认证请求。你也不能使用引用头锁定请求,因为它们很容易被伪造。你也不能在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

安装

运行以下composer命令

composer require cellcote/laravel-proxify

设置

  1. Cellcote\LaravelProxify\ApiProxyServiceProvider::class,添加到app/config/app.php中的服务提供者列表。
  2. Proxify' => Cellcote\LaravelProxify\Facades\ApiProxyFacade',添加到app/config/app.php中的别名列表。

配置

为了使用Api Proxy,首先发布其配置

php artisan vendor:publish

之后,编辑app/config/proxy.php文件以适应您的需求。

使用

app/config/routes.php中添加一个新的端点

Route::any('proxify/{url?}', function($url) {
	return Proxify::makeRequest(Request::method(), Input::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_tokenApiProxy将为您调用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许可下发布。