diff --git a/docs/license-replacement-test-record.md b/docs/license-replacement-test-record.md index de5d48b6..9c2ebff2 100644 --- a/docs/license-replacement-test-record.md +++ b/docs/license-replacement-test-record.md @@ -67,3 +67,27 @@ - 每个阶段测试通过后单独提交。 - 提交前确认改动只包含当前阶段范围。 - 客服、企业微信 DAO、库存扣减/回滚均作为最后阶段内容,不夹带到前置阶段。 + +## 阶段 1:外部账号 token 解析 + +### 自动化检查 + +| 命令 | 结果 | 备注 | +|------|------|------| +| `php -l app/services/auth/AccessTokenService.php` | 通过 | PHP 提示 `swoole_loader` 已加载,不影响语法检查结果。 | +| `php -l app/services/out/OutAccountServices.php` | 通过 | PHP 提示 `swoole_loader` 已加载,不影响语法检查结果。 | + +### 手工回归记录 + +| 阶段 | 接口/命令 | 方法 | 身份 | 关键参数 | HTTP 状态 | 业务 `status` | 关键字段 | 结果 | 备注 | +|------|-----------|------|------|----------|-----------|---------------|----------|------|------| +| 1 | 外部账号获取 token | POST | out | `appid`、`appsecret` | 待预发填写 | 待预发填写 | `token`、`exp_time` | 待测 | 当前本地无外部账号凭证和完整运行环境。 | +| 1 | 外部账号刷新 token | POST | out | `access_token` | 待预发填写 | 待预发填写 | `access_token`、`exp_time` | 待测 | 当前本地无外部账号凭证和完整运行环境。 | +| 1 | 外部账号受保护接口 | GET/POST | out | `access_token` | 待预发填写 | 待预发填写 | 业务数据 | 待测 | 当前本地无外部账号凭证和完整运行环境。 | +| 1 | 伪造/过期 token | GET/POST | out | 非法 token | 待预发填写 | 待预发填写 | 错误码 | 待测 | 当前本地无外部账号凭证和完整运行环境。 | + +### 阶段结论 + +- `app/services/out/OutAccountServices.php` 已移除 `crmeb\basic\BaseAuth` 依赖。 +- `app/services/kefu/LoginServices.php` 未修改,保留到最后阶段处理。 +- 外部账号手工接口回归需要在具备外部账号凭证的预发或生产验证窗口执行。 diff --git a/pro_v3.5.1/app/services/auth/AccessTokenService.php b/pro_v3.5.1/app/services/auth/AccessTokenService.php new file mode 100644 index 00000000..3c3d6939 --- /dev/null +++ b/pro_v3.5.1/app/services/auth/AccessTokenService.php @@ -0,0 +1,94 @@ +make(JwtAuth::class); + + return $jwtAuth->createToken($id, $type, $extra + ['auth' => $authHash]); + } + + /** + * 解析并校验 access token。 + * + * @param callable $resolver 根据 token 内的 id 读取账号模型或数组 + * @param callable $authHashResolver 根据账号返回当前有效的 auth hash + * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function parseToken(string $token, string $type, callable $resolver, callable $authHashResolver) + { + if (!$token || $token === 'undefined') { + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } + + /** @var JwtAuth $jwtAuth */ + $jwtAuth = app()->make(JwtAuth::class); + + [$id, $tokenType, $auth] = $jwtAuth->parseToken($token); + if (!$id || $tokenType !== $type) { + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + + $md5Token = md5($token); + $cacheToken = CacheService::redisHandler($type)->get($md5Token, null); + if (!$cacheToken) { + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } + + if (isset($cacheToken['invalidNum']) && $cacheToken['invalidNum'] >= 3) { + $this->clearToken($md5Token, $type); + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + + try { + $jwtAuth->verifyToken(); + CacheService::setTokenBucket($md5Token, $cacheToken, $cacheToken['exp'] ?? null, $type); + } catch (ExpiredException $e) { + $cacheToken['invalidNum'] = ($cacheToken['invalidNum'] ?? 0) + 1; + CacheService::setTokenBucket($md5Token, $cacheToken, $cacheToken['exp'] ?? null, $type); + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } catch (\Throwable $e) { + $this->clearToken($md5Token, $type); + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + + $account = $resolver($id); + if (!$account) { + $this->clearToken($md5Token, $type); + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } + + if ($auth !== $authHashResolver($account)) { + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + + return $account; + } + + protected function clearToken(string $md5Token, string $type): void + { + if (!request()->isCli()) { + CacheService::redisHandler($type)->delete($md5Token); + } + } +} diff --git a/pro_v3.5.1/app/services/out/OutAccountServices.php b/pro_v3.5.1/app/services/out/OutAccountServices.php index 91a3acf0..87468e6a 100644 --- a/pro_v3.5.1/app/services/out/OutAccountServices.php +++ b/pro_v3.5.1/app/services/out/OutAccountServices.php @@ -7,13 +7,12 @@ namespace app\services\out; use app\dao\out\OutAccountDao; -use crmeb\basic\BaseAuth; +use app\services\auth\AccessTokenService; use app\services\BaseServices; use crmeb\exceptions\AdminException; use crmeb\exceptions\AuthException; use crmeb\services\CacheService; use crmeb\services\HttpService; -use crmeb\utils\ApiErrorCode; use crmeb\utils\JwtAuth; use think\annotation\Inject; use think\exception\ValidateException; @@ -77,14 +76,15 @@ class OutAccountServices extends BaseServices */ public function parseToken(string $token) { - /** @var BaseAuth $services */ - $services = app()->make(BaseAuth::class); - $adminInfo = $services->parseToken($token, function ($id) { - return $this->dao->get($id); - }); - if (isset($adminInfo->auth) && $adminInfo->auth !== md5($adminInfo->appsecret)) { - throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); - } + /** @var AccessTokenService $services */ + $services = app()->make(AccessTokenService::class); + $adminInfo = $services->parseToken( + $token, + 'out', + fn($id) => $this->dao->get($id), + fn($adminInfo) => md5($adminInfo->appsecret) + ); + return $adminInfo->hidden(['appsecret', 'ip', 'status']); } @@ -175,7 +175,7 @@ class OutAccountServices extends BaseServices $authInfo = $this->dao->getOne(['id' => $id, 'is_del' => 0]); $this->checkAuth($authInfo, $md5Token, $cacheService); - $cacheService->delete($md5Token); + CacheService::redisHandler('out')->delete($md5Token); $token = $jwtAuth->createToken($id, $type); $data['last_time'] = time(); @@ -203,7 +203,8 @@ class OutAccountServices extends BaseServices $md5Token = md5($token); - if (!$cacheService->has($md5Token) || !($cacheToken = $cacheService->get($md5Token, '', NULL, 'out'))) { + $cacheToken = CacheService::redisHandler('out')->get($md5Token, null); + if (!$cacheToken) { throw new AuthException('登录已过期,请重新登录'); } @@ -217,7 +218,7 @@ class OutAccountServices extends BaseServices $jwtAuth->verifyToken(); } catch (\Throwable $e) { if (!request()->isCli()) { - $cacheService->delete($md5Token); + CacheService::redisHandler('out')->delete($md5Token); } throw new AuthException('登录失败'); } @@ -236,14 +237,14 @@ class OutAccountServices extends BaseServices { if (!$authInfo) { if (!request()->isCli()) { - $cacheService->delete($md5Token); + CacheService::redisHandler('out')->delete($md5Token); } throw new AuthException('登录已过期,请重新登录'); } if ($authInfo->status == 2) { if (!request()->isCli()) { - $cacheService->delete($md5Token); + CacheService::redisHandler('out')->delete($md5Token); } throw new AuthException('您已被禁止登录'); }