AdminRealtimeTicket.php 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. <?php
  2. namespace App\Services\AdminRealtime;
  3. use App\Services\MenuService;
  4. use App\Services\RoleService;
  5. use App\Services\RoleUserService;
  6. use RuntimeException;
  7. /**
  8. * 推送网关连接票据:后台登录态(admin.jwt)换取短时效票据,网关用同一密钥校验签名。
  9. * 票据内携带该管理员可订阅的频道,口径与 CheckButtonPermission 对列表接口的按钮权限一致。
  10. */
  11. final class AdminRealtimeTicket
  12. {
  13. /**
  14. * @param int|null $sessionExpiresAt 后台 JWT 的过期时间戳,网关到期主动断开
  15. */
  16. public static function issue(int $adminId, ?int $sessionExpiresAt): array
  17. {
  18. $secret = (string) config('admin_realtime.ticket_secret');
  19. if ($secret === '') {
  20. throw new RuntimeException('未配置 ADMIN_WS_SECRET');
  21. }
  22. $channels = self::allowedChannels($adminId);
  23. $ttl = max(10, (int) config('admin_realtime.ticket_ttl', 60));
  24. $now = time();
  25. $body = self::base64Url(json_encode([
  26. 'aid' => $adminId, 'ch' => $channels, 'iat' => $now, 'exp' => $now + $ttl,
  27. 'sxp' => $sessionExpiresAt, 'nonce' => bin2hex(random_bytes(8)),
  28. ]));
  29. return [
  30. 'ticket' => $body . '.' . self::base64Url(hash_hmac('sha256', $body, $secret, true)),
  31. 'expires_in' => $ttl,
  32. 'url' => (string) config('admin_realtime.public_url'),
  33. 'channels' => $channels,
  34. ];
  35. }
  36. /** @return list<string> */
  37. public static function allowedChannels(int $adminId): array
  38. {
  39. $channels = array_keys(AdminRealtimeRows::CHANNELS);
  40. if ($adminId === 1) {
  41. return $channels;
  42. }
  43. $roleIds = collect(RoleUserService::findAll(['user_id' => $adminId]))->pluck('role_id')->toArray() ?: [-1];
  44. $granted = [];
  45. foreach (RoleService::model()::with(['menus' => function ($query) {
  46. $query->where('type', MenuService::model()::TYPE_BUTTON);
  47. }])->whereIn('id', $roleIds)->get() as $role) {
  48. $granted = array_merge($granted, $role['menus_uris'] ?: []);
  49. }
  50. return array_values(array_filter($channels, static function (string $channel) use ($granted): bool {
  51. $uri = AdminRealtimeRows::CHANNELS[$channel][1];
  52. $menu = MenuService::findOne(['uri' => $uri, 'type' => MenuService::model()::TYPE_BUTTON, 'status' => MenuService::model()::STATUS_SHOW]);
  53. return empty($menu) || in_array($uri, $granted, true);
  54. }));
  55. }
  56. private static function base64Url(string $value): string
  57. {
  58. return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
  59. }
  60. }