154 lines
4.2 KiB
PHP
154 lines
4.2 KiB
PHP
<?php
|
||
|
||
namespace app\common\service;
|
||
|
||
use think\Db;
|
||
use think\facade\Log;
|
||
|
||
/**
|
||
* 用户 API Key 服务
|
||
*
|
||
* Key 格式与"场景获客"保持一致:
|
||
* 5 组 × 5 位(大小写字母 + 数字),组间用 "-" 连接
|
||
* 示例:aB3k9-Z8c1Q-0f4Xk-M9n2P-1A2b3
|
||
*
|
||
* 绑定对象:ck_users 表(`users`)中的 `apiKey` 字段
|
||
*/
|
||
class UserApiKeyService
|
||
{
|
||
/** 字符集:大小写字母 + 数字 */
|
||
private static $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||
|
||
/**
|
||
* 生成一个在 ck_users 中唯一的 API Key
|
||
*
|
||
* @return string 格式:xxxxx-xxxxx-xxxxx-xxxxx-xxxxx
|
||
*/
|
||
public static function generate(): string
|
||
{
|
||
$chars = self::$chars;
|
||
$charLen = strlen($chars);
|
||
|
||
while (true) {
|
||
$key = '';
|
||
for ($i = 0; $i < 5; $i++) {
|
||
$segment = '';
|
||
for ($j = 0; $j < 5; $j++) {
|
||
$segment .= $chars[mt_rand(0, $charLen - 1)];
|
||
}
|
||
$key .= ($i > 0 ? '-' : '') . $segment;
|
||
}
|
||
|
||
// 确保全局唯一
|
||
$exists = Db::name('users')->where('apiKey', $key)->find();
|
||
if (!$exists) {
|
||
return $key;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为指定用户绑定 API Key(幂等:已有则直接返回,没有则生成并写入)
|
||
*
|
||
* @param int $userId ck_users.id
|
||
* @return string 该用户的 apiKey
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function bindOrGet(int $userId): string
|
||
{
|
||
$user = Db::name('users')
|
||
->where('id', $userId)
|
||
->where('deleteTime', 0)
|
||
->field('id, apiKey')
|
||
->find();
|
||
|
||
if (!$user) {
|
||
throw new \RuntimeException('用户不存在');
|
||
}
|
||
|
||
if (!empty($user['apiKey'])) {
|
||
return $user['apiKey'];
|
||
}
|
||
|
||
return self::forceGenerate($userId);
|
||
}
|
||
|
||
/**
|
||
* 强制为指定用户重新生成 API Key(会覆盖旧 Key)
|
||
*
|
||
* @param int $userId ck_users.id
|
||
* @return string 新生成的 apiKey
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function forceGenerate(int $userId): string
|
||
{
|
||
$user = Db::name('users')
|
||
->where('id', $userId)
|
||
->where('deleteTime', 0)
|
||
->field('id')
|
||
->find();
|
||
|
||
if (!$user) {
|
||
throw new \RuntimeException('用户不存在');
|
||
}
|
||
|
||
$apiKey = self::generate();
|
||
|
||
Db::name('users')
|
||
->where('id', $userId)
|
||
->update([
|
||
'apiKey' => $apiKey,
|
||
'updateTime' => time(),
|
||
]);
|
||
|
||
Log::info("UserApiKeyService: 用户 #{$userId} 生成/更新 apiKey");
|
||
|
||
return $apiKey;
|
||
}
|
||
|
||
/**
|
||
* 通过 apiKey 查找用户(用于对外接口身份校验)
|
||
*
|
||
* @param string $apiKey
|
||
* @return array|null ck_users 行,或 null(key 无效/用户已删除/已禁用)
|
||
*/
|
||
public static function findUserByKey(string $apiKey): ?array
|
||
{
|
||
if (empty($apiKey)) {
|
||
return null;
|
||
}
|
||
|
||
$user = Db::name('users')
|
||
->where('apiKey', $apiKey)
|
||
->where('deleteTime', 0)
|
||
->where('status', 1)
|
||
->find();
|
||
|
||
return $user ?: null;
|
||
}
|
||
|
||
/**
|
||
* 验证签名
|
||
*
|
||
* 只有三个固定字段参与签名:account、timestamp、apiKey
|
||
* stringToSign = account + timestamp (按字段名 ASCII 升序拼接值)
|
||
* firstMd5 = MD5(stringToSign)
|
||
* sign = MD5(firstMd5 + apiKey)
|
||
*
|
||
* @param string $account 请求中传入的 account(ck_users.account)
|
||
* @param string $timestamp 请求中传入的 timestamp
|
||
* @param string $apiKey 用户 apiKey
|
||
* @param string $sign 客户端传来的签名
|
||
* @return bool
|
||
*/
|
||
public static function validateSign(string $account, string $timestamp, string $apiKey, string $sign): bool
|
||
{
|
||
// account < timestamp(ASCII 升序)
|
||
$stringToSign = $account . $timestamp;
|
||
$firstMd5 = md5($stringToSign);
|
||
$expectedSign = md5($firstMd5 . $apiKey);
|
||
|
||
return hash_equals($expectedSign, $sign);
|
||
}
|
||
}
|