Laravel6通过jwt(tymon/jwt-auth)实现API用户无感刷新TOKEN

27 篇文章 0 订阅

1、TOKEN是什么

TOKEN翻译为令牌,就是鉴别身份的凭据,类似于身份证;
TOKEN本质就是一大串字符串,最常用的场景就是接口对接的鉴权。
TOKEN通过一次登录验证,得到一个鉴权字符串,随后其它的请求每次都携带这个鉴权字符串,即可完成鉴权。

2、jwt是什么

​ jwt 全称json web token(翻译为数据网络令牌)
​ jwt 是一种规范化的 token,让数据在网络中安全传输。
​ jwt 使用场景:
​ 1.无状态的 RESTful API
​ 2.SSO 单点登录

3、jwt安装&配置

3.1、通过composer安装

composer require tymon/jwt-auth

3.2、发布配置

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

会自动生成一个配置文件:config/jwt.php

3.3、生成加密密钥

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

php artisan jwt:secret

.env 文件下生成一个加密密钥

3.4、修改 auth.php

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

        'api' => [
            'driver' => 'jwt', // 原来是 token 改成jwt
            'provider' => 'users'
        ],
    ],

3.5、更改User模型

  1. 这里为了以后开发方便,把app\user.php 移动到 app\Models\user.php,并全局搜索 App\User 修改为 App\Models\User
  2. 让user模型实现JWTSubject接口,实现getJWTIdentifier()和getJWTCustomClaims()方法
<?php

namespace App\Models;

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

class User extends Authenticatable implements JWTSubject
{

	use Notifiable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

4、自定义认证中间件&&异常

4.1、接口统一返回

新建统一返回trait ApiResponse

<?php

namespace App\Http\Helpers\Api;

use Symfony\Component\HttpFoundation\Response as FoundationResponse;

trait ApiResponse
{
    /**
     * @var int
     */
    protected $statusCode = FoundationResponse::HTTP_OK;
    protected $token = '';

    /**
     * @return mixed
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * @param $statusCode
     * @return $this
     */
    public function setStatusCode($statusCode,$httpCode=null)
    {
        $httpCode = $httpCode ?? $statusCode;
        $this->statusCode = $statusCode;
        return $this;
    }

    /**
     * @param $statusCode
     * @param null $httpCode
     * @return $this
     */
    public function setToken($token)
    {
        $this->token = $token;
        return $this;
    }

    /**
     * @param $data
     * @param array $header
     * @return mixed
     */
    public function respond($data)
    {
        $response = response()->json($data, $this->getStatusCode());
        if($this->token) {
            $response->headers->set('Authorization', 'Bearer '.$this->token);
        }
        return $response;
    }

    /**
     * @param $status
     * @param array $data
     * @param null $code
     * @param array $header
     * @return mixed
     */
    public function status($status, array $data, $code = null)
    {
        if ($code){
            $this->setStatusCode($code);
        }
        $status = [
            'status' => $status,
            'code' => $this->statusCode
        ];

        $data = array_merge($status,$data);
        return $this->respond($data);
    }

    /**
     * @param $message
     * @param int $code
     * @param string $status
     * @return mixed
     */
    /*
     * 格式
     * data:
     *  code:422
     *  message:xxx
     *  status:'error'
     */
    public function failed($message, $code = FoundationResponse::HTTP_BAD_REQUEST,$status = 'error')
    {
        return $this->setStatusCode($code)->message($message,$status);
    }

    /**
     * @param $message
     * @param string $status
     * @return mixed
     */
    public function message($message, $status = "success")
    {
        if(!is_array($message)) {
            $message = [$message];
        }
        return $this->status($status,[
            'message' => $message
        ]);
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function internalError($message = "Internal Error!")
    {
        return $this->failed($message,FoundationResponse::HTTP_INTERNAL_SERVER_ERROR);
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function created($message = "created")
    {
        return $this->setStatusCode(FoundationResponse::HTTP_CREATED)
            ->message($message);
    }

    /**
     * @param $data
     * @param string $status
     * @param array $header
     * @return mixed
     */
    public function success($data, $status = "success")
    {
        return $this->status($status,compact('data'));
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function notFond($message = 'Not Fond!')
    {
        return $this->failed($message,Foundationresponse::HTTP_NOT_FOUND);
    }
}

4.2、创建中间件

用户提供账号密码前来登录。如果登录成功,那么接口返回前端一个 TOKEN, 如果用户的TOKEN如果过期了,可以暂时通过此次请求,并在此次请求中刷新该用户的 TOKEN,最后在响应头中将新的 token 返回给前端,这样子可以无痛的刷新TOKEN,用户可以获得一个很良好的体验。
生成中间件:

php artisan make:middleware Api/RefreshTokenMiddleware

代码如下:

<?php

namespace App\Http\Middleware\Api;

use Illuminate\Support\Facades\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 RefreshTokenMiddleware extends BaseMiddleware
{
    /**
     * @param $request
     * @param Closure $next
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed
     * @throws JWTException
     */
    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);
    }
}

4.2、异常处理

4.2.1、创建异常集合

创建异常集合文件App\Http\Helpers\Api\ExceptionReport.php

<?php

namespace App\Http\Helpers\Api;

use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;

class ExceptionReport
{
    use ApiResponse;

    /**
     * @var Exception
     */
    public $exception;
    /**
     * @var Request
     */
    public $request;

    /**
     * @var
     */
    protected $report;

