首页 > 解决方案 > Vue 前端与 CSRF 中间件蛋糕后端交互

问题描述

我正在开发一个带有 Vue 前端和 CakePHP 3.8 后端的应用程序。目前,我正在考虑在开始应用程序的实际编码之前设置安全性。我正在研究的一件事是我在 Cake 中的 API 端点的 CSRF 保护。

我正在做的是让 Vue 在页面加载时在 created() 方法中从 Cake 检索 CSRF 令牌。然后,Vue 将需要在对我的 EndPoints 的请求中使用 CSRF 令牌。然后我用检索到的令牌设置了一个 CsrfToken cookie。我的 CSRF 中间件在到达后端时将发送的令牌与此 cookie 进行比较。如果此 cookie 为空或值不匹配,则会引发错误 InvalidCsrfTokenException。

如何在请求参数中添加新条目“_csrfToken”?

这是一个通过 CSRF 中间件检查的请求。

object(Cake\Http\ServerRequest) {
    trustProxy => false
    [protected] params => [
        'controller' => 'Customers',
        'action' => 'add',
        'pass' => [],
        'plugin' => null,
        '_matchedRoute' => '/{controller}/{action}/*',
        '_ext' => null,
        '_csrfToken' => '37dfc3327fe642bce88a6aca79c222921b75b752c855b683d9043d8cbbd59ab6ceb44cb3b6b3350aa54d6d1e04d011d0ccec7273150e12b58b4ef23faa47ac3b',
        '_Token' => [
            'unlockedFields' => []
        ],
        'isAjax' => false
    ]

这是我目前的请求。问题在于我没有在 $this->request->params 中设置可以与 csrf Cookie 进行比较的“_csrfToken”。

{ "controller": "Placetostays", 
"action": "apitest", 
"pass": [], 
"plugin": null, 
"_matchedRoute": "/{controller}/{action}/*", 
"_ext": null }

发出表单 POST 请求时出现以下错误。这是正常的,因为我的请求参数中没有设置“_csrfToken”。

Cake 错误日志中的错误堆栈跟踪

2020-07-16 09:25:16 Error: [Cake\Http\Exception\InvalidCsrfTokenException] Missing CSRF token cookie (C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Middleware\CsrfProtectionMiddleware.php:230)
#0 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Middleware\CsrfProtectionMiddleware.php(154): Cake\Http\Middleware\CsrfProtectionMiddleware->_validateToken(Object(Cake\Http\ServerRequest))
#1 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Middleware\CsrfProtectionMiddleware.php(122): Cake\Http\Middleware\CsrfProtectionMiddleware->_validateAndUnsetTokenField(Object(Cake\Http\ServerRequest))
#2 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(65): Cake\Http\Middleware\CsrfProtectionMiddleware->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response), Object(Cake\Http\Runner))
#3 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(51): Cake\Http\Runner->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#4 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Routing\Middleware\RoutingMiddleware.php(168): Cake\Http\Runner->run(Object(Cake\Http\MiddlewareQueue), Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#5 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(65): Cake\Routing\Middleware\RoutingMiddleware->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response), Object(Cake\Http\Runner))
#6 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Routing\Middleware\AssetMiddleware.php(88): Cake\Http\Runner->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#7 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(65): Cake\Routing\Middleware\AssetMiddleware->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response), Object(Cake\Http\Runner))
#8 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Error\Middleware\ErrorHandlerMiddleware.php(96): Cake\Http\Runner->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#9 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(65): Cake\Error\Middleware\ErrorHandlerMiddleware->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response), Object(Cake\Http\Runner))
#10 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\debug_kit\src\Middleware\DebugKitMiddleware.php(53): Cake\Http\Runner->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#11 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(65): DebugKit\Middleware\DebugKitMiddleware->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response), Object(Cake\Http\Runner))
#12 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Runner.php(51): Cake\Http\Runner->__invoke(Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#13 C:\wampserver\www\wamp_projects\holidays_backend\vendor\cakephp\cakephp\src\Http\Server.php(97): Cake\Http\Runner->run(Object(Cake\Http\MiddlewareQueue), Object(Cake\Http\ServerRequest), Object(Cake\Http\Response))
#14 C:\wampserver\www\wamp_projects\holidays_backend\webroot\index.php(40): Cake\Http\Server->run()
#15 {main}
Request URL: /placetostays/apitest
Referer URL: http://localhost:8080/formtesting

Vue 前端

<template>
  <div id="formtest">
        
