Laravel jwt 多表(多用户端)验证隔离

Tips: tymon/jwt-auth 作者已通过增加prv字段修复这一问题#1167,但是如果你是用dingo api + jwt 的话,该问题依然存在。

JWT 多表验证隔离

为什么要做隔离

当同一个laravel项目有多端(移动端、管理端......)都需要使用jwt做用户验证时,如果用户表有多个(一般都会有),就需要做token隔离,不然会发生移动端的token也能请求管理端的问题,造成用户越权。

会引发这个问题的原因是laravel的jwt token默认只会存储数据表的主键的值,并没有区分是那个表的。所以只要token里携带的ID在你的用户表中都存在,就会导致越权验证。

我们来看看laravel的jwt token 的原貌:

{
    "iss": "http://your-request-url",
    "iat": 1558668215,
    "exp": 1645068215,
    "nbf": 1558668215,
    "jti": "XakIDuG7K0jeWGDi",
    "sub": 1
}

携带数据的是sub字段,其他字段是jwt的验证字段。

我们只看到sub的值为1,并没有说明是那个表或是哪个验证器的。这个token通过你的验证中间件时,你使用不同的guard就能拿到对应表id为1的用户(了解guard请查看laravel的文档)。

解决办法

想要解决用户越权的问题,我们只要在token上带上我们的自定义字段,用来区分是哪个表或哪个验证器生成的,然后再编写自己的中间件验证我们的自定义字段是否符合我们的预期。

添加自定义信息到token

我们知道要使用jwt验证,用户模型必须要实现JWTSubject的接口(代码取自jwt文档):

<?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.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

我们可以看看实现的这两个方法的作用:

  • getJWTIdentifier的:获取会储存到jwt声明中的标识,其实就是要我们返回标识用户表的主键字段名称,这里是返回的是主键'id',
  • getJWTCustomClaims:返回包含要添加到jwt声明中的自定义键值对数组,这里返回空数组,没有添加任何自定义信息。

接下来我们就可以在实现了getJWTCustomClaims方法的用户模型中添加我们的自定义信息了。

管理员模型:

/**
 * 额外在 JWT 载荷中增加的自定义内容
 *
 * @return array
 */
public function getJWTCustomClaims()
{
    return ['role' => 'admin'];
}

移动端用户模型:

/**
 * 额外在 JWT 载荷中增加的自定义内容
 *
 * @return array
 */
public function getJWTCustomClaims()
{
    return ['role' => 'user'];
}

这里添加了一个角色名作为用户标识。

这样管理员生成的token会像这样:

{
    "iss": "http://your-request-url",
    "iat": 1558668215,
    "exp": 1645068215,
    "nbf": 1558668215,
    "jti": "XakIDuG7K0jeWGDi",
    "sub": 1,
    "role": "admin"
}

移动端用户生成的token会像这样:

{
    "iss": "http://your-request-url",
    "iat": 1558668215,
    "exp": 1645068215,
    "nbf": 1558668215,
    "jti": "XakIDuG7K0jeWGDi",
    "sub": 1,
    "role": "user"
}

我们可以看到这里多了一个我们自己加的role字段,并且对应我们的用户模型。

接下来我们自己写一个中间件,解析token后判断是否是我们想要的角色,对应就通过,不对应就报401就好了。

编写jwt角色校验中间件

这里提供一个可全局使用的中间件(推荐用在用户验证中间件前):

<?php
/**
 * Created by PhpStorm.
 * User: wlalala
 * Date: 2019-04-17
 * Time: 13:55
 */

namespace App\Http\Middleware;

use Closure;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

class JWTRoleAuth extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param $request
     * @param Closure $next
     * @param null $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role = null)
    {
        try {
            // 解析token角色
            $token_role = $this->auth->parseToken()->getClaim('role');
        } catch (JWTException $e) {
            /**
             * token解析失败,说明请求中没有可用的token。
             * 为了可以全局使用(不需要token的请求也可通过),这里让请求继续。
             * 因为这个中间件的责职只是校验token里的角色。
             */
            return $next($request);
        }

        // 判断token角色。
        if ($token_role != $role) {
            throw new UnauthorizedHttpException('jwt-auth', 'User role error');
        }

        return $next($request);
    }
}

注册jwt角色校验中间件

在app/Http/Kernel.php中注册中间件:

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        // ...省略 ...

        // 多表jwt验证校验
        'jwt.role' => \App\Http\Middleware\JWTRoleAuth::class,
    ];

使用jwt角色校验中间件

接下来在需要用户验证的路由组中添加我们的中间件:


Route::group([
    'middleware' => ['jwt.role:admin', 'jwt.auth'],
], function ($router) {
    // 管理员验证路由
    // ...
});

Route::group([
    'middleware' => ['jwt.role:user', 'jwt.auth'],
], function ($router) {
    // 移动端用户验证路由
    // ...
});

