sanctum是官方新推出的认证组件
laravel/sanctum
文档上表示,这个轻量化的 token 面向两类认证场景而生,思路起源为 Github 的 personal token 。
一是 为 api 令牌,发放长期 token ,认证时 在 header 头部传入。二为 基于 laravel api 的 spa 单页,将信息存入 cookie 。
对比下 jwt 与 sanctum 或者叫自定义 token 的 方式
认证方式 |
不需要数据库支持 |
多租户支持 |
便于管理 |
高性能 |
jwt |
✔️ |
✔️ |
❌ |
✔️ |
sanctum |
❌ |
✔️ |
✔️ |
❌ |
总之对于体量不大的应用内部使用 sanctum 没什么问题,要是做对外开放相关的业务还是建议 jwt
token 加解密实现
很想吐槽的就是这个 token 的生成方式,如
Bearer 195|JswKUO3O9Jsmh9ks5fNQoS1Qlk6Ub6KvJ137g00q
为什么这么设计,其实看头部就知道,用主键ID开头来加速 查询,但是 缺点也很明显,一个是对外暴露了内部信息的主键,第二点是 默认使用 bigint为主键导致的 token 长度不固定。
看下内部的加解密实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public function createToken(string $name, array $abilities = ['*']) { $token = $this->tokens()->create([ 'name' => $name, 'token' => hash('sha256', $plainTextToken = Str::random(40)), 'abilities' => $abilities, ]); return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken); }
public static function findToken($token) { if (strpos($token, '|') === false) { return static::where('token', hash('sha256', $token))->first(); } [$id, $token] = explode('|', $token, 2); if ($instance = static::find($id)) { return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null; }}
|
其核心 代码 也就是
hash('sha256', $plainTextToken = Str::random(40))
与 hash_equals
数据库存的 token 是 随机字符串后 hash 出来的 64位字符串,对外暴露的为 主键ID + | + 40位随机字符串
可选优化思路
token 多端通用
根据其生成方法 createToken(string $name, array $abilities = ['*'])
可知,数据库字段 name
与 abilities
都可以实现类似于多租户/多端 的功能,特别是 abilities
这个字段,可以做到更加精细的权限控制
比如 生成的时候
1 2 3 4 5 6
| $user->createToken('admin',["order","goods"]);
auth('admin')->user()->currentAccessToken()->abilities;
|
简单一点的直接用 $name
控制 多端也ok, 因为使用了数据库,所以扩展性也还 OK
发放 token 有效期控制
因为 官方 给出的过期时间很死板,没法做到 不同类型的 token 有不同的有效期,但还好也提供了一个回调函数来弥补
1 2 3 4 5 6 7 8 9 10
| protected function isValidAccessToken($accessToken): bool { if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) { $isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid); } return $isValid; }
|
重点就在 Sanctum::$accessTokenAuthenticationCallback
这个回调的属性上,
我们可以自定义这个回调函数 ,比如在 AuthServiceProvider>boot()
内,通过
Sanctum::authenticateAccessTokensUsing
来注入我们自定义的有效期检查逻辑
hook 每次请求的更新操作 , 来减少数据库消耗
我们可以通过 sql 监听发现,每次使用 sanctum , 都会产生两条 sql 记录,一条 通过id查询的sql,另一条为
1
| update `personal_access_tokens` set `last_used_at` = '2022-07-01 19:03:02', `personal_access_tokens`.`updated_at` = '2022-07-01 19:03:02' where `id` = 195 {"time":29.06}
|
看到这个执行时间就知道,这个 sql 肯定会成为整个系统的性能消耗点,现在让我们看看,这是在哪里触发的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public function __invoke(Request $request) {
if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') && method_exists($accessToken->getConnection(), 'setRecordModificationState')) { tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) { $accessToken->forceFill(['last_used_at' => now()])->save(); $accessToken->getConnection()->setRecordModificationState($hasModifiedRecords); }); } else { $accessToken->forceFill(['last_used_at' => now()])->save(); } return $tokenable; }
|
问题就转换成了 对于
$accessToken->forceFill(['last_used_at' => now()])->save()
这个代码的优化,很显然,好多时候我们并不需要 通过这种方式去,更新 last_used_at
字段,甚至不客气的说,这有点过度设计,因为这个字段基本用不到,提供一个可选的回调属性,来替换这一个操作更加合理。
个人提供的思路为 ,替换原来 model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
Sanctum::usePersonalAccessTokenModel(\App\Models\PersonalAccessToken::class);
public function forceFill(array $attributes) { if (count($attributes) === 1 && isset($attributes['last_used_at'])){ return $this; } return static::unguarded(function () use ($attributes) { return $this->fill($attributes); }); }
|
这样,去掉了对于 last_used_at
的更新操作,每次请求就只有一次 sql 操作了,至于需要属性的,可以自行调整更新频率
- 最后可能要去优化的点,对于每次的通过ID查整条token的操作,很明显是一个读多写少的场景,可以考虑上缓存去替换这个查询 sql ,但是不是极限场景(秒杀之类的),我不建议这么去做,收益太少了~