【问题标题】:Handling expired token in Laravel在 Laravel 中处理过期令牌
【发布时间】:2015-10-05 14:48:41
【问题描述】:

在 laravel 5 中处理过期令牌的最佳方法是什么。

我的意思是我有一个页面,它有一些执行 ajax 请求的链接。当页面加载时它们工作正常,但是当我等待某个时间时,我得到一个 TOKEN MISMATCH 错误。

现在,我必须刷新页面才能让它再次工作。但是,我不想刷新页面。我想要一些方法来刷新令牌或其他一些解决方法来修复它。

希望你明白我的意思。

【问题讨论】:

  • 您使用的是 ajax 还是单页应用程序?
  • 我会发布一个解决方法
  • 发送 csrf 令牌,如果 ajax req 是 post 类型,如果它的 get 类型 ajax 应该在没有 mismtach 的情况下工作 yoken 在你的接受 get 或 post 方法的路由中是特定的。
  • 我真的很喜欢这个问题。我真的被这个问题困扰了。现在我将在下面写下我的答案。

标签: php laravel laravel-5


【解决方案1】:

2021 年更新:

你好 Stackoverflow!我们几年前发布的答案似乎引发了一些争议。

总而言之,我们发布的方法确实解决了问题的技术方面。但是,从网络安全的角度来看,这似乎是值得商榷的。

凭借我们有限的专业知识,我们仍然相信我们的解决方案是可行的,但为减少疑问,请务必仔细阅读 cmets 部分以及Ryan 发布的答案,因为他们不这么认为在你做出决定之前。谢谢。

2015 年的原始答案

解决它的方法是每隔一定时间实际获取新令牌,否则您将违背 csrf 令牌的目的:

<html>
    <head>
        <meta name="csrf_token" content="{{ csrf_token() }}">
    </head>
    <body>
        <script type="text/javascript">
            var csrfToken = $('[name="csrf_token"]').attr('content');
            
            setInterval(refreshToken, 3600000); // 1 hour 
            
            function refreshToken(){
                $.get('refresh-csrf').done(function(data){
                    csrfToken = data; // the new token
                });
            }

            setInterval(refreshToken, 3600000); // 1 hour 

        </script>
    </body>
</html>

在 laravel 路由中

Route::get('refresh-csrf', function(){
    return csrf_token();
});

如果有任何语法错误,我很抱歉,很长时间没有使用 jquery,但我想你明白了

【讨论】:

  • 这样写的内容违背了 csrf_token 的要点。您应该传递当前令牌,并且仅在旧令牌有效时才返回新令牌。否则,恶意人员可能会点击 refresh-csrf 路由来获取令牌并使用它来发出通常会受到保护的任何请求。
  • 我在这里错过了什么@jfadich 并评论支持者吗?仅仅能够向 csrf 端点发出请求并不能让攻击者有办法击败 csrf 保护。攻击者仍然需要以某种方式将令牌放入用户的会话中(例如使用 SOP 错误或 XSS 漏洞),所以我不知道上面的评论是什么意思。
  • 这个解决方案是可行的,特别是对于我们这些使用$.ajaxSetup({ headers: {'X-CSRF-TOKEN': csrfToken} });的人来说,但是,你也应该$("input[name=_token]").val(data);
  • @jfadich,您能否详细说明“恶意人员”如何访问受身份验证保护的“刷新-csrf 路由”?请分享你的智慧!
  • @berrunder,当我说“auth protected”时,我的意思是 auth protected。您对 auth-protected 路由的请求将在任何 csrf 被添加到任何内容之前下降。刷新 csrf 用于不将您的输入发送到过期 void,以防用户仍处于活动状态。
【解决方案2】:

增加您的会话的lifetime。您可以通过在 laravel 配置中编辑 config/session.php 文件来实现。

/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/

'lifetime' => 120,

【讨论】:

  • 是的,你说得对。但如果 dat 时间已过且令牌已过期,则为 wt。
  • 好的,但是如果您希望使用超过两个小时的会话,那么您应该根据预期的时间范围设置该值。意味着,如果您的用户应该能够在两小时或一周后再次使用他的会话,那么这就是您应该使用的设置。任何其他解决方案都不是一种干净的方式 - 至少在我看来 ^^
  • 但是有时候用户没有登录,会抛出token不匹配的错误。我不明白为什么会这样。
  • 几周前我不得不再次处理这样的问题。在进行任何其他 ajax 调用之前,使用 ajax 请求获取新令牌。没有干净的方法,但由于我们必须处理一些问题,所以这种方法(在我的特定情况下)是最好的方法。看看这个答案:stackoverflow.com/questions/31449434/…
