使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌
208

filex


最近在做一个公司的项目,前端使用 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

<?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')]);

正确执行结果如下图:

file

然后打开 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,配置简单,使用方便。

文章结束,感谢阅读。

本帖由 Summer 于 9个月前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 76
Ken

学习了 老哥!

9个月前

@Ken 牺牲了午睡时间赶出来的 :smile:

9个月前
overtrue

666

9个月前

@overtrue :stuck_out_tongue:

9个月前

很好啊,这次总算可以比较系统的学习到新知识了。

9个月前

多谢分享。我现在也在用jwt ,使用过程中发现请求多个api接口的时候,如果token过期去刷新的话,因为多个请求,会导致多次刷新token,产生token不能用的问题。这个有什么解决办法吗?

9个月前

@fordongu 试着设置一下配置项中的 blacklist_grace_period 看看?这个配置项的作用就是为了防止并发高的时候刷新掉了
token 导致其他的请求失效,你可以根据你的业务逻辑设置合适的时长。

9个月前

@Seaony 好的 非常感谢

9个月前

@fordongu 不用客气,很帮到你我很高兴 :beer:

9个月前

爸爸牛批 :cry:

9个月前

无论如何还是建议使用新版本, 旧版本 RC1之前都有多用户token认证的安全问题(User, Admin同时使用JWT驱动验证, User1id为1获取到token, 然后用该token访问Admin的任意路由,JWT-Auth都会通过认证, 因为JWT-Auth使用的是id做为校验方式,而没考虑驱动的模型)

9个月前

@tradzero 受教了,已修改文章 :+1:

9个月前

@Seaony 不敢不敢 还是多谢你的分享

9个月前
Jourdon

如果只是做API认证,确实还是JWT方便。。passport有点大了

9个月前
Destiny

老铁高质量文章

9个月前

@王东哲 是的

9个月前

@Destiny 谢谢,老铁稳! :stuck_out_tongue:

9个月前
Qi

只能说简直完美

file

9个月前
Qi

@Seaony 实验下来出了点了问题 过期后 直接不让刷新了 抛出的是UnauthorizedHttpException 过期设置为1分钟 刷新时间默认 ....

9个月前
LOST

@tradzero 之前也有用不同的模型进行认证,确实也发现了这个问题。可以为不同的模型配置不同的 jwt-secret,这样使用 user 模型生成的 token 就无法通过 admin 模型的认证了。

9个月前

@Qi 你使用的中间件是这个中间件嘛?要在 kernel 中注册这个中间件,然后再使用它

9个月前
Littlesqx

我不是很理解这样的做法有什么好处。。。第一,这样做有一个问题,如果token过期了,接下来如果前端有两个或以上的请求发出(这些请求的token都是过期的),那最后其实刷新多次了。第二,为了保证token长时间可用,这大可以直接修改token有效期,绕了一个圈,不就是为了这个吗?(反正中间件自动帮忙延长时间)。第三,我更推荐token过期后前端主动刷新或者过期后直接销毁token,需要重新登录。毕竟刷新token的需求很大程度不是给WEB 应用 API设计的。

9个月前

@Littlesqx

  1. jwt-auth 配置项里的 blacklist_grace_period 就是为了解决此问题,如果你配置了此项,在高并发的情况下,服务器同时接收到了数个来自前端的请求,而这时又恰好第一个请求过期了,刷新了 Token ,那么 jwt-auth 在一段时间内是允许余下请求通过验证的,具体的时间取决于你配置的时间。
  2. 的确,绕的这一大圈就是为了延长 Token 的使用时间,但是我文章中的方法和你说的方法不同的是,在一段时间内你使用的都是一个 Token,而我这边却是在不停的更换 Token,而且每个 Token 都是唯一的,从某种程度上来说,它提升了很大的安全性。安全性这个话题很虚无缥缈,可却是网站服务正常运行很重要的一个保障。
  3. refresh_ttl 就是专门为了此要求设计的,只要 refresh 时间到期,那么服务端会强制性的拒绝颁发 Token 并要求重新登录,确保了你真的是这个账号的主人(知道账号密码)。

总结概括一下就是一句话:这样做更加具有安全性

9个月前
Qi

@Seaony 注册了 结果用错了 。。。哈哈哈 用的dingo+jwt

9个月前
Littlesqx

@Seaony 1. 请问余下不通过验证的请求响应你让前端怎么做?从你的代码上看,并没有解决这个问题。本质上讲,这不是并发的问题,是你这个所谓无痛刷新的设计缺陷。2. 这根本没有提高安全性,过期的token只要没有销毁,一样可以访问(因为在可刷新期限内,所谓的无痛刷新会帮忙更新)。 3. 我们的讨论不在一个点上。

9个月前

@Littlesqx 你真的有看我文章嘛....里面写的很清楚了。

第一点,如果你配置了 blacklist_grace_period 项,那么 jwt-auth 会在此配置的时间里让余下已经过期的 token 通过验证,相当于变相的使 token 的过期时间延长了,以满足并发下的请求。并不存在你说的余下不通过的请求响应。

第二点,针对一个 token 只会抛出一次已经过期的异常,也就是说只要不主动刷新,每一个 token 都且仅有只有一次被动刷新的机会。只要一个携带 token 的请求过期了,而且没有刷新,其他携带此 token 的请求都不会有被动刷新的机会,只会抛出认证未通过的异常。而且 jwt-auth 里的 token 并不存在销毁这一概念,只有过期时间,只要这个 token 过期且过期的时候没有刷新那么它就无法通过认证了,我实际测试也证明了这一点,如下图:

fileQQ20171227-193405@2x.png

我也觉得我们的讨论不在一个点上

9个月前

@Qi 哈哈,写代码还是要细心一点好,我写代码就是太粗心了,老是错在一些小细节上。

9个月前
Littlesqx

@Seaony blacklist_grace_period 这个配置确实我理解错了,抱歉,有空我自己会测试验证一下。关于第二点,我说了我认为并没有更安全的理由,我希望你从(你认为)更安全的角度解释下。你所说的 只要一个携带 token 的请求过期了,而且没有刷新,其他携带此 token 的请求都不会有被动刷新的机会,只会抛出认证未通过的异常不是正和第一点自相矛盾吗。。。token被加入黑名单不就是销毁的概念吗。最后,我说的讨论不在一个点上,请仔细看我上条回复的,我说针对回复你的第三点。

9个月前

@Littlesqx 意思就是那个意思,可能我表述有错误,blacklist_grace_period 配置和 token 刷新是两个概念,因为 blacklist_grace_period 的存在,即使前面的请求已经刷新了 token,后面携带此 token 的请求在 blacklist_grace_period 配置的时间内还是会通过认证。你也可以去看一下 jwt-auth 的源码,其间的流程确实有点复杂,所以这里就不细讲了,有空我会单独写一篇文章来分析一下它的验证机制,不过可以肯定的是你说的安全问题是不存在的。

9个月前
Toiu

虽然我个人更倾向于passport, 但还是点赞.. 因为我是菜鸟:smile: , 而且我们坐标一致

9个月前

@LOST 还有这种操作吗 是重写了什么方法吧 jwt-secret不是在config统一配置的么

9个月前
LOST

@tradzero 写个中间件,用 config 函数覆盖 jwt 中原有的配置。

9个月前

@LOST 66666

9个月前

666,其实前端中使用axios 拦截器就可以做到无痛刷新token。看你了的解决办法,又学习了一种姿势

9个月前

@tanjibo :joy: 既然学到了不如给个赞,哈哈哈哈

9个月前

@Seaony 必须给赞

9个月前

很棒的文章,感謝分享。
由於 JWT 沒有銷毀概念,此作法若被有心人士取得 token,一樣也可以無痛 refresh token:sweat_smile:

9个月前

走过路过瞄过。

9个月前

老铁看过你好多文章了, 这篇JWT给了并发请求的一些思路, 赞
另外有试过laravoole吗?性能无痛提升10倍, 就是跨域问题不知道怎么解决

9个月前

@泽 有空去看看,这个没玩过,平时用 Swoole 都是直接用的

9个月前

为啥说passport比较麻烦馁(手动问号(●'◡'●))

9个月前

@LOST jwt 好像只支持一个密码,请问你的实现方式是什么?

9个月前

哦哦 看到了

9个月前

@Seaony 有空的话恳请发布个swoole话题呀:+1:

9个月前

@泽 嗯呐,有空会搞的,但是目前实在是忙得喘不过气来,哈哈

9个月前

AuthControlleruse App\Transformers\UserTransformer, 引入后在 loginlogout 方法中貌似没用到, 也可能是我孤陋寡闻了 :)

9个月前

请教一下大神,看到你在项目里面用的vue单页,在考虑seo上的问题是怎么处理的啊?

9个月前

@mayne 先前写的时候用了的,后来写文章的时候去掉了,怕看文章的人理解出错,也懒得再去介绍 Transformer 的作用

9个月前

@dptms emmm,老实说,这个项目我并没有考虑 SEO,因为是后台相关的项目。如果你希望在前台用 SPA 的形式,可以去看一下 Vue 文章里服务端渲染的章节,我对这方面没有什么经验 =。=

9个月前

@Seaony 谢谢大神

9个月前

非常好!!谢谢大神,学习了!

9个月前

学习了,请教一下jwt多站点登录有什么好办法吗?我现在是把JWT_SECRET在多个站点中共享的,但是感觉这样管理起来不是很方便。

9个月前

@xcaptain 多个网站用一套账号?如果是这样的话要搞个主从应用然后做 OAuth 2.0 会比较好一点吧?

9个月前

假如我们的api要提供学员登录,后台登陆。。
用 Oauth 这样就不行了吧。。。只能用 dingo 的 V1、V2、V3这种版本的吧

9个月前

@Seaony 我们公司现在内部的系统就是做了个中心的授权服务,每个应用都接入这个服务进行登陆和登陆校验,但是感觉这种方法不是很好,要走http请求去校验登陆状态。

9个月前

学习学习

8个月前
杨子鳄鱼
7个月前

如果有些接口在登陆时需要token,不登陆时不需要token;
php里面怎么解决?

7个月前

这样是不是refresh_token就没啥用了,前端发出一个请求若返回401直接跳转登录,不需要再用refresh_token刷新token然后再次发送请求了对吧?

7个月前

探讨以下情况:如果token被盗用并且在到期后成功刷新了,那么盗用者就等于永久拥有了这个token的使用权了。而我作为原token的所有者会被告知token失效只能重新授权,而我也没有任何途径使盗用者的token失效。

7个月前
kinyou

有一种临界点不知道楼主是否考虑过!
例如: 当一个token快要过期的那一刹那, 前端多个请求并发上来, 这样就会到第一个请求成功, 后面的多个请求都会是403或者401

6个月前
raybon

本地环境 Laravel 5.6 的可以 执行 composer require tymon/jwt-auth 1.0.0-rc.2

6个月前
largezhou

在你的segmentfault上看了这篇文章,然后就采用了,,我也是看laravel的passport好麻烦的样子,,

感谢

5个月前

请问,是不是不用vuex,就用localstore就能实现了。。。

5个月前

@finaljoy 是的,同理

5个月前

@史沟飞
你的回答是正解, token 过期了就得拿 refresh_token 去颁发新 token 看来楼主还是不懂什么是 oauth2.0

3个月前

一个问题,BaseMiddleware里的authenticate方法,token验证成功后其实每次都会执行JWTGuard的onceUsingId方法,就是去请求userprovider的retrieveById方法。这样每个请求验证都会去查数据库了,不是跟jwt的状态无关的优点冲突了吗?jwt中间件只验证token好了啊,因为不是每次请求都会去取auth::user(),浪费数据库操作

3个月前

@BIBIBABIBO 是你没有理解什么才是 oauth2.0,如果你理解了,就会知道我这篇文章根本就不是在讲 oauth2.0。

3个月前

@ebenmo 有空我再过一遍,看看能不能优化一下~

3个月前

@史沟飞 如果被盗用了,是会有一个 refresh 时间的,在 refresh 时间结束后会强制要求重新获取新的 token,而不能继续刷新了,并不是说可以一直拿着这个 token 去刷新。每个 token 都有两个时间概念,一个是 access time,一个是 refresh time,access time 可以理解为这个 token 的有效时间,refresh time 则是共享的刷新时间。

举个例子,例如我使用账号与密码拿到了一个 A token,这个 A token 的 access time 是 1 小时,refresh time 是 7 天。在一个小时后,我的 A token 的 access time 到期了,我可以拿这个 A token 去换取一个新的 token,这个新的 token 的access time 是重新计算的,但是它的 refresh time 是累计的。也就是不管你换取多少次,token 始终会在 refresh time 到期的时间点,也就是 7 天后失效,无法继续刷新。

3个月前

@kinyou 请看 blacklist_grace_period 配置项

3个月前

提问前请先仔细阅读文章,多找一点资料看一下。
很多问题真是蠢萌得让我不想回答 :unamused:

3个月前

@Seaony 我说是TOKEN被盗用,并且被盗用者在你系统自动刷新了TOKEN。原TOKEN持有人就无法作废这个TOKEN了。
用你的例子:你拿到一个A token,access time 是 1 小时。但保管不善被人盗取了,而你并不知情。在一小时后盗用者使用这个A token访问接口并且系统『自动』refresh为B token。那么盗用者就掌握了这个B token的所有权了,而你用A token去refresh系统会提示你已经blacklist了,你只能眼睁睁看着人家盗用你的身份啦~

3个月前

@Seaony 哦,好的看明白了,refresh time是累计的。但被盗用的token还是可以在7天内可以被人任意使用了,而原token所有人却不能作废他。

3个月前

@Seaony 如果 token 在 7 天之后就无法刷新的话。比如在 app 中,不可能要求用户 7 天就重新登录一次吧?当然这个 7 天可以改长,但是改长了确实又和 @史沟飞 所说的,安全风险也增加了。

2个月前

大神您好,如果想要动态设置这个刷新期限,就是每一个token的刷新期限可能我想不一样,有什么好的解决办法吗,谷歌百度转了一圈都没找到解决办法,希望您能指点指点。

2个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!