Почему время сеанса с jwt-auth составляет 1 час с момента входа в систему?

В приложении laravel 7 backend rest api app я использую jwt-auth, и у меня есть проблема, что при входе в систему я могу работать во внешнем интерфейсе, но через 1 час я получаю ошибку TOKEN_EXPIRED.

1) Я пытался установить время сеанса больше, но не смог. На стадии разработки мне нужно время сеанса более 1 часа. В прямом эфире я установлю время сеанса 30 минут.

2) Я ожидал, что сессия больше на 1 час от последнего запроса зарегистрированного пользователя к серверу, но не от входа в систему

У меня есть метод обновления, реализованный ниже, но похоже, что обновление не работает...

приложение/Http/Контроллеры/API/AuthController.php:

<?php

namespace App\Http\Controllers\API;

use App\library\CheckValueType;
use App\Settings;
use Auth;
use Config;
use DB;
use Validator;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Support\Str;
use App\UserGroup;
use App\Http\Resources\UserCollection;
use Avatar;
use Storage;

class AuthController extends Controller
{

    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('jwt.auth', ['except' => ['login', 'register', 'activate']]);
    }

    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if ($token = $this->guard('api')->attempt($credentials)) {
            $loggedUser = $this->guard('api')->user();

            if ($loggedUser->status != 'A') {
                return response()->json(['error' => 'Unauthorized'], HTTP_RESPONSE_NOT_UNAUTHORIZED);
            }
            $loggedUser->last_logged = Carbon::now(config('app.timezone'));
            $loggedUser->save();

            $userGroupsCount = UserGroup
                ::getByUserId($loggedUser->id)
                ->count();
            if ($userGroupsCount == 0) {
                return response()->json(['error' => 'Unauthorized'], HTTP_RESPONSE_NOT_UNAUTHORIZED);
            }

            return $this->respondWithToken($token);
        }

        return response()->json(['error' => 'Unauthorized'], HTTP_RESPONSE_NOT_UNAUTHORIZED);
    }


    public function me()
    {
        return response()->json($this->guard('api')->user());
    }

    public function logout()
    {
        $this->guard('api')->logout();
        return response()->json(['message' => 'Successfully logged out']);
    }

    public function refresh()
    {
        return $this->respondWithToken($this->guard()->refresh());
    }

    protected function respondWithToken($token)
    {
        $loggedUser = $this->guard()->user();

        $user_avatar_path = 'public/' . User::getUserAvatarPath($loggedUser->id, $loggedUser->avatar);
        $filenameData     = User::setUserAvatarProps($loggedUser->id, $loggedUser->avatar, true);
        $usersGroups = User::getUserGroupByUserId($loggedUser->id, false);

        return response()->json([
            'access_token'     => $token,
            'user'             => $loggedUser,
            'token_type'       => 'bearer',
            'user_avatar_path' => $user_avatar_path,
            'filenameData'     => $filenameData,
            'usersGroups'      => $usersGroups,
            'expires_in'       => $this->guard('api')->factory()->getTTL() * 999360 // I SET VERY BIG VALUE
        ]);
    }

    public function guard()
    {
        return \Auth::Guard('api');
    }

В /config/auth.php:

<?php

return [


    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 99360, // I SET BIG VALUE
            'throttle' => 98860,
        ],
    ],


    'password_timeout' => 10800,  // I SET BIG VALUE

В app/Exceptions/Handler.php у меня есть:

