audit_mini_identity_conflict.php 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * 小程序身份冲突盘点。
  5. *
  6. * 一个账号只能绑定一个业务身份。Access::identities() 在识别到多个生效身份时抛出 40903,
  7. * 而该调用位于 AuthService::issue() 的事务内,异常会把本次登录整体回滚——这样的账号既拿不到
  8. * Token,也没有自助恢复路径,只能由管理员逐条停用旧授权。
  9. *
  10. * 默认只读输出。--apply 在事务内停用同一账号除最新一条以外的生效授权,并打印被停用的授权编号,
  11. * 回滚方式是把这些编号的 status 改回 1。脚本不删除任何记录,也不改动用户和业务档案。
  12. */
  13. require dirname(__DIR__) . '/vendor/autoload.php';
  14. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  15. \support\App::loadAllConfig(['route']);
  16. use app\miniapi\model\MiniGrant;
  17. use app\miniapi\service\Access;
  18. $apply = in_array('--apply', $argv, true);
  19. $active = MiniGrant::query()->where('status', 1)->orderBy('user_id')->orderBy('id')->get();
  20. $live = [];
  21. $stale = [];
  22. foreach ($active as $grant) {
  23. $model = Access::resourceModel($grant->role);
  24. $resource = $model === null
  25. ? null
  26. : $model::query()->where('id', $grant->resource_id)->where('status', 1)->first();
  27. if ($resource === null) {
  28. $stale[] = $grant;
  29. continue;
  30. }
  31. $live[(int) $grant->user_id][] = $grant;
  32. }
  33. $conflicts = array_filter($live, static fn (array $rows): bool => count($rows) > 1);
  34. printf("生效授权 %d 条,其中指向已删除或已停用档案的 %d 条(Access 不计入,不影响登录)。\n", $active->count(), count($stale));
  35. printf("身份冲突账号 %d 个。\n", count($conflicts));
  36. foreach ($conflicts as $userId => $rows) {
  37. printf(" user_id=%d 生效身份 %d 个:", $userId, count($rows));
  38. foreach ($rows as $grant) {
  39. printf('grant#%d %s/%d ', $grant->id, $grant->role, $grant->resource_id);
  40. }
  41. printf("(保留最新 grant#%d)\n", end($rows)->id);
  42. }
  43. if (!$conflicts) {
  44. echo "无需处理。\n";
  45. exit;
  46. }
  47. if (!$apply) {
  48. echo "计划:如需停用多余身份,执行 php scripts/audit_mini_identity_conflict.php --apply\n";
  49. exit;
  50. }
  51. $disabled = [];
  52. (new MiniGrant())->getConnection()->transaction(function () use ($conflicts, &$disabled): void {
  53. foreach ($conflicts as $userId => $rows) {
  54. $keep = end($rows);
  55. foreach ($rows as $grant) {
  56. if ((int) $grant->id === (int) $keep->id) {
  57. continue;
  58. }
  59. MiniGrant::query()->where('id', $grant->id)->update([
  60. 'status' => 0,
  61. 'updated_at' => date('Y-m-d H:i:s'),
  62. ]);
  63. $disabled[] = [
  64. 'grant_id' => (int) $grant->id,
  65. 'role' => $grant->role,
  66. 'resource_id' => (int) $grant->resource_id,
  67. 'kept_grant_id' => (int) $keep->id,
  68. ];
  69. }
  70. }
  71. });
  72. printf("已停用 %d 条多余授权。受影响的账号可重新登录;如判断有误,将下列授权的 status 改回 1 即可恢复。\n", count($disabled));
  73. echo json_encode($disabled, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;