简体   繁体   English

在 Laravel 中处理过期的令牌

[英]Handling expired token in Laravel

What is the best way to handle expired tokens in laravel 5.在 laravel 5 中处理过期令牌的最佳方法是什么?

I mean I have a page and it has some links which perform ajax requests.我的意思是我有一个页面,它有一些执行 ajax 请求的链接。 They work fine when the page is loaded but when I wait for sometime then I get a TOKEN MISMATCH error.当页面加载时它们工作正常,但是当我等待一段时间时,我收到一个 TOKEN MISMATCH 错误。

Now, I have to refresh the page to make it work again.现在,我必须刷新页面以使其再次工作。 BUT, I don't want to refresh the page.但是,我不想刷新页面。 I want some way to refresh the token or some other work around to make it fix.我想要一些方法来刷新令牌或其他一些解决方法来修复它。

I hope you got my point.我希望你明白我的意思。

I think the answer by @UX Labs is misleading.我认为@UX Labs 的回答具有误导性。 And then the comment from @jfadich seems completely incorrect.然后@jfadich 的评论似乎完全不正确。

For Laravel 5.4 in May 2017, I solved the problem this way:对于 2017 年 5 月的 Laravel 5.4,我是这样解决问题的:

Here Is an Answer That Works这是一个有效的答案

In web.php :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
});

In javascript in your view:在您看来,在 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'));
        });
    }

});

Note that you must not list 'keep-token-alive' in the exclusions within VerifyCsrfToken.php .请注意,必须清单'keep-token-alive'在中排除VerifyCsrfToken.php As @ITDesigns.eu implied in a comment, it's important for this route to verify that there is a valid token currently and that it just needs to have its expiration extended.正如@ITDesigns.eu 在评论中暗示的那样,此路由必须验证当前是否存在有效令牌并且只需要延长其到期时间。

Why this approach solves my problem为什么这种方法可以解决我的问题

My Laravel site allows users to watch a video (an hour long), and it uses ajax to post their progress every minute.我的 Laravel 站点允许用户观看视频(一个小时),它每分钟使用 ajax 发布他们的进度。

But many users load the page and then don't start the video until many hours later.但是许多用户加载页面然后直到几个小时后才开始播放视频。

I don't know why they leave their browser tab open so long before watching, but they do.我不知道他们为什么在观看之前让浏览器标签打开这么久,但他们确实这样做了。

And then I'd get a ton of TokenMismatch exceptions in my logs (and would miss out on the data of their progress).然后我会在我的日志中得到大量 TokenMismatch 异常(并且会错过他们的进度数据)。

In session.php , I changed 'lifetime' from 120 to 360 minutes, but that still wasn't enough.session.php ,我将'lifetime'从 120 分钟更改为 360 分钟,但这还不够。 And I didn't want to make it longer than 6 hours.我不想让它超过 6 个小时。 So I needed to enable this one page to frequently extend the session via ajax.所以我需要启用这个页面来频繁地通过ajax扩展会话。

How you can test it and get a sense for how the tokens work:如何测试它并了解令牌的工作原理:

In web.php :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.';
});

In javascript in your view:在您看来,在 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>');
            });
        });


    });
})();

In session.php , temporarily change 'lifetime' to something very short for testing purposes.session.php ,为了测试目的,暂时将'lifetime'更改为非常短的内容。

Then play around.然后随便玩玩。

This is how I learned how the Laravel token works and how we really just need to successfully POST to a CSRF-protected route frequently so that the token continues to be valid.这就是我了解 Laravel 令牌如何工作的方式,以及我们如何真正需要频繁地成功 POST 到受 CSRF 保护的路由,以便令牌继续有效。

Update 2021: 2021 年更新:

Hello Stackoverflow!你好 Stackoverflow! It seems that the answer we've posted a few years ago has sparked some controversy.似乎我们几年前发布的答案引发了一些争议。

To sum it up, the approach we've posted does solve the technical aspect of the problem.总而言之,我们发布的方法确实解决了问题的技术方面。 However, from web security standpoint it seems to be debatable.然而,从网络安全的角度来看,这似乎是有争议的。

With our limited expertise, we still believe our solution is viable, but to reduce doubt please make sure to go through the comments section as well as the answer posted by Ryan since they think otherwise before you make your decision.由于我们的专业知识有限,我们仍然相信我们的解决方案是可行的,但为了减少疑问,请务必仔细阅读评论部分以及Ryan发布的答案,因为在您做出决定之前,他们另有想法。 Thanks.谢谢。

Original Answer From 2015 2015 年的原始答案

a work around for it, is to actually get the new token every certain time, otherwise you are defeating the purpose of the csrf token:解决它的方法是在每个特定时间实际获取新令牌,否则您将违背 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>

In laravel routes在 Laravel 路线中

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

I apologize in case of any syntax errors, haven't used jquery for long time, but i guess you get the idea如果出现任何语法错误,我深表歉意,很久没有使用 jquery,但我想你明白了

