laravel中的JWT,tymon/jwt-auth源码分析

coposer地址:https://packagist.org/packages/tymon/jwt-auth

github地址:https://github.com/tymondesigns/jwt-auth

使用文档:https://jwt-auth.readthedocs.io/en/develop/

从项目名称看,它包含 jwt 和 auth 两个功能,jwt 解出subject的值,auth 使用这个值去数据库查找是否存在,而 auth 不是必须的,你也可以自己去查一下。

Laravel-5.7开启jwt-auth

安装依赖: composer require tymon/jwt-auth

在config 目录下生成 jwt.php 配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

最小配置(env)

bash 复制代码
JWT_SECRET=xxxxxxxxxxxxx
JWT_TTL=525600

修改User模型,实现 Tymon\JWTAuth\Contracts\JWTSubject 接口

php 复制代码
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
	// 设置 sub 的值,此处返回 user 模型的主键ID
	return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
	// 自定义 jwt claim 的键值对,非必须
	return [];
}

jwt.php 配置文件中的 public和private是非对称加密的秘钥,是jwt的另一种用途,此处不涉及。

vendor\tymon\jwt-auth\src\Claims\Factory.php 中定义了各个 claim 的生成算法

php 复制代码
public function iss() // Issuer
{
	return $this->request->url();
}
public function iat() // Issued At
{
	return Utils::now()->getTimestamp();
}
public function exp() // Expiration
{
	return Utils::now()->addMinutes($this->ttl)->getTimestamp();
}
public function nbf() // Not Before
{
	return Utils::now()->getTimestamp();
}
public function jti() // JWT Id
{
	return Str::random(); // 默认长度16
}   

关于 claim 中的 prv 字段,其说明如下

php 复制代码
/*
|--------------------------------------------------------------------------
| Lock Subject
|--------------------------------------------------------------------------
|
| This will determine whether a `prv` claim is automatically added to
| the token. The purpose of this is to ensure that if you have multiple
| authentication models e.g. `App\User` & `App\OtherPerson`, then we
| should prevent one authentication request from impersonating another,
| if 2 tokens happen to have the same id across the 2 different models.
|
| Under specific circumstances, you may want to disable this behaviour
| e.g. if you only have one authentication model, then you would save
| a little on token size.
|
*/

'lock_subject' => true,

vendor\tymon\jwt-auth\src\JWT.php

php 复制代码
/**
 * Get the claims associated with a given subject.
 *
 * @param  \Tymon\JWTAuth\Contracts\JWTSubject  $subject
 *
 * @return array
 */
protected function getClaimsForSubject(JWTSubject $subject)
{
    return array_merge([
        'sub' => $subject->getJWTIdentifier(),
    ], $this->lockSubject ? ['prv' => $this->hashSubjectModel($subject)] : []);
}

/**
 * Hash the subject model and return it.
 *
 * @param  string|object  $model
 *
 * @return string
 */
protected function hashSubjectModel($model)
{
    return sha1(is_object($model) ? get_class($model) : $model);
}

/**
 * Check if the subject model matches the one saved in the token.
 *
 * @param  string|object  $model
 *
 * @return bool
 */
public function checkSubjectModel($model)
{
    if (($prv = $this->payload()->get('prv')) === null) {
        return true;
    }

    return $this->hashSubjectModel($model) === $prv;
}

例如

bash 复制代码
get_class(User): App\Model\User
sha1('App\Model\User'): f6b71549db8c2c42b75827aa44f02b7ee529d24d

所以 prv 的作用就是标识 sub 是来自哪个模型,比如 \App\Model\User\App\Model\OtherPerson,因此在解析 token 的时候,得到 sub 之后,还要调用 checkSubjectModel 方法来鉴别具体是哪个模型,才知道要去查哪个表,当然 prv 是可选的,视情况而定。

php 复制代码
$id = $auth->getPayload()->get('sub');
$auth->checkSubjectModel(\App\Model\User::class)
$auth->checkSubjectModel(\App\Model\OtherPerson::class)

生成 jwt token

php 复制代码
public function createToken($id)
{
    $user = new User();
    $user->id = $id;
    return \JWTAuth::claims([new Custom('iss', 'BK-JTW')])->fromUser($user);
}

解析jwt token