        <div id="formdiv">
          <form v-on:submit.prevent="addPlace">
            <h2>Add Placetostay</h2>
            <br>
            <input class="input" type="text" id="name" v-model="name" placeholder="name"><br>
            <input class="input" type="text" id="city" v-model="city" placeholder="city"><br>    
            <input class="input" type="number" id="postal_code" v-model="postcode" placeholder="postal code"><br> 
            <input class="input" type="text" id="street" v-model="street" placeholder="street"><br>
            <input class="input" type="number" id="house_number" v-model="housenum" placeholder="house number"><br>        
            <input class="input" type="tel" id="tel_number" v-model="telnum" placeholder="phone number"><br><br>             
            <input type="submit" value="Submit">  <button v-on:click="addtoArray">Next</button> 
          </form>           
        </div>

  </div>
</template>

<script>
        
    import $ from 'jquery';   
    
    var token = "";
    
    
    // you will use v-model & data on edits.. 
    export default{
       name :'formtest', 
       data(){
          return{
            user: '',
            name: '',
            city: '',
            postcode: '',
            street: '',
            housenum: '',
            telnum: '',
            post_data: '',
            errors: [],
            data: [],
            token: '',
            total_payload: [],
            }
        },
       
       methods: {
            
            addPlace(){
                 
               this.data[0] = this.name;
               this.data[1] = this.city;   
               this.data[2] = this.postcode;
               this.data[3] = this.street;     
               this.data[4] = this.housenum;
               this.data[5] = this.telnum;   

               // THE QUESTION: HOW DO I SET THIS COOKIE IN MY PARAMS['_csrfToken']?
                        
               this.total_payload.push(this.data);   
               console.log(this.total_payload);            
                        
                        
               var url = 'http://wampprojects/holidays_backend/placetostays/apitest/';
               // this accesses the pass parameter in $this->request->params, does not create new params parameter.. 
               // var url = 'http://wampprojects/holidays_backend/placetostays/apitest/csrftoken:bar';

               // in order for CSRF Middleware & Security Component to work, need to be able to access request parameters 
               // _csrfToken & Token values.. 
               fetch(url, {
                   method: 'POST',
                   mode: 'cors',
                   headers: {
                            'Content-Type': 'application/json'
                          },
                   body: JSON.stringify(this.total_payload),
                        })
                   .then(response => response.json())
                   .then(json_data => this.post_data = json_data) 
                   .catch(error => {

                           console.log("error");

                        });                  

                   this.name = this.city = this.postcode = this.street = this.housenum = this.telnum = '';                        
                        
                    } 
                } 
            },           
       },
       
       created(){
           
            fetch('http://wampprojects/holidays_backend/placetostays/', {
              method: 'GET',
              mode: 'cors',
              headers: {
                'Content-Type': 'application/json',             
              },
            })
            .then(response => response.json())  
            .then(json_data => this.token = json_data) 
            .then(json_data => {
              
               token = json_data;
                // need to initialize the session for when no one is logged in yet..
                // this will be very important!!!  
                this.$session.start();   
                console.log(this.$session.getAll());   
                
                this.$cookies.set('theme', 'default');
                // when to set the cookie value? 
                this.$cookies.set('csrfToken', token['token']);                
                
            })    
            .catch(error => {

               console.log("error");

            }); 
       },
    }
    
</script>

蛋糕控制器后端

public function apitest(){

    $data = $this->request->data;        
    $sendback = "";   
    
    // this check is necessary, will otherwise cause problem at startup 
    if($data){
                    
        // where exactly does the middleware perform this test? when the call arrives @ backend..  
        
        foreach($data as $newplace):
            
            $new_placetostay = $this->Placetostays->newEntity();
            $new_placetostay->name = $newplace[0];
            $new_placetostay->city = $newplace[1]; 
            $new_placetostay->postal_code = $newplace[2];
            $new_placetostay->street = $newplace[3]; 
            $new_placetostay->number = $newplace[4];
            $new_placetostay->tel_number = $newplace[5];   
            
            $this->Placetostays->save($new_placetostay);
            
        endforeach;
                     
    }

    // no automatic view, only data returned
    $this->autoRender = false;
    
    $this->response = $this->response->cors($this->request)
        ->allowOrigin(['http://localhost:8080'])
        ->allowMethods(['GET', 'POST'])
        ->allowHeaders(['*'])
        ->allowCredentials()
        ->exposeHeaders(['Link'])
        ->maxAge(300)
        ->build();          
    
    return $this->response
    ->withType('application/json')
    ->withStringBody(json_encode($parameters));            
    
}

