376 lines
10 KiB
Markdown
376 lines
10 KiB
Markdown
|
|
# 代码审查综合报告
|
|||
|
|
|
|||
|
|
**审查日期**:2026-03-21
|
|||
|
|
**审查范围**:用户管理系统(UMS)全栈代码
|
|||
|
|
**技术栈**:Go (Gin + GORM) + React 18 + TypeScript + Ant Design
|
|||
|
|
**审查专家**:代码审查专家
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、审查总结
|
|||
|
|
|
|||
|
|
### 整体评价
|
|||
|
|
|
|||
|
|
| 维度 | 评分 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **正确性** | ⭐⭐⭐⭐☆ | 核心功能实现正确,边界条件处理良好 |
|
|||
|
|
| **安全性** | ⭐⭐⭐⭐☆ | 安全措施到位,有少量改进空间 |
|
|||
|
|
| **可维护性** | ⭐⭐⭐⭐☆ | 代码结构清晰,命名规范 |
|
|||
|
|
| **性能** | ⭐⭐⭐⭐☆ | 缓存设计合理,限流机制完善 |
|
|||
|
|
| **测试覆盖** | ⭐⭐⭐⭐☆ | 测试覆盖较好 |
|
|||
|
|
|
|||
|
|
**总体评价**:项目代码质量良好,达到生产级标准。存在少量可改进之处,详见下文。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、🔴 阻塞级问题(必须修复)
|
|||
|
|
|
|||
|
|
审查过程中**未发现**阻塞级问题。项目在安全性方面做得较好:
|
|||
|
|
- 使用 Argon2id 密码哈希
|
|||
|
|
- 参数化查询防止 SQL 注入
|
|||
|
|
- JWT Token 黑名单机制
|
|||
|
|
- 权限检查中间件完善
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、🟡 建议级问题
|
|||
|
|
|
|||
|
|
### 3.1 后端 Go 部分
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-安全] SanitizeSQL/SanitizeXSS 方法不够健壮
|
|||
|
|
|
|||
|
|
**文件**:`internal/security/validator.go:69-93`
|
|||
|
|
|
|||
|
|
**问题**:简单的字符串替换无法有效防护复杂攻击场景,且可能破坏正常输入。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 当前实现
|
|||
|
|
func (v *Validator) SanitizeSQL(input string) string {
|
|||
|
|
dangerousChars := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "exec", "sp_"}
|
|||
|
|
result := input
|
|||
|
|
for _, char := range dangerousChars {
|
|||
|
|
result = strings.ReplaceAll(result, char, "")
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**建议**:
|
|||
|
|
- 使用 GORM 的参数化查询(已正确使用),不需要额外的 SanitizeSQL
|
|||
|
|
- XSS 防护应该在输出端处理,而非输入端
|
|||
|
|
- 如果必须做输入清理,考虑使用成熟的库如 `bluemonday`
|
|||
|
|
|
|||
|
|
**优先级**:中
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-安全] IP 地址验证正则不够完整
|
|||
|
|
|
|||
|
|
**文件**:`internal/security/validator.go:108-121`
|
|||
|
|
|
|||
|
|
**问题**:IPv6 正则仅支持完整格式,遗漏了压缩格式(如 `::1`, `2001:db8::1`)。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 当前实现
|
|||
|
|
pattern = `^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$`
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**建议**:使用 `net.ParseIP()` 进行验证,或使用 `go-playground/validator` 库。
|
|||
|
|
|
|||
|
|
**优先级**:低
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-可维护性] OAuth 用户名生成可能冲突
|
|||
|
|
|
|||
|
|
**文件**:`internal/service/auth.go:606`
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
Username: oauthUser.Nickname + "_" + oauthUser.OpenID[:8],
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:
|
|||
|
|
1. `oauthUser.Nickname` 可能为空或包含非法字符
|
|||
|
|
2. 不同 OAuth 用户可能有相同的昵称,OpenID 前 8 位也可能冲突
|
|||
|
|
|
|||
|
|
**建议**:
|
|||
|
|
```go
|
|||
|
|
// 使用 UUID 或雪花 ID 生成唯一用户名
|
|||
|
|
Username: fmt.Sprintf("oauth_%s_%d", provider, user.ID)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优先级**:中
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-可维护性] 用户搜索存在 LIKE 注入风险
|
|||
|
|
|
|||
|
|
**文件**:`internal/repository/user.go:157-159`
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
query = r.db.WithContext(ctx).Model(&domain.User{}).Where(
|
|||
|
|
"username LIKE ? OR email LIKE ? OR phone LIKE ? OR nickname LIKE ?",
|
|||
|
|
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%",
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:虽然使用了参数化查询,但 LIKE 模式中直接拼接 `%%` 是安全的。不过如果 keyword 包含特殊 LIKE 字符(如 `%`, `_`),可能导致意外匹配。
|
|||
|
|
|
|||
|
|
**建议**:转义特殊字符
|
|||
|
|
```go
|
|||
|
|
func escapeLike(s string) string {
|
|||
|
|
return strings.ReplaceAll(strings.ReplaceAll(s, "%", "\\%"), "_", "\\_")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优先级**:低
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-性能] 中间件权限检查存在 N+1 查询
|
|||
|
|
|
|||
|
|
**文件**:`internal/api/middleware/auth.go:146-170`
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
for _, rid := range roleIDs {
|
|||
|
|
role, err := m.roleRepo.GetByID(ctx, rid) // N 次查询
|
|||
|
|
// ...
|
|||
|
|
permIDs, err := m.rolePermissionRepo.GetPermissionIDsByRoleID(ctx, rid) // N 次查询
|
|||
|
|
// ...
|
|||
|
|
perm, err := m.permissionRepo.GetByID(ctx, pid) // N*M 次查询
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:嵌套循环导致大量数据库查询。
|
|||
|
|
|
|||
|
|
**建议**:使用批量查询
|
|||
|
|
```go
|
|||
|
|
// 一次性获取所有角色
|
|||
|
|
roles, _ := m.roleRepo.GetByIDs(ctx, roleIDs)
|
|||
|
|
// 一次性获取所有权限
|
|||
|
|
permIDs, _ := m.rolePermissionRepo.GetPermissionIDsByRoleIDs(ctx, roleIDs)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优先级**:中(用户权限少时影响不大)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-可维护性] JWT JTI 生成使用 math/rand
|
|||
|
|
|
|||
|
|
**文件**:`internal/auth/jwt.go:55-57`
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func generateJTI() string {
|
|||
|
|
return fmt.Sprintf("%d-%d", time.Now().UnixNano(), mathrand.Int63())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:`math/rand` 是伪随机数生成器,不够安全。
|
|||
|
|
|
|||
|
|
**建议**:使用 `crypto/rand`
|
|||
|
|
```go
|
|||
|
|
import cryptorand "crypto/rand"
|
|||
|
|
|
|||
|
|
func generateJTI() string {
|
|||
|
|
b := make([]byte, 16)
|
|||
|
|
cryptorand.Read(b)
|
|||
|
|
return hex.EncodeToString(b)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优先级**:中
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.2 前端 React/TypeScript 部分
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-安全] 前端 App.tsx 是 Vite 模板代码
|
|||
|
|
|
|||
|
|
**文件**:`frontend/admin/src/App.tsx`
|
|||
|
|
|
|||
|
|
**问题**:整个文件是 Vite 默认模板内容,未替换为实际应用代码。
|
|||
|
|
|
|||
|
|
**建议**:替换为实际的 React Router 配置和根组件。
|
|||
|
|
|
|||
|
|
**优先级**:高(用户体验问题,不是安全漏洞)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-可维护性] HTTP Client 缺少请求超时处理
|
|||
|
|
|
|||
|
|
**文件**:`frontend/admin/src/lib/http/client.ts`
|
|||
|
|
|
|||
|
|
**问题**:所有 fetch 请求没有设置默认超时时间。
|
|||
|
|
|
|||
|
|
**建议**:
|
|||
|
|
```typescript
|
|||
|
|
const DEFAULT_TIMEOUT = 30000 // 30秒
|
|||
|
|
|
|||
|
|
const controller = new AbortController()
|
|||
|
|
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(url, {
|
|||
|
|
...options,
|
|||
|
|
signal: options.signal || controller.signal,
|
|||
|
|
})
|
|||
|
|
clearTimeout(timeoutId)
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优先级**:中
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-安全] 缺少 CSRF Token 机制
|
|||
|
|
|
|||
|
|
**文件**:`frontend/admin/src/lib/http/client.ts`
|
|||
|
|
|
|||
|
|
**问题**:对于状态变更请求(POST/PUT/DELETE),缺少 CSRF 保护。
|
|||
|
|
|
|||
|
|
**建议**:
|
|||
|
|
1. 登录后从服务端获取 CSRF Token
|
|||
|
|
2. 将 Token 存入 cookie(HttpOnly)或请求头
|
|||
|
|
3. 服务端验证请求来源
|
|||
|
|
|
|||
|
|
**优先级**:中(如果使用 JWT Bearer Token,CORS 配置正确的情况下风险较低)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-可维护性] 组件缺少 key props 警告处理
|
|||
|
|
|
|||
|
|
**文件**:`frontend/admin/src/pages/admin/UsersPage/UsersPage.tsx:86-96`
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchRoles = async () => {
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
fetchRoles()
|
|||
|
|
}, []) // 依赖数组为空,eslint 可能警告
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:空依赖数组的 useEffect 可能导致 stale closure。
|
|||
|
|
|
|||
|
|
**建议**:使用 eslint-plugin-react-hooks 规则强制检查。
|
|||
|
|
|
|||
|
|
**优先级**:低
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 🟡 [建议-性能] 表格组件缺少虚拟滚动
|
|||
|
|
|
|||
|
|
**文件**:`frontend/admin/src/pages/admin/UsersPage/UsersPage.tsx:455-469`
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
<Table
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={users}
|
|||
|
|
rowKey="id"
|
|||
|
|
// ... 当用户数超过 100 时可能卡顿
|
|||
|
|
/>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**问题**:大列表(>100)缺少虚拟滚动优化。
|
|||
|
|
|
|||
|
|
**建议**:使用 `@tanstack/react-virtual` 或 Ant Design Table 的虚拟滚动功能。
|
|||
|
|
|
|||
|
|
**优先级**:低(当前系统规模下影响不大)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、💭 挑剔级问题
|
|||
|
|
|
|||
|
|
### 4.1 后端
|
|||
|
|
|
|||
|
|
| 文件 | 行号 | 问题 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `internal/auth/jwt.go` | 119 | RSA 密钥生成在运行时,可能影响启动性能 |
|
|||
|
|
| `internal/service/auth.go` | 606 | OAuth 用户名未验证唯一性 |
|
|||
|
|
| `internal/repository/user.go` | 271-277 | SortBy 字段白名单硬编码,可提取为常量 |
|
|||
|
|
|
|||
|
|
### 4.2 前端
|
|||
|
|
|
|||
|
|
| 文件 | 行号 | 问题 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `UsersPage.tsx` | 88-93 | 角色列表加载失败静默忽略,可添加错误提示 |
|
|||
|
|
| `client.ts` | 386-389 | upload 函数 401 处理不完整,未清除会话 |
|
|||
|
|
| `App.tsx` | 全文件 | 整个文件是模板代码 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、✅ 做得好的地方
|
|||
|
|
|
|||
|
|
### 后端
|
|||
|
|
|
|||
|
|
1. **密码安全**:使用 Argon2id 哈希算法,支持 bcrypt 兼容
|
|||
|
|
2. **JWT 安全**:分离 access_token 和 refresh_token,支持 JTI 黑名单
|
|||
|
|
3. **SQL 注入防护**:GORM 参数化查询,无直接 SQL 拼接
|
|||
|
|
4. **限流机制**:支持多种限流算法(令牌桶、漏桶、滑动窗口)
|
|||
|
|
5. **错误处理**:统一的错误码和响应格式
|
|||
|
|
6. **依赖注入**:Service 层通过接口解耦,便于测试
|
|||
|
|
7. **测试覆盖**:包含基准测试、健壮性测试、集成测试
|
|||
|
|
|
|||
|
|
### 前端
|
|||
|
|
|
|||
|
|
1. **Token 管理**:内存存储 access_token,localStorage 存储 refresh_token
|
|||
|
|
2. **并发控制**:实现了 refresh_token 并发刷新锁
|
|||
|
|
3. **401 处理**:自动刷新并重试机制完善
|
|||
|
|
4. **类型安全**:TypeScript 严格模式,类型定义完整
|
|||
|
|
5. **组件拆分**:UI 组件和业务组件分离
|
|||
|
|
6. **守卫机制**:RequireAuth 和 RequireAdmin 路由守卫完善
|
|||
|
|
7. **错误处理**:统一的错误处理和用户反馈
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、改进建议优先级
|
|||
|
|
|
|||
|
|
### 高优先级(建议尽快实施)
|
|||
|
|
|
|||
|
|
| # | 问题 | 影响 | 工作量 |
|
|||
|
|
|---|------|------|--------|
|
|||
|
|
| 1 | 替换前端 App.tsx 模板代码 | 用户体验 | 0.5d |
|
|||
|
|
| 2 | OAuth 用户名冲突问题 | 用户注册失败 | 0.5d |
|
|||
|
|
| 3 | 添加 HTTP 请求超时 | 防止请求挂起 | 0.5d |
|
|||
|
|
|
|||
|
|
### 中优先级(建议本版本实施)
|
|||
|
|
|
|||
|
|
| # | 问题 | 影响 | 工作量 |
|
|||
|
|
|---|------|------|--------|
|
|||
|
|
| 4 | JWT JTI 使用 crypto/rand | 安全性提升 | 0.5d |
|
|||
|
|
| 5 | 权限检查优化 N+1 查询 | 性能提升 | 1d |
|
|||
|
|
| 6 | 添加 CSRF 保护 | 安全性提升 | 1d |
|
|||
|
|
|
|||
|
|
### 低优先级(建议后续版本考虑)
|
|||
|
|
|
|||
|
|
| # | 问题 | 影响 | 工作量 |
|
|||
|
|
|---|------|------|--------|
|
|||
|
|
| 7 | IP 验证正则完善 | 边界情况 | 0.5d |
|
|||
|
|
| 8 | LIKE 特殊字符转义 | 边界情况 | 0.5d |
|
|||
|
|
| 9 | 大表格虚拟滚动 | 性能优化 | 1d |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、附录:审查文件清单
|
|||
|
|
|
|||
|
|
### 后端 (31 files)
|
|||
|
|
- `internal/api/handler/*.go` (6 files)
|
|||
|
|
- `internal/api/middleware/*.go` (5 files)
|
|||
|
|
- `internal/auth/*.go` (10 files)
|
|||
|
|
- `internal/service/*.go` (12 files)
|
|||
|
|
- `internal/repository/*.go` (14 files)
|
|||
|
|
- `internal/security/*.go` (4 files)
|
|||
|
|
- `internal/domain/*.go` (8 files)
|
|||
|
|
|
|||
|
|
### 前端 (18 files)
|
|||
|
|
- `frontend/admin/src/App.tsx`
|
|||
|
|
- `frontend/admin/src/lib/http/*.ts` (5 files)
|
|||
|
|
- `frontend/admin/src/lib/storage/*.ts` (2 files)
|
|||
|
|
- `frontend/admin/src/components/guards/*.tsx` (2 files)
|
|||
|
|
- `frontend/admin/src/pages/admin/UsersPage/*.tsx` (5 files)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*本报告由代码审查专家制定,遵循 CODE_REVIEW_STANDARD.md 规范*
|