Аутентификация пользователя API и безболезненное обновление токена доступа с использованием Jwt-Auth

PHP API Laravel Vue.js
Аутентификация пользователя API и безболезненное обновление токена доступа с использованием Jwt-Auth

Недавно я работал над проектом компании, используя интерфейсVue.js, серверная часть используетLaravelСоздайте службу API, пакет проверки подлинности пользователя изначально предназначался для использованияLaravel PassportДа, но это было немного хлопотно, поэтому я использовалjwt-auth.


Установить

jwt-authПоследняя версия1.0.0 rc.1версия, уже поддерживаетсяLaravel 5.5. если вы используетеLaravel 5.5версию, вы можете использовать следующую команду для установки. Согласно разделу комментариев@tradzeroбратан совет если тыLaravel 5.5В следующих версиях также рекомендуется использовать последнюю версию: Версии до RC.1 имеют проблемы с безопасностью при многопользовательской аутентификации с помощью токена.

$ composer require tymon/jwt-auth 1.0.0-rc.1

настроить


### Добавить поставщика услуг

Добавьте следующую строку вconfig/app.phpдокументprovidersВ массиве:

app.php

'providers' => [

    ...

    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]

опубликовать профиль

Запустите следующую команду в вашей оболочке, чтобы выдатьjwt-authфайл конфигурации:

shell

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

Эта команда будетconfigсоздать каталогjwt.phpФайл конфигурации, где вы можете настроить конфигурацию.


сгенерировать ключ

jwt-authзаранее определилArtisanКоманда удобна вам для генерации Secret, вам нужно толькоshellЗапустите следующую команду в:

shell

$ php artisan jwt:secret

Эта команда будет в вашем.envДобавить новую строку в файлJWT_SECRET=secret.


Настроить защиту авторизации

существуетconfig/auth.phpфайл, нужно поставитьguards/driverобновить доjwt:

auth.php

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

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

только при использованииLaravel 5.2Его можно использовать только в случае вышеуказанной версии.


Изменить модель

При необходимости используйтеjwt-authВ качестве аутентификации пользователя нам необходимо аутентифицировать нашUserМодель вносит небольшие изменения, реализует интерфейс, а измененныйUserМодель выглядит следующим образом:

User.php

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Подробное объяснение элементов конфигурации

