2020-04-28 15:46:14  5448 0

Laravel jwt 实现多用户认证并实现刷新token全过程

 标签:   

大家好,最近工作繁忙,很久没有更新文章了,今天给大家写一篇关于laravel 结合 jwt 实现多用户认证 并实现 刷新token的的全过程!希望能帮助到大家下面开始我们的教程

首先我们要去安装 laravel jwt的 包 

安装

1:

composer require tymon/jwt-auth

官方地址:https://jwt-auth.readthedocs.io/en/develop/laravel-installation

有什么不懂的可以查看官方文档

由于官方文档写的比较简单,下面我们开始详细的安装过程

image.png

安装完成后 开始我们的配置

2: 

添加服务提供者(大于 Laravel 5.5 版本的跳过)

/app/config/app.php 下修改

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

为了防止意外发生建议 高版本的也配置上避免出错。

3: 生成配置文件

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

image.png

4: 生成 secret

php artisan jwt:secret

image.png

安装初步完成! 下面开始配置多用户 下面我已 我们创建一张 rider表 测试下

我们先生成我们需要的模型

php artisan make:model Rider -m

然后我们在 database 文件夹下找到 我们刚才创建的迁移文件

创建以下字段

Schema::create('riders', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name',20)->comment('姓名');
    $table->string('tel',20)->unique()->comment('手机号');
    $table->string('password');
    $table->timestamps();
});

这里为了测试方便就只创建了姓名 手机号 密码 我们使用手机号和密码登录

然后我们执行 laravel 6.0 以下执行

 php artisan make:auth

 laravel 6.0移除了make:auth 需要单独安装 composer require laravel/ui 来单独安装这个权限包


5:修改模型为jwt 认证的模型

<?php

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


class Rider extends Authenticatable implements JWTSubject
{

    /**
     * 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-Auth验证表格的主键(默认ID)
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }

    protected $fillable = [
        'name',
        'password',
        'tel',
    ];

    
}

5: 修改配置文件 app/config/auth.php 配置 Authentication Guards

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
    
    'riders' => [
        'driver' => 'eloquent',
        'model' => App\Rider::class,
    ],
    
],

6配置 User Providers

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'token',
        'provider' => 'users',
        'hash' => false,
    ],
    
    'rider' => [
        'driver' => 'jwt',
        'provider' => 'riders',
    ],
],

现在我们的多用户配置完成,为了能在jwt中正常使用 我们还需要配置一个中间件 让laravel内置的权限系统能区分我们自己定义的 guards 让jwt 认识它

我们执行 以下命令 

php artisan make:middleware AssignGuard

我们打开 app/http/Middleware 文件夹找到 AssignGuard 这个文件

写入如下代码

<?php

namespace App\Http\Middleware;

use Closure;

class AssignGuard
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        // 这里的作用是指定当前请求是使用哪种 guard,比如 auth()->shouldUse('rider')
        if($guard != null){
            auth()->shouldUse($guard);
        }
        return $next($request);
    }
}

现在我们的jwt已经可以使用内置的auth权限 识别用户了 下面我们创建一个控制器测试下

执行 以下命令

php artisan make:controller rider/AuthController

找到我们生成的控制器 app/Http/Controllers/AuthController

代码如下:

<?php

namespace App\Http\Controllers\rider;

use App\Rider;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware(['assign.guard:rider'], ['except' => ['login','register']]);
    }

    /**
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
     * @throws \Illuminate\Validation\ValidationException
     */
    public function login(Request $request)
    {
        // 验证规则,由于业务需求,这里我更改了一下登录的用户名,使用手机号码登录
        $rules = [
            'tel'   => [
                'required',
                'exists:riders',
            ],
            'password' => 'required|string|min:6|max:20',
        ];
        $params = $this->validate($request, $rules);
// 使用 Auth 登录用户,如果登录成功,则返回 201 的 code 和 token,如果登录失败则返回
        return ($token = Auth::guard('rider')->attempt($params))
            ? response()->json(['token' => 'Bearer ' . $token],200)
            : response()->json(['error' => '账号或密码错误'],422);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(Auth::guard('rider')->user());
    }


    public function register(Request $request)
    {
        $tel = $request->tel;
        $password = $request->password;
        $check_password = $request->check_password;

        if (!$tel || !$password) {
            return response()->json(['success' => false, 'message' => '用户名、密码必填!']);
        }

        if ($check_password != $password) {
            return response()->json(['success' => false, 'message' => '两次密码输入不一致!']);
        }

        $admin = Rider::where('tel', $tel)->first();
        if ($admin) {
            return response()->json(['success' => false, 'message' => '用户名已被注册!']);
        }

        $password = Hash::make($password);
        $admin = Rider::create([
            'tel' => $tel,
            'password' => $password
        ]);

        return response()->json(['success' => true, 'message' => '注册成功!', 'admin' => $admin]);
    }


    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('rider')->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('rider')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('rider')->factory()->getTTL() * 60
        ]);
    }
}

