# 017 实施计划 ## 2026-09-08 执行增量 1. 在 `InfoUserControllerTest` 增加苹果/谷歌业务端绑定、角色拒绝、token 分端与用户原协议回归;为凭证校验增加本地 HTTP / 签名 JWT 测试,先验证缺失行为。 2. 新增 `OAuthAccountType` 集中描述 provider 对应业务角色;`InfoUserController` 复用现有绑定流程,保持原 apple/google 用户分支。`OAuthVerifyService` 新增独立业务端 client ID,缺失时仅拒绝对应新渠道。 3. `LineOAuthProperties`、`LineCallbackController` 兼容旧用户 provider 与无参数回调;绑定查询兼容 line / line_user。不得改变原用户授权地址和强制旧 App 增加参数。 4. 更新 application.yml 环境变量、DTO 注释、四语言错误与前端接入文档;SQL 仅写人工执行说明,不连接数据库修改数据。 - 业务 LINE 由 `LineOAuthStateService` 签发/单次消费 state;新增 `/auth/line/authorize` 与 DTO,回调及直接 code 登录均校验。用户旧协议不受强制校验影响。 5. JDK 21 运行 OAuth 定向测试、关联账号回归和 `ruoyi-admin -am` 构建;记录真实 App / 渠道配置等外部验收限制。继续在当前 `test-202609v2` 工作区实施。 ## 架构:最大化复用现有登录链路 ``` provider 凭证 ──▶ OAuthVerifyService.verify(provider, credential) ──▶ providerUid │ ┌───────────────────────────────────────────────────────┘ ▼ 查 info_user_oauth (provider, provider_uid) ├─ 命中 → 取 InfoUser → 校 status/del_flag → issueToken(provider) → 返回 └─ 未命中 → Redis 缓存 {provider,providerUid}(tempKey,TTL5m) → 返回 needPhone+tempKey │ ┌───────────────────────────────────────────────────────┘ ▼ (前端引导输手机号 → getcode 发短信) oauthBindPhone(tempKey, phone, code) 原子消费 tempKey + 取 providerUid + 验短信(复用 lodeing: redis code==input || "8888") getuser(phone): ├─ 已注册 → 关联(写 oauth) └─ 未注册 → createUser(phone) 新建 → 写 oauth → issueToken(provider) → 返回 ``` - **token 签发**:复用 `JwtUtil.setToken(CacheConstants.USER_TOKEN_KEY, loginDto)`,仅给 `LoginUserDto` 加 `provider` 字段并在 setToken 写入 claim。 - **新建用户**:复用 `createUser` 的「建 InfoUser + 建钱包 + 删旧 token」逻辑;昵称=手机号不变。 - **短信**:复用 `getcode` 发送 + `lodeing` 的校验(key=phone 去+,万能码 8888)。 - **一次性绑定凭证**:`tempKey` 在绑定请求开始时以删除成功作为唯一消费权;任一后续校验失败都必须重新发起 OAuth 登录,避免重放和并发重复绑定。 ## 数据模型 ```sql CREATE TABLE info_user_oauth ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '关联 info_user.user_id', provider VARCHAR(16) NOT NULL COMMENT 'apple/google/line_user/line_rider/line_merchant', provider_uid VARCHAR(64) NOT NULL COMMENT '三方稳定用户ID(Apple sub / Google sub / LINE userId)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_provider_uid (provider, provider_uid), KEY idx_user_id (user_id) ) COMMENT='三方账号绑定'; ``` ## API 契约 ### 1) `POST /infouser/user/oauthLogin` (`@Anonymous`) 请求 `OAuthLoginDto`: ``` provider apple|google|line|line_user|apple_rider|google_rider|line_rider|apple_merchant|google_merchant|line_merchant credential identityToken(Apple) / idToken(Google) / authorizationCode(LINE) cid, cidType, deviceToken, voIPToken // 推送字段,与现有登录一致 state // 业务 LINE 自行提交 code 时必填,由 /auth/line/authorize 签发 ``` 响应(命中): ``` { code:200, msg, data: InfoUser, token } // token claim provider=本次渠道 ``` 响应(未命中,首次): ``` { code:200, msg, data:{ status:"needPhone", tempKey } } ``` ### 2) `POST /infouser/user/oauthBindPhone` (`@Anonymous`) 请求 `OAuthBindDto`: ``` tempKey, phone, code // code=短信验证码 cid, cidType, deviceToken, voIPToken ``` 响应:`{ code:200, msg, data: InfoUser, token }` ## 三家凭证校验(OAuthVerifyService) | provider | 凭证 | 校验 | 取 providerUid | 依赖 | |---|---|---|---|---| | apple | identityToken(ES256 JWT) | 拉 `appleid.apple.com/auth/keys`(JWKS) 验签 + 校 iss/aud/exp | `sub` | nimbus-jose-jwt(新增) | | google | ID-Token | GET `oauth2.googleapis.com/tokeninfo?id_token=` 取 claims + 校 aud=clientId | `sub` | httpclient4(已有) | | line_user / line_rider / line_merchant | authorization code | 按 provider 选择 Channel,POST 换 access_token,再 GET `api.line.me/v2/profile`(Bearer) | `userId` | httpclient4(已有) | > Google 走 tokeninfo HTTP 而非 firebase-admin,因后者需初始化 FirebaseApp+服务账号(项目未配置),tokeninfo 零配置即可。 ## 文件清单 | 类型 | 文件 | |---|---| | 新增实体 | `ruoyi-system/.../domain/InfoUserOauth.java` | | 新增 mapper | `ruoyi-system/.../mapper/InfoUserOauthMapper.java`(BaseMapper,无 XML) | | 新增校验服务 | `ruoyi-admin/.../utils/oauth/OAuthVerifyService.java` | | 新增 DTO | `ruoyi-admin/.../user/dto/OAuthLoginDto.java`、`OAuthBindDto.java` | | 改 controller | `ruoyi-admin/.../user/InfoUserController.java`(+2 方法) | | 改 token | `ruoyi-common/.../model/LoginUserDto.java`、`ruoyi-system/.../utils/JwtUtil.java` | | 改配置 | `ruoyi-admin/src/main/resources/application.yml`(oauth 段) | | 改依赖 | `ruoyi-admin/pom.xml`(+nimbus-jose-jwt) | | SQL | `updatesql/sql.md` | ## 风险 / 待验证 - Apple / Google / LINE 的凭证校验**需用各家真实 token + 正确 clientId/bundleId 联调**才能端到端验证;配置值(clientId/jwks url)由前端/运营提供后填入 application.yml。 - Apple JWKS 建议加缓存(key 偶尔轮换);初版每次拉取或加内存缓存。 ## 2026-09-04 增量:用户/骑手/商家 LINE 登录 ### 最小改造方案 - 保留前端自行构造 LINE 授权 URL 的现有方式,不新增后端授权入口。 - 共用 `GET /auth/line/callback`,回调 URL 增加 `provider=line_user|line_rider|line_merchant`。 - `OAuthVerifyService` 按 provider 选择对应 Channel ID、Channel Secret 和 redirect URI;token/profile URL 继续共用。 - `info_user_oauth.provider` 直接保存三个 provider 值;现有 `line` 数据通过 SQL 迁移为 `line_user`。 - `line_user` 使用 `phone` 并保留首次自动创建;`line_rider`、`line_merchant` 使用 `tel_phone` 且只绑定已有账号。 - 登录成功后分别使用 `USER_TOKEN_KEY`、`QS_TOKEN_KEY`、`SH_APP_TOKEN_KEY`。 - 商家子账号除启用状态外,还必须能解析到有效归属商家/门店权限,失效归属不得通过 LINE 登录。 - 骑手、商家及商家子账号统一使用 `tel_phone`。新增/修改入口在业务层检查有效业务账号手机号唯一,软删除账号可复用;不增加数据库唯一索引。 ### 受影响文件 - `ruoyi-admin/src/main/resources/application.yml`:三组 LINE Channel 与 App 回跳配置。 - `ruoyi-admin/.../utils/oauth/OAuthVerifyService.java`:按 provider 选择 LINE Channel。 - `ruoyi-admin/.../user/LineCallbackController.java`:共用回调按 provider 分流。 - `ruoyi-admin/.../user/InfoUserController.java`:分角色绑定、签发 token、手机号唯一校验。 - `ruoyi-admin/.../stall/StallController.java`:摊主新增统一写入 `tel_phone` 并检查业务手机号唯一。 - `ruoyi-admin/.../user/BusinessPhoneService.java`:有效业务账号手机号唯一检查。 - `ruoyi-admin/.../user/MerchantSubaccountApplicationService.java`:子账号改用 `tel_phone`。 - `updatesql/sql.md`:冲突预检后迁移 `line` provider 和历史业务账号手机号字段。 - `specs/017-oauth-login/*`:同步接口及前端参数说明。 三组 Channel Secret 不写入仓库,部署环境分别提供 `LINE_USER_CLIENT_SECRET`、`LINE_RIDER_CLIENT_SECRET`、`LINE_MERCHANT_CLIENT_SECRET`;缺失时 LINE 登录配置校验直接拒绝请求。 ### 验证 - 单元测试覆盖 provider 配置选择、角色与 token key 映射、手机号重复判断。 - JDK 21 定向测试与 `ruoyi-admin` 模块编译。 - 真实 LINE Channel code、回调地址和三个 App scheme 仍需客户端联调验证。 - `state` 的完整 CSRF 防护需服务端签发/消费并由三个 App 保存、回跳比对,作为三端协议联调项统一实施,不能只改单侧回调。