    /**
     * ExceptionReport constructor.
     * @param Request $request
     * @param Exception $exception
     */
    function __construct(Request $request, Exception $exception)
    {
        $this->request = $request;
        $this->exception = $exception;
    }

    /**
     * @var array
     */
    //当抛出这些异常时,可以使用我们定义的错误信息与HTTP状态码
    //可以把常见异常放在这里
    public $doReport = [
        AuthenticationException::class => ['未授权', 401],
        ModelNotFoundException::class => ['该模型未找到', 404],
        AuthorizationException::class => ['没有此权限', 403],
        ValidationException::class => [],
        UnauthorizedHttpException::class => ['未登录或登录状态失效', 422],
        TokenInvalidException::class => ['token不正确', 400],
        NotFoundHttpException::class => ['没有找到该页面', 404],
        MethodNotAllowedHttpException::class => ['访问方式不正确', 405],
        QueryException::class => ['参数错误', 401],
    ];

    public function register($className, callable $callback)
    {

        $this->doReport[$className] = $callback;
    }

    /**
     * @return bool
     */
    public function shouldReturn()
    {
        //只有请求包含是json或者ajax请求时才有效
//        if (! ($this->request->wantsJson() || $this->request->ajax())){
//
//            return false;
//        }
        foreach (array_keys($this->doReport) as $report) {
            if ($this->exception instanceof $report) {
                $this->report = $report;
                return true;
            }
        }

        return false;

    }

    /**
     * @param Exception $e
     * @return static
     */
    public static function make(Exception $e)
    {

        return new static(\request(), $e);
    }

    /**
     * @return mixed
     */
    public function report()
    {
        if ($this->exception instanceof ValidationException) {
//            $error = array_key_first($this->exception->errors());
            return $this->failed(current($this->exception->errors()), $this->exception->status);
        }
        $message = $this->doReport[$this->report];
        return $this->failed($message[0], $message[1]);
    }

    public function prodReport()
    {
        return $this->failed('服务器错误', '500');
    }
}

4.2.2、异常处理

app/Exceptions/Handler.php 中的 render方法,自定义处理一些异常。

<?php

namespace App\Exceptions;

use App\Http\Helpers\Api\ExceptionReport;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

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
     *
     * @throws \Exception
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Exception
     */
    public function render($request, Exception $exception)
    {
        if($request->is("api/*")){
            // 将方法拦截到自己的ExceptionReport
            $reporter = ExceptionReport::make($exception);
            if ($reporter->shouldReturn()){
                return $reporter->report();
            }
            if(env('APP_DEBUG')){
                //开发环境,则显示详细错误信息
                return parent::render($request, $exception);
            }else{
                //线上环境,未知错误,则显示500
                return $reporter->prodReport();
            }
        }
        return parent::render($request, $exception);
    }
}

5、控制器&&路由

5.1、创建控制器

  1. 创建接口控制器 Api/Controller.php
php artisan make:controller Api/Controller

然后增加如下方法

<?php

namespace App\Http\Controllers\Api;

use App\Http\Helpers\Api\ApiResponse;
use App\Http\Controllers\Controller as BaseController;

class Controller extends BaseController
{

    use ApiResponse;
    // 其他通用的Api帮助函数
}

  1. 创建接口控制器 Api/UserController.php
php artisan make:controller Api/UserController

然后增加如下方法

<?php

namespace App\Http\Controllers\Api;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    /**
     * @param Request $request
     * @return mixed
     */
    public function login(Request $request){
        $token = Auth::guard('api')->attempt(['name'=>$request->name,'password'=>$request->password]);
        if($token) {
            return $this->setStatusCode(201)->success(['token' => 'bearer ' . $token]);
        }
        return $this->failed('账号或密码错误',400);
    }

    /**
     * @return mixed
     */
    public function loginOut(){
        Auth::guard('api')->logout();
        return $this->success('退出成功...');
    }


    /**
     * 返回当前登录用户信息
     * @param Request $request
     * @return mixed
     */
    public function info(Request $request){

        $user = $request->user();
        return $this->success($user);
    }

    /**
     * @param Request $request
     * @return mixed
     * @throws \Exception
     */
    public function register(Request $request)
    {
        $member = [
            'name' => $request->name,
            'password' => $request->password,
        ];
        User::create($member);
        return $this->setStatusCode(201)->success('用户注册成功');
    }

    /**
     * @param Request $request
     * @return mixed
     */
    public function changePw(Request $request)
    {
        $user = $request->user();
        $user->update(['password' => $request->password]);
        $token = Auth::guard('api')->refresh();
        return $this->setStatusCode(201)->setToken($token)->success('修改密码成功');
    }

}

5.2、路由

routes\api.php 新增如下代码:

Route::prefix('user/')->group(function() {
        //用户注册
        Route::post('register', 'UserrController@register')->name('user.register');
        //用户登录
        Route::post('login', 'UserrController@login')->name('user.login');

        Route::middleware('api.refresh')->group(function() {
            //当前用户信息
            Route::get('info', 'UserrController@info')->name('user.info');
            Route::post('changepw', 'UserrController@changePw')->name('user.changepw');
            //用户退出
            Route::post('logout', 'UserrController@logout')->name('users.logout');
        });
    });

5.3、前端TOKEN更新

前端监测返回的header是否包含TOKEN ,有的话就替换本地TOKEN,从而实现API用户无感刷新TOKEN。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值