编辑:我正在尝试为控制器“placetostays”创建一个单独的路由范围,但是在对该控制器进行 API 调用时它仍然给我一个 CSRF 错误。我究竟做错了什么?

Router::scope('/', function (RouteBuilder $routes) {
    $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true
    ]));
    $routes->applyMiddleware('csrf');
    
    $routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
    $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
    
    $routes->fallbacks(DashedRoute::class);
    
});

Router::scope('/apitest', function (RouteBuilder $routes) {
    
    // in this controller I want to have JWT enabled 
    // I would expect CSRF middleware here not to throw an error, since I do not load it for this scope.. 
    
});

标签: vue.jssecuritycakephpcsrf

解决方案


我认为你有错误的设置。您正在端口 8080 上开发 vuejs,并且在单独的环境中运行 cakephp。从 csrf 的角度来看,主机是不同的。您应该:

  1. 在 cake 模板中运行 vuejs 编译版本并根据 cakephp 文档加载所有前端资产。虽然这种方法有效,但它已经过时且蹩脚,因为您每次编译 vuejs 时都必须适应和复制,并且您将失去开发服务器的能力。您仍然可以在不加载 CsrfMiddleware 的 CakePhp 中使用您的设置和范围用于开发目的的路线。无论哪种方式,我建议您执行以下操作:

  2. 在他们自己的环境中运行 cakephp 和 vuejs,并用 csrf 代替 jwt。文档中提到了一个专用的 cakephp 插件 ( https://github.com/ADmad/cakephp-jwt-auth )。您应该在 cake 中创建一个不加载 CsrfMiddleWare 并加载 JWT 插件的范围路由。在您的控制器中相应地配置身份验证,您的后端应该很好。在您的 Vuejs 方面,您需要获取一个令牌,或者通过设置授权标头

Csrf 令牌和 jwt 是不同的,用于不同的目的。Csrf 应该在同一主机上工作。这意味着在您的 wamp 服务器中,前端和后端应位于同一主机上。你的设置没有。

Jwt 令牌被构建为在同一主机或不同主机上运行。每个令牌都可以与一个用户相关联,或者在适用的情况下可以是匿名的。

POST、PUT、PATCH 和 DELETE Http 方法应始终受到保护。JWT 是一种更好的方法,因为您将处理更“现代”的分离的前端和后端。

随便问什么

编辑

在 routes.php 中添加:

Router::prefix('api', function(RouteBuilder $routes) {
$routes->fallbacks(DashedRoute::class);
});

创建 src/Controllers/Api/AppController.php 并添加:

<?php
namespace App\Controller\Api;
use Cake\Controller\Controller;
use Cake\Event\Event;
class AppController extends Controller
{
public function initialize()
{
    parent::initialize();

    $this->loadComponent('RequestHandler');

$this->loadComponent('Auth', [
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ],
            'ADmad/JwtAuth.Jwt' => [
                'userModel' => 'Users',
                'fields' => [
                    'email' => 'id'
                ],

                'parameter' => 'token',

                // Boolean indicating whether the "sub" claim of JWT payload
                // should be used to query the Users model and get user info.
                // If set to `false` JWT's payload is directly returned.
                'queryDatasource' => true,
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login',
            'prefix' => 'api'
        ],
         // If unauthorized, return them to page they were just on
        'unauthorizedRedirect' => $this->referer()
    ]);
}
}

这是发生了什么:

您在 router.php 中为路由添加前缀。CakePhp 推断它应该加载命名空间 App\Controllers\Api\AppController.php。您使用您的 Jwt 信息初始化该类,这里假设您使用电子邮件作为用户名进行身份验证,您可以根据需要更改所有这些。

请注意,您还在 routes.php 中声明了一个备用路由。每个指向 api/* 的请求都将解析为一个控制器,即 'api/users' 假定 src/Controllers/Api/UsersController.php 存在,否则会抛出一个缺少的控制器异常。在您的每个控制器中,在命名空间 App\Controllers\Api 中,您现在可以控制对不同视图的访问。例如,在 src/Controlelrs/Api/UsersController.php 中,您需要向 Auth 对象添加允许约束,并包含不需要 JWT auth 的路由:

$this->Auth->allow(['register', 'token']); 

这意味着 api/users/register 和 api/users/token 路由设置为覆盖 JWT 身份验证约束,这是有道理的,因为通常您需要一个端点供用户声明 JWT 令牌,并且您希望用户注册。


推荐阅读