Laravel 5.5 使用 Jwt-Auth 实现 API 用户认证以及刷新访问令牌

最近在作一个公司的项目,前端使用 Vue.js,后端使用 Laravel 构建 Api 服务,用户认证的包原本是想用 Laravel Passport 的,可是感受有点麻烦,因而使用了 jwt-authjavascript

安装

jwt-auth 最新版本是 1.0.0 rc.1 版本,已经支持了 Laravel 5.5。若是你使用的是 Laravel 5.5 版本,可使用以下命令安装。根据评论区 @tradzero 兄弟的建议,若是你是 Laravel 5.5 如下版本,也推荐使用最新版本,RC.1 前的版本都存在多用户token认证的安全问题。php

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

配置

添加服务提供商

将下面这行添加至 config/app.php 文件 providers 数组中:css

app.php前端

'providers' => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ] 

发布配置文件

在你的 shell 中运行以下命令发布 jwt-auth 的配置文件:vue

shelljava

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

此命令会在 config 目录下生成一个 jwt.php 配置文件,你能够在此进行自定义配置。ios

生成密钥

jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret,你只须要在你的 shell 中运行以下命令便可:laravel

shellgit

$ php artisan jwt:secret 

此命令会在你的 .env 文件中新增一行 JWT_SECRET=secretgithub

配置 Auth guard

config/auth.php 文件中,你须要将 guards/driver 更新为 jwt

auth.php

'defaults' => [ 'guard' => 'api', 'passwords' => 'users', ], ... 'guards' => [ 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ], 

只有在使用 Laravel 5.2 及以上版本的状况下才能使用。

更改 Model

若是须要使用 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。为了达到无痛刷新 token 的效果,咱们须要对 axios 定义一个拦截器,用以接收咱们刷新的 Token,代码以下:

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) }) }, } }) 

更新异常处理的 Handler

因为咱们构建的是 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')]); 

正确执行结果以下图:

 
1.png

而后打开 Postman 来进行 api 测试

 
2.png

正确的请求结果以下图:

 
3.png

能够看到咱们已经成功的拿到了 token,接下来咱们就去验证一下刷新 token 吧

 
4.png

如图能够看到咱们已经拿到了新的 token,接下来的事情便会交由咱们前面设置的 axios 拦截器处理,它会将本地的 token 替换为此 token。

版本科普

感受蛮多人对版本没什么概念,因此在这里科普下常见的版本。

  • α(Alpha)版

    ​ 这个版本表示该 Package 仅仅是一个初步完成品,一般只在开发者内部交流,也有不多一部分发布给专业测试人员。通常而言,该版本软件的 Bug 较多,普通用户最好不要安装。

  • β(Beta)版

    该版本相对于 α(Alpha)版已有了很大的改进,修复了严重的错误,但仍是存在着一些缺陷,须要通过大规模的发布测试来进一步消除。经过一些专业爱好者的测试,将结果反馈给开发者,开发者们再进行有针对性的修改。该版本也不适合通常用户安装。

  • RC/ Preview版

    RC 即 Release Candidate 的缩写,做为一个固定术语,意味着最终版本准备就绪。通常来讲 RC 版本已经完成所有功能并清除大部分的 BUG。通常到了这个阶段 Package 的做者只会修复 Bug,不会对软件作任何大的更改。

  • 普通发行版本

    通常在经历了上面三个版本后,做者会推出此版本。此版本修复了绝大部分的 Bug,而且会维护必定的时间。(时间根据做者的意愿而决定,例如 Laravel 的通常发行版本会提供为期一年的维护支持。)

  • LTS(Long Term Support) 版

    该版本是一个特殊的版本,和普通版本旨在支持比正常时间更长的时间。(例如 Laravel 的 LTS 版本会提供为期三年的 维护支持。)

结语

jwt-auth 确实是一个很棒的用户认证 Package,配置简单,使用方便。


原文:https://segmentfault.com/a/1190000012606246

做者:琯琯 连接:https://www.jianshu.com/p/9e95a5f8ac4a 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
相关文章
相关标签/搜索