docs(course): 建立Agent学习路线并新增第一课
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
# 第1课:Django项目配置与开发/生产环境拆分
|
||||
|
||||
## 本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 解释Django设置模块(settings module)如何被加载。
|
||||
2. 将单一`settings.py`拆成公共、开发和生产三套配置。
|
||||
3. 使用`DJANGO_SETTINGS_MODULE`选择运行环境。
|
||||
4. 将生产密钥和域名移出Git仓库。
|
||||
5. 使用Django部署检查发现危险配置。
|
||||
|
||||
本课只学习配置拆分,不接入PostgreSQL、Celery或Agents SDK。
|
||||
|
||||
## 前置知识
|
||||
|
||||
- 能运行`python manage.py check`和`python manage.py runserver`。
|
||||
- 理解Python模块和`import`。
|
||||
- 了解Spring Boot的Profile和`application-{profile}.yml`。
|
||||
|
||||
## 在JobRadar架构中的位置
|
||||
|
||||
JobRadar最终部署到公网,并需要数据库密码、模型API Key、招聘网站凭据等敏感配置。如果开发与生产共用一个`settings.py`,很容易出现以下问题:
|
||||
|
||||
- 将开发环境的`DEBUG = True`带到公网。
|
||||
- 将生产密钥提交到Git。
|
||||
- 本地SQLite和生产PostgreSQL配置互相覆盖。
|
||||
- 本地地址、正式域名和HTTPS策略混在一起。
|
||||
- Agent运行时读取了错误的模型或密钥。
|
||||
|
||||
因此,安全配置是用户系统和Agent功能之前的工程基线。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 1. settings本质上是Python模块
|
||||
|
||||
Django设置不是特殊配置格式,而是包含模块级变量的Python模块。例如:
|
||||
|
||||
```python
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ["jobradar.example.com"]
|
||||
```
|
||||
|
||||
Django启动时读取`DJANGO_SETTINGS_MODULE`,然后导入该变量指定的Python模块:
|
||||
|
||||
```text
|
||||
JobRadar.settings.development
|
||||
JobRadar.settings.production
|
||||
```
|
||||
|
||||
点号是Python包路径,不是文件系统斜杠。
|
||||
|
||||
### 2. 公共配置与环境差异分离
|
||||
|
||||
本课采用三层结构:
|
||||
|
||||
```text
|
||||
JobRadar/
|
||||
└── settings/
|
||||
├── __init__.py
|
||||
├── base.py
|
||||
├── development.py
|
||||
└── production.py
|
||||
```
|
||||
|
||||
- `base.py`:应用、Middleware、模板、国际化等公共配置。
|
||||
- `development.py`:开发密钥、`DEBUG = True`和本地地址。
|
||||
- `production.py`:生产密钥、正式域名、HTTPS和安全Cookie。
|
||||
|
||||
`development.py`和`production.py`通过`from .base import *`继承公共配置。这种星号导入在普通业务代码中不推荐,但Django分层settings是一个边界明确的配置场景。
|
||||
|
||||
### 3. 与Spring Boot Profile对照
|
||||
|
||||
| Spring Boot | Django本课方案 |
|
||||
| --- | --- |
|
||||
| `application.yml` | `settings/base.py` |
|
||||
| `application-dev.yml` | `settings/development.py` |
|
||||
| `application-prod.yml` | `settings/production.py` |
|
||||
| `spring.profiles.active=dev` | `DJANGO_SETTINGS_MODULE=JobRadar.settings.development` |
|
||||
| `${DB_PASSWORD}` | `os.environ["DB_PASSWORD"]` |
|
||||
| `@Profile`控制Bean | Django通常在settings中切换组件配置 |
|
||||
|
||||
最大的区别是:Spring配置主要是YAML/Properties,Django settings本身就是Python代码。它更灵活,也意味着不能在里面随意写复杂业务逻辑。
|
||||
|
||||
## 完整参考代码
|
||||
|
||||
参考目录位于本课的`reference/`中:
|
||||
|
||||
```text
|
||||
reference/
|
||||
├── settings/
|
||||
│ ├── __init__.py
|
||||
│ ├── base.py
|
||||
│ ├── development.py
|
||||
│ └── production.py
|
||||
├── manage.py
|
||||
├── asgi.py
|
||||
└── wsgi.py
|
||||
```
|
||||
|
||||
这些文件用于阅读和手动输入,不能直接覆盖当前项目。你需要理解每一处路径变化后,再把对应结构写入项目。
|
||||
|
||||
## 关键代码解析
|
||||
|
||||
### `BASE_DIR`为什么多一个`parent`
|
||||
|
||||
原始`settings.py`位于:
|
||||
|
||||
```text
|
||||
JobRadar/settings.py
|
||||
```
|
||||
|
||||
拆分后的`base.py`位于:
|
||||
|
||||
```text
|
||||
JobRadar/settings/base.py
|
||||
```
|
||||
|
||||
文件多进入了一层`settings`目录,因此项目根目录改为:
|
||||
|
||||
```python
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
```
|
||||
|
||||
如果仍使用两个`parent`,SQLite和模板目录都会指向错误位置。
|
||||
|
||||
### 为什么生产配置使用`os.environ[名称]`
|
||||
|
||||
```python
|
||||
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
|
||||
```
|
||||
|
||||
方括号读取在变量缺失时立即抛出`KeyError`,让生产进程启动失败。相比提供一个不安全默认值,这种“快速失败”(fail fast)更安全。
|
||||
|
||||
开发配置可以使用明确标记为仅限本地的默认密钥,但生产配置绝不能提供默认生产密钥。
|
||||
|
||||
### 为什么入口的默认环境不同
|
||||
|
||||
- `manage.py`默认开发配置,方便本地运行。
|
||||
- `asgi.py`和`wsgi.py`默认生产配置,降低部署时误启用开发设置的风险。
|
||||
- 命令行仍可通过`--settings`显式覆盖,便于检查两个环境。
|
||||
|
||||
## 手动实现步骤
|
||||
|
||||
1. 在`JobRadar`包内创建`settings`目录及`__init__.py`。
|
||||
2. 参考`base.py`移动原`settings.py`的公共配置,并修正`BASE_DIR`。
|
||||
3. 编写`development.py`和`production.py`。
|
||||
4. 修改项目根目录的`manage.py`,默认指向开发配置。
|
||||
5. 修改`JobRadar/asgi.py`和`JobRadar/wsgi.py`,默认指向生产配置。
|
||||
6. 删除旧`JobRadar/settings.py`前,逐项确认内容已经迁移。
|
||||
7. 分别运行开发和生产配置检查。
|
||||
|
||||
不要直接复制整个`reference`目录覆盖项目,因为参考文件的层级和真实文件位置并不完全相同。
|
||||
|
||||
## 运行方法
|
||||
|
||||
### 检查开发配置
|
||||
|
||||
```powershell
|
||||
conda activate JobRadar
|
||||
python manage.py check --settings=JobRadar.settings.development
|
||||
```
|
||||
|
||||
预期结果:
|
||||
|
||||
```text
|
||||
System check identified no issues (0 silenced).
|
||||
```
|
||||
|
||||
### 检查生产配置的缺失密钥保护
|
||||
|
||||
先确保当前PowerShell没有设置课程变量:
|
||||
|
||||
```powershell
|
||||
Remove-Item Env:DJANGO_SECRET_KEY -ErrorAction SilentlyContinue
|
||||
python manage.py check --settings=JobRadar.settings.production
|
||||
```
|
||||
|
||||
预期结果:命令失败,并明确提示缺少`DJANGO_SECRET_KEY`。这是安全保护生效,不是课程代码错误。
|
||||
|
||||
### 临时设置生产变量并检查
|
||||
|
||||
以下值只在当前PowerShell进程有效:
|
||||
|
||||
```powershell
|
||||
$env:DJANGO_SECRET_KEY = "仅用于本地检查的长随机字符串-请勿用于生产"
|
||||
$env:DJANGO_ALLOWED_HOSTS = "jobradar.example.com"
|
||||
$env:DJANGO_CSRF_TRUSTED_ORIGINS = "https://jobradar.example.com"
|
||||
python manage.py check --settings=JobRadar.settings.production
|
||||
python manage.py check --deploy --settings=JobRadar.settings.production
|
||||
```
|
||||
|
||||
普通`check`应通过。`check --deploy`可能继续提示HSTS时长等部署建议;本课重点是能区分错误和安全告警,不要求为了消除告警而盲目开启尚未理解的配置。
|
||||
|
||||
检查完成后清理临时变量:
|
||||
|
||||
```powershell
|
||||
Remove-Item Env:DJANGO_SECRET_KEY
|
||||
Remove-Item Env:DJANGO_ALLOWED_HOSTS
|
||||
Remove-Item Env:DJANGO_CSRF_TRUSTED_ORIGINS
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 错误1:`No module named 'JobRadar.settings.development'`
|
||||
|
||||
原因通常是:
|
||||
|
||||
- 没有创建`settings/__init__.py`。
|
||||
- 包或模块名称拼错。
|
||||
- 旧`settings.py`仍存在,导致目录结构未正确调整。
|
||||
|
||||
### 错误2:SQLite文件出现在`JobRadar/`包内
|
||||
|
||||
原因是拆分后没有为`BASE_DIR`增加一层`parent`。
|
||||
|
||||
### 错误3:生产检查仍然使用开发配置
|
||||
|
||||
先查看命令是否传入:
|
||||
|
||||
```text
|
||||
--settings=JobRadar.settings.production
|
||||
```
|
||||
|
||||
也可以运行:
|
||||
|
||||
```powershell
|
||||
python manage.py diffsettings --settings=JobRadar.settings.production
|
||||
```
|
||||
|
||||
### 错误4:把生产密钥写入`production.py`
|
||||
|
||||
生产密钥必须由服务器部署环境注入。Git中的示例只能写变量名和虚假示例,不能包含真实值。
|
||||
|
||||
### 错误5:`ALLOWED_HOSTS = ["*"]`
|
||||
|
||||
星号允许任意Host,失去了Django主机头校验的主要意义。公网项目应填写明确域名;本地开发单独允许`127.0.0.1`和`localhost`。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
练习要求见[practice.py](practice.py)。该文件只有题目和验收标准,你的实际代码应写入JobRadar项目配置文件中。
|
||||
|
||||
## 自检清单
|
||||
|
||||
- [ ] 能解释`DJANGO_SETTINGS_MODULE`的作用。
|
||||
- [ ] 能说明`base.py`中为什么使用三个`parent`。
|
||||
- [ ] 开发环境默认开启`DEBUG`,生产环境固定关闭。
|
||||
- [ ] 生产环境缺少密钥时会快速失败。
|
||||
- [ ] 生产域名和CSRF可信来源来自外部配置。
|
||||
- [ ] ASGI/WSGI默认指向生产配置。
|
||||
- [ ] Git差异中没有真实密钥。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. `python manage.py check --settings=JobRadar.settings.development`通过。
|
||||
2. 缺少`DJANGO_SECRET_KEY`时,生产配置检查按预期失败。
|
||||
3. 临时提供生产变量后,普通生产配置检查通过。
|
||||
4. `python manage.py makemigrations --check --dry-run --settings=JobRadar.settings.development`显示无遗漏迁移。
|
||||
5. `git diff --check`通过。
|
||||
6. `git diff`中没有真实密钥、Cookie或密码。
|
||||
7. 学习者能解释开发和生产入口为何采用不同默认settings。
|
||||
|
||||
## 本课小结
|
||||
|
||||
本课没有增加业务功能,但建立了公网Agent项目的安全配置基础。Django通过`DJANGO_SETTINGS_MODULE`选择一个Python配置模块;公共设置放入`base.py`,开发和生产只覆盖差异。生产环境应关闭`DEBUG`、限制域名、启用HTTPS相关设置,并在缺少密钥时拒绝启动。
|
||||
|
||||
完成手动实现并通过验收后,下一课进入“Django用户系统与用户数据归属”。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 第1课练习:Django项目配置与开发/生产环境拆分
|
||||
#
|
||||
# 练习目标:
|
||||
# 1. 将当前单文件settings.py拆分为base、development和production三个配置模块。
|
||||
# 2. 让本地管理命令默认使用开发配置,让ASGI和WSGI默认使用生产配置。
|
||||
# 3. 验证生产密钥缺失时快速失败,提供临时变量后能够通过普通系统检查。
|
||||
#
|
||||
# 操作要求:
|
||||
# 1. 在JobRadar包内创建settings目录和__init__.py。
|
||||
# 2. 把公共配置迁移到base.py,并根据新目录层级修正BASE_DIR。
|
||||
# 3. 在development.py中配置仅限本地使用的SECRET_KEY、DEBUG和ALLOWED_HOSTS。
|
||||
# 4. 在production.py中从外部读取DJANGO_SECRET_KEY、DJANGO_ALLOWED_HOSTS和
|
||||
# DJANGO_CSRF_TRUSTED_ORIGINS,固定关闭DEBUG并配置HTTPS安全项。
|
||||
# 5. 调整manage.py、asgi.py和wsgi.py使用正确的默认配置模块。
|
||||
# 6. 确认全部配置迁移后再删除旧settings.py。
|
||||
#
|
||||
# 预期结果:
|
||||
# 1. 开发配置执行Django系统检查时通过。
|
||||
# 2. 未设置DJANGO_SECRET_KEY时,生产配置检查明确失败。
|
||||
# 3. 设置三个临时生产变量后,生产配置普通检查通过。
|
||||
# 4. 项目根目录仍是BASE_DIR,SQLite和templates路径没有移动到JobRadar包中。
|
||||
# 5. Git变更中不存在真实密钥、密码或Cookie。
|
||||
#
|
||||
# 自检问题:
|
||||
# 1. DJANGO_SETTINGS_MODULE保存的是文件路径还是Python模块路径?
|
||||
# 2. 为什么生产配置不应该为SECRET_KEY提供默认值?
|
||||
# 3. 为什么DEBUG=False时必须正确设置ALLOWED_HOSTS?
|
||||
# 4. 为什么manage.py和ASGI/WSGI选择不同的默认配置?
|
||||
# 5. 为什么本课暂时不配置PostgreSQL和Agents SDK?
|
||||
#
|
||||
# 验收命令:
|
||||
# python manage.py check --settings=JobRadar.settings.development
|
||||
# python manage.py check --settings=JobRadar.settings.production
|
||||
# python manage.py check --deploy --settings=JobRadar.settings.production
|
||||
# python manage.py makemigrations --check --dry-run --settings=JobRadar.settings.development
|
||||
# git diff --check
|
||||
#
|
||||
# 验收标准:
|
||||
# 1. 能解释三个配置文件各自负责什么。
|
||||
# 2. 能解释BASE_DIR层级变化。
|
||||
# 3. 能复现生产密钥缺失时的失败,并确认这是预期安全行为。
|
||||
# 4. 能区分普通系统检查错误与部署安全告警。
|
||||
# 5. practice.py保持纯注释,不在此文件填写答案或运行记录。
|
||||
@@ -0,0 +1,14 @@
|
||||
"""JobRadar生产ASGI入口的参考实现。"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
|
||||
# 部署入口默认选择生产配置,避免遗漏环境选择时启用DEBUG。
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
"JobRadar.settings.production",
|
||||
)
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
"""JobRadar本地管理命令入口的参考实现。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""默认使用开发配置执行Django管理命令。"""
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
"JobRadar.settings.development",
|
||||
)
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"无法导入Django,请确认已经激活JobRadar Conda环境。"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
"""JobRadar分环境配置包的参考入口。"""
|
||||
@@ -0,0 +1,77 @@
|
||||
"""JobRadar所有运行环境共享的Django配置参考。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# base.py比原settings.py多位于一层settings目录中,因此需要向上三级到达项目根目录。
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "JobRadar.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "JobRadar.wsgi.application"
|
||||
ASGI_APPLICATION = "JobRadar.asgi.application"
|
||||
|
||||
# 第一课保留SQLite,下一阶段学习PostgreSQL时再调整数据库配置。
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "zh-hans"
|
||||
TIME_ZONE = "Asia/Shanghai"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
@@ -0,0 +1,16 @@
|
||||
"""JobRadar本地开发环境的Django配置参考。"""
|
||||
|
||||
import os
|
||||
|
||||
from .base import * # noqa: F403
|
||||
|
||||
|
||||
# 默认值只能用于本机开发,生产环境不会导入本模块。
|
||||
SECRET_KEY = os.getenv(
|
||||
"DJANGO_SECRET_KEY",
|
||||
"django-insecure-jobradar-local-development-only",
|
||||
)
|
||||
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""JobRadar公网生产环境的Django配置参考。"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .base import * # noqa: F403
|
||||
|
||||
|
||||
def get_required_environment_value(name: str) -> str:
|
||||
"""读取必需的生产变量,缺失或空白时立即阻止应用启动。"""
|
||||
value = os.getenv(name, "").strip()
|
||||
if not value:
|
||||
raise ImproperlyConfigured(f"缺少必需的环境变量:{name}")
|
||||
return value
|
||||
|
||||
|
||||
def get_required_environment_list(name: str) -> list[str]:
|
||||
"""读取逗号分隔的必需列表,并去除每一项两侧的空白。"""
|
||||
raw_value = get_required_environment_value(name)
|
||||
values = [item.strip() for item in raw_value.split(",") if item.strip()]
|
||||
if not values:
|
||||
raise ImproperlyConfigured(f"环境变量没有有效配置项:{name}")
|
||||
return values
|
||||
|
||||
|
||||
SECRET_KEY = get_required_environment_value("DJANGO_SECRET_KEY")
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = get_required_environment_list("DJANGO_ALLOWED_HOSTS")
|
||||
CSRF_TRUSTED_ORIGINS = get_required_environment_list(
|
||||
"DJANGO_CSRF_TRUSTED_ORIGINS"
|
||||
)
|
||||
|
||||
# HTTPS在反向代理终止时,Django通过该请求头识别原始请求协议。
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
@@ -0,0 +1,14 @@
|
||||
"""JobRadar生产WSGI入口的参考实现。"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
|
||||
# 部署入口默认选择生产配置,避免遗漏环境选择时启用DEBUG。
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
"JobRadar.settings.production",
|
||||
)
|
||||
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user