您现在的位置是:首页 >PHP网站首页PHP
Laravel 5.5 使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌
- 学无止境
- 2019-03-08
- 6028 已阅读
- 0
最近在做一个公司的项目,前端使用 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 前的版本都存在多用户token认证的安全问题。
$ composer require tymon/jwt-auth 1.0.0-rc.1
配置
添加服务提供商
将下面这行添加至 config/app.php
文件 providers
数组中:
app.php
'providers' => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ]
发布配置文件
在你的 shell 中运行如下命令发布 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
。
配置 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
getKey(); } /** * Return a key value array, containing any custom claims to be added to the JWT. * * @return array */ public function getJWTCustomClaims() { return []; } }
配置项详解
jwt.php
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
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
<pre class="php hljs" style="box-sizing: border-box; overflow: auto; font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New", monospace; font-size: 0.93em; padding: 1em; margin-bottom: 1.5em; line-height: 1.45; word-break: br