## Context

现有 Vue 应用使用 Hash 路由，侧栏直接展示所有资产页；后端使用 Spring Boot、MyBatis-Plus 和 MySQL，尚未引入 Spring Security，也没有任何登录接口。`SystemUserEntity` 已映射 `as_system_user` 的 `username`、`password_hash`、`role_code` 与 `status`，但当前没有 Service、Controller 或测试使用它。

本设计服务于开发者、超级管理员和业务角色。唯一开发者为固定账号 `Jeddy`，它是系统最高权限角色，但账号绝不进入前端账号列表、筛选项、下拉项或接口返回；其初始密码哈希仅由受控数据库操作维护。超级管理员可管理除开发者外的账号但不能修改密码；财务、人事和运营仅访问被分配的页面，且没有账号管理能力。

## Goals / Non-Goals

**Goals:**

- 以 `as_system_user` 作为唯一账号来源，完成可验证的登录、登出、禁用和仅开发者可执行的密码设置/重置闭环。
- 在后端强制执行账号创建边界和页面读写权限，前端仅负责呈现服务端已判定的结果。
- 将逐页权限保存在可扩展的结构中，并在设置页以独立面板勾选“无权限 / 只读 / 编辑”。
- 保证密码从不以明文写入数据库、接口响应、日志或前端持久化存储。

**Non-Goals:**

- 本期不做手机号/邮箱找回、第三方单点登录、角色自定义、数据级（行级）权限或审批流。
- 本期不提供任何账号的自助改密页；不允许前端创建、修改或展示开发者账号；也不改动现有资产业务字段和接口响应结构。
- 本期不把权限配置送入 AI 或聊天链路。

## Decisions

### 1. 固定角色负责“能管理谁”，逐页权限负责“能做什么”

角色固定为 `DEVELOPER`、`SUPER_ADMIN`、`FINANCE`、`HR`、`OPERATIONS`。服务端的创建规则为：`DEVELOPER` 可创建全部五种角色并设置、重置所有非开发者账号密码；`SUPER_ADMIN` 可创建 `SUPER_ADMIN`、`FINANCE`、`HR`、`OPERATIONS`，但创建时不得提交密码且不能修改或重置密码；其余角色一律拒绝创建与管理账号。所有对开发者账号的查询、详情、编辑和列表 API 都返回“不可见/不可操作”，即使调用者本身是开发者。

每个非开发者账号保存页面权限映射，例如 `{"phone-assets":"EDIT","reference-wecom":"READ"}`。首期页面注册表固定为“总览、域名资料、企微资料、手机号资产、提醒中心”；权限值只能是 `NONE`、`READ`、`EDIT`，`EDIT` 自动包含读取能力。超级管理员固定获得这五页的 `EDIT`；开发者拥有隐式全量 `EDIT`，均不依赖前端配置。新页面默认 `NONE`，必须由开发者明确扩展注册表和权限面板后才能开放。

备选方案是在数据库拆出角色表、权限表和关联表。它更适合大量自定义角色，但本期只有五个固定角色、页面数量有限；先用受校验的 JSON 映射可以减少迁移和管理复杂度。若未来出现自定义角色，再迁移至关联表而不改变前端权限枚举。

### 2. 账号表扩展和兼容迁移

保留既有 `as_system_user` 列，新增：`page_permissions`（JSON 文本，默认 `{}`）、`password_updated_at`、`auth_version`（默认 `1`）和必要的唯一索引 `uk_as_system_user_username`。`status` 统一为 `ACTIVE`/`DISABLED`；超级管理员创建的账号以 `DISABLED` 保存，待开发者设置密码后才可启用。

用户已明确授权扩展 `as_system_user`：迁移先检查线上实际列和重复用户名，检查通过后新增列、默认值和索引，并增加“密码哈希变更”数据库触发器。触发器仅在 `password_hash` 实际变化时自动将 `password_updated_at` 写为当前时间、将 `auth_version` 加一；因此受控人工更新 `Jeddy` 的 BCrypt 哈希也会留下时间记录并立即使旧会话失效。固定开发者账号 `Jeddy` 由人工受控写入用户名、角色、状态和 BCrypt 哈希，迁移不得写入任何密码或哈希。旧数据会保留：空 `page_permissions` 解释为 `NONE`，已有角色码需要被映射到新枚举或列入人工处理清单，绝不猜测为高权限。