jwt.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | JWT Authentication Secret
    |--------------------------------------------------------------------------
    |
    | 用于加密生成 token 的 secret
    |
    */

    'secret' => env('JWT_SECRET'),

    /*
    |--------------------------------------------------------------------------
    | JWT Authentication Keys
    |--------------------------------------------------------------------------
    |
    | 如果你在 .env 文件中定义了 JWT_SECRET 的随机字符串
    | 那么 jwt 将会使用 对称算法 来生成 token
    | 如果你没有定有,那么jwt 将会使用如下配置的公钥和私钥来生成 token
    |
    */

    'keys' => [

        /*
        |--------------------------------------------------------------------------
        | Public Key
        |--------------------------------------------------------------------------
        |
        | 公钥
        |
        */

        'public' => env('JWT_PUBLIC_KEY'),

        /*
        |--------------------------------------------------------------------------
        | Private Key
        |--------------------------------------------------------------------------
        |
        | 私钥
        |
        */

        'private' => env('JWT_PRIVATE_KEY'),

        /*
        |--------------------------------------------------------------------------
        | Passphrase
        |--------------------------------------------------------------------------
        |
        | 私钥的密码。 如果没有设置,可以为 null。
        |
        */

        'passphrase' => env('JWT_PASSPHRASE'),

    ],

    /*
    |--------------------------------------------------------------------------
    | JWT time to live
    |--------------------------------------------------------------------------
    |
    | 指定 access_token 有效的时间长度(以分钟为单位),默认为1小时,您也可以将其设置为空,以产生永不过期的标记
    |
    */

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

    /*
    |--------------------------------------------------------------------------
    | Refresh time to live
    |--------------------------------------------------------------------------
    |
    | 指定 access_token 可刷新的时间长度(以分钟为单位)。默认的时间为 2 周。
    | 大概意思就是如果用户有一个 access_token,那么他可以带着他的 access_token 
    | 过来领取新的 access_token,直到 2 周的时间后,他便无法继续刷新了,需要重新登录。
    |
    */

    'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),

    /*
    |--------------------------------------------------------------------------
    | JWT hashing algorithm
    |--------------------------------------------------------------------------
    |
    | 指定将用于对令牌进行签名的散列算法。
    |
    */

    'algo' => env('JWT_ALGO', 'HS256'),

    /*
    |--------------------------------------------------------------------------
    | Required Claims
    |--------------------------------------------------------------------------
    |
    | 指定必须存在于任何令牌中的声明。
    | 
    |
    */

    'required_claims' => [
        'iss',
        'iat',
        'exp',
        'nbf',
        'sub',
        'jti',
    ],

    /*
    |--------------------------------------------------------------------------
    | Persistent Claims
    |--------------------------------------------------------------------------
    |
    | 指定在刷新令牌时要保留的声明密钥。
    |
    */

    'persistent_claims' => [
        // 'foo',
        // 'bar',
    ],

    /*
    |--------------------------------------------------------------------------
    | Blacklist Enabled
    |--------------------------------------------------------------------------
    |
    | 为了使令牌无效,您必须启用黑名单。
    | 如果您不想或不需要此功能,请将其设置为 false。
    |
    */

    'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),

    /*
    | -------------------------------------------------------------------------
    | Blacklist Grace Period
    | -------------------------------------------------------------------------
    |
    | 当多个并发请求使用相同的JWT进行时,
    | 由于 access_token 的刷新 ,其中一些可能会失败
    | 以秒为单位设置请求时间以防止并发的请求失败。
    |
    */

    'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),

    /*
    |--------------------------------------------------------------------------
    | Providers
    |--------------------------------------------------------------------------
    |
    | 指定整个包中使用的各种提供程序。
    |
    */

    'providers' => [

        /*
        |--------------------------------------------------------------------------
        | JWT Provider
        |--------------------------------------------------------------------------
        |
        | 指定用于创建和解码令牌的提供程序。
        |
        */

        'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,

        /*
        |--------------------------------------------------------------------------
        | Authentication Provider
        |--------------------------------------------------------------------------
        |
        | 指定用于对用户进行身份验证的提供程序。
        |
        */

        'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,

        /*
        |--------------------------------------------------------------------------
        | Storage Provider
        |--------------------------------------------------------------------------
        |
        | 指定用于在黑名单中存储标记的提供程序。
        |
        */

        'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,

    ],

];

Пользовательское промежуточное ПО для аутентификации

Во-первых, позвольте мне объяснить эффект, которого я хочу достичь.Я надеюсь, что пользователь предоставит учетную запись и пароль для входа в систему. Если вход в систему будет успешным, я выпущу внешний интерфейс сaccess _token, установлен наheaderдля запроса маршрутов, требующих аутентификации пользователя.

В то же время я надеюсь, что если срок действия токена пользователя истечет, я смогу временно передать этот запрос и обновить токен пользователя в этом запросе.access _token, и, наконец, поместите новый в заголовок ответаaccess _tokenВернитесь на переднюю часть, чтобы вы могли обновиться без болиaccess _token, пользователь может получить очень хороший опыт, поэтому начните писать код.

Выполните следующую команду, чтобы создать новое промежуточное ПО:

php artisan make:middleware RefreshToken

Код промежуточного ПО выглядит следующим образом:

RefreshToken.php

<?php

namespace App\Http\Middleware;

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

// 注意,我们要继承的是 jwt 的 BaseMiddleware
class RefreshToken extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     *
     * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // 检查此次请求中是否带有 token,如果没有则抛出异常。 
        $this->checkForToken($request);

       // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException  异常
        try {
            // 检测用户的登录状态,如果正常则通过
            if ($this->auth->parseToken()->authenticate()) {
                return $next($request);
            }
            throw new UnauthorizedHttpException('jwt-auth', '未登录');
        } catch (TokenExpiredException $exception) {
          // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中
            try {
                // 刷新用户的 token
                $token = $this->auth->refresh();
               // 使用一次性登录以保证此次请求的成功
                Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);
            } catch (JWTException $exception) {
               // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。
                throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage());
            }
        }
		
        // 在响应头中返回新的 token
        return $this->setAuthenticationHeader($next($request), $token);
    }
}


