Files
full/docs/passport.md

200 lines
17 KiB
Markdown
Raw Permalink Normal View History

2026-09-22 18:53:53 +08:00
# 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`
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。
2026-09-22 21:15:34 +08:00
## 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 事务与错误处理缺失、全模块无测试,以及报告中「中/低」级别的各项。