AuthService.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. <?php
  2. namespace App\Services;
  3. use App\Http\Api\UserApi;
  4. use App\Models\AiModel;
  5. use Firebase\JWT\JWT;
  6. use Firebase\JWT\Key;
  7. use Illuminate\Http\Request;
  8. class AuthService
  9. {
  10. /**
  11. * 人类用户登录 token 的有效期(秒)。
  12. */
  13. private const USER_TOKEN_TTL = 60 * 60 * 24 * 365;
  14. /**
  15. * AI 模型身份 token 的有效期(秒)。
  16. *
  17. * 模型 token 会被复制到外部客户端(如 wikipali-write Skill)的凭据文件里,
  18. * 泄漏面比人类登录 token 大得多,故取远短的 30 天。
  19. */
  20. private const AI_MODEL_TOKEN_TTL = 60 * 60 * 24 * 30;
  21. /**
  22. * 模型身份 token 的 typ 标记。带此标记的 token 每次校验都要比对版本号,
  23. * 人类 token 不带,避免为每个请求多查一次库。
  24. */
  25. private const AI_MODEL_TOKEN_TYPE = 'ai-model';
  26. public static function getUserToken(string $userUid)
  27. {
  28. // 先判模型:UserApi::getByUuid() 查不到用户时会回落到 AiAssistantApi,
  29. // 分不清「模型」和「查无此人」,而这两者签出的 token 完全不同。
  30. $aiModel = AiModel::where('uid', $userUid)->first();
  31. if ($aiModel) {
  32. return self::encode([
  33. 'uid' => $aiModel->uid,
  34. 'id' => 0,
  35. 'typ' => self::AI_MODEL_TOKEN_TYPE,
  36. 'ver' => (int) $aiModel->token_version,
  37. ], self::AI_MODEL_TOKEN_TTL);
  38. }
  39. $user = UserApi::getByUuid($userUid);
  40. if (! $user || ! isset($user['sn'])) {
  41. // 查无此人时 UserApi 返回的是 id=0 的占位结构,不能拿它签 token
  42. return null;
  43. }
  44. return self::encode([
  45. 'uid' => $user['id'],
  46. 'id' => $user['sn'],
  47. ], self::USER_TOKEN_TTL);
  48. }
  49. public static function getJwtKey()
  50. {
  51. return config('mint.app.jwt_secrets_key');
  52. }
  53. public static function getToken(Request $request)
  54. {
  55. $token = $request->bearerToken();
  56. return $token;
  57. }
  58. public static function current(Request $request)
  59. {
  60. $token = $request->bearerToken();
  61. if ($token) {
  62. try {
  63. $jwt = JWT::decode($token, new Key(self::getJwtKey(), 'HS512'));
  64. } catch (\Exception $e) {
  65. return false;
  66. }
  67. if ($jwt->exp < time()) {
  68. // 过期
  69. return false;
  70. }
  71. if (! self::modelTokenIsValid($jwt)) {
  72. return false;
  73. }
  74. // 有效的token
  75. return ['user_uid' => $jwt->uid, 'user_id' => $jwt->id];
  76. } elseif (isset($_COOKIE['user_uid'])) {
  77. return [
  78. 'user_uid' => $_COOKIE['user_uid'],
  79. 'user_id' => $_COOKIE['user_id'],
  80. ];
  81. } else {
  82. return false;
  83. }
  84. }
  85. /**
  86. * 校验模型身份 token 是否已被撤销。
  87. *
  88. * 撤销即把 ai_models.token_version 自增,旧 token 里的 ver 随即对不上。
  89. * 模型被删除同样视为失效。人类 token(id 为用户自增主键,恒 > 0)直接放行,不查库。
  90. */
  91. private static function modelTokenIsValid(object $jwt): bool
  92. {
  93. if (isset($jwt->typ) && $jwt->typ === self::AI_MODEL_TOKEN_TYPE) {
  94. $version = AiModel::where('uid', $jwt->uid)->value('token_version');
  95. return $version !== null && (int) $version === (int) ($jwt->ver ?? 0);
  96. }
  97. // 引入版本号之前签出的模型 token(typ 缺失、id 恒为 0)无法撤销,一律作废,
  98. // 持有者须重新签发。
  99. return (int) ($jwt->id ?? 0) !== 0;
  100. }
  101. /**
  102. * @param array<string, mixed> $claims
  103. */
  104. private static function encode(array $claims, int $ttl): string
  105. {
  106. return JWT::encode(array_merge([
  107. 'nbf' => time(),
  108. 'exp' => time() + $ttl,
  109. ], $claims), self::getJwtKey(), 'HS512');
  110. }
  111. }