create-dwy 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/package.json +1 -1
  2. package/src/create.js +50 -3
  3. package/src/sync.js +228 -130
  4. package/src/utils.js +4 -0
  5. package/templates/claude-global/CLAUDE.md +81 -6
  6. package/templates/claude-global/rules/dwy-postgres.md +89 -0
  7. package/templates/claude-global/rules/dwy-python-backend.md +492 -0
  8. package/templates/claude-global/rules/dwy-python-core.md +94 -634
  9. package/templates/claude-global/rules/dwy-python-testing.md +115 -13
  10. package/templates/claude-global/rules/dwy-redis.md +88 -0
  11. package/templates/claude-global/rules/dwy-vue-core.md +341 -645
  12. package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
  13. package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
  14. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
  15. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
  16. package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
  17. package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
  18. package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
  19. package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
  20. package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
  21. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
  22. package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
  23. package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
  24. package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
  25. package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
  26. package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
  27. package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
  28. package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
  29. package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
  30. package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
  31. package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
  32. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
  33. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
  34. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
  35. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
  36. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
  37. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
  38. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
  39. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
  40. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
  41. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
  42. package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
  43. package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
  44. package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
  45. package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
  46. package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
  47. package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
  48. package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
  49. package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
  50. package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
  51. package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
  52. package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
  53. package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
  54. package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
  55. package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
  56. package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
  57. package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
  58. package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
  59. package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
  60. package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
  61. package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
  62. package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
  63. package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
  64. package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
  65. package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
  66. package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
  67. package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
  68. package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
  69. package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
  70. package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
  71. package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
  72. package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
  73. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
  74. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
  75. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
  76. package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
  77. package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
  78. package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
  79. package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
  80. package/templates/claude-global/rules/dwy-api-design.md +0 -110
  81. package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
  82. package/templates/claude-global/rules/dwy-git-security.md +0 -53
  83. package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
  84. package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
  85. package/templates/claude-global/rules/dwy-project-structure.md +0 -181
  86. package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
  87. package/templates/claude-global/rules/dwy-python-orm.md +0 -61
  88. package/templates/claude-global/rules/dwy-python-security.md +0 -636
  89. package/templates/claude-global/rules/dwy-server-security.md +0 -518
  90. package/templates/claude-global/rules/dwy-vue-api.md +0 -146
  91. package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
  92. package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
  93. package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
  94. package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
  95. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
  96. /package/templates/{claude-global/commands → project/fullstack-monorepo/backend/alembic/versions}/.gitkeep +0 -0
