最近一直在忙公司小程序,还有微信的一些H5开发,固然做为后端的我一直都只是写写简单的逻辑。因为以前一直都是混编开发,内心确实是累啊。因此也想能够提高下组内的技术水平,同时也是为了更好的分工,因而就想往先后端分离这种模式进行探索。php
虽知道接触一门新的东西,是要付出巨大的努力的。为了可让之后本身少走点弯路,在项目进度符合预期的同时,专门抽了时间针对API开发进行了碎片的学习。本篇文章借鉴了不少同行的事例代码,同时也结合了本身的一点分析。但愿各位道友能在其中有所感悟。前端
谈起API开发,我想第一个要解决的就是用户的状态问题吧,毕竟HTTP是属于无状态的协议,咱们在构建API的时候不能像普通的应用与系统开发同样,直接把用户的数据存放session而后再进行登陆的校验。为此有大神就想出了JWT相似的解决方案,经过token来对用户的受权以及状态进行一个感知。node
在介绍 JWT 以前, 咱们首先介绍一下, 传统的服务器端使用 session 对多用户进行受权的方式. 固然在 session 以前还有 cookie 的方式来保存用户的受权信息在客户机(好比浏览器)上, 不过纯 cookie 的方式过于不安全, 咱们就把 cookie 跟 session 一块儿说.laravel
之因此须要受权机制, 是由于 http 的无状态性(stateless).web
也就是说当一个用户发送一次请求, 请求中附带帐户名和密码登陆成功以后, 若是这个这个用户再次发送一次请求, 服务器是不能知道这个用户是已经登陆过的, 这个时候服务器就还须要用户再次提供受权信息, 也就是用户名和密码.算法
若是客户端能更少的把本身的身份受权信息在网络上传输, 那么客户端就能更大程度上避免本身的身份信息被泄露.数据库
而 session 和 token 都是为了解决此问题出现的.json
认证流程小程序
session 的出现解决了一部分的问题, 但随着时间的推移和互联网的发展, 一些缺陷也随之暴露出来, 好比但不只限于如下几点后端
为了解决此类问题, token 应运而生了.
认证流程
token 只是一种思路, 一种解决用户受权问题的思考方式, 基于这种思路, 针对不一样的场景能够有不少种的实现. 而在众多的实现中, JWT(JSON Web Token) 的实现最为流行.
JWT 这个标准提供了一系列如何建立具体 token 的方法, 这些缘故方法和规范可让咱们建立 token 的过程变得更加合理和效率.
好比, 传统的作法中, 服务器会保存生成的token, 当客户端发送来token时, 与服务器的进行比对, 可是 jwt 的不须要在服务器保存任何 token, 而是使用一套加密/解密算法 和 一个密钥 来对用户发来的token进行解密, 解密成功后就能够获得这个用户的信息.
这样的作法同时也增长了多服务器时的扩展性, 在传统的 token 验证中, 一旦用户发来 token, 那么必需要先找到存储这个 token 的服务器是哪台服务器, 而后由那一台服务器进行验证用户身份. 而 jwt 的存在, 只要每一台服务器都知道解密密钥, 那么每一台服务器均可以拥有验证用户身份的能力.
这样一来, 服务器就再也不保存任何用户受权的信息了, 也就解决了 session 曾出现的问题.
简单介绍完了 JWT 以后, 接下来咱们就简单看一下在实际场景中 JWT 的应用.
# 建议使用1.0以上版本
composer require tymon/jwt-auth 1.*@rc
复制代码
这里指的注意的是,有些文档会说要添加 Tymon\JWTAuth\Providers\LaravelServiceProvider::class
,这只在 Laravel 5.4 及如下版本是必要的,更新的 Laravel 版本无需添加。
还有一些文档说要添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider
这是好久之前的 JWT 版本的(大概0.5.3 之前的版本)。
# 这条命令会在 config 下增长一个 jwt.php 的配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
复制代码
# 这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar
php artisan jwt:secret
复制代码
若是你使用默认的 User 表来生成 token,你须要在该模型下增长一段代码
<?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.
* 获取将存储在JWT的中的标识符token。
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
* 返回一个键值数组,其中包含要添加到JWT中的任何自定义声明
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
复制代码
上面两个方法,其实就是实现了JWTSubject借口必须实现的方法。这个也是按照官方文档进行配置便可。
这两个 Facade 并非必须的,可是使用它们会给你的代码编写带来一点便利。
config/app.php
'aliases' => [
...
// 添加如下两行
'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth',
'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory',
],
复制代码
若是你不使用这两个 Facade,你可使用辅助函数 auth()
auth() 是一个辅助函数,返回一个guard,暂时能够当作 Auth Facade。
关于Auth Facade。建议你们参考这篇文章,我在学习JWT的时候也是看了好几遍才看懂的。
config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt', // 原来是 token 改为jwt
'provider' => 'users',
],
],
复制代码
注意:在 Laravel 下,route/api.php
中的路由默认都有前缀 api
。
Route::group([
'prefix' => 'auth'
], function ($router) {
Route::post('login', 'AuthController@login');
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
Route::post('me', 'AuthController@me');
});
复制代码
php artisan make:controller AuthController
复制代码
AuthController
值得注意的是 Laravel 这要用 auth('api')
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
class AuthController extends Controller
{
/**
* Get a JWT via given credentials.
* 该方法用于生成token
* @return \Illuminate\Http\JsonResponse
*/
public function login()
{
$credentials = request(['email', 'password']);
if (! $token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
/**
* Get the authenticated User.
* 该方法经过token获取对应的用户信息
* @return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json(auth('api')->user());
}
/**
* Log the user out (Invalidate the token).
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth('api')->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
* 刷新token,若是开启黑名单,之前的token便会失效。
* 值得注意的是用上面的getToken再获取一次Token并不算作刷新,两次得到的Token是并行的,即两个均可用。
* @return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth('api')->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('api')->factory()->getTTL() * 60
]);
}
}
复制代码
有两种使用方法:
?token=你的token
Authorization:Bearer 你的token
删除 token
后,token就会失效,没法再利用其获取数据。
到此咱们就已经能够成功地把JWT的开发例子演示完毕了,固然下面我会继续的把一些经常使用的方法展现出来,另外也会附上JWT的一些数据结构的专门文章。
前面的 AuthController.php
中有两行展示了这一种 token
的建立方法,即用用户所给的帐号和密码进行尝试,密码正确则用对应的 User
信息返回一个 token
。
但 token
的建立方法不止这一种,接下来介绍 token
的三种建立方法:
这就是刚刚说的哪种,贴出具体代码。
// 使用辅助函数
$credentials = request(['email', 'password']);
$token = auth('api')->attempt($credentials)
// 使用 Facade
$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials);
复制代码
// 使用辅助函数
$user = User::first();
$token = auth('api')->login($user);
// 使用 Facade
$user = User::first();
$token = JWTAuth::fromUser($credentials);
复制代码
// 使用辅助函数
$token = auth('api')->tokenById(1);
复制代码
只有 Facade 须要这样。
// 把请求发送过来的直接解析到对象
JWTAuth::parseToken();
复制代码
// 辅助函数
$user = auth()->user();
// Facade
$user = JWTAuth::parseToken()->authenticate();
复制代码
若是 token 被设置则会返回,不然会尝试使用方法从请求中解析 token ,若是token未被设置或不能解析最终返回false。
// 辅助函数
$token = auth()->getToken();
// Facade
$token = JWTAuth::parseToken()->getToken();
复制代码
直接 base64
解码 token
的前两段便可以知道所需的信息。
载荷信息会在 token 解码时获得,同时越大的数组会生成越长的 token ,因此不建议放太多的数据。同时由于载荷是用 Base64Url
编码,因此至关于明文,所以绝对不能放密码等敏感信息。
$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
// 辅助函数
$token = auth()->claims($customClaims)->attempt($credentials);
// Facade - 1
$token = JWTAuth::claims($customClaims)->attempt($credentials);
复制代码
从请求中把载荷解析出来。能够去看扩展源代码,里面还有不少的方法。
// 辅助函数
$exp = auth()->payload()->get('exp');
$json = auth()->payload()->toJson();
$array = auth()->payload()->jsonSerialize();
$sub = $array['sub'];
// Facade - 1
$payload = JWTAuth::parseToken()->getPayload();
$payload->get('sub'); // = 123
$payload['jti']; // = 'asfe4fq434asdf'
$payload('exp') // = 123456
$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4fq434asdf'] etc
// Facade - 2
$exp = JWTAuth::parseToken()->getClaim('exp');
复制代码
一个 token
通常来讲有三个时间属性,其配置都在 config/jwt.php 内。
有效时间指的的是你得到 token
后,在多少时间内能够凭这个 token
去获取内容,逾时无效。
// 单位:分钟
'ttl' => env('JWT_TTL', 60)
复制代码
刷新时间指的是在这个时间内能够凭旧 token
换取一个新 token
。例如 token
有效时间为 60 分钟,刷新时间为 20160 分钟,在 60 分钟内能够经过这个 token
获取新 token
,可是超过 60 分钟是不能够的,而后你能够一直循环获取,直到总时间超过 20160 分钟,不能再获取。
// 单位:分钟
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160)
复制代码
宽限时间是为了解决并发请求的问题,假如宽限时间为 0s ,那么在新旧 token
交接的时候,并发请求就会出错,因此须要设定一个宽限时间,在宽限时间内,旧 token
仍然可以正常使用
// 宽限时间须要开启黑名单(默认是开启的),黑名单保证过时token不可再用,最好打开
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true)
// 设定宽限时间,单位:秒
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60)
复制代码
a) token 为何要刷新吗?
首先 Basic Auth
是一种最简单的认证方法,可是因为每次请求都带用户名和密码,频繁的传输确定不安全,因此才有 cookies
和 session
的运用。若是 token
不刷新,那么 token
就至关于上面的用户名+密码,只要获取到了,就能够一直盗用,所以 token
设置有效期并可以进行刷新是必要的。
b) token 有效期多久合适,刷新频率多久合适?
有效期越长,风险性越高,有效性越短,刷新频率越高,刷新就会存在刷新开销,因此这须要综合考虑。我我的通常考虑的范围是:15min ~ 120min。
c) 有没有必要每次都刷新 token ?
上面考虑的 15min ~ 120min,会存在一个问题,就是重放攻击风险,防护这个风险,在 JWT
可用的方案是每次请求后都刷新一次 token
,但这样又会存在一个新的问题:并发请求。一次并发请求是用的一个 token
,第一个完成的请求会致使后面的请求所有失败。可用的解决方案是设置宽限时间,即一个 token
刷新后,旧 token
仍然短暂的可用。惋惜这样并不能完美的解决重放攻击,只是增大了不法者攻击的成本。这个问题在 JWT
中并无很好的解决。
下面是可用的中间件,第一二个功能同样,可是第二个不会抛出错误,第三四个功能同样,没什么区别。
tymon\jwt-auth\src\Providers\AbstractServiceProvider.php
protected $middlewareAliases = [
'jwt.auth' => Authenticate::class,
'jwt.check' => Check::class,
'jwt.refresh' => RefreshToken::class,
'jwt.renew' => AuthenticateAndRenew::class,
];
复制代码
由于没法彻底解决重放攻击,因此在因重放攻击会致使巨大安全问题和损失的地方,建议使用其余安全认证措施。而平常 Api
使用建议以下设置:
有效时间:15min ~ 120min
刷新时间:7天 ~ 30天
宽限时间:60s
复制代码
JWTAuth::parseToken()->方法()
通常均可以换成 auth()->方法()
。
attempt
根据 user 帐密新建一个 token。
$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials);
复制代码
fromUser or fromSubject
根据 user 对象生成一个 token。后者是前者别名
$user = User::find(1);
$token = JWTAuth::fromUser($user);
复制代码
refresh
更新 token。
$newToken = JWTAuth::parseToken()->refresh();
复制代码
invalidate
让一个 token 无效。
JWTAuth::parseToken()->invalidate();
复制代码
check
检验 token 的有效性。
if(JWTAuth::parseToken()->check()) {
dd("token是有效的");
}
复制代码
authenticate or toUser or user
这三个效果是同样的,toUser
是 authenticate
的别名,而 user
比前二者少一个 user id 的校验,但并无什么影响。
$user = JWTAuth::parseToken()->toUser();
复制代码
parseToken
从 request 中解析 token 到对象中,以便进行下一步操做。
JWTAuth::parseToken();
复制代码
getToken
从 request 中获取token。
$token = JWTAuth::getToken(); // 这个不用 parseToken ,由于方法内部会自动执行一次
复制代码
customClaims or claims
设置载荷的 customClaims 部分。后者是前者的别名。
$customClaims = ['sid' => $sid, 'code' => $code];
$credentials = $request->only('email', 'password');
$token = JWTAuth::customClaims($customClaims)->attempt($credentials);
复制代码
getCustomClaims
获取载荷的 customClaims 部分,返回一个数组。
$customClaims = JWTAuth::parseToken()->getCustomClaims()
复制代码
getPayload or payload
获取全部载荷,三个都是同样的,最后一个通常用来检验 token 的有效性
$payload = JWTAuth::parseToken()->payload();
// then you can access the claims directly e.g.
$payload->get('sub'); // = 123
$payload['jti']; // = 'asfe4fq434asdf'
$payload('exp') // = 123456
$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4fq434asdf'] etc
复制代码
getClaim
获取载荷中指定的一个元素
$sub = JWTAuth::parseToken()->getClaim('sub');
复制代码
这个 Facade 主要进行载荷的管理,返回一个载荷对象,而后能够经过 JWTAuth 来对其生成一个 token
// 载荷的高度自定义
$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar' => 'baz'])->make();
$token = JWTAuth::encode($payload);
复制代码
$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);
复制代码
这里用 auth 的写法,由于 Laravel 有多个 guard,默认 guard 也不是 api ,因此须要写成 auth('api')
不然,auth()
便可。
$token = auth('api')->claims(['foo' => 'bar'])->attempt($credentials);
复制代码
$user = auth('api')->setToken('eyJhb...')->user();
复制代码
$user = auth('api')->setRequest($request)->user();
复制代码
$token = auth('api')->setTTL(7200)->attempt($credentials);
复制代码
$boolean = auth('api')->validate($credentials);
复制代码
最后若是你想统一设置一下规则建议你仍是使用中间件吧,这样你就能够经过你的中间件去保护你须要验证的路由与方法。文章贴出的是其余大神写的中间件,怎么说好呢?各位道友本身好好去摸索摸索吧!
中间件代码以下:
<?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);
}
}复制代码
因为咱们构建的是 api
服务,因此咱们须要更新一下 app/Exceptions/Handler.php
中的 render
方法,自定义处理一些异常。
<?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);
}
}复制代码
至此,laravel的相关结合与学习暂告一段落!