Laravel6通过jwt实现API用户无感刷新TOKEN
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模型
- 这里为了以后开发方便,把app\user.php 移动到 app\Models\user.php,并全局搜索 App\User 修改为 App\Models\User
- 让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、创建控制器
- 创建接口控制器 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帮助函数
}
- 创建接口控制器 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。