@@ -1,636 +0,0 @@
1
- ---
2
- description: Python 后端安全编码(认证/校验/注入/存储/RBAC 等)
3
- paths:
4
- - "**/*.py"
5
- ---
6
-
7
- # Python 后端安全编码
8
-
9
- ## 十九、安全编码
10
-
11
- ### 强制规则
12
- - SQL 查询必须使用参数化绑定,**禁止**字符串拼接
13
- - 用户输入必须校验后使用,**禁止**直接信任
14
- - 文件路径操作必须防止路径遍历攻击
15
- - 序列化/反序列化**禁止**用不安全的反序列化库处理不可信数据
16
- - 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
17
- - JWT 密钥从环境变量读取,**禁止**硬编码
18
-
19
- ```python
20
- # BAD: SQL 注入
21
- cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
22
-
23
- # GOOD: 参数化查询
24
- cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
25
-
26
- # BAD: 路径遍历
27
- file_path = f"/uploads/{user_input_filename}"
28
-
29
- # GOOD: 路径安全校验
30
- safe_path = Path("/uploads") / Path(user_input_filename).name
31
-
32
- # BAD: 硬编码密钥
33
- SECRET_KEY = "my-super-secret-key"
34
-
35
- # GOOD: 环境变量
36
- SECRET_KEY = settings.secret_key
37
- ```
38
-
39
- ---
40
-
41
- # 后端安全编码规范
42
-
43
- FastAPI + PostgreSQL + Redis + MinIO + DolphinDB 项目的安全编码规则。AI 生成或修改代码时,必须严格按照这些规则执行。
44
-
45
- ## 一、ID 设计(防遍历攻击)
46
-
47
- ### 核心原则
48
- **数据库内部用自增 ID(性能),对外暴露 UUID(安全)。** 攻击者无法通过递增 ID 遍历所有数据。
49
-
50
- ### 强制规则
51
-
52
- ```python
53
- # GOOD: ORM 模型同时有 id 和 uuid
54
- import uuid as uuid_lib
55
- from sqlalchemy import String
56
- from sqlalchemy.orm import Mapped, mapped_column
57
-
58
- class User(Base, TimestampMixin):
59
- __tablename__ = "users"
60
-
61
- id: Mapped[int] = mapped_column(primary_key=True)
62
- uuid: Mapped[str] = mapped_column(String(36), default=lambda: str(uuid_lib.uuid4()), unique=True, index=True)
63
- name: Mapped[str] = mapped_column(String(50))
64
-
65
-
66
- # GOOD: 路由用 UUID 查询
67
- @router.get("/users/{user_uuid}", response_model=UserResponse)
68
- async def get_user(user_uuid: str, service: UserService = Depends(get_user_service)):
69
- return await service.get_by_uuid(user_uuid)
70
-
71
-
72
- # GOOD: Response Schema 只返回 uuid,不返回 id
73
- class UserResponse(BaseModel):
74
- uuid: str
75
- name: str
76
- # 不暴露 id 字段
77
-
78
-
79
- # BAD: 路由直接用自增 ID
80
- @router.get("/users/{user_id}")
81
- async def get_user(user_id: int): ...
82
-
83
- # BAD: Response 暴露自增 ID
84
- class UserResponse(BaseModel):
85
- id: int # 攻击者可遍历 1,2,3...
86
- ```
87
-
88
- ### 违规检测
89
- 如果在路由路径中发现 `/{name}_id` 且类型为 `int`(如 `user_id: int`),必须提示用户改为 UUID。
90
-
91
- ## 二、密码安全
92
-
93
- ### 强制规则
94
-
95
- ```python
96
- # GOOD: bcrypt 存储
97
- from dwyeapi.security import hash_password, verify_password
98
-
99
- hashed = hash_password("user_password") # 存储到数据库
100
- is_valid = verify_password("input", hashed) # 验证时比对
101
-
102
- # BAD: 明文存储
103
- user.password = "123456"
104
-
105
- # BAD: MD5/SHA 存储(可被彩虹表破解)
106
- user.password = hashlib.md5(password.encode()).hexdigest()
107
-
108
- # BAD: 接口返回密码字段
109
- class UserResponse(BaseModel):
110
- id: int
111
- name: str
112
- password: str # 绝对禁止
113
- ```
114
-
115
- | 规则 | 说明 |
116
- |------|------|
117
- | 存储算法 | **只允许 bcrypt**(或 argon2),禁止 MD5/SHA/明文 |
118
- | 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
119
- | 密码强度 | 用户密码最少 8 位,管理员密码最少 12 位 |
120
- | 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
121
-
122
- ## 三、接口认证与授权
123
-
124
- ### 强制规则
125
-
126
- ```python
127
- # GOOD: 所有 API 必须 JWT 认证(白名单除外)
128
- WHITELIST = ["/api/health", "/api/auth/login", "/api/auth/register"]
129
-
130
- # GOOD: 路由级别认证
131
- @router.get("/users", dependencies=[Depends(get_current_user)])
132
- async def list_users(): ...
133
-
134
- # GOOD: 角色检查
135
- @router.delete("/users/{uuid}", dependencies=[Depends(require_admin)])
136
- async def delete_user(uuid: str): ...
137
-
138
- # BAD: 无认证的数据接口
139
- @router.get("/users")
140
- async def list_users(): ...
141
- ```
142
-
143
- ### Token 安全
144
-
145
- | 规则 | 说明 |
146
- |------|------|
147
- | Access Token 有效期 | **30 分钟**(最长不超过 2 小时) |
148
- | Refresh Token 有效期 | **7 天** |
149
- | Token 黑名单 | 登出后 token 必须失效(Redis 存黑名单,TTL = 剩余有效期) |
150
- | Token 存储 | 前端存 `localStorage`(SPA),**禁止**存 Cookie 不设 HttpOnly |
151
- | Secret Key | 从环境变量读取,最少 32 位随机字符串 |
152
-
153
- ## 四、输入校验
154
-
155
- ### 强制规则
156
-
157
- ```python
158
- # GOOD: Pydantic Schema 严格校验
159
- class UserCreate(BaseModel):
160
- name: str = Field(min_length=2, max_length=50)
161
- email: EmailStr
162
- password: str = Field(min_length=8, max_length=128)
163
- age: int = Field(ge=0, le=150)
164
-
165
- # GOOD: 列表查询参数限制
166
- class ListParams(BaseModel):
167
- page: int = Field(default=1, ge=1)
168
- page_size: int = Field(default=20, ge=1, le=100) # 上限 100
169
-
170
- # BAD: 不校验直接使用
171
- @router.post("/users")
172
- async def create_user(data: dict): ...
173
-
174
- # BAD: page_size 无上限
175
- class ListParams(BaseModel):
176
- page_size: int = 20 # 攻击者可传 999999
177
- ```
178
-
179
- ### 校验规则清单
180
-
181
- | 字段类型 | 校验 |
182
- |---------|------|
183
- | 数字 | `ge` + `le`(最小值 + 最大值) |
184
- | 邮箱 | `EmailStr` |
185
- | 手机号 | `pattern=r"^1[3-9]\d{9}$"` |
186
- | URL | `HttpUrl` |
187
-
188
- ## 五、响应安全
189
-
190
- ### 强制规则
191
-
192
- ```python
193
- # GOOD: Response Schema 严格控制返回字段
194
- class UserResponse(BaseModel):
195
- uuid: str
196
- name: str
197
- email: str
198
- is_active: bool
199
- created_at: datetime
200
-
201
- model_config = ConfigDict(from_attributes=True)
202
-
203
- # BAD: 直接返回 ORM 对象
204
- @router.get("/users/{uuid}")
205
- async def get_user(uuid: str, db=Depends(get_db)):
206
- user = await db.get(User, uuid)
207
- return user # 可能暴露 password、内部 ID 等
208
-
209
- # GOOD: 列表接口必须分页
210
- @router.get("/users", response_model=PaginatedResponse[UserResponse])
211
- async def list_users(params: PaginationParams = Depends()): ...
212
-
213
- # BAD: 无分页返回全量数据
214
- @router.get("/users")
215
- async def list_users():
216
- return await db.execute(select(User)) # 全量泄露
217
- ```
218
-
219
- ### PII 脱敏
220
-
221
- ```python
222
- # GOOD: 工具函数
223
- def mask_phone(phone: str) -> str:
224
- """138****5678"""
225
- if len(phone) != 11:
226
- return phone
227
- return phone[:3] + "****" + phone[7:]
228
-
229
- def mask_id_card(id_card: str) -> str:
230
- """420***********1234"""
231
- if len(id_card) != 18:
232
- return id_card
233
- return id_card[:3] + "***********" + id_card[14:]
234
-
235
- # GOOD: 在 Response Schema 中使用 validator 脱敏
236
- class UserResponse(BaseModel):
237
- phone: str
238
-
239
- @field_validator("phone", mode="before")
240
- @classmethod
241
- def mask_phone_field(cls, v: str) -> str:
242
- return mask_phone(v) if v else v
243
- ```
244
-
245
- ## 六、错误处理安全
246
-
247
- ### 强制规则
248
-
249
- ```python
250
- # GOOD: 生产环境只返回通用错误
251
- raise NotFoundError("用户")
252
- raise BusinessError("操作失败")
253
-
254
- # BAD: 回显用户输入
255
- raise BusinessError(f"未找到: {user_input}") # XSS/信息泄露
256
-
257
- # BAD: 暴露内部错误
258
- except Exception as e:
259
- return {"error": str(e)} # 可能包含 SQL、文件路径、堆栈
260
-
261
- # BAD: 暴露数据库结构
262
- except IntegrityError as e:
263
- return {"error": f"数据库错误: {e}"} # 暴露表名、字段名
264
-
265
- # GOOD: 统一错误处理
266
- except IntegrityError:
267
- raise BusinessError("数据冲突,请检查是否重复提交", code="CONFLICT")
268
- ```
269
-
270
- ### 生产环境禁止
271
-
272
- | 禁止返回的内容 | 原因 |
273
- |--------------|------|
274
- | 完整异常堆栈 | 暴露代码结构和文件路径 |
275
- | SQL 语句 | 暴露表结构和查询逻辑 |
276
- | 文件系统路径 | 暴露服务器目录结构 |
277
- | 用户输入回显 | XSS 和信息泄露 |
278
- | 数据库字段名 | 暴露数据模型 |
279
- | 第三方 API Key | 凭证泄露 |
280
-
281
- ## 七、注入防护
282
-
283
- ### SQL 注入
284
-
285
- ```python
286
- # GOOD: ORM 参数化查询(安全)
287
- result = await session.execute(select(User).where(User.id == user_id))
288
-
289
- # GOOD: 原生 SQL 参数化(安全)
290
- result = await session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
291
-
292
- # BAD: 字符串拼接(SQL 注入)
293
- result = await session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
294
- ```
295
-
296
- ### 命令注入
297
-
298
- ```python
299
- # BAD: shell 命令注入(禁止)
300
- # 使用 os 模块的 system 调用执行用户输入拼接的命令 → 注入风险
301
- # 使用 subprocess.run(... , shell=True) 执行用户输入拼接的字符串 → 注入风险
302
-
303
- # GOOD: 参数列表(不经过 shell)
304
- subprocess.run(["convert", safe_filename, "output.png"], shell=False)
305
-
306
- # BEST: 不调用外部命令,用 Python 库
307
- from PIL import Image
308
- img = Image.open(safe_path)
309
- ```
310
-
311
- ### 路径遍历
312
-
313
- ```python
314
- # BAD: 用户控制文件路径
315
- file_path = f"/uploads/{user_input_filename}"
316
-
317
- # GOOD: 安全提取文件名
318
- from pathlib import Path
319
- safe_name = Path(user_input_filename).name # 去除 ../../../ 等
320
- file_path = Path("/uploads") / safe_name
321
- ```
322
-
323
- ### SSRF(服务端请求伪造)
324
-
325
- ```python
326
- # BAD: 用户控制 URL 发起请求
327
- url = request_body.url # 用户传入
328
- response = await httpx.get(url) # 可能访问内网 169.254.169.254
329
-
330
- # GOOD: 白名单限制
331
- ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"]
332
- parsed = urlparse(url)
333
- if parsed.hostname not in ALLOWED_HOSTS:
334
- raise BusinessError("不允许的目标地址")
335
- ```
336
-
337
- ## 八、软删除
338
-
339
- ### 强制规则
340
-
341
- ```python
342
- # GOOD: 模型添加软删除字段
343
- class User(Base, TimestampMixin):
344
- __tablename__ = "users"
345
-
346
- id: Mapped[int] = mapped_column(primary_key=True)
347
- is_deleted: Mapped[bool] = mapped_column(default=False)
348
- deleted_at: Mapped[datetime | None] = mapped_column(default=None)
349
-
350
- # GOOD: 删除操作只更新标记
351
- async def delete_user(self, user_uuid: str) -> None:
352
- user = await self.get_by_uuid(user_uuid)
353
- user.is_deleted = True
354
- user.deleted_at = datetime.now(timezone.utc)
355
- await self.db.commit()
356
-
357
- # GOOD: 查询时自动排除已删除
358
- async def list_users(self) -> list[User]:
359
- result = await self.db.execute(
360
- select(User).where(User.is_deleted == False)
361
- )
362
- return list(result.scalars().all())
363
-
364
- # BAD: 物理删除
365
- async def delete_user(self, user_id: int) -> None:
366
- user = await self.db.get(User, user_id)
367
- await self.db.delete(user) # 数据永久丢失
368
- await self.db.commit()
369
- ```
370
-
371
- ### 例外
372
- 以下场景允许物理删除(但需用户确认):
373
- - 合规要求的数据清除(如 GDPR 被遗忘权)
374
- - 临时数据清理(验证码、过期 session)
375
-
376
- ## 九、审计日志
377
-
378
- ### 强制规则
379
-
380
- 需要记录审计日志的操作:
381
-
382
- | 操作 | 记录内容 |
383
- |------|---------|
384
- | 登录成功/失败 | 用户名、IP、时间、User-Agent |
385
- | 权限变更 | 操作人、目标用户、旧角色、新角色 |
386
- | 数据删除 | 操作人、删除的资源类型和 UUID |
387
- | 敏感数据查询 | 查询人、查询条件(脱敏) |
388
- | 密码修改 | 操作人、时间 |
389
- | 导出操作 | 操作人、导出数据范围 |
390
-
391
- ```python
392
- # GOOD: 审计日志示例
393
- import logging
394
-
395
- audit_logger = logging.getLogger("audit")
396
-
397
- async def delete_user(self, user_uuid: str, operator: dict) -> None:
398
- user = await self.get_by_uuid(user_uuid)
399
- user.is_deleted = True
400
- await self.db.commit()
401
-
402
- audit_logger.info(
403
- "user_deleted operator=%s target=%s",
404
- operator["uuid"],
405
- user_uuid,
406
- )
407
- ```
408
-
409
- ## 十、PostgreSQL 安全
410
-
411
- ### 强制规则
412
-
413
- | 规则 | 说明 |
414
- |------|------|
415
- | 参数化查询 | **禁止**字符串拼接 SQL,必须 ORM 或参数化 |
416
- | 应用用户权限 | 应用数据库用户**只给** SELECT/INSERT/UPDATE/DELETE,**不给** DROP/CREATE/ALTER |
417
- | 连接池限制 | `pool_size` 不超过 20,`max_overflow` 不超过 10 |
418
- | SSL 连接 | 生产环境建议启用 SSL(`sslmode=require`) |
419
- | 敏感字段加密 | 身份证号等字段使用 AES 加密存储,不只是展示时脱敏 |
420
-
421
- ### 应用用户权限配置
422
- ```sql
423
- -- 创建应用专用用户(非超级用户)
424
- CREATE USER app_user WITH PASSWORD 'strong_password';
425
- GRANT CONNECT ON DATABASE mydb TO app_user;
426
- GRANT USAGE ON SCHEMA public TO app_user;
427
- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
428
- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
429
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
430
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user;
431
-
432
- -- 禁止给应用用户的权限
433
- -- GRANT ALL PRIVILEGES
434
- -- SUPERUSER
435
- -- CREATEDB
436
- -- CREATEROLE
437
- ```
438
-
439
- ## 十一、Redis 安全
440
-
441
- ### 强制规则
442
-
443
- | 规则 | 说明 |
444
- |------|------|
445
- | 认证 | **必须设置密码**(`requirepass`),不允许无密码 Redis |
446
- | 绑定地址 | `bind 127.0.0.1`,**禁止** `bind 0.0.0.0` |
447
- | 危险命令 | **禁用** `FLUSHALL`/`FLUSHDB`/`CONFIG`/`KEYS`/`DEBUG` |
448
- | Key TTL | 所有 key **必须设置过期时间**,禁止无 TTL 的 key |
449
- | Key 前缀 | 所有 key 必须有项目前缀(如 `myapp:token:xxx`) |
450
- | 存储内容 | **禁止**在 Redis 存储敏感原文(密码、身份证明文) |
451
-
452
- ### Redis 配置
453
- ```
454
- # redis.conf
455
- requirepass your_strong_password
456
- bind 127.0.0.1
457
- rename-command FLUSHALL ""
458
- rename-command FLUSHDB ""
459
- rename-command CONFIG ""
460
- rename-command KEYS ""
461
- rename-command DEBUG ""
462
- maxmemory 256mb
463
- maxmemory-policy allkeys-lru
464
- ```
465
-
466
- ### Key 设计规范
467
- ```python
468
- # GOOD: 有前缀、有 TTL
469
- await redis.set("myapp:token:blacklist:abc123", "1", ex=3600)
470
- await redis.set("myapp:cache:user:uuid123", json.dumps(user_data), ex=300)
471
-
472
- # BAD: 无前缀
473
- await redis.set("abc123", "1")
474
-
475
- # BAD: 无 TTL
476
- await redis.set("myapp:data:xxx", value) # 永不过期,内存泄漏
477
-
478
- # BAD: 存储敏感原文
479
- await redis.set("myapp:user:password", "123456")
480
- ```
481
-
482
- ## 十二、MinIO / 文件存储安全
483
-
484
- ### 强制规则
485
-
486
- | 规则 | 说明 |
487
- |------|------|
488
- | Bucket 访问 | **默认私有**,不开放 public 读 |
489
- | 文件访问 | 通过**预签名 URL** 限时访问(默认 15 分钟) |
490
- | 文件名 | **UUID 重命名**,禁止保留用户原始文件名做存储路径 |
491
- | 文件类型 | 校验文件 **magic bytes**(不只检查扩展名) |
492
- | 文件大小 | 后端二次校验大小限制(不信任前端或 Nginx) |
493
-
494
- ```python
495
- # GOOD: UUID 重命名 + 预签名 URL
496
- import uuid as uuid_lib
497
- from minio import Minio
498
-
499
- async def upload_file(file: UploadFile) -> str:
500
- # 校验文件类型(magic bytes)
501
- header = await file.read(8)
502
- await file.seek(0)
503
- if not is_allowed_file_type(header):
504
- raise BusinessError("不允许的文件类型")
505
-
506
- # UUID 重命名
507
- ext = Path(file.filename).suffix
508
- safe_name = f"{uuid_lib.uuid4()}{ext}"
509
-
510
- client.put_object("uploads", safe_name, file.file, file.size)
511
- return safe_name
512
-
513
- def get_download_url(filename: str) -> str:
514
- # 预签名 URL,15 分钟有效
515
- return client.presigned_get_object("uploads", filename, expires=timedelta(minutes=15))
516
-
517
-
518
- # BAD: 用户原始文件名
519
- safe_name = file.filename # 可能包含 ../../../etc/passwd
520
-
521
- # BAD: 公开 bucket
522
- client.set_bucket_policy("uploads", public_read_policy)
523
-
524
- # BAD: 只检查扩展名
525
- if file.filename.endswith(".jpg"): # 攻击者可改扩展名
526
- ```
527
-
528
- ## 十三、DolphinDB 安全
529
-
530
- ### 强制规则
531
-
532
- | 规则 | 说明 |
533
- |------|------|
534
- | 输入白名单 | 所有外部输入**必须经过正则白名单**验证后才能插入 DolphinDB 脚本 |
535
- | 禁止拼接 | **禁止**将用户输入直接拼接到 DolphinDB 脚本字符串中 |
536
- | 连接认证 | DolphinDB 连接**必须使用账号密码**,从环境变量读取 |
537
- | 端口隔离 | DolphinDB 端口**禁止对外暴露**,仅 Docker 内部网络或 127.0.0.1 |
538
-
539
- ```python
540
- # GOOD: 白名单验证后再插入脚本
541
- import re
542
-
543
- SAFE_COLUMN_NAME = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,63}$')
544
- SAFE_TABLE_NAME = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,63}$')
545
-
546
- def validate_column_name(name: str) -> str:
547
- if not SAFE_COLUMN_NAME.match(name):
548
- raise BusinessError(f"非法字段名: {name}")
549
- return name
550
-
551
- # 使用
552
- col = validate_column_name(user_input_column)
553
- script = f"select {col} from loadTable('dfs://mydb', 'mytable')"
554
-
555
-
556
- # BAD: 直接拼接用户输入
557
- script = f"select * from loadTable('dfs://mydb', '{user_input}')" # DolphinDB 注入
558
-
559
- # BAD: 无验证
560
- factor_name = request.query_params.get("factor")
561
- script = f"select * from factors where factor_name = '{factor_name}'"
562
- ```
563
-
564
- ### DolphinDB 连接安全
565
- ```python
566
- # GOOD: 从环境变量读取
567
- ddb_conn = ddb.Session()
568
- ddb_conn.connect(
569
- host=settings.dolphindb_host,
570
- port=settings.dolphindb_port,
571
- userid=settings.dolphindb_user,
572
- password=settings.dolphindb_password,
573
- )
574
-
575
- # BAD: 硬编码
576
- ddb_conn.connect("localhost", 8848, "admin", "123456")
577
- ```
578
-
579
- ## 十四、API 限流
580
-
581
- ### 强制规则
582
-
583
- 不同接口使用不同的限流策略:
584
-
585
- | 接口类型 | 限流 | 原因 |
586
- |---------|------|------|
587
- | 登录 `/auth/login` | 3 次/分钟/IP | 防暴力破解 |
588
- | 注册 `/auth/register` | 5 次/分钟/IP | 防批量注册 |
589
- | 普通查询 | 60 次/分钟/IP | 防爬虫 |
590
- | 数据导出 | 5 次/分钟/IP | 防数据泄露 |
591
- | 文件上传 | 10 次/分钟/IP | 防资源滥用 |
592
-
593
- ```python
594
- # GOOD: 使用 slowapi 限流
595
- from slowapi import Limiter
596
- from slowapi.util import get_remote_address
597
-
598
- limiter = Limiter(key_func=get_remote_address)
599
-
600
- @router.post("/auth/login")
601
- @limiter.limit("3/minute")
602
- async def login(request: Request, body: LoginRequest): ...
603
-
604
- @router.get("/users/export")
605
- @limiter.limit("5/minute")
606
- async def export_users(request: Request): ...
607
- ```
608
-
609
- ## 十五、违规检测清单
610
-
611
- **AI 在编写或审查代码时,必须检查以下违规模式。检测到违规时按严重程度执行对应动作:**
612
-
613
- ### 严重程度与动作
614
-
615
- | 严重程度 | 动作 |
616
- |---------|------|
617
- | **致命** | **立即 STOP,不得继续编写或提交代码。** 必须修正后重新检查,修正前不执行任何其他操作。 |
618
- | **高** | 必须修正后才能继续。向用户说明违规点和修正方案。 |
619
- | **中** | 提示用户存在风险,建议修正。用户确认后可继续。 |
620
-
621
- ### 检查清单
622
-
623
- | 检查项 | 违规模式 | 严重程度 |
624
- |--------|---------|---------|
625
- | 密码明文 | `password` 字段出现在 Response Schema | **致命 → STOP** |
626
- | 密码弱存储 | 使用 MD5/SHA 而非 bcrypt | **致命 → STOP** |
627
- | SQL 拼接 | f-string 拼接 SQL 语句 | **致命 → STOP** |
628
- | 命令注入 | 直接执行 shell 命令或 `shell=True` | **致命 → STOP** |
629
- | DolphinDB 拼接 | 用户输入直接进 DolphinDB 脚本 | **致命 → STOP** |
630
- | 自增 ID 暴露 | 路由参数为 `int` 类型的 ID | 高 |
631
- | 无认证接口 | 数据接口无 `Depends(get_current_user)` | 高 |
632
- | 全量返回 | 列表接口无分页限制 | 高 |
633
- | 错误回显 | `str(exception)` 返回给客户端 | 高 |
634
- | 文件名不安全 | 使用用户原始文件名做存储路径 | 高 |
635
- | 物理删除 | `session.delete()` 用于业务数据 | 中 |
636
- | Redis 无 TTL | `redis.set()` 没有 `ex` 参数 | 中 |