### 3. 使用短期签名会话令牌和服务端版本失效

采用 Spring Security 的无状态认证过滤器，并使用 HMAC 签名的短期 JWT（默认 8 小时）。令牌只放在 `HttpOnly`、`Secure`（生产环境）、`SameSite=Lax` Cookie 中；前端不读取、不写入 token。本地跨端口开发使用 `fetch(..., { credentials: 'include' })`，后端 CORS 仅放行已配置的前端来源和凭据。

JWT 载有用户 ID、角色和 `authVersion`。每次访问受保护接口，过滤器同时验证签名、过期时间、用户状态和数据库中的 `auth_version`。禁用和开发者设置/重置密码都会递增 `auth_version`，使旧会话立即失效。写操作采用 Spring Security CSRF Token Cookie + 请求头校验；登录接口仅接受账号密码并有统一的失败响应，避免泄漏“用户名是否存在”。

备选方案是把访问令牌放进 `localStorage` 或 `sessionStorage`。它实现更快，但脚本注入时更容易被窃取，故不采用。备选方案二是服务器内存 Session；它依赖单机内存，在重启或多实例扩展时会中断，且不利于立即撤销，故不采用。

### 4. 密码存储和后续改密方案

使用 Spring Security `BCryptPasswordEncoder` 生成 `password_hash`，成本系数由配置控制（初始建议 12）。数据库只存哈希值，盐由 BCrypt 每次编码自动生成；不增加“加密后的明文密码”列，也不把初始密码写进迁移、日志、测试快照或接口响应。

只有开发者能在创建账号、为超级管理员已创建的禁用账号补设密码，或重置非开发者账号密码时输入新密码与确认密码；后端校验 12–72 字符、拒绝与用户名相同及常见弱密码。系统不提供本人改密入口，所有账号均通过开发者操作更新密码哈希和时间、递增 `auth_version` 并使旧会话失效。管理员重置不会回显或再次读取任何密码；`Jeddy` 的密码变更仍由受控数据库操作人工写入 BCrypt 哈希。

未来调整密码规则或 BCrypt 成本时，在开发者重置密码时检测 `passwordEncoder.upgradeEncoding(hash)` 并重写哈希；这让业务账号在下一次受控重置时逐步升级，无需知道旧密码或执行全表重置。

### 5. 路由、菜单和接口三层一致授权

新增 `/login` 与 `/settings/users-permissions`。应用启动先读取 `/api/auth/me`，未登录时路由守卫跳转登录页；登录成功后根据服务端返回的 `pagePermissions` 渲染菜单。当当前登录身份为 `Jeddy` 且角色为 `DEVELOPER` 时，前端只在登录成功事件中弹出“🎉 欢迎系统开发者-BOSS：Jeddy 上线”；其他账号绝不显示该提示。设置菜单采用 `Setting` 图标和“账号与权限”文字，仅开发者与超级管理员可见。权限页内有独立“页面权限”抽屉或卡片面板，为首期五个注册页面选择三档权限；开发者角色不会作为任何创建选项或账号行出现。

后端为每个资产 Controller 方法声明所需的页面键和最低权限，统一由权限服务校验。若用户绕过菜单直接打开 Hash 地址，路由守卫显示无权页；若绕过前端调用接口，后端返回统一 403。这避免仅隐藏菜单的“假权限”。

### 工程术语注释表

| 术语 | 白话解释 | 使用位置 | 不这样做的风险 | 示例 |
| --- | --- | --- | --- | --- |
| BCrypt 密码哈希 | 把密码变成不可逆校验值，验证时只比对，不会还原原密码。这里用于保护 `password_hash`；若存明文或可逆密文，数据库泄露会直接暴露所有账号。 | 创建、登录、改密、重置密码 | 密码泄露和横向撞库风险显著增加。 | `encoder.matches(输入密码, passwordHash)` |
| JWT（签名登录凭证） | 服务端签名的一张短期“登录票”，浏览器只带回票据，服务端验证票据是否真实和过期。这里用于无状态登录；若没有签名校验，攻击者可伪造身份。 | 认证过滤器、登录、登出 | 未授权用户可能伪造高权限身份。 | Cookie 中的短期访问凭证 |
| `auth_version`（登录票据版本号） | 用户每次改密、重置或禁用时递增的数字；旧票据版本不一致即作废。这里用于立即踢出旧会话；若没有它，改密后被盗会话仍可使用到过期。 | `as_system_user`、认证过滤器 | 密码重置无法及时阻断旧设备。 | 令牌版本 2，数据库版本 3 → 401 |
| CSRF（借用登录态的伪造请求防护） | 防止别的网站借浏览器已有登录 Cookie 偷发写操作。这里用请求头令牌校验；不做会导致用户访问恶意页面时可能被悄悄改数据。 | 所有 POST/PUT/DELETE | 跨站页面可能冒用用户登录态提交写操作。 | `X-XSRF-TOKEN` 与 Cookie 值匹配 |
| 迁移/回滚 | 迁移是按步骤调整数据库；回滚是失败时恢复到原结构和可用版本。这里必须先检查旧数据再加列；否则可能因重复用户名或空权限锁死用户。 | 数据库部署 | 数据损坏或系统无法登录。 | 先备份、加可空列、回填、建索引 |

## Risks / Trade-offs

- [线上 `as_system_user` 实际结构与实体不一致] → 部署前执行只读列/索引/数据质量检查；迁移脚本分阶段执行并保留回滚脚本，检查失败则不写库。
- [固定开发者 `Jeddy` 缺失、哈希不正确或被禁用] → 部署前只读核对该账号的用户名、角色、状态和 BCrypt 哈希格式；运行时 UI 不提供开发者管理入口，紧急恢复仅走受控数据库维护流程。
- [人工直接更新哈希未撤销旧会话] → 数据库触发器仅在 `password_hash` 实际变化时更新时间和 `auth_version`；部署后用测试账号验证手工更新后旧 Cookie 返回 401。
- [权限配置遗漏新页面] → 页面注册表要求每个路由同时声明菜单元数据和后端权限键；未知页面默认 `NONE`，并有测试验证。
- [Cookie 跨域配置错误导致本地无法登录] → 明确配置本地允许源、`allowCredentials(true)`、`credentials: include` 与 Playwright 登录 smoke；生产环境通过同域反向代理优先消除跨域。
- [密码/令牌误写日志] → 对认证 DTO、异常处理和测试日志做脱敏；禁止 `toString()`、接口响应、审计记录包含密码或 Cookie。
- [管理员误禁用唯一可管理账号] → 禁止禁用最后一个可用开发者或超级管理员；操作前返回可读错误。

## Migration Plan

1. 只读核对生产数据库的 `as_system_user` 列、索引、重复用户名、角色码和状态值，并备份该表。
2. 发布包含安全配置但尚未强制保护现有资产接口的兼容版本，验证认证与权限接口；随后执行可回滚的列新增和索引迁移。
3. 由受控数据库操作写入或核对固定开发者 `Jeddy` 的用户名、`DEVELOPER` 角色、启用状态和 BCrypt 哈希；明文密码与哈希均不进入仓库、迁移文件、接口响应或日志。
4. 发布前端登录页、设置入口和权限页；确认管理员能创建业务账号和分配最小权限。
5. 启用 `/api/**` 的认证与权限拦截，运行资产接口、登录、越权、开发者重置密码和禁用回归。
6. 若失败，先关闭新安全强制开关、恢复上一版应用；数据库只新增且保留兼容列时无需删数据。若已启用索引或回填失败，使用迁移脚本按相反顺序恢复，并保留备份供人工核对。

## Open Questions

- 已确认：固定开发者账号为 `Jeddy`，其密码哈希由受控数据库操作人工维护；所有密码仅由开发者创建、重置和修改。
- 已确认：超级管理员固定拥有首期五个页面的全部编辑权限。
- 已确认：首期权限面板覆盖总览、域名资料、企微资料、手机号资产和提醒中心；查询/查看为 `READ`，新增、编辑、删除、导入/导出为 `EDIT`。
- 已确认：允许在 `as_system_user` 新增 `page_permissions`、`password_updated_at`、`auth_version` 和用户名唯一索引；登录会话默认 8 小时，用户名仅允许字母、数字、下划线，超级管理员创建账号默认 `DISABLED`。
