milotischler/laravel-proxify

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

v0.1.1 2015-12-14 21:20 UTC

This package is auto-updated.

Last update: 2024-09-07 19:01:05 UTC


README

Software License Build Status Code Quality Total Downloads

摘要

简介

此包最初由 Michele Andreoli 创建,但由于包已过时且不适用于Laravel版本5,因此我对其进行了修改以适应Laravel 5。

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

假设你已经开发了一个闪亮的Angular/Ember/Backbone等单页Web应用,它通过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
}

这里已经存在了主要问题。

首先,在应用的请求中,我们发送了API使用的客户端ID和密钥,以确保请求来自已知的来源。由于没有Web应用的后端,这些必须存储在前端代码中,并且不能在代码中加密,因为无法在JavaScript中安全地进行加密。因此,我们已经有了问题,即通过使用凭据来识别Web应用的方式已经泄露在公共代码中,并且允许攻击者尝试独立于应用进行身份验证请求。您也不能使用引用头锁定请求,因为它们很容易被伪造。您也不能在cookie中以加密的形式存储凭据,因为该cookie可以像嵌入源代码中的客户端凭据一样容易被攻击者抓取。

继续前进,在请求的响应中,服务器已经给了我们一个访问令牌,用于对API进行身份验证请求,以及一个刷新令牌,用于在它过期时获取新的访问令牌。

首先,我们有一个问题,即访问令牌现在对攻击者可用。他现在不需要其他任何东西来对您的API进行请求并且疯狂地抓取所有用户的私人数据并执行API允许的任何操作。服务器没有任何方法知道这不是Web应用在发起请求。

因此,由于这是一个您编写的应用,它与您的后端进行通信,您已经决定使用 resource owner password credentials grant(也称为 password grant)来获取访问令牌。访问令牌可以用来对API请求进行身份验证。

Web应用将发出ajax请求到API,在您捕获到他们的凭据后对用户进行登录(添加换行符以提高可读性)。这是一个有效的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,将Authorization头添加到请求中,并将请求转发到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代理,首先需要发布其配置。

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许可证发布。