【解决方案3】:

我在这个案例中结合了两件事:

1.延长会话寿命

//In config/session.php replace this:

'lifetime' => 120

//with:

'lifetime' => 360

Laravel 5 默认生命周期为 120(分钟),您可以将其更改为您喜欢的任何值,例如 360(6 小时)

2。捕获异常并显示错误消息

//In app/Exceptions/Handler.php replace this:

public function render($request, Exception $e)
{
    if ($e instanceof ModelNotFoundException) {
        $e = new NotFoundHttpException($e->getMessage(), $e);
    }

    return parent::render($request, $e);
}

//with:

public function render($request, Exception $e)
{
    if ($e instanceof ModelNotFoundException) {
        $e = new NotFoundHttpException($e->getMessage(), $e);
    }

    if ($e instanceof \Illuminate\Session\TokenMismatchException) {            
        return redirect('/')->withErrors(['token_error' => 'Sorry, your session seems to have expired. Please try again.']);
    }

    return parent::render($request, $e);
}

所以基本上你将用户重定向到根目录“/”(你可以将它更改为你想要的任何路径)并显示错误消息,并且在该页面上你必须这样做才能显示错误消息:支持>

@if ($errors->has('token_error'))
    {{ $errors->first('token_error') }}
@endif

【讨论】:

  • 我非常努力地通过这个解决方案,因为我发现它非常优雅:没有令牌 = 没有会话,所以重定向闪烁错误。问题是错误被放置在会话中,但随着重定向创建另一个错误而被更新,因此在打印错误时它就消失了。我真的被困在这里了 2 天。
  • @Xavi - 我不知道你到底有什么,但如果你使用重定向尝试添加会话错误消息作为参数,这样你就可以在新页面上拥有它。
  • 如果您重定向到登录页面并出现令牌不匹配错误错误怎么办?这是一个好方法吗??
  • 所以当令牌过期时,您将用户重定向到主页并显示“哈哈,你来不及了,slowpoke”消息?这很“方便” :) 想象一下用户在写了几个小时的文章并在失去所有工作的同时获得了“有用”的信息后的满意度。
  • @ITDesigns.eu - 我们都经历过这种情况并且令人沮丧,但与此同时,当我们将应用程序打开 2 周及之后,我们不能责怪它我们想从同一个地方继续。这是不可能的 !您想做什么而不是显示消息?每 1 分钟调用一次 ajax 并让用户知道他的会话何时过期?我不认为这是方法。所以一个不错的方法是增加会话时间,同时显示错误消息。用户会话过期不是你的错,是他的。
【解决方案4】:

我认为最好的选择是取 config/session.php 文件的生命周期配置,然后在 JavaScript 代码中将生命周期值乘以 60 * 1000。使用 laravel 提供的辅助函数 config(),它可能是这样的:

<script type="text/javascript">
    var timeout = ({{config('session.lifetime')}} * 60) * 1000;
    setTimeout(function() {
        //reload on current page
        window.location = '';
    }, timeout);
</script>

【讨论】:

  • 假设用户在您的网站表单上写了一个小时长的文章,他在笔记本电脑退出待机状态后点击了提交按钮。噗去文章了。
  • 我忘了说setTimeoutterrible when tab is out of focus
【解决方案5】:

我认为@UX Labs 的回答具有误导性。 然后@jfadich 的评论似乎完全不正确。

对于 2017 年 5 月的 Laravel 5.4,我这样解决了这个问题:

这是一个有效的答案

web.php:

Route::post('keep-token-alive', function() {
    return 'Token must have been valid, and the session expiration has been extended.'; //https://stackoverflow.com/q/31449434/470749
});

在你看来的javascript中:

$(document).ready(function () {

    setInterval(keepTokenAlive, 1000 * 60 * 15); // every 15 mins

    function keepTokenAlive() {
        $.ajax({
            url: '/keep-token-alive', //https://stackoverflow.com/q/31449434/470749
            method: 'post',
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        }).then(function (result) {
            console.log(new Date() + ' ' + result + ' ' + $('meta[name="csrf-token"]').attr('content'));
        });
    }

});

