最近着手做一款应用后端,在否定了 BaaS 后,决定用 Laravel 框架自己做一个 RESTful API。我的环境是 Laravel 5.2 ,另外使用了 Dingo/API 和 JWTAuth。不过在使用的过程中遇到了很多的坑,所以在这里记录一下。

JWTAuth 默认使用 Users 表做为登录认证的表。而我的需求比较奇葩,共有两个不同的表;除此之外,还需要对 JWTAuth 的错误进行自定义。在搜索无果后,只好自己动手实现这两个需求。

首先解决第二个问题,对 JWTAuth 进行错误自定义。这种情况下,我们可以自己去添加一个中间件处理身份认证。

添加中间件处理身份验证

1、添加一个 Middleware

可以使用命令行添加:

php artisan make:middleware GetUserFromToken

此命令将会在 app/Http/Middleware 目录内置立一个名称为 GetUserFromToken 的类。

2、在 GetUserFromToken 中编辑代码,这里仿照 JWTAuth 写了 Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;

class GetUserFromToken
{
    public function handle($request, Closure $next)
    {
        $auth = JWTAuth::parseToken();
        if (! $token = $auth->setRequest($request)->getToken()) {
            return response()->json([
                'code' => '',
                'message' => 'token_not_provided',
                'data' => '',
            ]);
        }
        
        try {
            $user = $auth->authenticate($token);
        } catch (TokenExpiredException $e) {
            return response()->json([
                'code' => '',
                'message' => 'token_expired',
                'data' => '',
            ]);
        } catch (JWTException $e) {
            return response()->json([
                'code' => '',
                'message' => 'token_invalid',
                'data' => '',
            ]);
        }

        if (! $user) {
            return response()->json([
                'code' => '',
                'message' => 'user_not_found',
                'data' => '',
            ]);
        }

        //$this->events->fire('tymon.jwt.valid', $user);

        return $next($request);
    }
}

我将每次错误返回数据替换成自己设置的错误信息。

3、在 /app/Http/Kernel.php$routeMiddleware 新增如下内容:

protected $routeMiddleware = [
    ...
    'jwt.api.auth' => \App\Http\Middleware\GetUserFromToken::class, //新增注册的中间件
];

4、在路由中指定使用 jwt.api.auth

['middleware' => 'jwt.api.auth']

完成上面的操作,我们新增处理接口身份认证中间件就完成了。

现在需要处理前一个问题。

多表配置

在 JWTAuth 中,可以在配置文件 jwt.php 中设置 User Model namespace,所以可以在 Middlewarehandle 部分添加如下代码来动态配置 User Model namespace

config(['jwt.user' => 'App\Models\User']);

这里,我把 User 表放到了 App\Models\ 中和其他的统一进行管理。不过我在测试中一直出现 App\User 未定义错误。然后就开始了漫长的定位之旅。首先在访问 authenticate 得到

public function authenticate($token = false)
{
    $id = $this->getPayload($token)->get('sub');

    if (! $this->auth->byId($id)) {
        return false;
    }

    return $this->auth->user();
}

然后,在 Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter 中找到 byIduser 对应代码如下

public function byId($id)
{
    return $this->auth->onceUsingId($id);
}

public function user()
{
    return $this->auth->user();
}

经过测试发现 auth 实际上是一个 Illuminate\Auth\SessionGuard 实例,然后在其中发现了 onceUsingIduser 部分代码

public function onceUsingId($id)
{
    if (! is_null($user = $this->provider->retrieveById($id))) {
        $this->setUser($user);

        return true;
    }

    return false;
}

在查找 provider 所在位置时定位到文件 Illuminate\Auth\CreatesUserProviders.php 中找到如下代码

public function createUserProvider($provider)
{
    $config = $this->app['config']['auth.providers.'.$provider];
    if (isset($this->customProviderCreators[$config['driver']])) {
        return call_user_func(
            $this->customProviderCreators[$config['driver']], $this->app, $config
        );
    }

    switch ($config['driver']) {
        case 'database':
            return $this->createDatabaseProvider($config);
        case 'eloquent':
            return $this->createEloquentProvider($config);
        default:
            throw new InvalidArgumentException("Authentication user provider [{$config['driver']}] is not defined.");
    }
}

这里通过 auth.providers.users 配置设置 $config,而 auth.providers.users 在文件 auth.php 中默认配置如下

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

所以程序走到了 return $this->createEloquentProvider($config); 这一步,继续跟踪得到:

protected function createEloquentProvider($config)
{
    return new EloquentUserProvider($this->app['hash'], $config['model']);
}

其中 $config['model'] 则就是原型:

public function __construct(HasherContract $hasher, $model)
{
    $this->model = $model;
    $this->hasher = $hasher;
}

到此,确定了 model 所在位置,只需要在 Middleware 中添加如下配置

config(['auth.providers.users.model' => \App\Models\User::class]);

最终代码如下

config(['jwt.user' => '\App\Models\User']);
config(['auth.providers.users.model' => \App\Models\User::class]);
$auth = JWTAuth::parseToken();
if (! $token = $auth->setRequest($request)->getToken()) {
    return response()->json([
        'code' => '',
        'message' => 'token_not_provided',
        'data' => '',
    ]);
}

try {
    $user = $auth->authenticate($token);
} catch (TokenExpiredException $e) {
    return response()->json([
        'code' => '',
        'message' => 'token_expired',
        'data' => '',
    ]);
} catch (JWTException $e) {
    return response()->json([
        'code' => '',
        'message' => 'token_invalid',
        'data' => '',
    ]);
}

if (! $user) {
    return response()->json([
        'code' => '',
        'message' => 'user_not_found',
        'data' => '',
    ]);
}

//$this->events->fire('tymon.jwt.valid', $user);

return $next($request);

到这里为止,实现了自定义表名功能,在结合自定义 Middleware 部分,就可以实现多表认证。只需要对每一种认证都实现对应的 Middleware ,在接口处分别对不同接口使用不同的 Middleware 进行验证就好。

当然,这样的实现肯定不完美,因为所有的事件部分代码全部删除了。这部分还没有想到什么好的解决办法,自己实现 event 应该是可行的,这里就么有尝试。