I combine 2 things for this case:对于这种情况,我结合了两件事:

1. Increase session lifetime 1.增加会话寿命

//In config/session.php replace this:

'lifetime' => 120

//with:

'lifetime' => 360

Laravel 5 default lifetime is 120 (minutes), you can change it to whatever value you like, for example 360 (6 hours) Laravel 5 默认生命周期为 120(分钟),您可以将其更改为您喜欢的任何值,例如 360(6 小时)

2. Catch the exception and display an error message 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);
}

So basicaly you redirect the user to the root "/" (you can change this to any path you want) with an error message and on that page you have to do this to display the error message:因此,基本上您将用户重定向到根“/”(您可以将其更改为您想要的任何路径)并显示错误消息,并且在该页面上您必须执行此操作以显示错误消息:

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

According to the docs :根据文档

Laravel automatically generates a CSRF "token" for each active user session managed by the application. Laravel 会为应用程序管理的每个活动用户会话自动生成一个 CSRF “令牌”。

This means, for any individual the csrf code is the same for any page that the user visits.这意味着,对于任何个人,用户访问的任何页面的 csrf 代码都是相同的。 It becomes invalid once your session expires.一旦您的会话过期,它就会失效。 Thus if you set the lifetime to 1 week, CSRF token will only expire after 1 week.因此,如果您将生命周期设置为 1 周,CSRF 令牌只会在 1 周后过期。

This can achieved like this in config/session.php :这可以在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,

Why I dont like any of the above answers:为什么我不喜欢以上任何一个答案:

  1. Answer from UX Labs:来自 UX 实验室的回答:

Keeps the session forever alive and recreates a new CSRF token after a fixed time.使会话永远保持活动状态并在固定时间后重新创建新的 CSRF token This is an issue if the user has multiple taps open.如果用户打开多个水龙头,这是一个问题。 Everytime one tap refreshes the CSRF token, all the other tabs become invalid.每次点击刷新CSRF令牌时,所有其他选项卡都会失效。

  1. Answer from Ryan瑞安的回答

This answer is better, because it does not change the CSRF token , so multiple tabs are not effected.这个答案更好,因为它不会更改 CSRF token ,因此不会影响多个选项卡。 It simply keeps the session alive with making a js call after a fixed time with setInterval .它只是通过在setInterval固定时间后进行 js 调用来保持会话活动。 However, setInterval does not work while the PC goes to sleep.但是,当 PC 进入睡眠状态时, setInterval不起作用。 So the session may expire when the PC goes to sleep, which is also a likly scenario.因此,当 PC 进入睡眠状态时,会话可能会过期,这也是一种可能的情况。 Therefore, instead of trying to keep the session alive by js-calls,just increase lifetime.因此,与其尝试通过 js 调用保持会话处于活动状态,不如增加生命周期。

  1. Answer from paulalexandru来自 paulalexandru 的回答

Displaying an error when session is timed out is okay, but it would be better if the issue never happens.当会话超时时显示错误是可以的,但如果问题永远不会发生会更好。 Setting lifetime to 6h is not enough, because its likly that a tab may be open for a couple of days.将生命周期设置为 6 小时是不够的,因为标签页很可能会打开几天。

  1. Other answers其他答案

All the other answers propose to disable CSRF for the route in questions, but this is of course no option, because it creates a big security risk.所有其他答案都建议为问题中的路由禁用 CSRF,但这当然不是选项,因为它会带来很大的安全风险。

Best way to handle this Exception is with App\\Exceptions\\Handler.php .处理此异常的最佳方法是使用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);
    }


and where ever you wanna show this message (in all your pages that contains csrf_token ), add this piece:以及您想在何处显示此消息(在包含csrf_token所有页面中),添加以下内容:

<div>
@if(count($errors)>0)
    @foreach($errors->all() as $error)
        <ul>
            <li>{{$error}}</li>
        </ul>
    @endforeach
@endif
</div>

try this in your main layout file在你的主布局文件中试试这个

@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

Increase the lifetime of your sessions.增加会话的lifetime You can do so by editing the config/session.php file in your laravel configuration.您可以通过编辑 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,

a short and fast way.... for handling ajax requests,when token expire : add this script to the end of master layout or your document一种简短而快速的方法.... 用于处理 ajax 请求,当令牌过期时:将此脚本添加到主布局或文档的末尾

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

and for handling http requests when token expires, create 419.blade.php in this path: \\resources\\views\\errors and add this script to it:为了在令牌过期时处理 http 请求,请在此路径中创建 419.blade.php:\\resources\\views\\errors 并将此脚本添加到其中:

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

</script>

I have a simple solution that:我有一个简单的解决方案:

  • Doesn't require you to extend the session lifetime.不需要您延长会话寿命。
  • Works with multiple tabs open.适用于打开多个选项卡。
  • Also works if the session did time out because the device was turned off.如果会话确实因为设备关闭而超时,也可以使用。

