laravel 结合JWT开发API

    最近一直在忙公司小程序,还有微信的一些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 原理概述

认证流程小程序

  1. 当用户使用用户名和密码登陆以后, 服务器就会生成一个 session 文件, session 文件中保存着对这个用户的受权信息, 这个文件能够储存在硬盘/内存/数据库中.
  2. 同时还要生成一个对应这个 session 文件的 sessionid, 经过 sessionid 就可以找到这个 session 文件.
  3. 而后将 sessionid 发送给客户端, 客户端就将 sessionid 保存起来, 保存的方式有不少种, 目前大多状况是经过 cookie 来保存 sessionid.
  4. 保存以后, 当客户机之后再向服务器发送请求的时候, 请求携带上 sessionid, 这样服务器收到 sessionid 以后, 本身就会在服务区上查找对应的 session 文件, 若是查找成功, 就会获得该用户的受权信息, 从而完成一次受权.

session 的出现解决了一部分的问题, 但随着时间的推移和互联网的发展, 一些缺陷也随之暴露出来, 好比但不只限于如下几点后端

  • 随着用户量的增长, 每一个用户都须要在服务器上建立一个 session 文件, 这对服务器形成了压力
  • 对于服务器压力的分流问题, 若是一个用户的 session 被存储在某台服务器上, 那么当这个用户访问服务器时, 用户就只能在这台服务器上完成受权, 其余的分流服务器没法进行对这种请求进行分流
  • 一样也是 session 存储的问题, 当咱们在一台服务器上成功登陆, 若是咱们想要另外的一台别的域名的服务器也能让用户不登陆就能完成受权, 这个时候就会有不少麻烦

为了解决此类问题, token 应运而生了.

Token 原理概述

认证流程

  1. 客户端发送认证信息(通常就是用户名/密码), 向服务器发送请求
  2. 服务器验证客户端的认证信息, 验证成功以后, 服务器向客户端返回一个 加密的token(通常状况下就是一个字符串)
  3. 客户端存储(cookie, session, app 中均可以存储)这个 token, 在以后每次向服务器发送请求时, 都携带上这个 token
  4. 服务器验证这个 token 的合法性, 只要验证经过, 服务器就认为该请求是一个合法的请求

JWT 概述

token 只是一种思路, 一种解决用户受权问题的思考方式, 基于这种思路, 针对不一样的场景能够有不少种的实现. 而在众多的实现中, JWT(JSON Web Token) 的实现最为流行.

JWT 这个标准提供了一系列如何建立具体 token 的方法, 这些缘故方法和规范可让咱们建立 token 的过程变得更加合理和效率.

好比, 传统的作法中, 服务器会保存生成的token, 当客户端发送来token时, 与服务器的进行比对, 可是 jwt 的不须要在服务器保存任何 token, 而是使用一套加密/解密算法 和 一个密钥 来对用户发来的token进行解密, 解密成功后就能够获得这个用户的信息.

这样的作法同时也增长了多服务器时的扩展性, 在传统的 token 验证中, 一旦用户发来 token, 那么必需要先找到存储这个 token 的服务器是哪台服务器, 而后由那一台服务器进行验证用户身份. 而 jwt 的存在, 只要每一台服务器都知道解密密钥, 那么每一台服务器均可以拥有验证用户身份的能力.

这样一来, 服务器就再也不保存任何用户受权的信息了, 也就解决了 session 曾出现的问题.


简单介绍完了 JWT 以后, 接下来咱们就简单看一下在实际场景中 JWT 的应用.


Laravel

1. 使用 composer 安装

# 建议使用1.0以上版本
composer require tymon/jwt-auth 1.*@rc
复制代码

2. 进行一些配置

这里指的注意的是,有些文档会说要添加 Tymon\JWTAuth\Providers\LaravelServiceProvider::class,这只在 Laravel 5.4 及如下版本是必要的,更新的 Laravel 版本无需添加。

还有一些文档说要添加 Tymon\JWTAuth\Providers\JWTAuthServiceProvider 这是好久之前的 JWT 版本的(大概0.5.3 之前的版本)。

2.1 发布配置文件

# 这条命令会在 config 下增长一个 jwt.php 的配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
复制代码

2.2 生成加密密钥

# 这条命令会在 .env 文件下生成一个加密密钥,如:JWT_SECRET=foobar
php artisan jwt:secret
复制代码

2.3 更新你的模型

若是你使用默认的 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借口必须实现的方法。这个也是按照官方文档进行配置便可。

2.4 注册两个 Facade

这两个 Facade 并非必须的,可是使用它们会给你的代码编写带来一点便利。

config/app.php

'aliases' => [
        ...
        // 添加如下两行
        'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth',
        'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory',
],
复制代码

若是你不使用这两个 Facade,你可使用辅助函数 auth()

auth() 是一个辅助函数,返回一个guard,暂时能够当作 Auth Facade。

关于Auth Facade。建议你们参考这篇文章,我在学习JWT的时候也是看了好几遍才看懂的。

Laravel 辅助函数 auth 与 JWT 扩展详解


2.5 修改 auth.php

config/auth.php

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

    'api' => [
        'driver' => 'jwt',      // 原来是 token 改为jwt
        'provider' => 'users',
    ],
],
复制代码