Настройка перехватчиков Axios

Мой предпочтительный набор HTTP-запросов:axios. Чтобы добиться эффекта безболезненного обновления токена, нам нужноaxiosОпределите перехватчик для получения нашего обновленного токена, код выглядит следующим образом:

app.js

import Vue from 'vue'
import router from './router'
import store from './store'
import iView from 'iview'
import 'iview/dist/styles/iview.css'

Vue.use(iView)


new Vue({
    el: '#app',
    router,
    store,
    created() {
        // 自定义的 axios 响应拦截器
        this.$axios.interceptors.response.use((response) => {
            // 判断一下响应中是否有 token,如果有就直接使用此 token 替换掉本地的 token。你可以根据你的业务需求自己编写更新 token 的逻辑
            var token = response.headers.authorization
            if (token) {
                // 如果 header 中存在 token,那么触发 refreshToken 方法,替换本地的 token
                this.$store.dispatch('refreshToken', token)
            }
            return response
        }, (error) => {
            switch (error.response.status) {
                
                // 如果响应中的 http code 为 401,那么则此用户可能 token 失效了之类的,我会触发 logout 方法,清除本地的数据并将用户重定向至登录页面
                case 401:
                    return this.$store.dispatch('logout')
                    break
                // 如果响应中的 http code 为 400,那么就弹出一条错误提示给用户
                case 400:
                    return this.$Message.error(error.response.data.error)
                    break
            }
            return Promise.reject(error)
        })
    }
})

VuexКод внутри выглядит следующим образом:

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        name: null,
        avatar: null,
        mobile: null,
        token: null,
        remark: null,
        auth: false,
    },
    mutations: {
        // 用户登录成功,存储 token 并设置 header 头
        logined(state, token) {
            state.auth = true
            state.token = token
            localStorage.token = token
        },
        // 用户刷新 token 成功,使用新的 token 替换掉本地的token
        refreshToken(state, token) {
            state.token = token
            localStorage.token = token
            axios.defaults.headers.common['Authorization'] = state.token
        },
        // 登录成功后拉取用户的信息存储到本地
        profile(state, data) {
            state.name = data.name
            state.mobile = data.mobile
            state.avatar = data.avatar
            state.remark = data.remark
        },
        // 用户登出,清除本地数据
        logout(state){
            state.name = null
            state.mobile = null
            state.avatar = null
            state.remark = null
            state.auth = false
            state.token = null

            localStorage.removeItem('token')
        }
    },
    actions: {
         // 登录成功后保存用户信息
        logined({dispatch,commit}, token) {
            return new Promise(function (resolve, reject) {
                commit('logined', token)
                axios.defaults.headers.common['Authorization'] = token
                dispatch('profile').then(() => {
                    resolve()
                }).catch(() => {
                    reject()
                })
            })
        },
        // 登录成功后使用 token 拉取用户的信息
        profile({commit}) {
            return new Promise(function (resolve, reject) {
                axios.get('profile', {}).then(respond => {
                    if (respond.status == 200) {
                        commit('profile', respond.data)
                        resolve()
                    } else {
                        reject()
                    }
                })
            })
        },
        // 用户登出,清除本地数据并重定向至登录页面
        logout({commit}) {
            return new Promise(function (resolve, reject) {
                commit('logout')
                axios.post('auth/logout', {}).then(respond => {
                    Vue.$router.push({name:'login'})
                })
            })
        },
        // 将刷新的 token 保存至本地
        refreshToken({commit},token) {
            return new Promise(function (resolve, reject) {
                commit('refreshToken', token)
            })
        },
    }
})


Обновите обработчик для обработки исключений

Поскольку мы строимapiсервис, поэтому нам нужно обновитьapp/Exceptions/Handler.phpсерединаrender

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