php 复制代码
public function checkToken(Request $request, JWTAuth $auth)
{
    try {
        $token = $request->input('token', '');
        if (!$token) {
            return $this->ajaxError('params error');
        }
        $auth->setRequest($request);
        if (!$auth->parser()->hasToken()) {
            return $this->ajaxError('token not found', '');
        }

        $auth->parseToken();
        $id = $auth->getPayload()->get('sub');
        $userInfo = $this->repository->getUserById($id);
        if ($userInfo) {
            return $this->ajaxSuccess('success', $userInfo);
        } else {
            return $this->ajaxError('customer not found');
        }
    } catch (Exception $e) {
        return $this->ajaxError($e->getMessage(), "");
    }
}

$auth->getPayload()方法中就已经对 token 进行了校验,如果 signature 校验失败会抛出 Tymon\\JWTAuth\\Exceptions\\TokenInvalidException异常Token Signature could not be verified.

jwt 解析网站 https://www.jwt.io/

它能解析出 Header 和 Payload,但是由于不知道 jwt_secret,所以校验不通过。

退出登录(invalidate token)源码解析

php 复制代码
public function logout(Request $request, JWTAuth $auth)
{
    try {

        $token = $request->input('token', '');
        $auth->setToken($token);
        $result = $auth->invalidate();

        return $this->ajaxSuccess('success', $result);
    } catch (Exception $e) {
        return $this->ajaxError($e->getMessage(), '');
    }
}

如果 signature 校验失败会抛出 Tymon\\JWTAuth\\Exceptions\\TokenInvalidException异常Token Signature could not be verified.

退出登录本质是将 jti 值加入到 Blacklist,会产生3个key

bash 复制代码
dbuc_cache:tag:tymon.jwt:key
s:22:"69882bbf2ec4b334764625";

dbuc_cache:69882bbf2ec4b334764625:standard_ref
1   dbuc_cache:33f80c37acd56e44b79280b77f88dc0c45f3cf07:yJK1TEqpBEys5FSJ

dbuc_cache:33f80c37acd56e44b79280b77f88dc0c45f3cf07:yJK1TEqpBEys5FSJ
a:1:{s:11:"valid_until";i:1770531775;}

源码vendor\tymon\jwt-auth\src\JWT.php

bash 复制代码
storage: Tymon\JWTAuth\Providers\Storage\Illuminate
cache: Illuminate\Cache\RedisTaggedCache
store: Illuminate\Cache\RedisStore

cache tag: tymon.jwt

tagKey: 'tag:'.$tagName.':key'

cache prefix: 默认为 {APP_NAME}_cache

3个key的关系如下
namespace: 69882bbf2ec4b334764625,其生成规则为 str_replace('.', '', uniqid('', true))
sha1(namespace): 33f80c37acd56e44b79280b77f88dc0c45f3cf07
yJK1TEqpBEys5FSJ 为 jwt claim 中的 jti 值
itemKey: 33f80c37acd56e44b79280b77f88dc0c45f3cf07:yJK1TEqpBEys5FSJ

tag的存在是为了方便管理keys,为key打上tag,本质上就是为key增加了一个前缀,减小管理的粒度。

vendor\tymon\jwt-auth\src\Blacklist.php 中已经规定了将 token 的 jti 值加入到 blacklist,并设置解封时间,这个时间就是 token 的 exp 字段的值,如果没有设置 exp ,那就是永久封禁。

valid_until 为添加时间,valid_until + TTL = exp

这就要求 jti 值必须是唯一的,jti(JWT ID) 是一个可选的标准声明(registered claim),用于唯一标识一个 JWT 令牌,主要目的是防止重放攻击(replay attack)。同一个 user_id 在生成 token 的时候会得到不同的 jti 值,所以,退出操作只针对单个的 token

相关推荐
ZHOUPUYU11 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
冬奇Lab16 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读
泓博18 小时前
Android中仿照View selector自定义Compose Button
android·vue.js·elementui
Tony Bai18 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
zhangphil19 小时前
Android性能分析中trace上到的postAndWait
android
十里-19 小时前
vue2的web项目打包成安卓apk包
android·前端
p***199419 小时前
MySQL——内置函数
android·数据库·mysql
兆子龙21 小时前
我成了🤡, 因为不想看广告,花了40美元自己写了个鸡肋挂机脚本
android·javascript
GIS追梦人1 天前
笔记-Laravel12 开发API前置准备
php·laravel
儿歌八万首1 天前
Android 全局监听神器:registerActivityLifecycleCallbacks 解析
android·kotlin·activity