2.6 注册一些路由

注意:在 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');

});
复制代码

2.7 建立 token 控制器

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
        ]);
    }
}
复制代码

JWT Token 详解

1. token 的获取、使用、删除和刷新

  • 如下用 postman 演示,
  • Laravel 环境下写在 api.php 中的路由默认有前缀 api

1.1 获取 token


1.2 使用 token

有两种使用方法:

  • 加到 url 中:?token=你的token
  • 加到 header 中,建议用这种,由于在 https 状况下更安全:Authorization:Bearer 你的token


1.3 删除 token


删除 token 后,token就会失效,没法再利用其获取数据。


1.4 刷新 token



到此咱们就已经能够成功地把JWT的开发例子演示完毕了,固然下面我会继续的把一些经常使用的方法展现出来,另外也会附上JWT的一些数据结构的专门文章。

JWT实现原理

2. token 的建立

前面的 AuthController.php 中有两行展示了这一种 token 的建立方法,即用用户所给的帐号和密码进行尝试,密码正确则用对应的 User 信息返回一个 token

token 的建立方法不止这一种,接下来介绍 token 的三种建立方法:

  • 基于帐密参数
  • 基于 users 模型返回的实例
  • 基于 users 模型中的用户主键 id

a) 基于帐密参数

这就是刚刚说的哪种,贴出具体代码。

// 使用辅助函数
$credentials = request(['email', 'password']); 
$token = auth('api')->attempt($credentials)

// 使用 Facade
$credentials = $request->only('email', 'password');
$token = JWTAuth::attempt($credentials);
复制代码

b) 基于 users 模型返回的实例

// 使用辅助函数
$user = User::first();
$token = auth('api')->login($user);

// 使用 Facade
$user = User::first();
$token = JWTAuth::fromUser($credentials);
复制代码

c) 基于 users 模型中的主键 id

// 使用辅助函数
$token = auth('api')->tokenById(1);
复制代码

2.1 token 的解析

a) 解析 token 到对象

只有 Facade 须要这样。

// 把请求发送过来的直接解析到对象
JWTAuth::parseToken();
复制代码

b) 获取 token 中的 user 信息

// 辅助函数
$user = auth()->user();

// Facade
$user = JWTAuth::parseToken()->authenticate();
复制代码

c) 获取 token

若是 token 被设置则会返回,不然会尝试使用方法从请求中解析 token ,若是token未被设置或不能解析最终返回false。

// 辅助函数
$token = auth()->getToken();

// Facade
$token = JWTAuth::parseToken()->getToken();
复制代码

d) 若是是前端

直接 base64 解码 token 的前两段便可以知道所需的信息。


3. 载荷的设置和获取

a) 载荷设置

载荷信息会在 token 解码时获得,同时越大的数组会生成越长的 token ,因此不建议放太多的数据。同时由于载荷是用 Base64Url 编码,因此至关于明文,所以绝对不能放密码等敏感信息。

$customClaims = ['foo' => 'bar', 'baz' => 'bob'];

// 辅助函数
$token = auth()->claims($customClaims)->attempt($credentials);

// Facade - 1
$token = JWTAuth::claims($customClaims)->attempt($credentials);
复制代码

b) 载荷解析

从请求中把载荷解析出来。能够去看扩展源代码,里面还有不少的方法。

// 辅助函数
$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');
复制代码

4. token 的三个时间

一个 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)
复制代码

5.token 的刷新问题?

a) token 为何要刷新吗?

首先 Basic Auth 是一种最简单的认证方法,可是因为每次请求都带用户名和密码,频繁的传输确定不安全,因此才有 cookiessession 的运用。若是 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,
];
复制代码

5.1 token 的刷新总结

由于没法彻底解决重放攻击,因此在因重放攻击会致使巨大安全问题和损失的地方,建议使用其余安全认证措施。而平常 Api 使用建议以下设置:

有效时间:15min ~ 120min
刷新时间:7天 ~ 30天
宽限时间:60s
复制代码


其余经常使用方法附录

1. JWT 的 两个 Facade

1.1 JWTAuth

JWTAuth::parseToken()->方法() 通常均可以换成 auth()->方法()

token 生成

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);
复制代码

token 控制

refresh

更新 token。

$newToken = JWTAuth::parseToken()->refresh();
复制代码

invalidate

让一个 token 无效。

JWTAuth::parseToken()->invalidate();
复制代码

check

检验 token 的有效性。

if(JWTAuth::parseToken()->check()) {
    dd("token是有效的");
}
复制代码

token 解析

authenticate or toUser or user

这三个效果是同样的,toUserauthenticate 的别名,而 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');
复制代码

1.2 JWTGuard

这个 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);
复制代码

1.3 其余一些用法

这里用 auth 的写法,由于 Laravel 有多个 guard,默认 guard 也不是 api ,因此须要写成 auth('api') 不然,auth() 便可。

设置载荷

$token = auth('api')->claims(['foo' => 'bar'])->attempt($credentials);
复制代码

显示设置 token

$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);
    }
}复制代码

更新异常处理的 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);
    }
}复制代码

至此,laravel的相关结合与学习暂告一段落!

相关文章
相关标签/搜索