Handler.php

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class Handler extends ExceptionHandler
{
    ...

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Exception $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        // 参数验证错误的异常,我们需要返回 400 的 http code 和一句错误信息
        if ($exception instanceof ValidationException) {
            return response(['error' => array_first(array_collapse($exception->errors()))], 400);
        }
        // 用户认证的异常,我们需要返回 401 的 http code 和错误信息
        if ($exception instanceof UnauthorizedHttpException) {
            return response($exception->getMessage(), 401);
        }

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

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


использовать

Теперь мы можем в нашемroutes/api.phpДобавьте несколько новых маршрутов в файл маршрутизации, чтобы протестировать его:

api.php

Route::prefix('auth')->group(function($router) {
    $router->post('login', 'AuthController@login');
    $router->post('logout', 'AuthController@logout');


});

Route::middleware('refresh.token')->group(function($router) {
    $router->get('profile','UserController@profile');
});

В твоемshell Выполните следующую команду, чтобы добавить новый контроллер:

$ php artisan make:controller AuthController

Откройте этот контроллер и напишите следующее

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Transformers\UserTransformer;

class AuthController extends Controller
{

    /**
     * Get a JWT token via given credentials.
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        // 验证规则,由于业务需求,这里我更改了一下登录的用户名,使用手机号码登录
        $rules = [
            'mobile'   => [
                'required',
                'exists:users',
            ],
            'password' => 'required|string|min:6|max:20',
         ];
          
        // 验证参数,如果验证失败,则会抛出 ValidationException 的异常
        $params = $this->validate($request, $rules);

	   // 使用 Auth 登录用户,如果登录成功,则返回 201 的 code 和 token,如果登录失败则返回
        return ($token = Auth::guard('api')->attempt($params))
            ? response(['token' => 'bearer ' . $token], 201)
            : response(['error' => '账号或密码错误'], 400);
    }

    /**
     * 处理用户登出逻辑
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        Auth::guard('api')->logout();

        return response(['message' => '退出成功']);
    }
}

затем мы входимtinker:

$ php artisan tinker

Выполните следующую команду, чтобы создать тестового пользователя, имя пользователя здесь — это номер мобильного телефона, вы можете самостоятельно заменить его адресом электронной почты. Не забудьте установить пространство имен:

>>> namespace App\Models;
>>> User::create(['name' => 'Test','mobile' => 17623239881,'password' => bcrypt('123456')]);

Правильный результат выполнения выглядит следующим образом:

file

Затем откройте Postman для тестирования API.

Правильный результат запроса выглядит следующим образом:

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

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


научно-популярная версия

Я чувствую, что многие люди не имеют понятия о версии, так что вот общепринятая научно-популярная версия.

  • Альфа (Альфа) версия

    Эта версия указывает на то, что Пакет является лишь предварительным готовым продуктом, обычно сообщаемым только разработчикам, а небольшая часть выпускается для профессиональных тестировщиков. Вообще говоря, в этой версии программы много ошибок, и обычным пользователям лучше ее не устанавливать.

  • Бета (бета) версия

    Эта версия была значительно улучшена по сравнению с альфа-версией (Alpha), в ней исправлены серьезные ошибки, но все еще есть некоторые ошибки, которые необходимо устранить путем масштабного тестирования выпуска. Через тесты некоторых профессиональных энтузиастов результаты возвращаются разработчикам, которые затем вносят целевые модификации. Эта версия также не подходит для обычной пользовательской установки.

  • RC/Предварительный просмотр

    RC — это сокращение от Release Candidate, и фиксированный термин означает, что окончательный релиз готов. Вообще говоря, в RC-версии реализованы все функции и исправлено большинство ошибок. Как правило, на этом этапе автор пакета только исправляет ошибки и не вносит каких-либо серьезных изменений в программное обеспечение.

  • нормальное распределение

    Как правило, после прохождения трех вышеуказанных версий автор запускает эту версию. Эта версия исправляет большинство ошибок и будет поддерживаться в течение определенного периода времени. (Время определяется пожеланиями автора, т.е.Laravelобщий выпуск поддерживается в течение одного года обслуживания. )

  • Версия LTS (долгосрочная поддержка)

    Этот выпуск является специальным выпуском, а обычный выпуск рассчитан на более длительную поддержку, чем обычно. (Например, LTS-версия Laravel предлагает трехлетнююТехническая поддержка. )


Эпилог

jwt-authЭто действительно отличный пакет аутентификации пользователей, простой в настройке и использовании.

Статья окончена, спасибо за прочтение.

Категории