in /routes/web.php:在 /routes/web.php 中:

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

This simply returns the current csrf token.这只是返回当前的 csrf 令牌。

  • In case the token is invalid by the time this route gets called (for example when the device was turned off for a long time), it will return a new token, which was created by starting the session.如果在调用此路由时令牌无效(例如,当设备长时间关闭时),它将返回一个新令牌,该令牌是通过启动会话创建的。
  • In case there still is a valid token, it will be returned.如果仍然有一个有效的令牌,它将被返回。 Since calling this route will extend the session, the token lifetime is extended as well.由于调用此路由会延长会话,因此令牌生命周期也会延长。

Because this only returns a new token when necessary, there are no problems when having multiple tabs open as described by @Adam.因为这仅在必要时返回一个新令牌,所以在打开多个选项卡时没有问题,如@Adam 所述。

You just need to make sure to call the above route every X minutes (where X is your session lifetime - 5 minutes), and update any _token inputs.您只需要确保每 X 分钟调用一次上述路由(其中 X 是您的会话生存期 - 5 分钟),并更新任何_token输入。 I do this as follows (i use momentjs and axios here):我这样做如下(我在这里使用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();
        });
    });
}

I know it is not the best solution, but one of the simplest. 我知道这不是最好的解决方案,而是最简单的解决方案之一。 Just disable CSRF protection for that page, where user spends a lot of time. 只需禁用该页面的CSRF保护,用户就会花费大量时间。 For example on my site, they can write article for hours on one page. 例如,在我的网站上,他们可以在一页上写几个小时的文章。 And it is very frustrating if you can't save article because of CSRF protection. 如果由于CSRF保护而无法保存文章,这将非常令人沮丧。

Circum-navigating the token is generally accepted as a terrible approach but there are problems with using js timers mentioned above too.循环导航令牌通常被认为是一种糟糕的方法,但使用上面提到的 js 计时器也存在问题。 js seetTimeout/setInterval is unreliable when the browser tab is either not it focus, minimised or in the case of many users, thier laptop/device is sleeping/closed etc.当浏览器选项卡未聚焦、最小化或在许多用户的情况下,他们的笔记本电脑/设备处于睡眠/关闭状态等时,js seetTimeout/setInterval 是不可靠的。

A better route might be to use a js timer to recalculate the 'time to die' from a datestamp set in a cookie (or meta tag for fussy GDPR no-cookie users).更好的方法可能是使用 js 计时器根据 cookie 中设置的日期戳(或针对挑剔的 GDPR 无 cookie 用户的元标记)重新计算“死亡时间”。 this datestamp will be the realworld (timezoned) time the session will die and is updated every page refresh.此日期戳将是会话结束的真实世界(时区)时间,并在每次页面刷新时更新。 This way, it doesn't matter what the browser/device was doing/not doing whilst you were away AND it'll still be acurate for those with 'keep me signed in' etc.这样,当您离开时浏览器/设备在做什么/不做什么并不重要,并且对于那些具有“保持登录状态”等的人来说仍然是准确的。

The next problem is what to do instead of auto resfreshing the token - present the user with a 're-login in' form (modal/popup) that ajaxes the new token to the page as mentioned above.下一个问题是如何做而不是自动刷新令牌 - 向用户显示“重新登录”表单(模式/弹出窗口),将新令牌添加到上述页面。

您可以尝试使用Caffeine for Laravel 包,它设置一个间隔,然后按照某些答案中的建议刷新令牌,并且它将自动添加到具有 csrf 令牌的每种形式中

I think the best option is to take the lifetime configuration of the config/session.php file, then the lifetime value multiplied by 60 * 1000 in the javascript code.我认为最好的选择是在 config/session.php 文件中进行生命周期配置,然后在 javascript 代码中将生命周期值乘以 60 * 1000。 Use helper function config() provided by laravel, it might look like this:使用 laravel 提供的辅助函数 config() ,它可能看起来像这样:

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

This are all workarounds that I dont like.. (but I admit they can work) I dont know since witch version this exists on Laravel, but there is a way to exclude pages from CSRF token validation:这是我不喜欢的所有解决方法..(但我承认它们可以工作)我不知道,因为 Laravel 上存在女巫版本,但是有一种方法可以从 CSRF 令牌验证中排除页面:

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

Simply adding a record on $except array on VerifyCsrfToken Middleware with your uri that you want to exclude.只需在 VerifyCsrfToken 中间件上的 $except 数组上添加一条记录,并使用您要排除的 uri。 Please take in account, this must only be done on specific cases.请注意,这只能在特定情况下进行。

This is the best solution for me... Simple and just like (almost) everything on Laravel, they already thought about it.这对我来说是最好的解决方案......简单并且就像(几乎)Laravel 上的所有内容一样,他们已经考虑过了。 ;) ;)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM