最近在做一个公司的项目,前端使用 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认证的安全问题。
1 |
$ composer require tymon/jwt-auth <span class="hljs-number">1.0</span><span class="hljs-number">.0</span>-rc<span class="hljs-number">.1</span> |
配置
添加服务提供商
将下面这行添加至 config/app.php
文件 providers
数组中:
app.php
1 2 3 4 5 6 |
<span class="hljs-string">'providers'</span> => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::<span class="hljs-class"><span class="hljs-keyword">class</span>,</span> ] |
发布配置文件
在你的 shell 中运行如下命令发布 jwt-auth 的配置文件:
shell
1 |
$ php artisan vendor:publish --provider="Tymon<span class="hljs-symbol">\J</span>WTAuth<span class="hljs-symbol">\P</span>roviders<span class="hljs-symbol">\L</span>aravelServiceProvider" |
此命令会在 config
目录下生成一个 jwt.php
配置文件,你可以在此进行自定义配置。
生成密钥
jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret,你只需要在你的 shell
中运行如下命令即可:
shell
1 |
<span class="hljs-variable">$ </span>php artisan <span class="hljs-symbol">jwt:</span>secret |
此命令会在你的 .env
文件中新增一行 JWT_SECRET=secret
。
配置 Auth guard
在 config/auth.php
文件中,你需要将 guards/driver
更新为 jwt
:
auth.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="hljs-string">'defaults'</span> => [ <span class="hljs-string">'guard'</span> => <span class="hljs-string">'api'</span>, <span class="hljs-string">'passwords'</span> => <span class="hljs-string">'users'</span>, ], ... <span class="hljs-string">'guards'</span> => [ <span class="hljs-string">'api'</span> => [ <span class="hljs-string">'driver'</span> => <span class="hljs-string">'jwt'</span>, <span class="hljs-string">'provider'</span> => <span class="hljs-string">'users'</span>, ], ], |
只有在使用 Laravel 5.2 及以上版本的情况下才能使用。
更改 Model
如果需要使用 jwt-auth 作为用户认证,我们需要对我们的 User
模型进行一点小小的改变,实现一个接口,变更后的 User
模型如下:
User.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<span class="php"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tymon</span>\<span class="hljs-title">JWTAuth</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">JWTSubject</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Notifications</span>\<span class="hljs-title">Notifiable</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Auth</span>\<span class="hljs-title">User</span> <span class="hljs-title">as</span> <span class="hljs-title">Authenticatable</span>; <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Authenticatable</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">JWTSubject</span> </span>{ <span class="hljs-keyword">use</span> <span class="hljs-title">Notifiable</span>; <span class="hljs-comment">// Rest omitted for brevity</span> <span class="hljs-comment">/** * Get the identifier that will be stored in the subject claim of the JWT. * * <span class="hljs-doctag">@return</span> mixed */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getJWTIdentifier</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->getKey(); } <span class="hljs-comment">/** * Return a key value array, containing any custom claims to be added to the JWT. * * <span class="hljs-doctag">@return</span> array */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getJWTCustomClaims</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">return</span> []; } }</span> |
配置项详解
jwt.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
<?php return [ /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| JWT Authentication Secret</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 用于加密生成 token 的 secret</span> <span class="hljs-string">|</span> */ 'secret' => env('JWT_SECRET'), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| JWT Authentication Keys</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 如果你在 .env 文件中定义了 JWT_SECRET 的随机字符串</span> <span class="hljs-string">| 那么 jwt 将会使用 对称算法 来生成 token</span> <span class="hljs-string">| 如果你没有定有,那么jwt 将会使用如下配置的公钥和私钥来生成 token</span> <span class="hljs-string">|</span> */ 'keys' => [ /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Public Key</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 公钥</span> <span class="hljs-string">|</span> */ 'public' => env('JWT_PUBLIC_KEY'), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Private Key</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 私钥</span> <span class="hljs-string">|</span> */ 'private' => env('JWT_PRIVATE_KEY'), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Passphrase</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 私钥的密码。 如果没有设置,可以为 null。</span> <span class="hljs-string">|</span> */ 'passphrase' => env('JWT_PASSPHRASE'), ], /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| JWT time to live</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定 access_token 有效的时间长度(以分钟为单位),默认为1小时,您也可以将其设置为空,以产生永不过期的标记</span> <span class="hljs-string">|</span> */ 'ttl' => env('JWT_TTL', <span class="hljs-number">60</span>), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Refresh time to live</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定 access_token 可刷新的时间长度(以分钟为单位)。默认的时间为 2 周。</span> <span class="hljs-string">| 大概意思就是如果用户有一个 access_token,那么他可以带着他的 access_token </span> <span class="hljs-string">| 过来领取新的 access_token,直到 2 周的时间后,他便无法继续刷新了,需要重新登录。</span> <span class="hljs-string">|</span> */ 'refresh_ttl' => env('JWT_REFRESH_TTL', <span class="hljs-number">20160</span>), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| JWT hashing algorithm</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定将用于对令牌进行签名的散列算法。</span> <span class="hljs-string">|</span> */ 'algo' => env('JWT_ALGO', 'HS256'), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Required Claims</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定必须存在于任何令牌中的声明。</span> <span class="hljs-string">| </span> <span class="hljs-string">|</span> */ 'required_claims' => [ 'iss', 'iat', 'exp', 'nbf', 'sub', 'jti', ], /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Persistent Claims</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定在刷新令牌时要保留的声明密钥。</span> <span class="hljs-string">|</span> */ 'persistent_claims' => [ <span class="hljs-comment">// 'foo',</span> <span class="hljs-comment">// 'bar',</span> ], /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Blacklist Enabled</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 为了使令牌无效,您必须启用黑名单。</span> <span class="hljs-string">| 如果您不想或不需要此功能,请将其设置为 false。</span> <span class="hljs-string">|</span> */ 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), /* <span class="hljs-string">| -------------------------------------------------------------------------</span> <span class="hljs-string">| Blacklist Grace Period</span> <span class="hljs-string">| -------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 当多个并发请求使用相同的JWT进行时,</span> <span class="hljs-string">| 由于 access_token 的刷新 ,其中一些可能会失败</span> <span class="hljs-string">| 以秒为单位设置请求时间以防止并发的请求失败。</span> <span class="hljs-string">|</span> */ 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', <span class="hljs-number">0</span>), /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Providers</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定整个包中使用的各种提供程序。</span> <span class="hljs-string">|</span> */ 'providers' => [ /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| JWT Provider</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定用于创建和解码令牌的提供程序。</span> <span class="hljs-string">|</span> */ 'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class, /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Authentication Provider</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定用于对用户进行身份验证的提供程序。</span> <span class="hljs-string">|</span> */ 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, /* <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">| Storage Provider</span> <span class="hljs-string">|--------------------------------------------------------------------------</span> <span class="hljs-string">|</span> <span class="hljs-string">| 指定用于在黑名单中存储标记的提供程序。</span> <span class="hljs-string">|</span> */ 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, ], ]; |
自定义认证中间件
先来说明一下我想要达成的效果,我希望用户提供账号密码前来登录。如果登录成功,那么我会给前端颁发一个 access _token ,设置在 header
中以请求需要用户认证的路由。
同时我希望如果用户的令牌如果过期了,可以暂时通过此次请求,并在此次请求中刷新该用户的 access _token,最后在响应头中将新的 access _token 返回给前端,这样子可以无痛的刷新 access _token ,用户可以获得一个很良好的体验,所以开始动手写代码。
执行如下命令以新建一个中间件:
1 |
<span class="hljs-selector-tag">php</span> <span class="hljs-selector-tag">artisan</span> <span class="hljs-selector-tag">make</span><span class="hljs-selector-pseudo">:middleware</span> <span class="hljs-selector-tag">RefreshToken</span> |
中间件代码如下:
RefreshToken.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<span class="php"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Middleware</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Auth</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Closure</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tymon</span>\<span class="hljs-title">JWTAuth</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">JWTException</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tymon</span>\<span class="hljs-title">JWTAuth</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Middleware</span>\<span class="hljs-title">BaseMiddleware</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tymon</span>\<span class="hljs-title">JWTAuth</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">TokenExpiredException</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Exception</span>\<span class="hljs-title">UnauthorizedHttpException</span>; <span class="hljs-comment">// 注意,我们要继承的是 jwt 的 BaseMiddleware</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RefreshToken</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseMiddleware</span> </span>{ <span class="hljs-comment">/** * Handle an incoming request. * * <span class="hljs-doctag">@param</span> \Illuminate\Http\Request $request * <span class="hljs-doctag">@param</span> \Closure $next * * <span class="hljs-doctag">@throws</span> \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException * * <span class="hljs-doctag">@return</span> mixed */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span><span class="hljs-params">($request, Closure $next)</span> </span>{ <span class="hljs-comment">// 检查此次请求中是否带有 token,如果没有则抛出异常。 </span> <span class="hljs-keyword">$this</span>->checkForToken($request); <span class="hljs-comment">// 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常</span> <span class="hljs-keyword">try</span> { <span class="hljs-comment">// 检测用户的登录状态,如果正常则通过</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>->auth->parseToken()->authenticate()) { <span class="hljs-keyword">return</span> $next($request); } <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedHttpException(<span class="hljs-string">'jwt-auth'</span>, <span class="hljs-string">'未登录'</span>); } <span class="hljs-keyword">catch</span> (TokenExpiredException $exception) { <span class="hljs-comment">// 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中</span> <span class="hljs-keyword">try</span> { <span class="hljs-comment">// 刷新用户的 token</span> $token = <span class="hljs-keyword">$this</span>->auth->refresh(); <span class="hljs-comment">// 使用一次性登录以保证此次请求的成功</span> Auth::guard(<span class="hljs-string">'api'</span>)->onceUsingId(<span class="hljs-keyword">$this</span>->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[<span class="hljs-string">'sub'</span>]); } <span class="hljs-keyword">catch</span> (JWTException $exception) { <span class="hljs-comment">// 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。</span> <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedHttpException(<span class="hljs-string">'jwt-auth'</span>, $exception->getMessage()); } } <span class="hljs-comment">// 在响应头中返回新的 token</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->setAuthenticationHeader($next($request), $token); } } </span> |
设置 Axios 拦截器
我选用的 HTTP 请求套件是 axios。为了达到无痛刷新 token 的效果,我们需要对 axios 定义一个拦截器,用以接收我们刷新的 Token,代码如下:
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span> <span class="hljs-keyword">import</span> router <span class="hljs-keyword">from</span> <span class="hljs-string">'./router'</span> <span class="hljs-keyword">import</span> store <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span> <span class="hljs-keyword">import</span> iView <span class="hljs-keyword">from</span> <span class="hljs-string">'iview'</span> <span class="hljs-keyword">import</span> <span class="hljs-string">'iview/dist/styles/iview.css'</span> Vue.use(iView) <span class="hljs-keyword">new</span> Vue({ el: <span class="hljs-string">'#app'</span>, router, store, created() { <span class="hljs-comment">// 自定义的 axios 响应拦截器</span> <span class="hljs-keyword">this</span>.$axios.interceptors.response.use(<span class="hljs-function">(<span class="hljs-params">response</span>) =></span> { <span class="hljs-comment">// 判断一下响应中是否有 token,如果有就直接使用此 token 替换掉本地的 token。你可以根据你的业务需求自己编写更新 token 的逻辑</span> <span class="hljs-keyword">var</span> token = response.headers.authorization <span class="hljs-keyword">if</span> (token) { <span class="hljs-comment">// 如果 header 中存在 token,那么触发 refreshToken 方法,替换本地的 token</span> <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'refreshToken'</span>, token) } <span class="hljs-keyword">return</span> response }, <span class="hljs-function">(<span class="hljs-params">error</span>) =></span> { <span class="hljs-keyword">switch</span> (error.response.status) { <span class="hljs-comment">// 如果响应中的 http code 为 401,那么则此用户可能 token 失效了之类的,我会触发 logout 方法,清除本地的数据并将用户重定向至登录页面</span> <span class="hljs-keyword">case</span> <span class="hljs-number">401</span>: <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.$store.dispatch(<span class="hljs-string">'logout'</span>) <span class="hljs-keyword">break</span> <span class="hljs-comment">// 如果响应中的 http code 为 400,那么就弹出一条错误提示给用户</span> <span class="hljs-keyword">case</span> <span class="hljs-number">400</span>: <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.$Message.error(error.response.data.error) <span class="hljs-keyword">break</span> } <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error) }) } }) |
Vuex 内的代码如下:
store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
import Vue <span class="hljs-keyword">from</span> 'vue' import Vuex <span class="hljs-keyword">from</span> 'vuex' import axios <span class="hljs-keyword">from</span> 'axios' Vue.use(Vuex) export <span class="hljs-keyword">default</span> new Vuex.Store({ <span class="hljs-keyword">state</span>: { name: null, avatar: null, mobile: null, token: null, remark: null, auth: false, }, mutations: { // 用户登录成功,存储 token 并设置 header 头 logined(<span class="hljs-keyword">state</span>, token) { <span class="hljs-keyword">state</span>.auth = true <span class="hljs-keyword">state</span>.token = token localStorage.token = token }, // 用户刷新 token 成功,使用新的 token 替换掉本地的token refreshToken(<span class="hljs-keyword">state</span>, token) { <span class="hljs-keyword">state</span>.token = token localStorage.token = token axios.defaults.headers.common['Authorization'] = <span class="hljs-keyword">state</span>.token }, // 登录成功后拉取用户的信息存储到本地 <span class="hljs-keyword">profile</span>(<span class="hljs-keyword">state</span>, data) { <span class="hljs-keyword">state</span>.name = data.name <span class="hljs-keyword">state</span>.mobile = data.mobile <span class="hljs-keyword">state</span>.avatar = data.avatar <span class="hljs-keyword">state</span>.remark = data.remark }, // 用户登出,清除本地数据 logout(<span class="hljs-keyword">state</span>){ <span class="hljs-keyword">state</span>.name = null <span class="hljs-keyword">state</span>.mobile = null <span class="hljs-keyword">state</span>.avatar = null <span class="hljs-keyword">state</span>.remark = null <span class="hljs-keyword">state</span>.auth = false <span class="hljs-keyword">state</span>.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('<span class="hljs-keyword">profile</span>').then(() => { resolve() }).catch(() => { reject() }) }) }, // 登录成功后使用 token 拉取用户的信息 <span class="hljs-keyword">profile</span>({commit}) { return new Promise(function (resolve, reject) { axios.get('<span class="hljs-keyword">profile</span>', {}).then(respond => { if (respond.status == <span class="hljs-number">200</span>) { commit('<span class="hljs-keyword">profile</span>', respond.data) resolve() } else { reject() } }) }) }, // 用户登出,清除本地数据并重定向至登录页面 logout({commit}) { return new Promise(function (resolve, reject) { commit('logout') axios.post('auth/logout', {}).then(respond => { Vue.<span class="hljs-variable">$router</span>.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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<span class="php"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Exceptions</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Exception</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Foundation</span>\<span class="hljs-title">Exceptions</span>\<span class="hljs-title">Handler</span> <span class="hljs-title">as</span> <span class="hljs-title">ExceptionHandler</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Validation</span>\<span class="hljs-title">ValidationException</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Exception</span>\<span class="hljs-title">UnauthorizedHttpException</span>; <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Handler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ExceptionHandler</span> </span>{ ... <span class="hljs-comment">/** * Render an exception into an HTTP response. * * <span class="hljs-doctag">@param</span> \Illuminate\Http\Request $request * <span class="hljs-doctag">@param</span> \Exception $exception * <span class="hljs-doctag">@return</span> \Illuminate\Http\Response */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span><span class="hljs-params">($request, Exception $exception)</span> </span>{ <span class="hljs-comment">// 参数验证错误的异常,我们需要返回 400 的 http code 和一句错误信息</span> <span class="hljs-keyword">if</span> ($exception <span class="hljs-keyword">instanceof</span> ValidationException) { <span class="hljs-keyword">return</span> response([<span class="hljs-string">'error'</span> => array_first(array_collapse($exception->errors()))], <span class="hljs-number">400</span>); } <span class="hljs-comment">// 用户认证的异常,我们需要返回 401 的 http code 和错误信息</span> <span class="hljs-keyword">if</span> ($exception <span class="hljs-keyword">instanceof</span> UnauthorizedHttpException) { <span class="hljs-keyword">return</span> response($exception->getMessage(), <span class="hljs-number">401</span>); } <span class="hljs-keyword">return</span> <span class="hljs-keyword">parent</span>::render($request, $exception); } } </span> |
更新完此方法后,我们上面自定义的中间件里抛出的异常和我们下面参数验证错误抛出的异常都会被转为指定的格式抛出。
使用
现在,我们可以在我们的 routes/api.php
路由文件中新增几条路由来测试一下了:
api.php
1 2 3 4 5 6 7 8 9 10 |
Route::prefix(<span class="hljs-string">'auth'</span>)->group(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($router)</span> </span>{ $router->post(<span class="hljs-string">'login'</span>, <span class="hljs-string">'AuthController@login'</span>); $router->post(<span class="hljs-string">'logout'</span>, <span class="hljs-string">'AuthController@logout'</span>); }); Route::middleware(<span class="hljs-string">'refresh.token'</span>)->group(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($router)</span> </span>{ $router->get(<span class="hljs-string">'profile'</span>,<span class="hljs-string">'UserController@profile'</span>); }); |
在你的 shel
l 中运行如下命令以新增一个控制器:
1 |
<span class="hljs-variable">$ </span>php artisan <span class="hljs-symbol">make:</span>controller AuthController |
打开此控制器,写入如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<span class="php"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Controllers</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Request</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Auth</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Transformers</span>\<span class="hljs-title">UserTransformer</span>; <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{ <span class="hljs-comment">/** * Get a JWT token via given credentials. * * <span class="hljs-doctag">@param</span> \Illuminate\Http\Request $request * * <span class="hljs-doctag">@return</span> \Illuminate\Http\JsonResponse */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">login</span><span class="hljs-params">(Request $request)</span> </span>{ <span class="hljs-comment">// 验证规则,由于业务需求,这里我更改了一下登录的用户名,使用手机号码登录</span> $rules = [ <span class="hljs-string">'mobile'</span> => [ <span class="hljs-string">'required'</span>, <span class="hljs-string">'exists:users'</span>, ], <span class="hljs-string">'password'</span> => <span class="hljs-string">'required|string|min:6|max:20'</span>, ]; <span class="hljs-comment">// 验证参数,如果验证失败,则会抛出 ValidationException 的异常</span> $params = <span class="hljs-keyword">$this</span>->validate($request, $rules); <span class="hljs-comment">// 使用 Auth 登录用户,如果登录成功,则返回 201 的 code 和 token,如果登录失败则返回</span> <span class="hljs-keyword">return</span> ($token = Auth::guard(<span class="hljs-string">'api'</span>)->attempt($params)) ? response([<span class="hljs-string">'token'</span> => <span class="hljs-string">'bearer '</span> . $token], <span class="hljs-number">201</span>) : response([<span class="hljs-string">'error'</span> => <span class="hljs-string">'账号或密码错误'</span>], <span class="hljs-number">400</span>); } <span class="hljs-comment">/** * 处理用户登出逻辑 * * <span class="hljs-doctag">@return</span> \Illuminate\Http\JsonResponse */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logout</span><span class="hljs-params">()</span> </span>{ Auth::guard(<span class="hljs-string">'api'</span>)->logout(); <span class="hljs-keyword">return</span> response([<span class="hljs-string">'message'</span> => <span class="hljs-string">'退出成功'</span>]); } }</span> |
然后我们进入 tinker:
1 |
<span class="hljs-variable">$ </span>php artisan tinker |
执行以下命令来创建一个测试用户,我这里的用户名是用的是手机号码,你可以自行替换为邮箱。别忘了设置命名空间哟:
1 2 |
>>> <span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>; >>> User::create([<span class="hljs-string">'name'</span> => <span class="hljs-string">'Test'</span>,<span class="hljs-string">'mobile'</span> => <span class="hljs-number">17623239881</span>,<span class="hljs-string">'password'</span> => bcrypt(<span class="hljs-string">'123456'</span>)]); |
正确执行结果如下图:
然后打开 Postman 来进行 api 测试
正确的请求结果如下图:
可以看到我们已经成功的拿到了 token,接下来我们就去验证一下刷新 token 吧
如图可以看到我们已经拿到了新的 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,配置简单,使用方便。