请注意,您必须VerifyCsrfToken.php 的排除项中列出'keep-token-alive'。正如@ITDesigns.eu 在评论中所暗示的那样,这条路线重要的是要验证当前存在一个有效的令牌并且它只需要延长其到期时间。

为什么这种方法可以解决我的问题

我的 Laravel 网站允许用户观看视频(一小时长),它使用 ajax 每分钟发布他们的进度。

但许多用户加载页面后,直到数小时后才开始播放视频。

我不知道为什么他们会在观看之前将浏览器选项卡打开这么久,但他们确实如此。

然后我的日志中会出现大量的 TokenMismatch 异常(并且会错过它们的进度数据)。

session.php 中,我将 'lifetime' 从 120 分钟更改为 360 分钟,但这仍然不够。而且我不想让它超过6小时。所以我需要启用这一页以通过ajax频繁扩展会话。

如何测试它并了解令牌的工作原理:

web.php:

Route::post('refresh-csrf', function() {//Note: as I mentioned in my answer, I think this approach from @UX Labs does not make sense, but I first wanted to design a test view that used buttons to ping different URLs to understand how tokens work. The "return csrf_token();" does not even seem to get used.
    return csrf_token();
});
Route::post('test-csrf', function() {
    return 'Token must have been valid.';
});

在你看来的javascript中:

<button id="tryPost">Try posting to db</button>
<button id="getNewToken">Get new token</button>

(function () {
    var $ = require("jquery");

    $(document).ready(function () {
        $('body').prepend('<div>' + new Date() + ' Current token is: ' + $('meta[name="csrf-token"]').attr('content') + '</div>');
        $('#getNewToken').click(function () {
            $.ajax({
                url: '/refresh-csrf',
                method: 'post',
                headers: {
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                }
            }).then(function (d) {
                $('meta[name="csrf-token"]').attr('content', d);
                $('body').prepend('<div>' + new Date() + ' Refreshed token is: ' + $('meta[name="csrf-token"]').attr('content') + '</div>');
            });
        });
        $('#tryPost').click(function () {
            $.ajax({
                url: '/test-csrf',
                method: 'post',
                headers: {
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                }
            }).then(function (d) {
                $('body').prepend('<div>' + new Date() + ' Result of test: ' + d + '</div>');
            });
        });


    });
})();

session.php 中,暂时将'lifetime' 更改为非常短的内容以进行测试。

然后玩。

这就是我如何了解 Laravel 令牌的工作原理以及我们真正需要如何频繁地成功 POST 到受 CSRF 保护的路由以使令牌继续有效。

【讨论】:

  • 这应该在不久前得到支持。令牌和会话超时相关但不相同。您可以通过 laravel-caffeine 之类的东西保持有效的会话,但仍然让表单的 csrf_token 过期。这有助于我看到光明。
  • 请注意,我对另一个答案的评论确实是正确的,但不适用于您所写的答案。主要区别在于您 POST 到带有 CSRF 令牌的路由,而另一个答案使用没有 CSRF 保护的 GET 请求。
  • 很简单,谢谢!使用 axios 更容易,因为它会在每个请求中自动发送最新的XSRF-TOKEN。你可以这样做:setInterval(() =&gt; axios.post('/keep-token-alive'), 1000 * 60 * 15);
  • 如果用户设备在会话期间进入睡眠状态,这将不起作用,因为当用户睡眠时,setInvall 也会进入睡眠状态:stackoverflow.com/a/6347336/2311074
  • 另外,没有必要将keep-token-alive 设为帖子或 CSRF 保护路由。
【解决方案6】:

这都是我不喜欢的解决方法..(但我承认它们可以工作) 我不知道 Laravel 上存在女巫版本,但有一种方法可以从 CSRF 令牌验证中排除页面:

https://laravel.com/docs/5.5/csrf

只需在 VerifyCsrfToken Middleware 上的 $except 数组上添加一条记录,其中包含您要排除的 uri。 请注意,这只能在特定情况下进行。

这对我来说是最好的解决方案...简单,就像(几乎)Laravel 上的所有东西一样,他们已经考虑过了。 ;)

【讨论】:

【解决方案7】:

一种短而快的方法.... 用于处理 ajax 请求,当令牌过期时:将此脚本添加到主布局或文档的末尾

$(window).load(function(){
    $.ajaxSetup({
        statusCode: {
            419: function(){
                    location.reload(); 
                }
        }
    });
});

为了在令牌过期时处理 http 请求,请在此路径中创建 419.blade.php:\resources\views\errors 并将此脚本添加到其中:

<script type="text/javascript">
    //reload on current page
    window.location = '';

</script>
【解决方案8】:

循环导航令牌通常被认为是一种糟糕的方法,但使用上面提到的 js 计时器也存在问题。当浏览器选项卡不是焦点、最小化或在许多用户的情况下,他们的笔记本电脑/设备处于睡眠/关闭等情况时,js seetTimeout/setInterval 是不可靠的。

更好的方法可能是使用 js 计时器从 cookie 中设置的日期戳重新计算“死亡时间”(或用于挑剔的 GDPR 无 cookie 用户的元标记)。此日期戳将是会话将终止的真实世界(时区)时间,并在每次页面刷新时更新。这样一来,您不在时浏览器/设备在做什么/不做什么都无关紧要,并且对于那些使用“让我登录”等的人来说仍然是准确的。

下一个问题是如何做而不是自动刷新令牌 - 向用户提供一个“重新登录”表单(模式/弹出窗口),如上所述将新令牌添加到页面。

【讨论】:

  • 不知道为什么这被否决了。浏览器中的计时器是可怕的计时器。在显示用户会话即将超时的警告模式之前,我们使用 setInterval() 检查当前服务器时间戳以匹配会话 cookie 过期时间。同样应该用于刷新 CSRF 令牌。
【解决方案9】:

您可以尝试Caffeine for Laravel package 它设置一个时间间隔,然后像某些答案中建议的那样刷新令牌,它也会自动添加到具有 csrf 令牌的每个表单中

