Handling expired token in Laravel

2019-01-14 01:01发布

问题:

What is the best way to handle expired tokens in laravel 5.

I mean I have a page and it has some links which perform ajax requests. They work fine when the page is loaded but when I wait for sometime then I get a TOKEN MISMATCH error.

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.

回答1:

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:

<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

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



回答2:

I combine 2 things for this case:

1. Increase session lifetime

//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)

2. Catch the exception and display an error message

//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


回答3:

I think the answer by @UX Labs is misleading. And then the comment from @jfadich seems completely incorrect.

For Laravel 5.4 in May 2017, I solved the problem this way:

Here Is an Answer That Works

In 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:

$(document).ready(function () {

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

    function keepTokenAlive() {
        $.ajax({
            url: '/keep-token-alive', //https://stackoverflow.com/q/31449434/470749
            type: '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. 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.

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.

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).

In session.php, I changed 'lifetime' from 120 to 360 minutes, but that still wasn't enough. And I didn't want to make it longer than 6 hours. So I needed to enable this one page to frequently extend the session via ajax.

How you can test it and get a sense for how the tokens work:

In 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:

<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',
                type: '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',
                type: '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.

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.



回答4:

Increase the lifetime of your sessions. You can do so by editing the config/session.php file in your laravel configuration.

/*
|--------------------------------------------------------------------------
| 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,


回答5:

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. 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.



回答6:

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. Use helper function config() provided by laravel, it might look like this:

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


回答7:

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:

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. 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. ;)



回答8:

a short and fast way.... for handling ajax requests,when token expire : add this script to the end of master layout or your document

$(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:

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

</script>


回答9:

Circum-navigating the token is generally accepted as a terrible approach but there are problems with using js timers mentioned above too. 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.

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). 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.