上面定义了登录 注册 和刷新 token 的方法 由于我们要实现 token在过期之前自动刷新token 这里的刷新token的方法 也就没什么用了,暂且保留

代码解释:

在上面的控制器方法中 我们 在构造方法里面 使用了 我们自己创建的中间件 

assign.guard:rider

这个中间件后面是告诉laravel 权限系统我们要使用rider守卫来认证我们的jwt 用户系统,不太了解的可以看看我的其他文章有专门解释中间件传递参数的

下面我们往表中插入测试数据测试下

测试效果如下:

image.png

已经可以成功请求到了

但是我们发现当jwt 有token 过期时间默认是60分钟 如果token过期了我们就没办法再次刷新了 如下图

image.png

有没有办法可以让他 快过期之前重新刷新个新的token 呢 答案是有的,我们要做的就是 在它快过期10分钟之前 重新刷新一次token 就行了

我们需要注册一个中间件 来拦截所有的请求给客户端下发token

执行 以下命令 

php artisan make:middleware RefreshToken

写入如下代码:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
// 注意,我们要继承的是 jwt 的 BaseMiddleware
class RefreshToken extends BaseMiddleware
{

    /**
     * @param $request
     * @param Closure $next
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed
     * @throws JWTException
     */
    public function handle($request, Closure $next)
    {
        //BaseMiddleware内方法
        try{
            if ($userInfo = $this->auth->parseToken()->authenticate()) {
                $time = Auth::guard('rider')->payload()['exp'];
                    //刷新Token
                    if(($time - time()) < 10*60 && ($time - time()) > 0){
                        $token = Auth::guard('rider')->refresh();
                        if($token){
                            $request->headers->set('Authorization', 'Bearer '.$token);
                        }else{
                            return response(['code'=>401,'msg'=>'The token has been blacklisted'],401);
                        }

                        // 在响应头中返回新的 token
                        $respone = $next($request);
                        if(isset($token) && $token){
                            $respone->headers->set('Authorization', 'Bearer '.$token);
                        }
                        return $respone;
                    }
                return $next($request);
            }
            throw new UnauthorizedHttpException('jwt-auth', '未登录');
        }catch (TokenExpiredException $exception){
            //是否可以刷新,刷新后加入到响应头
            throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage());
        }
    }
}

需要注意的是 之前我们测试时候没有加上这个中间件 我们需要在 AuthController 的构造方法里面添加上这个中间件

public function __construct()
{
    $this->middleware(['assign.guard:rider','refresh'], ['except' => ['login','register']]);
}

注意中间件的顺序 assign.guard:rider 这个中间件一定要在前面不然jwt无法区分是那个guard


解释下上面的代码 我们先尝试拿到用户信息 如果拿不到就会抛出异常消息 拦截 然后我们在 异常消息处理类返回401错误,如果能拿到用户信息 就

在token过期的前10分钟刷新token 然后返回新的token给 header头 前端进行无痛刷新即可! 

具体设置如下:在app/Exceptions/Handeler.php 中我们捕获异常消息 处理jwt 的异常 如果不处理 前端将会返回401的网页 这不是我们预期的结果

代码如下:

<?php

namespace App\Exceptions;

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

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * @param  \Exception  $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * 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()->json([
                'msg' => 'token过期',
                'status' => 401
            ],401);
        }
        if ($exception instanceof JWTException) {
            return response()->json([
                'msg' => 'token验证失败请重新登录',
                'status' => 401
            ],401);
        }
        return parent::render($request, $exception);
    }
}




这样我们就能正常返回jwt的报错信息给前端了,下面我们开始验证下 自动刷新的结果是否符合我们的预期



这里为了测试方便我把token 的过期时间设置为6分钟方便我们测试  在 app/config/jwt.php 文件设置过期时间

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

我们来看下测试结果 这里我们可以看到

image.png

从图上看出我们已经成功刷新了token 这样前端 只需拦截下heaer头判断是否有token 如果有就替换掉旧的token 这样可以无感知的刷新token 结果符合预期!这是本人一直在使用的比较成熟的

jwt token 刷新方案 感谢大家能看到这里,本次教程到此结束。希望猪哥的教程能给大家带来一点启发,最后祝大家工作顺利万事如意!