至此完成jwt多表用户验证隔离。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 24

Get到了,好东西

4年前 评论
阿周日记

666666

4年前 评论

学习到了,感谢分享

4年前 评论

原来jwt不存储表和guard啊,我说怎么多表登录怎么老有问题呢,问一下楼主怎么解析jwt token的原貌?

4年前 评论

@邪恶的咖啡
在过了jwt验证的情况下调用以下方法就有了:

 \Tymon\JWTAuth\Facades\JWTAuth::payload()->toJson() 
4年前 评论
   $payload = [
        "iss" => "", //签发者
        "aud" => "", //面向的用户
        "iat" => time(), //签发时间
        "nbf" => time(), //在什么时候jwt开始生效 
        "exp" => time() + 86400, //token 过期时间 秒
        'xx' => 'xxx'  //自定义
    ];
4年前 评论
bestcyt

学到了,刚刚好最近遇到这个问题,赞~

4年前 评论
bestcyt

等等,那你这边怎么验证这个token?不同用户表的话要指定不同guardName啊,如果是auth:guradname那不就可以直接区分不同用户表了吗?

4年前 评论
coder_russell 3年前

@bestcyt 其实一开始我我也是你这个思路,直接指定看守器对应到相应的Model,但似乎还有一点小小的BUG,Token里面的payload(载荷)里面只寄存了主键ID,这是唯一区分用户的字段,也就是说即使你指定了看守器,JWT也有可能拿着别的看守器生成的Token验证通过,因为它只记录了ID。



例如

我的admin表有一个用户,id为1

我的user表有一个用户,id为1

我在用户后台用user表的用户进行登录(用户后台),则会返回一个Token

现在我拿着我user(用户后台)生成的Token去操作admin的操作(管理后台),则能操作成功,JWT也没有抛出异常。

但是如果两张表的两个用户的ID不同,那就没什么问题,但是如果一旦有有相同,则属于一个验证的系统漏洞。

这是一个Token中载荷的内容:

{
    "iss": "http://your-request-url",
    "iat": 1558668215,
    "exp": 1645068215,
    "nbf": 1558668215,
    "jti": "XakIDuG7K0jeWGDi",
    "sub": 1
}

这是我在日常开发中亲自试过的,说实话以前从来没出现过这种情况,也没了解过载荷的概念,可能是更新了JWT版本问题导致的吧。

4年前 评论
coder_russell 3年前
bestcyt 4年前
coder_russell 3年前
coder_russell 3年前
邪恶的咖啡 (作者) 3年前
bestcyt

$token = auth()->claims(['role' => 'admin'])->attempt($credentials);或者在生成token的时候注意返回貌似也是可以的?如何在通过中间件判断

4年前 评论

@bestcyt

//先引入
use Tymon\JWTAuth\Facades\JWTAuth;
//通过门面获取
if(JWTAuth::parseToken()->getClaim('role') == 'admin'){
        return $next($request);
}
4年前 评论
bestcyt 4年前
sssword

mark

4年前 评论

mark 最近在用这东西

4年前 评论

@wlalala 我想请教一下,多用户端,jwt 认证的provider 配置项,是不是需要配置多个'model' ?

/config/auth.php

 'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
]
4年前 评论
matteao (作者) 4年前
抄底斗罗

你的头像上的动漫人物叫:L、龙崎

4年前 评论

@matteao 不同的用户端是不同的表的话是需要配置多个model的

4年前 评论
matteao 4年前

'middleware' => ['jwt.role:user', 'jwt.auth'],我的路由也是这样写的,为啥我把需要登录的接口写在中间件里面,就要报错User not found,写在中间件外面就正常访问了呢?

4年前 评论
zero123 3年前

file,两个路由本来都要写在group里面,但是现在是写在里面的要报错

file

4年前 评论
lzxprogrammer 4年前

@raulzhou 因为jwt.auth默认验证的是默认的guard, 也就是走了另一个用户表

4年前 评论
osang 3年前

"我们可以看到这里多了一个我们自己加的 role 字段,并且对应我们的用户模型。" 这里的用户模型指的是 Model 吗????

4年前 评论
wlalala (楼主) 4年前

老哥,验证成功以后,返回的时候报错了

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use Tymon\JWTAuth\Facades\JWTAuth;
use Illuminate\Support\Facades\Validator;
class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * [@return](https://learnku.com/users/31554) void
     */
    public function __construct()
    {
       $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * [@return](https://learnku.com/users/31554) \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        // 在这里的时候报错了,Method Illuminate\Auth\SessionGuard::factory does not exist.
        return $this->respondWithToken( $token );
    }

    /**
     * Get the authenticated User.
     *
     * [@return](https://learnku.com/users/31554) \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * [@return](https://learnku.com/users/31554) \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * [@return](https://learnku.com/users/31554) \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * [@return](https://learnku.com/users/31554) \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}
4年前 评论
wlalala (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!