Files
full/docs/passport.md
2026-09-22 21:15:34 +08:00

200 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# passport用户账号与身份认证代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/passport` |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件约 44 个(含 `cmd/``internal/``service/``proto/` 6 个);`pb/` 下约 21 个生成 `.go` 仅看签名 |
| 入口 | `cmd/main`gRPC + grpc-gateway 单进程)、`cmd/cli`(调试入口)、聚合入口 `pkgs/all``pkgs/ecmall` |
| 对外协议 | gRPC`passport.Account/Login/Register/Forget/Verify`+ RESTgrpc-gateway路径形如 `POST /passport.Login/Pwd` |
| 结论摘要 | 认证核心存在**致命缺陷**:验证码登录/注册/找回均**不校验验证码**;找回密码仅凭 `identity` 即可改密且以**未加盐 MD5** 存储;密码哈希使用 `bcrypt.MinCost`cost=4KYC 回调为**空 stub**Token 缓存键前缀大小写不一致。 |
## 1. 服务定位与职责
用户通行证账号、密码、第三方登录、KYC 认证、标签、统计)的统一身份服务。负责账号注册、密码/验证码登录、密码找回、KYC 验证回调、用户资料与标签管理。它是**认证与账户存储**服务,不负责网关鉴权本身(鉴权依赖上层网关注入 metadata`service.ParseMetaCtx` 读取)。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:配置→初始化 impl→建 gRPC/Gateway→`server.New(nil)``service.New` 启动 |
| `cmd/cli/main.go` | 调试入口:仅打印数据库配置 |
| `internal/config/config.go` | 配置结构(`Base/Databases/MicroService/Gateway/Token/Kyc/WeChat` 等)与校验;设置 JWT 过期 |
| `internal/impl/impl.go` | 初始化 Redis、DB、Etcd、内存缓存实例 |
| `internal/vars/{token,status,provider}.go` | 常量:`TokenPrefix="/TOKEN/"`、账号状态、第三方 provider |
| `internal/excode/ex.go` | **空文件**,无内容 |
| `internal/models/*` | 11 个模型 + `query.go`(InitData/Exists) + `cache.go`(Redis 缓存与验证码接口) |
| `internal/logic/common/token.go` | 用 `env.Runtime.JwtSecretKey` 签发 JWT |
| `internal/logic/{login,register,forget,account,verify}/*` | 各业务方法实现 |
| `internal/server/*` | gRPC server 注册5 个 service 全部委托 logic |
| `service/{expose,dependencies}.go` | 聚合宿主注入接口(供 `pkgs/all` 复用共享连接) |
| `proto/*.proto` | 5 个服务契约(**无 `google.api.http` 注解**REST 路径由网关生成) |
依赖注入:`service.Dependencies` 支持外部传入 Redis/Etcd/DB/Cache非 nil 时覆盖 `internal/impl` 对象,供聚合入口复用共享连接。
## 3. 接口清单
> proto 中未声明 `ping/hello` 服务,但 `etc/*.yaml` 的 `MicroService.Anonymous` 列出 `passport.ping.hello`,与当前模块实现不一致(疑似残留)。
| 方法 | 路径(gRPC / REST) | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| `Login.Pwd` | `passport.Login/Pwd` | 账号+密码登录 | 否(公开) | `logic/login/pwd.go` |
| `Login.Code` | `passport.Login/Code` | 验证码登录 | 否(公开) | `logic/login/code.go` |
| `Login.Quick` | `passport.Login/Quick` | 验证码快捷登录并注册 | 否(公开) | `logic/login/quick.go` |
| `Register.Pwd` | `passport.Register/Pwd` | 账号密码注册 | 否(公开) | `logic/register/{pwd,do}.go` |
| `Register.Code` | `passport.Register/Code` | 手机验证码注册 | 否(公开) | `logic/register/{code,do}.go` |
| `Forget.Verify` | `passport.Forget/Verify` | 验证手机号和验证码 | 否(公开) | `logic/forget/verify.go` |
| `Forget.Reset` | `passport.Forget/Reset` | 重置密码 | 否(公开) | `logic/forget/reset.go` |
| `Account.Get` | `passport.Account/Get` | 获取完整用户信息 | 是(`ParseMetaCtx` | `logic/account/get.go` |
| `Account.SetData` | `passport.Account/SetData` | 更新用户资料 | 是 | `logic/account/set_data.go` |
| `Account.SetPassword` | `passport.Account/SetPassword` | 修改密码 | 是 | `logic/account/set_password.go` |
| `Account.TagCreate` | `passport.Account/TagCreate` | 新增标签 | 是 | `logic/account/tag_create.go` |
| `Account.TagRemove` | `passport.Account/TagRemove` | 删除标签 | 是 | `logic/account/tag_remove.go` |
| `Account.Statistics` | `passport.Account/Statistics` | 获取统计数据 | 是 | `logic/account/statistics.go` |
| `Verify.Request` | `passport.Verify/Request` | 发起 KYC 验证 | 是 | `logic/verify/request.go` |
| `Verify.JumioCallback` | `passport.Verify/JumioCallback` | KYC 回调 | 否(公开) | `logic/verify/jumio_callback.go` |
> 全部声明的 gRPC 方法均已委托到 logicserver 层无 Unimplemented 未实现项);但 `Verify.JumioCallback` 与 `Forget.Verify` 为**逻辑占位/伪实现**(见 6.3)。
## 4. 数据模型与表
| 表名 | 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- | --- |
| `passport_account` | `id` | uint | PK | 自增主键 |
| | `identity` | varchar | - | 用户唯一码(对外公开) |
| | `account` | varchar(255) | 默认 `''` | 账号 |
| | `phone` | varchar(20) | 默认 `''` | 手机号 |
| | `email` | varchar(255) | 默认 `''` | 邮箱 |
| | `password` | varchar(255) | not null | 密码bcrypt重置时误写为 MD5 |
| | `salt` | varchar(255) | not null | 密码盐 |
| `passport_data` | `passport_id`/`passport_identity` | - | - | 关联账号 |
| | `nickname/avatar/sign/cover` | varchar | 默认 `''` | 资料 |
| | `sex/birthday` | int8/time | - | 性别/生日 |
| | `country/province/city/area` | varchar | 默认 `''` | 地区 |
| | `score/level` | int32 | 默认 0 | 积分/等级 |
| | `rights` | varchar(255) | 默认 `''` | 权限 |
| | `email_verify/phone_verify/face_verify/document_verify/kyc_verify` | int32 | 默认 0 | 各验证状态 |
| `passport_tags` | `passport_id`/`name`/`icon` | - | - | 用户标签 |
| `passport_verify` | `passport_id` + 各验证时间/证件/KYC 字段 | - | - | 认证详情KYC 回调未写入) |
| `passport_provider` | `provider`/`provider_id`/`access_token`/`refresh_token` | - | - | 第三方登录绑定 |
| `passport_score` / `passport_statistics` / `passport_rights_expiry` / `passport_notify` | 见各模型文件 | - | - | 积分/统计/特权有效期/消息 |
## 5. 核心流程
```mermaid
flowchart TD
A["POST passport.Login/Code"] --> B["校验 phone/code/country 非空"]
B --> C["GetPassportAccountByField(phone)"]
C --> D["直接签发 JWT 返回"]
D --> E["未比对 Redis 验证码"]
F["POST passport.Forget/Reset"] --> G["校验 identity/password 非空"]
G --> H["Update password = Md5(pwd)"]
H --> I["账号被接管/密码无法登录"]
J["POST passport.Login/Pwd"] --> K["bcrypt.CompareHashAndPassword"]
K --> L["签发 JWT 并写 Redis(/TOKEN/)"]
M["POST passport.Register/Pwd"] --> N["bcrypt.MinCost 哈希+盐"]
N --> O["Create account + data"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/login/code.go:16-59` | **验证码登录不校验验证码**。仅判断 `in.Phone/in.Code/in.Country` 非空后直接 `GetPassportAccountByField("phone",...)` 并签发 JWT从未调用 `GetVerificationCodeCache` 比对 `in.Code`。任意已知手机号 + 任意 code 即可登录。 |
| **高** | `internal/logic/login/quick.go:20-64` | **快捷登录并注册同样不校验 code**,且当手机号不存在时(:32-64自动创建账号。攻击者可用任意 code 对任意手机号批量建号并登录。 |
| **高** | `internal/logic/forget/reset.go:17-41` | **找回密码无验证码/令牌校验**。仅凭 `identity` + `password``Update("password", ...)`。前置 `Forget.Verify` 也未真正校验 code见 6.3)。`identity` 在登录/注册响应中公开返回,攻击者可据此直接接管任意账号。 |
| **高** | `internal/logic/forget/reset.go:31` | **以未加盐 MD5 存储密码**`Update("password", utils.Md5(in.Password))`,与全模块 bcrypt 体系不一致;且登录校验走 bcrypt`login/pwd.go:27`),重置后的密码既不安全又无法用于登录(逻辑自相矛盾)。 |
| 中 | `internal/logic/register/do.go:30``internal/logic/account/set_password.go:35``internal/models/query.go:21` | **密码哈希 cost 过低**:使用 `bcrypt.MinCost`=4而非默认 10抗暴力破解能力弱。 |
| 中 | `internal/vars/token.go:3` vs `internal/logic/register/do.go:84` | **Token 缓存键前缀不一致**`vars.TokenPrefix="/TOKEN/"` 用于 `login/pwd.go:48``login/code.go:41`;而 `register/do.go:84``config.Spec.Token.Prefix`yaml 为 `/token/`)。同一 identity 在 Redis 中形成两把不同键,登出/校验可能取错或失效。 |
| 低 | `internal/logic/login/code.go:41` | 写 Redis token 时 TTL 传 `0`(永不过期),与 `login/pwd.go:48``_vars.JwtExpire` 不一致;缓存 token 永久留存(仅依赖 JWT 自身过期)。 |
| 低 | `test/grpc/main.go:45` | 硬编码测试 JWT`authorization` 明文 token`id/identity`)出现在测试代码,若被复用可越权。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/logic/account/get.go:44` | `impl.DBService.Where("passport_id=?", AUTH.ID).First(data)` **未处理 error**;查询失败或记录不存在时 `data` 仍是零值指针(非 nil`if data != nil` 恒真,后续 `data.Birthday.Format` 等执行DB 错误被吞掉、返回空资料。 |
| 中 | `internal/logic/forget/reset.go:31` | 重置密码**不同步更新 `salt`**`passport_account.salt` 列存在),且写入 MD5与 bcrypt 校验路径不兼容(同 6.1)。 |
| 中 | `internal/logic/register/do.go:53,70` | 账号与扩展数据两次 `Create` **非事务**:若第二步失败,会留下无扩展数据的孤儿账号。 |
| 低 | `internal/logic/register/code.go:13-31` | `Code` 注册仅校验手机号格式与唯一性,**未校验 `in.Code`**,与"验证码注册"语义不符(同 6.1 高)。 |
### 6.3 未完成实现
- `internal/logic/verify/jumio_callback.go:13-35`KYC 回调**仅 `printer.Info` 打印完整 payload 后返回成功**,未校验回调签名、未更新 `passport_verify` 的 KYC 状态——典型占位实现。
- `internal/logic/forget/verify.go:14-32`:名义"验证手机号和验证码",实际**不比对 code**(无任何 Redis 验证码查询),属伪实现。
- `internal/models/query.go:12-39``InitData` 计划初始化 `demo/welcome` 默认账号(密码 bcrypt MinCost`cmd/main/main.go:37` 已注释未启用;含**硬编码默认口令**,属潜在隐患(一旦启用即成后门)。
- `internal/models/cache.go:144-172``IncrementLoginAttempts`/`ClearLoginAttempts` **从未被调用**README 声称的"内置限流"未落地。
- `internal/models/cache.go:124-142``SetVerificationCodeCache`/`GetVerificationCodeCache` 提供验证码存取,但**所有登录/注册/找回逻辑均未调用**,验证码形同虚设(与 6.1 高关联)。
- `internal/excode/ex.go`:空文件,错误码未定义。
- `cmd/cli/main.go`:仅打印 DB 配置,无实际用途(调试残留)。
- `etc/*.yaml``SecretKey: CHANGE_ME` 字段在 `config.go``SrvConfig` **无对应字段**,未被解析;真实 JWT 密钥来自 `env.Runtime.JwtSecretKey``logic/common/token.go:10`),其初始化不在本模块可见范围【信息不足】。
- 全模块**无 `*_test.go` 单元测试**。
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/logic/account/get.go:44` | DB 错误未处理(同 6.2)。 |
| 中 | `internal/logic/login/*``forget/reset.go``verify/jumio_callback.go` | 多处 `printer.Error/Info` 输出内部错误与敏感参数(如 `jumio_callback.go:21` 打印完整 KYC 回调含 PII、`reset.go:33` 打印 identity、`login/pwd.go:34` 打印 DB 错误),存在信息泄露。 |
| 低 | `internal/vars/token.go:3` vs `config` | Token 前缀两套定义(见 6.1)。 |
| 低 | 全模块 | 无单元测试;`InitData`/`CreateAccount` 等存在死代码或未被调用路径。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| P1 | 高 | 验证码登录不校验 code | 任意手机号可登录,认证失效 |
| P2 | 高 | 快捷登录/注册不校验 code 且自动建号 | 批量伪造账号 |
| P3 | 高 | 找回密码仅凭 identity 改密、无校验 | 任意账号被接管 |
| P4 | 高 | 重置密码以未加盐 MD5 存储且与 bcrypt 不兼容 | 密码泄露风险 + 重置后无法登录 |
| P5 | 中 | bcrypt.MinCostcost=4 | 密码暴力破解成本降低 |
| P6 | 中 | Token 缓存键前缀大小写不一致 | 登录态缓存错乱/登出失效 |
| P7 | 中 | KYC 回调空 stub、Forget.Verify 伪实现 | 认证状态不可信 |
| P8 | 中 | 验证码缓存接口从未被调用 | 全链路验证码机制失效 |
| P9 | 低 | 内部错误/PII 经日志泄露 | 信息泄露 |
| P10 | 低 | DB 错误未处理、无事务、无测试 | 健壮性与可维护性 |
## 8. 修复建议(务实项)
1. **验证码必须校验**:在 `login/code.go``login/quick.go``register/code.go``forget/verify.go` 中调用 `GetVerificationCodeCache(phone)` 并比对 `in.Code`(一次一消);`forget/reset.go` 必须要求先通过 `Verify` 且校验一次性令牌,再允许改密。
2. **重置密码用 bcrypt**`forget/reset.go:31` 改为 `bcrypt.GenerateFromPassword([]byte(newPwd+salt), bcrypt.DefaultCost)` 并同步更新 `salt`,与登录校验一致。
3. **提高哈希 cost**`register/do.go``account/set_password.go``query.go``bcrypt.MinCost` 改为 `bcrypt.DefaultCost`10
4. **统一 Token 前缀**:仅保留一处定义(建议用 `config.Spec.Token.Prefix``login``register` 共用,避免 `/TOKEN/``/token/` 双键。
5. **KYC 回调落地**`jumio_callback.go` 增加签名校验并更新 `passport_verify``kyc_verify`/`kyc_status``Forget.Verify` 真正比对验证码。
6. **收敛默认账号**:保持 `InitData` 不启用,或删除该函数与硬编码 `welcome` 口令。
7. **错误处理**`account/get.go:44` 处理 `First` 的 error`register/do.go` 两次 Create 用事务包裹。
8. **清理**:移除 `cmd/cli` 调试输出、空 `excode/ex.go`、未调用的限流函数(或真正接入);敏感信息勿进 `printer`
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。
## 9. 整改记录
### 2026-09-22密码哈希统一为 bcrypt对应建议 2、3
| 位置 | 改动 | 说明 |
| --- | --- | --- |
| `internal/logic/forget/reset.go:32-43` | `utils.Md5(in.Password)``bcrypt.GenerateFromPassword(密码+新salt, bcrypt.DefaultCost)`,并在同一条 `Updates` 中同步写入新的 `salt` | 修复 P4重置密码与登录校验`bcrypt.CompareHashAndPassword(密码+sa.Salt)`)恢复一致;同时消除了"未加盐 MD5"与"重置后无法登录"两个问题 |
| `internal/logic/register/do.go:30` | `bcrypt.MinCost``bcrypt.DefaultCost` | 修复 P5cost 4 → 10 |
| `internal/logic/account/set_password.go:35` | `bcrypt.MinCost``bcrypt.DefaultCost` | 修复 P5 |
| `internal/models/query.go:21` | `bcrypt.MinCost``bcrypt.DefaultCost` | 修复 P5`InitData` 仍未被调用) |
验证:`gofmt` 无差异;`GOWORK=off go build ./...``go vet ./internal/...` 均通过。
> 历史 MD5 密码记录无法用 bcrypt 校验,需通过"重置密码"流程迁移(本次改动后重置将写入 bcrypt
### 2026-09-22验证码校验、找回密码票据与 Token 前缀(对应 P1、P2、P3、P6
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| S1原 P1/P2 | 高 | 验证码登录/快捷登录/注册/找回四条路径均不校验验证码 | 已修复:新增模块内验证码校验函数(`internal/vars/cache.go` 定义 Redis 键前缀常量,与 sender 侧 `/SMS/Code/` + 手机号规则一致),按 Redis 读取比对,**校验成功后删除键(一次性)**;四条路径全部接入 |
| S2原 P3 | 高 | `Forget.Reset` 仅凭 `identity` 即可改任意账号密码 | 已修复:`Forget.Verify` 真实校验 `phone + code` 后写入一次性重置票据Redis带 TTL`Forget.Reset` 先检查票据存在才允许改密,改密成功后立即删除票据(防重放)。未改 proto用现有两步流程实现 |
| H3原 P6 | 高 | Token 缓存键前缀 `/TOKEN/``/token/` 不一致 | 已修复:统一引用 `internal/vars.TokenPrefix` 常量,签发与校验/登出的键完全一致 |
> 本轮未修改 `proto/*.proto` 与 `pb/*.go`。
**遗留(未处理)**P7 KYC 空 stub、P8 验证码接口未接入、P9 日志泄露 PII、P10 事务与错误处理缺失、全模块无测试,以及报告中「中/低」级别的各项。