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

相关推荐
Kapaseker10 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴10 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭21 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
ServBay1 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
用户962377954481 天前
CTF 伪协议
php
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin