按 DDD 重构认证流程并复用用户 bind 实现 LDAP 仓储
This commit is contained in:
+18
-11
@@ -1,13 +1,17 @@
|
||||
# AD 第一因素接入
|
||||
|
||||
`/signin` 接收 AD 用户名(sAMAccountName)或本域 UPN,使用 Spring Security
|
||||
`ActiveDirectoryLdapAuthenticationProvider` 以用户身份执行 LDAPS bind 与目录查询。
|
||||
`/signin` 接收 AD 用户名(sAMAccountName)或本域 UPN,通过 Spring LDAP `ContextSource.getContext` 以用户身份执行 LDAPS bind,
|
||||
随后由 Spring Data LDAP 仓储复用这条已认证连接查询用户与组。
|
||||
不使用额外目录服务账号,不写入 AD,不复制 Authelia 的绑定密码。
|
||||
|
||||
领域仓储接口为 `UserRepository`,`User` 与 `LoginTransaction` 为领域模型。应用层以
|
||||
try-with-resources 管理已认证用户仓储会话;基础设施的 `AdUserRepository` 通过
|
||||
`SimpleLdapRepository<AdUserEntry>`、`LdapTemplate`、ODM 实现读取与转换,关闭后不可继续查询。
|
||||
`AdUserEntry` 的 LDAP 注解不会进入领域对象。
|
||||
|
||||
成功后重定向到 `/signin/mfa`,显示目录账号、objectGUID、邮箱、直接所属组及组 DN。
|
||||
**这是密码因素验收页面,MFA 尚未接入,不是完整登录成功。** 不创建 Spring Security
|
||||
认证上下文,不调用 Hydra,不替换现役 Go/Authelia/Gitea 登录链路。演示入口 `/preview`
|
||||
仍完全隔离,默认关闭,演示码不能推进真实认证。
|
||||
认证上下文,不调用 Hydra,不替换现役 Go/Authelia/Gitea 登录链路。
|
||||
|
||||
## 目录和组语义
|
||||
|
||||
@@ -17,7 +21,8 @@
|
||||
不把 Spring 的 `FACTOR_PASSWORD` 当作组。结果排序,不做应用专用组改写。
|
||||
- 此轮不展开嵌套组、不推导 primaryGroupID,也不宣称与 Authelia 的有效组集合完全一致。
|
||||
遇到 ranged memberOf 或不同 DN 同名 CN 拒绝映射,不静默丢组或合并不同主体。
|
||||
- AD bind 执行密码及账号状态检查;Spring 解析禁用、锁定、密码过期等 AD 子码。
|
||||
- AD bind 执行密码及账号状态检查;基础设施层将禁用、锁定、密码过期等 AD 子码转换为
|
||||
应用层认证失败原因,不向领域层泄露 LDAP/Spring 异常。
|
||||
搜索排除 computer 对象,只接受唯一用户条目和合法 objectGUID/sAMAccountName。
|
||||
- 邮箱作为目录属性展示,不声称 `email_verified=true`。
|
||||
|
||||
@@ -69,7 +74,7 @@ java -Djavax.net.ssl.trustStore=/run/iam/truststore \
|
||||
这里 truststore 仅含公开信任锚,口令不是目录密码。保留所需公共根证书;不得禁用
|
||||
LDAP endpoint identification 或用信任所有证书的 socket factory。连接/读取超时为 3/5 秒。
|
||||
AD 根范围查询可能返回 DomainDnsZones/ForestDnsZones 等分区 referral;配置为 ignore,
|
||||
由 Spring AD provider 忽略 partial result,不使用 throw 打断用户查询,也不 follow 转发用户凭据。
|
||||
由仓储的 LdapTemplate 忽略 partial result,不使用 throw 打断用户查询,也不 follow 转发用户凭据。
|
||||
|
||||
浏览器入口只接受 HTTPS;明文请求返回 426。默认不信任转发头。若以后由代理终结 TLS,
|
||||
必须配合仅受信代理可达的后端网络和转发头配置,不能公开一个信任任意 forwarded header
|
||||
@@ -86,7 +91,7 @@ AD 根范围查询可能返回 DomainDnsZones/ForestDnsZones 等分区 referral
|
||||
入口限流、审计、MFA、Hydra 事务和恢复策略。
|
||||
|
||||
基础存活检查使用 `/actuator/health/liveness`。Boot 自动配置的 LDAP 健康项并未连接这里
|
||||
独立配置的 AD provider,不能把该项当作此认证路径的可用性验证。
|
||||
按用户 bind 创建的仓储连接,不能把该项当作此认证路径的可用性验证。
|
||||
|
||||
## 本轮验证边界
|
||||
|
||||
@@ -95,12 +100,14 @@ AD 根范围查询可能返回 DomainDnsZones/ForestDnsZones 等分区 referral
|
||||
仍未完整认证。UnboundID 的 UPN bind 与 AD 子码由测试拦截器模拟,不能替代 Samba AD。
|
||||
测试证书、私钥与账号全为虚构夹具,不用于实际部署。
|
||||
|
||||
2026-09-25:JVM 测试共 12 项通过(原有 6 项、新增 AD 6 项),`bootJar` 构建通过。
|
||||
实际 JVM 使用受信 CA 完成 Samba AD RootDSE 查询,HTTPS 页面返回 200。
|
||||
2026-09-25:DDD/Spring Data LDAP 版本通过 20 项 JVM 测试和 `bootJar` 构建,
|
||||
包含同一次 bind 连接完成仓储查询、用例结束关闭连接、领域规则及监控集成。
|
||||
开发 HTTPS 实例已更新为该版本。此前 JVM 使用受信 CA 完成 Samba AD RootDSE 查询。
|
||||
浏览器已检查登录表单渲染、真实 CSRF 原生 POST 和失败后清空密码;只使用在访问 AD 前
|
||||
即拒绝的合成外域用户名,不尝试猜测人类密码。浏览器回归共 1 项通过,包含移动端布局。
|
||||
复现:`IAM_AD_URL=https://验收域名:端口 npm --prefix frontend run test:browser -- ad-login.spec.ts`。
|
||||
维护者已在 HTTPS 页面完成真实密码验证,成功到达待 MFA 页面,并反馈目录标识、邮箱与
|
||||
重构前,维护者已在 HTTPS 页面完成真实密码验证,成功到达待 MFA 页面,并反馈目录标识、邮箱与
|
||||
六个直接所属组的查询结果。该验收覆盖 Samba AD 第一因素与属性读取,不表示 MFA、
|
||||
嵌套组/主组等价性或 Hydra 登录已完成。不在聊天、命令行或日志中传递人类密码。
|
||||
嵌套组/主组等价性或 Hydra 登录已完成。Spring Data LDAP 重构后的真实人类复验仍待反馈。
|
||||
不在聊天、命令行或日志中传递人类密码。
|
||||
新增 AD 路径尚未进行 Native 测试,不能复用旧 UI 原型的 Native 结论。
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# 浏览器登录流程原型
|
||||
|
||||
参考 Keycloakify:Spring 返回 HTML 时内联当前页面上下文,React 用 `createRoot` 渲染,
|
||||
表单原生 POST 到 Spring,后端按 session 中的步骤校验并返回 303 重定向。
|
||||
每次导航重新挂载 React,带 hash 的 JS/CSS 可长期缓存。局部帮助展开不发请求。
|
||||
|
||||
这是浏览器交互实验,不是身份验证实现:没有 AD 查询、真实密码、TOTP、WebAuthn 或
|
||||
Hydra accept;不会创建 Spring Security 登录身份。演示码 **123456** 仅用于切换页面,
|
||||
不得作为 MFA 实现复用。默认关闭,显式设置 `iam.ui-preview.enabled=true` 才开放 `/preview`。
|
||||
所有其他受保护入口仍需认证,Prometheus 权限保持不变。
|
||||
|
||||

|
||||
|
||||
## 本地体验
|
||||
|
||||
有 Node 24 和 JDK 25 时:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build
|
||||
cd ..
|
||||
./gradlew bootRun --args='--server.address=127.0.0.1 --server.port=18081 --iam.ui-preview.enabled=true'
|
||||
```
|
||||
|
||||
访问 <http://127.0.0.1:18081/preview>。填写称呼,尝试错误演示码,再用 123456 完成。
|
||||
后退链接、刷新、重新体验都走服务端流程。启用 DevTools 的 Network 面板观察 document
|
||||
POST、303、GET;不要勾选 Disable cache,否则无法观察正常的静态资源缓存。
|
||||
|
||||
Docker 开发:
|
||||
|
||||
```sh
|
||||
IAM_DOCKER_USE_SUDO=1 scripts/gradle-in-docker bootRun \
|
||||
--args='--server.address=127.0.0.1 --server.port=18081 --iam.ui-preview.enabled=true'
|
||||
```
|
||||
|
||||
`gradle-in-docker` 先用固定 Node 镜像构建前端,再运行 GraalVM 容器。
|
||||
Gradle 缓存默认 `$HOME/.cache/iam-login/gradle`,npm 缓存默认 `$HOME/.cache/iam-login/npm`;
|
||||
可用 `IAM_GRADLE_CACHE` / `IAM_NPM_CACHE` 指定持久目录。Node 仅参与构建,部署无 Node 服务。
|
||||
直接调用 Gradle 时先构建前端;缺少 `frontend/dist/index.html` 会明确失败。
|
||||
前端 watch 可用 `npm run watch`,修改后仍需让后端重新复制资源并重启;本轮不实现 HMR 桥接。
|
||||
|
||||
## 验证
|
||||
|
||||
```sh
|
||||
IAM_DOCKER_USE_SUDO=1 scripts/gradle-in-docker test testAot nativeTest nativeCompile
|
||||
python3 scripts/native-smoke.py
|
||||
build/native/nativeCompile/iam-login --server.address=127.0.0.1 --server.port=18081 \
|
||||
--iam.ui-preview.enabled=true
|
||||
# 另一个终端,应用保持运行
|
||||
cd frontend
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
npm run test:browser
|
||||
```
|
||||
|
||||
浏览器测试覆盖原生页面导航、错误重试、局部交互零请求、无 fetch/XHR、静态 JS 缓存、
|
||||
移动端布局、脚本结束标记转义,以及完成预览仍不能访问受保护应用。
|
||||
额外计时使用 Chromium 模拟 60ms 网络延迟、1.5Mbps 下载和四倍 CPU slowdown,
|
||||
用于比较首屏和缓存后的页面切换,不代表真实 LAN、Tailscale 或手机性能。
|
||||
|
||||
## 实现边界
|
||||
|
||||
- 页面壳在 `frontend/index.html`,Vite 构建后作为私有 classpath 资源 `ui/index.html` 打包,
|
||||
不提供静态 index 入口;控制器仅替换一个固定 JSON 数据位置。
|
||||
- Java 使用 JSON 序列化后转义 `<`、`>`、`&` 和 Unicode 行分隔符,避免 `</script>` 逃逸;
|
||||
React 按文本输出动态内容,不通过 HTML 字符串插入用户名。
|
||||
- session 持有演示步骤。表单带 Spring Security CSRF token,缺失被拒绝;
|
||||
非当前步骤的提交拒绝,未知/过期 session 的后续页面回到初始步骤。
|
||||
- 页面与重定向 `no-store`,静态 hash 资源 public/immutable。CSP 不允许内联可执行脚本。
|
||||
- 单 session 仅有一个演示流程,多标签页会共享步骤。正式认证需要独立事务、过期策略、
|
||||
主体与因素绑定;本原型不提供这些保证。
|
||||
- 当前采取整页切换,不提前实现 fetch 优化。后续根据测量选择需要局部更新的步骤。
|
||||
- 首屏依赖 JavaScript,没有 React SSR、Flight、客户端路由、FreeMarker 或模板引擎。
|
||||
关闭 JavaScript 时显示明确提示,不宣称无 JS 可用。
|
||||
|
||||
来源:[Keycloakify 入口](https://github.com/keycloakify/keycloakify-starter/blob/main/src/main.tsx)、
|
||||
[登录表单](https://github.com/keycloakify/keycloakify/blob/main/src/login/pages/Login.tsx)、
|
||||
[Vite 构建](https://vite.dev/guide/build)。
|
||||
|
||||
## 2026-09-25 本地验证结果
|
||||
|
||||
本轮应用代码为 `dcf634d`,随后修正 smoke 对 HTML 入口的 Accept 请求头。
|
||||
使用固定 GraalVM Java 25.0.2 镜像,构建限制 4 CPU / 8 GiB;原生应用采用默认 O2。
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| 前端 TypeScript / Vite、bootJar | 通过 |
|
||||
| JVM test / testAot / nativeTest | 各 6 项,0 失败、0 跳过 |
|
||||
| Native 应用 smoke | liveness UP;匿名应用及指标 401;认证指标 200;健康请求计数增加 3 |
|
||||
| 默认关闭 / 显式开启预览 | HTML 请求分别 404 / 200,在同一 Native 构建上实测 |
|
||||
| Native 上的 Chromium 测试 | 3 项通过,包含整页原生 POST、无 fetch/XHR、缓存、转义与移动端 |
|
||||
| ELF 文件大小 | 126,291,016 bytes,约 120.44 MiB,不是容器镜像大小 |
|
||||
| 启动到 liveness 可响应 | 单次 0.351 秒 |
|
||||
| smoke 请求后 RSS | 148,996 KiB,约 145.50 MiB |
|
||||
| 模拟限速下的首屏 | 从导航开始到 React 提交 DOM:1,702 ms |
|
||||
| 模拟限速下的缓存后切换 | Playwright 点击开始到下一页标题可见:560 ms |
|
||||
|
||||
浏览器计时条件为 60ms 网络延迟、1.5Mbps 下载、0.75Mbps 上传及四倍 CPU slowdown。
|
||||
这是单次、本机、模拟网络测量,不是生产 SLA,两个计时区间也不同;不据此声称 Native
|
||||
比 JVM 快多少。原型没有启用 HTTP 压缩,首屏实际下载约 223KB JS;构建日志中的约 70KB
|
||||
是 gzip 估算,不是本轮实际传输大小。后续页面的 JS `transferSize=0`,确认命中浏览器缓存。
|
||||
|
||||
本轮没有新增反射补丁。Native 测试日志以及 smoke 的启动、请求、关闭阶段未发现 Native
|
||||
反射或资源注册错误。生成目录中的配套 `.so` 文件应随 Native 产物保留;这里不承诺单文件
|
||||
静态链接交付。AD、真实 MFA、Hydra 和人类验收不在这些结果范围内。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Reference in New Issue
Block a user