【讨论】:

    【解决方案10】:

    处理此异常的最佳方法是使用App\Exceptions\Handler.php

    public function render($request, Exception $e) {
    
            if ($e instanceof \Illuminate\Session\TokenMismatchException) {            
                return Redirect::back()->withErrors(['session' => 'Désolé, votre session semble avoir expiré. Veuillez réessayer.']);
            }
    
            return parent::render($request, $e);
        }
    
    
    

    以及您想在哪里显示此消息(在您所有包含 csrf_token 的页面中),添加此部分:

    <div>
    @if(count($errors)>0)
        @foreach($errors->all() as $error)
            <ul>
                <li>{{$error}}</li>
            </ul>
        @endforeach
    @endif
    </div>
    
    
    【解决方案11】:

    根据docs

    Laravel 自动为每个活跃用户生成一个 CSRF “token” 由应用程序管理的会话。

    这意味着,对于任何个人,csrf 代码对于用户访问的任何页面都是相同的。一旦您的会话到期,它就会失效。因此,如果您将生命周期设置为 1 周,CSRF 令牌只会在 1 周后过期。

    这可以在config/session.php中这样实现:

     /*
        |--------------------------------------------------------------------------
        | Session Lifetime
        |--------------------------------------------------------------------------
        |
        | Here you may specify the number of minutes that you wish the session
        | to be allowed to remain idle before it expires. If you want them
        | to immediately expire on the browser closing, set that option.
        |
        */
    
        'lifetime' => 60 * 24 * 7, // Set session lifetime to 1 week
    
        'expire_on_close' => true,
    

    为什么我不喜欢以上任何一个答案:

    1. UX 实验室的回答:

    保持会话永远活跃,并在固定时间后重新创建一个新的 CSRF token。如果用户打开了多个水龙头,这是一个问题。每次点击刷新CSRF令牌时,所有其他选项卡都将失效。

    1. Ryan 的回答

    这个答案更好,因为它不会改变 CSRF token,所以不会影响多个选项卡。它只是通过在setInterval 的固定时间后进行 js 调用来保持会话活跃。但是,setInterval 在 PC 进入睡眠状态时不起作用。因此,当 PC 进入睡眠状态时,会话可能会过期,这也是一种可能的情况。因此,与其尝试通过 js-calls 保持会话活动,不如增加生命周期。

    1. paulalexandru 的回答

    在会话超时时显示错误是可以的,但如果问题永远不会发生会更好。将生命周期设置为 6h 是不够的,因为标签可能会打开几天。

    1. 其他答案

    所有其他答案都建议为有问题的路由禁用 CSRF,但这当然不是选择,因为它会产生很大的安全风险。

    【讨论】:

    • 这不会创建一个巨大的数据库表/文件列表(取决于您用来保存会话的文件),会话价值可能长达 50 年?
    • @user125661 是的。我实际上遇到了问题,所以我将会话生命周期更改为一周并使用redis。调整答案。
    • 好的!您仍然在原因 2 中提到“50 年”,为什么您不喜欢其他答案。
    • @user125661 谢谢我调整了它。但最终,它可能只取决于您的流量有多大,您的会话有多少容量。一个好的解决方案是足够长的时间以避免会话超时但避免服务器上的溢出。
    • 是的。我刚刚在下面发布了一个解决方案,它不需要您延长会话寿命,可以打开多个选项卡,并且可以在会话过期的情况下使用。
    【解决方案12】:

    我有一个简单的解决方案:

    • 不需要您延长会话生命周期。
    • 适用于打开多个标签。
    • 如果会话因设备关闭而超时,也可以使用。

    在/routes/web.php:

    $router->get('csrf-token', function() {
       return request()->session()->token();
    });
    

    这只是返回当前的 csrf 令牌。

    • 如果在调用此路由时令牌无效(例如设备长时间关闭时),它将返回一个新令牌,该令牌是通过启动会话创建的。
    • 如果仍然有有效的令牌,它将被退回。由于调用此路由会延长会话,因此令牌的生命周期也会延长。

    因为这只会在必要时返回一个新令牌,所以在打开多个选项卡时没有问题,如 @Adam 所述。

    您只需要确保每 X 分钟调用一次上述路由(其中 X 是您的会话生命周期 - 5 分钟),并更新任何 _token 输入。我这样做如下(我在这里使用momentjs和axios):

    handleNewCsrfToken();
    
    // Use visbility API to make sure the token gets updated in time, even when the device went to sleep.
    document.addEventListener('visibilitychange', function() {
        if (document.visibilityState === 'visible') {
            setTimeoutToRefreshCsrfToken();
        } else if (document.visibilityState === 'hidden') {
            clearTimeout(refreshCsrfTokenTimeout);
        }
    });
    
    function handleNewCsrfToken() {
        updateCsrfTokenTimeoutTarget();
        setTimeoutToRefreshCsrfToken();
    }
    
    function updateCsrfTokenTimeoutTarget() {
        csrfTokenTimeoutTarget = moment().add(2, 'hour').subtract(5, 'minute');
    }
    
    function setTimeoutToRefreshCsrfToken() {
        refreshCsrfTokenTimeout = setTimeout(refreshCsrfToken, csrfTokenTimeoutTarget.diff());
    }
    
    function refreshCsrfToken() {
        axios.get('/csrf-token').then(function(response) {
            document.getElementsByName('_token').forEach(function(element) {
                element.value = response.data;
    
                handleNewCsrfToken();
            });
        });
    }
    

    【讨论】:

    • 这似乎是服务器端最现代的方法。您可能可以将return request()-&gt;session()-&gt;token(); 替换为return csrf_token();,因为助手也是这样做的。
    • 我相信您的解决方案与stackoverflow.com/a/31451123/2311074 相同,对吧?另外我可能对多标签问题有误。
    • 这违背了 CSRF 令牌的目的,因为任何人都可以在向您的 CSRF 保护表单发出 POST 请求之前向您的新 URI 发出请求。
    • @BinarWeb 任何请求 CSRF 的人都会得到一个专门绑定到他自己会话的。所以我看不出这会如何破坏 CSRF 代币的目的。
    • 为了安全起见,您必须在后端接受 POST 方法,而不是 GET。并且必须要求一个新的令牌,提供实际的令牌。这个解决方案是个好主意,但问题是如果计算机关闭(或选项卡更改)超过 2 小时,setTimeout 将工作以请求后端路由 atm 选项卡再次处于活动状态以获取新的 CSRF 令牌,如果它接受 POST 方法将不起作用,因为当前的 CSRF 已经过期。并且后端路由会响应 419 错误。
    【解决方案13】:

    在你的主布局文件中试试这个

    @guest
        <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Expires" content="0" />
        <meta http-equiv="refresh" content="{{config('session.lifetime') * 60}}">
    @endguest
    

    【讨论】:

    • 这会无意中刷新您的页面,如果您正在使用它,这不是您想要的。
    猜你喜欢
    • 2021-10-19
    • 2019-12-11
    • 2016-11-13
    • 2023-03-20
    • 1970-01-01
    • 2019-03-21
    • 2017-02-22
    • 2021-09-06
    相关资源
    最近更新 更多