public function render($request, Throwable $exception)
{

    if ($exception instanceof UnauthorizedHttpException) {
        if ($exception->getPrevious() instanceof TokenExpiredException) {
            \Log::info( '-2 UnauthorizedHttpException TokenExpiredException::' ); // I SEE THIS ERROR LOGGED
            return response()->json(['error' => 'TOKEN_EXPIRED'], $exception->getStatusCode());

У меня есть :

"laravel/framework": "^7.0",
"tymon/jwt-auth": "^1.0.0",

Что не так в моей конфигурации?

ИЗМЕНЕНО:

Я добавил файл app/Http/Middleware/JwtMiddleware.php с содержимым и 1 строкой журнала ошибок:

<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;

class JwtMiddleware extends BaseMiddleware
{
    public function handle($request, Closure $next)
    {
        try {
            if (! $user = $this->auth->parseToken()->authenticate()) {
                return response()->json(['success' => false, 'error' => __('Invalid User.')]);
            }
        } catch (TokenExpiredException $e) {
            try {
                $refreshed = $this->auth->refresh($this->auth->getToken());
                $user = $this->auth->setToken($refreshed)->toUser();
                header('Authorization: Bearer ' . $refreshed);
            } catch (JWTException $e) {
                return response()->json(['success' => false, 'error' => __('Could not generate refresh token')]);
            }
        } catch (JWTException $e) {
            \Log::info( '-1 JwtMiddleware$e->getMessage() ::' . print_r(  $e->getMessage(), true  ) );

            return response()->json(['success' => false, 'error' => __('Invalid request')]);
        }

        return  $next($request);
    }
}

и я добавил в файл app/Http/Kernel.php:

   ...
    protected $middleware = [
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \App\Http\Middleware\JwtMiddleware::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
    ];
   ...

and running the page site is broken and I see lines in log :
[2020-05-27 17:08:33] local.INFO: -1 JwtMiddleware$e->getMessage() ::The token could not be parsed from the request  
[2020-05-27 17:08:33] local.INFO: -1 JwtMiddleware$e->getMessage() ::The token could not be parsed from the request  
[2020-05-27 17:08:33] local.INFO: -1 JwtMiddleware$e->getMessage() ::The token could not be parsed from the request  

Может быть конфликт с кодом app/Http/Controllers/API/AuthController.php? Как это можно исправить?

ИЗМЕНЕНО №2: Спасибо! Я исправил ошибку и в .env добавил строки:

JWT_TTL=20 # 20 minutes
JWT_REFRESH_TTL=20160 # 2 weeks

и очистив кеш, я снова вошел в систему, и в результате во время работы в приложении я не выходил из системы в течение 20 минут, но когда я оставил приложение открытым, не работая с ним в течение примерно 30 минут, я мог продолжать работать без входа в систему, как я и ожидал. Это другие варианты?

ИЗМЕНЕНО №3: в клиентской части моего файла vuejs я добавил перехватчики запросов в src/App.vue:

created() {
    let self = this
    this.$http.interceptors.response.use(undefined, function (error) {

        return new Promise(function (/*resolve, reject*/) {
            if (typeof error.response.status !== 'undefined' && error.response.status === 401) {
                self.$store.dispatch('logout')  // DEBUGGING
                self.showPopupMessage('Access', 'Not authorized !', 'warn')
                let splitted0 = self.getSplitted(error.response.config.url, '/login', 0)

                if (splitted0 == '') { // not move from login page
                    self.$router.push('/login')   // DEBUGGING
                }
            }

            if (typeof error.response.status !== 'undefined') {
                if (error.response.status === 401) {
                    self.$store.dispatch('logout') // DEBUGGING
                    self.showPopupMessage('Access', 'Not authorized !', 'warn')
                    self.$router.push('/login')  // DEBUGGING
                }
            }

            throw error
        })
    })


}, //  created() {

Ловлю в нем ошибку 401 и интересно можно ли перехватить запрос с сервера

header('Authorization: Bearer ' . $refreshed);

а записать в новое значение access_token из $refreshed? Но как мне его поймать? Но какой-то специальный код запроса на возврат?

Спасибо!


person mstdmstd    schedule 27.05.2020    source источник
comment
После изменения таких конфигураций рекомендуется запустить php artisan config:clear и php artisan cache:clear, чтобы увидеть, как изменения вступят в силу.   -  person Digvijay    schedule 27.05.2020
comment
уверен, что я сделал это. Даже с помощью команды composer install (удаление /verdor) и composer dump-autoload .   -  person mstdmstd    schedule 27.05.2020
comment
уверен, что есть другие способы сделать это, и вы можете попробовать их, если у вас есть какие-либо проблемы с этим. Но если он работает, то зачем вам другой способ.   -  person Sudhansu Bhatta    schedule 28.05.2020
comment
Какой еще способ? Пожалуйста, дайте ссылку? Должен ли параметр JWT_TTL работать так, как ожидалось выше?   -  person mstdmstd    schedule 28.05.2020
comment
@mstdmstd Пожалуйста, смотрите мои обновления, я объяснил по-другому. Но я бы все же посоветовал вам придерживаться первого метода. Вы сталкиваетесь с какими-либо проблемами таким образом?   -  person Sudhansu Bhatta    schedule 29.05.2020
comment
Спасибо! Я думаю, что решение для моего случая будет таким: генерировать новый refresh_token с каждым запросом API и обновлять его в своем интерфейсе. Пожалуйста, подробно, как я могу это сделать?   -  person mstdmstd    schedule 29.05.2020
comment
Пожалуйста, посмотрите на ИЗМЕНЕНИЕ № 3   -  person mstdmstd    schedule 01.06.2020
comment
@mstdmstd Проверьте в перехватчике, есть ли в заголовке ответа токен, если да, то обновите токен в локальном хранилище.   -  person Sudhansu Bhatta    schedule 16.06.2020
comment
как я могу сделать эту проверку? Есть ли какой-то код, чтобы поймать? Пожалуйста, приведите пример подобной проверки   -  person mstdmstd    schedule 16.06.2020


Ответы (5)


попробуйте это в своем логине

if ($token = $this->guard('api')->attempt($credentials,['exp' => Carbon\Carbon::now()->addHours(2)->timestamp])) {

}

Или вы можете изменить ttl в config/jwt

Дайте мне знать, если это поможет!

person void    schedule 27.05.2020
comment
Спасибо! Полезно установить время сеанса больше. - person mstdmstd; 27.05.2020

  • Вам нужно добавить промежуточное ПО и проверить, не истек ли срок действия токена.
  • Если срок действия истек, проверьте, какое время генерации, чтобы проверить, следует ли создавать новый токен или нет. Думайте об этом как об истечении срока действия токена обновления. вы можете пропустить этот шаг, если хотите сгенерировать токен обновления без проверки времени.
  • Теперь, если все в порядке, сгенерируйте новый токен и отправьте его в заголовке/теле, как хотите, и сохраните его в своем внешнем приложении.
<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;

class JwtMiddleware extends BaseMiddleware
{
    /**
   * Handle an incoming request.
   *
   * @param  \Illuminate\Http\Request  $request
   * @param  \Closure  $next
   * @return mixed
   */
    public function handle($request, Closure $next)
    {
        try {
            if (! $user = $this->auth->parseToken()->authenticate()) {
                return response()->json(['success' => false, 'error' => __('Invalid User.')]);
            }
        } catch (TokenExpiredException $e) {
            try {
                $refreshed = $this->auth->refresh($this->auth->getToken());
                $user = $this->auth->setToken($refreshed)->toUser();
                header('Authorization: Bearer ' . $refreshed);
            } catch (JWTException $e) {
                return response()->json(['success' => false, 'error' => __('Could not generate refresh token')]);
            }
        } catch (JWTException $e) {
            return response()->json(['success' => false, 'error' => __('Invalid request')]);
        }

        return  $next($request);
    }
}

ОБНОВЛЕНИЯ:

Это метод, который я использовал в своем недавнем приложении Ionic/Angular. И это работает нормально. Есть способы, которые я делал раньше, но это трудно объяснить, дайте мне знать, если у вас возникнут какие-либо проблемы.

  • Вы создаете токен и refresh_token при успешном входе в систему и отправке обоих токенов в ваше внешнее приложение и сохраняете их там в локальном хранилище.
  • Теперь для всех защищенных API вам нужно отправить токен и refresh_token.
  • На стороне API проверьте токен и, если срок его действия истек, проверьте, является ли refresh_token действительным или истекшим. Если срок истек, вы вышли из системы.
  • Если refresh_token действителен, сгенерируйте новый токен и refresh_token, отправьте их во внешний интерфейс и сохраните там.
  • У этого метода есть одна проблема: сеанс входа в систему должен быть активен, пока пользователь использует приложение. Итак, допустим, ваш JWT_REFRESH_TTL составляет 1 день, и пользователь использует его незадолго до 24 часов, он будет работать до того, как пройдут 24 часа, но он не будет работать сразу после 1 минуты, поскольку и токен, и refresh_token истекут.
  • Чтобы преодолеть это, вы можете генерировать новый refresh_token с каждым запросом API и обновлять его в своем интерфейсе.

Как правило, JWT_TTL должен быть очень коротким, например 5 минут или около того, а JWT_REFRESH_TTL должен быть активным временем сеанса.

person Sudhansu Bhatta    schedule 27.05.2020
comment
Спасибо! Я предполагаю добавить его в файл app/Http/Kernel.php в защищенном $middleware = [ ? Означает ли это, что он должен быть первым или последним в этой группе? - person mstdmstd; 27.05.2020
comment
Добавьте защищенный файл $routeMiddleware. Его можно добавить как последний пункт, это не имеет значения. - person Sudhansu Bhatta; 27.05.2020
comment
Пожалуйста, посмотрите ИЗМЕНЕНО: - person mstdmstd; 27.05.2020
comment
Добавьте эти две строки в файл .ENV, очистите кеш и повторите попытку. JWT_TTL=1440 JWT_REFRESH_TTL=2880 По сути, вам нужно указать более высокий JWT_REFRESH_TTL, чем JWT_TTL, иначе метод обновления не будет работать и выдаст исключение. Извините, я забыл упомянуть об этом. - person Sudhansu Bhatta; 27.05.2020
comment
Пожалуйста, посмотрите на ИЗМЕНЕНИЕ # 2: - person mstdmstd; 28.05.2020

перейдите в ---> config/jwt.php Вы можете сделать это

'ttl' => env('JWT_TTL', 1440)

ИЛИ в контроллере

$token = JWTAuth::attempt($credentials, ['exp' => Carbon\Carbon::now()->addDays(7)->timestamp]);

OR

config()->set('jwt.ttl', 60*60*7);

OR

Config::set('jwt.ttl', 60*60*7);
person Uzair Saeed    schedule 11.06.2020
comment
Эти варианты я уже реализовал. Пожалуйста, проверьте ИЗМЕНЕНИЕ № 3 - person mstdmstd; 12.06.2020

У вас должен быть файл config/jwt.php. Если у вас еще нет этого, запустите:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

В этом файле вы можете увидеть несколько свойств, которые относятся к .env переменным, а именно:

  • 'ttl' (окружение = 'JWT_TTL')
  • 'refresh_ttl' (окружение = 'JWT_REFRESH_TTL')

Кроме того, вы можете добавить 'refresh' в этот список исключений промежуточного программного обеспечения jwt.auth, потому что иначе он не будет работать, когда вы вызываете функцию refresh() в контроллере.

ОДНАКО в этом пакете есть несколько важных моментов, на которые следует обратить внимание:

TTL аутентификации связаны друг с другом

Я считаю, что вы можете в значительной степени игнорировать свойство ttl, поскольку именно refresh_ttl фактически определяет, как долго ваши JWT действительны, основываясь на этой аналогии:

Слесарь (поставщик аутентификации) дает вам красный ключ (JWT). Этот ключ позволяет вам открыть Красную дверь (используйте свой бэкэнд). Ключ автоматически разделяется пополам через 5 минут (TTL = 5 минут). Однако вы можете вернуться к слесарю со сломанным ключом, и он заменит его в течение 2 недель (обновление TTL = 2 недели).

В основном вы можете вызвать конечную точку auth/refresh с истекшим сроком действия JWT, и он будет обновлять его до тех пор, пока не истечет Refresh-TTL.

См. также [эту проблему на github][1], где также объясняется, почему вам нужно добавить функцию refresh, чтобы перед ней не было промежуточного программного обеспечения jwt.auth.

Вы не сохраняете никаких вызовов БД при использовании JWT, как это предусмотрено этим пакетом.

Моя первоначальная мысль заключалась в том, что это может сэкономить мне время на обращение к базе данных, потому что пользовательский объект будет подписан и готов внутри JWT.

Я использовал этот пакет в нескольких проектах и ​​пришел к выводу, что это своего рода прославленный файл cookie сеанса. В tymon/jwt утверждение sub просто равно 1 (или любому идентификатору пользователя, указанному здесь).

Это означает, что вам нужно либо...

  • ... (по умолчанию) пусть tymon/jwt выполняет работу, которая просто извлекает пользователя из базы данных и применяет его к вашему провайдеру аутентификации, поэтому Auth::user() и т. д. будет работать.

OR

  • ... добавьте заявку с полным пользовательским объектом и измените этот пакет в разных местах. Это может сэкономить вам вызовы БД, поскольку этот объект будет частью вашего JWT и подписан им. Это приводит к отправке большего количества байтов JWT при каждом запросе, но вы можете в основном принимать данный объект как должное (т.е. принимать его по мере его получения) и использовать его. Затем вы также можете добавить разрешения/роли таким образом, чтобы их также не нужно было извлекать из базы данных. Эта настройка ДЕЙСТВИТЕЛЬНО означает, что при изменении каких-либо разрешений в вашей базе данных вам придется ждать обновления нового JWT, чтобы увидеть эти изменения.
person Flame    schedule 14.06.2020
comment
Я стараюсь следовать этому. Не могли бы вы указать на реализацию конечной точки аутентификации/обновления? - person mstdmstd; 16.06.2020
comment
@mstdmstd здесь есть документация github.com/tymondesigns/jwt-auth . Это стоит вашего времени, чтобы прочитать некоторые из этих i.c.w. jwt.io/introduction - person Flame; 16.06.2020

Как вам посоветовали другие люди, перейдите в свой config/jwt.php и измените срок действия токена:

// ...
'ttl' => env('JWT_TTL', 3600)

Важно! После изменения очистите кеш конфигурации.

php artisan config:clear

Кроме того, обратите внимание, что ранее сгенерированные токены будут иметь свои собственные утверждения, поэтому вполне вероятно, что эти токены не будут следовать измененному правилу.

Примечание

В качестве альтернативы самостоятельной реализации JWT вы можете использовать Sanctum, новый собственный пакет Laravel. Проверьте режим SPA в разделе документация Sanctum.

person Kenny Horna    schedule 15.06.2020