签入版本

This commit is contained in:
2026-09-12 23:12:15 +08:00
commit 91998056ca
1841 changed files with 341325 additions and 0 deletions
@@ -0,0 +1,276 @@
:::tip 本章概要
登录 -> Token 获取 -> 权限校验 -> Token 刷新 -> 退出登录的完整认证链路实战说明。
:::
# 认证流程实战
本章将登录 -> Token 获取 -> 权限校验 -> Token 刷新 -> 退出登录的完整认证链路串联说明,帮助开发者理解端到端的认证机制。
## 认证架构总览
```
+----------+ 1获取验证码 +----------+
| | -----------------> | |
| 前端 | | 后端 |
| Vue3 | 2登录(账号+密码+验证码) | Django |
| | -----------------> | |
| | 3返回 Token | |
| | <------------------ | |
| | | |
| | 4携带 Token 请求 | |
| | -----------------> | |
| | 5返回业务数据 | |
| | <------------------ | |
| | | |
| | 6刷新 Token | |
| | -----------------> | |
| | 7返回新 Token | |
| | <------------------ | |
| | | |
| | 8退出登录 | |
| | -----------------> | |
+----------+ +----------+
```
## 第 1 步:获取验证码
登录前需先获取图形验证码,防止暴力破解。
### 请求
```
GET /login/captcha
```
### 响应
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"captcha": "data:image/png;base64,iVBORw0KGgo...",
"key": 1
},
"ok": true
}
```
- `captcha`:Base64 编码的验证码图片,前端直接赋值给 `<img>` 的 `src`
- `key`:验证码记录 ID,登录时需回传
### 后端实现
```python
# application/login/views.py
from captcha.models import CaptchaStore
from captcha.views import captcha_image
class CaptchaView(View):
def get(self, request):
# 生成验证码密钥
hashkey = CaptchaStore.generate_key()
id_key = CaptchaStore.objects.filter(hashkey=hashkey).first().id
# 生成验证码图片并转 Base64
image = captcha_image(request, hashkey)
base64_image = base64.b64encode(image.content)
img_str = "data:image/png;base64," + base64_image.decode("utf-8")
return R.ok(idkey=id_key, data={"captcha": img_str, "key": id_key})
```
验证码通过 django-simple-captcha 生成,存储在数据库中。
## 第 2 步:用户登录
携带账号、密码、验证码和 Key 发起登录请求。
### 请求
```
POST /login/login
Content-Type: application/json
{
"username": "admin",
"password": "123456",
"code": "a3Kp",
"key": 1
}
```
### 成功响应
```json
{
"code": 0,
"msg": "登录成功",
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"must_change_password": false
},
"ok": true
}
```
- `access_token`:JWT 令牌,后续请求需携带
- `must_change_password`:是否需要修改密码
### 登录流程详解
```
1. 从请求体获取 username、password、code、key
2. 验证码校验(比对数据库中的验证码记录)
- 不正确 -> 返回失败
3. 查询用户(username + status=1)
- 不存在 -> 返回失败
4. 比对密码(bcrypt 双重加盐验证)
- 不匹配 -> 返回失败
5. 登录成功
- 生成 JWT Token
- 记录登录日志
- 返回 Token
```
## 第 3 步:携带 Token 请求业务接口
登录成功后,前端将 Token 存入 localStorage,后续所有请求自动携带。
### 请求头格式
```
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
```
### Token 解析流程
```
请求进入 -> check_login 装饰器
+-- 提取 Authorization 头(request.META['HTTP_AUTHORIZATION'])
+-- 移除 "Bearer " 前缀
+-- parse_payload() 解析 JWT Token
| +-- 验证签名(HS256 + JWT_SALT)
| +-- 验证过期时间
| +-- 检查黑名单(Redis)
+-- 验证通过 -> 执行原视图函数
+-- 验证失败 -> R.failed(code=401, msg=...)
```
### check_login 装饰器实现
```python
# middleware/login_middleware.py
def check_login(func):
def wrapper(request, *args, **kwargs):
ignoreURL = ['/login', '/captcha']
if request.path not in ignoreURL:
access_token = request.headers['Authorization']
access_token = access_token.replace('Bearer ', "")
result = parse_payload(access_token)
if result['code'] != 0:
return R.failed(code=401, msg=result['msg'])
return func(request, *args, **kwargs)
return wrapper
```
## 第 4 步:权限校验
每个业务接口通过 `PermissionRequired` 混入类校验权限。
### 权限校验流程
```
请求到达 -> PermissionRequired.has_permission()
+-- 用户 ID = 1(admin)-> 跳过权限校验,直接放行
+-- 其他用户
+-- 查询用户权限列表(menu.services.get_user_permissions)
+-- 匹配权限字符串
| +-- 匹配 -> 放行
| +-- 不匹配 -> 返回 "暂无操作权限"
```
### 权限字符串格式
```
sys:{module}:{action}
```
示例:`sys:user:add`、`sys:role:page`、`sys:article:delete`
## 第 5 步:Token 刷新
Token 支持无感刷新,刷新时生成新令牌。
### 刷新请求
```
POST /login/refreshToken
Authorization: Bearer <old_token>
```
### 刷新流程
```
1. 验证原令牌有效性
2. 移除 JWT 标准字段(exp/iat/nbf),保留业务数据
3. 生成新令牌
4. 返回新令牌
```
## 第 6 步:退出登录
退出时将当前 Token 加入 Redis 黑名单,使其立即失效。
### 请求
```
GET /login/logout
Authorization: Bearer <token>
```
### 响应
```json
{
"code": 0,
"msg": "注销成功",
"data": null,
"ok": true
}
```
### 退出流程
```
1. 提取当前 Token
2. 计算 Token 的 SHA256 指纹
3. 存入 Redis 黑名单(TTL = Token 剩余有效期)
4. 记录退出日志
5. 返回成功
```
### Token 黑名单机制
- **存储方式**:Redis,Key 格式 `token:blacklist:{sha256_fingerprint}`
- **过期策略**:TTL 与 Token 剩余有效期一致,自动清理
- **故障降级**:Redis 不可用时 fail-open 放行(避免全站不可用)
## 配置项
| 环境变量 | 默认值 | 说明 |
|:----|:----|:----|
| `JWT_SALT` | 内置默认密钥 | JWT 签名密钥,建议至少 32 字节 |
| `DEFAULT_TIMEOUT_MINUTES` | `20` | Token 过期时间(分钟) |
## 总结
认证流程覆盖了从获取验证码到退出登录的完整链路,核心安全机制包括:
- bcrypt 密码哈希 + 随机盐
- JWT HS256 签名 + 过期校验
- django-simple-captcha 验证码防暴力破解
- Token 黑名单即时失效
- Redis 不可用时 fail-open 降级
@@ -0,0 +1,132 @@
:::tip 验证码
验证码是防止自动化攻击的重要安全手段。项目使用 django-simple-captcha 库生成图片验证码,存储在数据库中,支持多种验证码格式。源码位于 `application/login/views.py`。
:::
## 验证码流程
```
1. 前端请求 GET /login/captcha
2. 后端通过 CaptchaStore.generate_key() 生成验证码
3. captcha_image() 生成图片 -> Base64 编码
4. 返回 {key, captcha} 给前端
5. 前端展示图片,用户输入验证码
6. 登录时后端通过 CaptchaStore 比对验证码
```
## 接口说明
### 获取验证码
```
GET /login/captcha
```
响应示例:
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"captcha": "data:image/png;base64,iVBORw0KGgo...",
"key": 1
},
"ok": true
}
```
### 后端实现
```python
# application/login/views.py
from captcha.models import CaptchaStore
from captcha.views import captcha_image
class CaptchaView(View):
def get(self, request):
# 生成验证码密钥
hashkey = CaptchaStore.generate_key()
# 从数据库中获取生成的验证码记录ID
id_key = CaptchaStore.objects.filter(hashkey=hashkey).first().id
# 生成验证码图片
image = captcha_image(request, hashkey)
# 将图片内容转换为Base64编码
base64_image = base64.b64encode(image.content)
img_str = "data:image/png;base64," + base64_image.decode("utf-8")
# 返回验证码图片和密钥
return R.ok(idkey=id_key, data={"captcha": img_str, "key": id_key})
```
### 验证码校验
验证码校验在登录流程中自动完成,通过 django-simple-captcha 内置的表单验证机制:
```python
# application/login/forms.py
from captcha.fields import CaptchaField
class LoginForm(forms.Form):
username = forms.CharField(max_length=30)
password = forms.CharField(max_length=128)
captcha = CaptchaField() # 自动校验验证码
```
:::warning 验证码字段
`CaptchaField` 会自动校验用户输入的验证码是否与数据库中存储的一致,验证成功后自动删除(防重放)。
:::
## 配置项
在 `settings.py` 中配置验证码相关参数:
```python
# application/settings.py
INSTALLED_APPS = [
...
'captcha',
]
# 验证码配置
CAPTCHA_IMAGE_SIZE = (160, 50) # 图片尺寸
CAPTCHA_FONT_SIZE = 28 # 字体大小
CAPTCHA_LETTER_ROTATION = (-35, 35) # 字符旋转角度
CAPTCHA_NOISE_LINES = 6 # 干扰线数量
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 随机字符
CAPTCHA_LENGTH = 4 # 验证码长度
CAPTCHA_TIMEOUT = 5 # 验证码有效期(分钟)
```
## 前端集成
```javascript
// 获取验证码
async function getCaptcha() {
const res = await axios.get('/login/captcha')
captchaKey.value = res.data.data.key
captchaImg.value = res.data.data.captcha
}
// 登录时提交
const loginData = {
username: 'admin',
password: '123456',
code: userInput.value,
key: captchaKey.value
}
```
## 总结
验证码方案具备以下特点:
```
1. django-simple-captcha:成熟的 Django 验证码库,支持多种格式
2. 数据库存储:验证码存储在数据库中,支持过期清理
3. 自动校验:CaptchaField 内置校验逻辑,验证成功后自动删除
4. Base64 传输:图片转 Base64 编码,无需额外的图片访问接口
5. 防重放:验证码一次性使用,验证成功后立即失效
```
@@ -0,0 +1,187 @@
:::tip 系统配置
系统配置模块用于管理动态的键值对配置项,与数据字典类似但更偏向系统级参数。支持分组管理,可通过接口动态修改,无需重启服务。模块位于 `application/config/`。
:::
## 模块结构
```
application/config/
├── models.py # 配置模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
application/config_item/
├── models.py # 配置项模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
```
## 配置模型
### 配置分组(Config)
```python
# application/config/models.py
class Config(BaseModel):
"""配置模型类"""
class Meta:
db_table = 'django_config'
name = models.CharField(max_length=150, verbose_name='配置名称')
code = models.CharField(max_length=150, verbose_name='配置编码')
sort = models.IntegerField(default=0, verbose_name='配置排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='配置备注')
```
### 配置项(ConfigItem)
```python
# application/config_item/models.py
class ConfigItem(BaseModel):
"""配置项模型类"""
class Meta:
db_table = 'django_config_item'
name = models.CharField(max_length=150, verbose_name='配置项名称')
code = models.CharField(max_length=150, verbose_name='配置项编码')
value = models.CharField(max_length=1000, null=True, blank=True, verbose_name='配置项值')
options = models.TextField(null=True, blank=True, verbose_name='配置选项')
config_id = models.IntegerField(default=0, verbose_name='配置ID')
type = models.CharField(max_length=150, verbose_name='配置类型')
status = models.IntegerField(default=1, verbose_name='配置状态:1-正常 2-停用')
sort = models.IntegerField(default=0, verbose_name='配置项顺序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='配置项备注')
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | str | 配置项名称 |
| `code` | str | 配置项编码(唯一标识) |
| `value` | str | 配置项值 |
| `options` | str | 配置选项(JSON 格式,用于下拉选择) |
| `config_id` | int | 所属配置分组 ID |
| `type` | str | 配置类型(text/number/select/switch) |
| `status` | int | 状态:1-正常 2-停用 |
| `sort` | int | 排序权重 |
## 使用场景
系统配置适用于以下场景:
```
1. 网站基本信息(站点名称、Logo、版权信息)
2. 邮件配置(SMTP 服务器、端口、账号)
3. 短信配置(API Key、模板 ID)
4. 存储配置(OSS Bucket、上传限制)
5. 业务参数(默认密码、分页大小等)
```
## 配置类型
| 类型 | 说明 | 示例 |
|------|------|------|
| `text` | 文本输入 | 站点名称、SMTP 服务器 |
| `number` | 数字输入 | 端口号、超时时间 |
| `select` | 下拉选择 | 存储类型(本地/OSS/S3) |
| `switch` | 开关切换 | 是否开启注册、是否开启验证码 |
## 网站配置
项目内置了网站配置模块,用于管理网站基本信息:
```python
# application/config_web/models.py
class ConfigWeb(BaseModel):
"""网站配置模型类"""
class Meta:
db_table = 'django_config_web'
name = models.CharField(max_length=150, verbose_name='网站名称')
logo = models.CharField(max_length=255, null=True, blank=True, verbose_name='网站Logo')
copyright = models.CharField(max_length=255, null=True, blank=True, verbose_name='版权信息')
record_number = models.CharField(max_length=255, null=True, blank=True, verbose_name='备案号')
# ... 其他字段
```
## API 接口
### 配置分组
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/config/page` | GET | `sys:config:page` | 配置分页列表 |
| `/config/detail/<int:id>` | GET | `sys:config:detail` | 配置详情 |
| `/config/add` | POST | `sys:config:add` | 新增配置 |
| `/config/update` | PUT | `sys:config:update` | 编辑配置 |
| `/config/delete/<int:id>` | DELETE | `sys:config:delete` | 删除配置 |
### 配置项
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/config/item/page` | GET | `sys:configItem:page` | 配置项分页列表 |
| `/config/item/detail/<int:id>` | GET | `sys:configItem:detail` | 配置项详情 |
| `/config/item/add` | POST | `sys:configItem:add` | 新增配置项 |
| `/config/item/update` | PUT | `sys:configItem:update` | 编辑配置项 |
| `/config/item/delete/<int:id>` | DELETE | `sys:configItem:delete` | 删除配置项 |
| `/config/item/status` | PUT | `sys:configItem:status` | 设置状态 |
### 网站配置
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/config/web/detail` | GET | 需登录 | 获取网站配置 |
| `/config/web/update` | PUT | `sys:configWeb:update` | 更新网站配置 |
## 与数据字典的区别
| 特性 | 数据字典 | 系统配置 |
|------|---------|---------|
| 用途 | 枚举值(下拉选项) | 系统参数(配置值) |
| 值类型 | 简单值(字符串) | 可存储较长的配置内容 |
| 典型场景 | 状态、性别、类型 | SMTP配置、站点信息 |
| 前端用法 | el-select 选项 | 配置表单 |
| 缓存策略 | Redis 缓存 | 按需查询 |
| 数据量 | 通常每个字典 < 50 项 | 每个分组 < 20 项 |
## 前端使用
### 配置项表单
配置项根据 `type` 字段动态渲染不同表单组件:
```vue
<template>
<el-form-item :label="item.name">
<!-- 文本输入 -->
<el-input v-if="item.type === 'text'" v-model="item.value" />
<!-- 数字输入 -->
<el-input-number v-else-if="item.type === 'number'" v-model="item.value" />
<!-- 下拉选择 -->
<el-select v-else-if="item.type === 'select'" v-model="item.value">
<el-option v-for="opt in parseOptions(item.options)" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<!-- 开关 -->
<el-switch v-else-if="item.type === 'switch'" v-model="item.value" />
</el-form-item>
</template>
```
## 总结
系统配置模块具备以下特点:
```
1. KV 结构:配置分组 + 配置项,灵活管理各类参数
2. 动态修改:通过接口修改,无需重启服务
3. 分组管理:按功能模块分组,清晰组织配置项
4. 类型丰富:支持 text/number/select/switch 多种类型
5. 网站配置:内置网站基本信息管理
6. 与字典互补:字典管理枚举值,配置管理系统参数
```
@@ -0,0 +1,232 @@
:::tip CORS跨域
CORS(Cross-Origin Resource Sharing)跨域资源共享是前后端分离架构必须处理的问题。项目使用 `django-cors-headers` 中间件处理跨域请求,通过 `.env` 配置允许的跨域源。
:::
## 什么是跨域
浏览器的同源策略限制了不同源(协议+域名+端口)之间的请求。前后端分离架构中,前端(如 `http://localhost:8001`)请求后端(如 `http://127.0.0.1:8000`)会产生跨域问题。
```
同源请求:http://localhost:8000/api/user → http://localhost:8000/api/role ✅
跨域请求:http://localhost:8001/api/user → http://localhost:8000/api/role ❌(端口不同)
```
## CORS 预检请求
对于非简单请求(如 PUT/DELETE 或自定义 Header),浏览器会先发送 OPTIONS 预检请求:
```
浏览器 → OPTIONS /api/user → 服务器
浏览器 ← Access-Control-Allow-Origin: * ← 服务器
浏览器 → PUT /api/user → 服务器(正式请求)
```
:::tip 预检请求
`django-cors-headers` 中间件自动处理 OPTIONS 预检请求,无需手动编写代码。预检请求不会到达视图层,由中间件直接返回。
:::
## 配置项
在 `settings.py` 中配置 CORS 相关参数:
```python
# application/settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 必须在最前面
'django.middleware.common.CommonMiddleware',
...
]
# CORS 配置
CORS_ALLOW_ALL_ORIGINS = True # 开发环境允许所有源
CORS_ALLOW_CREDENTIALS = True # 允许携带凭证(Cookie/Authorization)
CORS_ALLOW_METHODS = [ # 允许的请求方法
'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT',
]
CORS_ALLOW_HEADERS = [ # 允许的请求头
'accept-encoding',
'authorization', # JWT 认证头
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'XMLHttpRequest',
'X_FILENAME',
'Pragma',
]
```
## 环境配置
```bash
# .env
# 开发环境:允许所有源
CORS_ALLOW_ALL_ORIGINS=True
# 生产环境:指定域名
# CORS_ALLOW_ALL_ORIGINS=False
# CORS_ALLOWED_ORIGINS=https://admin.example.com,https://www.example.com
```
:::warning 生产环境
生产环境务必设置 `CORS_ALLOW_ALL_ORIGINS=False` 并配置 `CORS_ALLOWED_ORIGINS` 为具体域名,避免允许所有源带来的安全风险。
:::
## 生产环境配置
### 多域名配置
```python
# 生产环境:指定多个允许的域名
CORS_ALLOWED_ORIGINS = [
"https://admin.example.com",
"https://www.example.com",
"https://m.example.com",
]
```
### 正则匹配
```python
# 允许所有子域名
CORS_ALLOWED_ORIGIN_REGEXES = [
r"^https://.*\.example\.com$",
]
```
### 禁止凭证
如果不需要携带 Cookie,可以关闭凭证以提高安全性:
```python
CORS_ALLOW_CREDENTIALS = False # 不允许携带凭证
```
## 中间件顺序
CORS 中间件必须在 `MIDDLEWARE` 列表中**最前面**注册:
```python
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # ← 必须在最前面
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
```
:::warning 顺序重要
`CorsMiddleware` 必须在所有中间件之前,特别是要在 `CommonMiddleware` 之前。否则 OPTIONS 预检请求可能被 `CommonMiddleware` 拦截,导致 CORS 头未正确添加。
:::
## 前端代理配置
开发环境通过 Vite 代理解决跨域问题(无需 CORS):
```javascript
// ui/vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
}
}
}
```
:::tip 开发环境 vs 生产环境
- **开发环境**:Vite 代理将 `/api` 请求转发到后端,浏览器视为同源请求,不触发 CORS
- **生产环境**:Nginx 统一处理前端静态资源和 API 转发,同样不触发 CORS
- **跨域场景**:当前端和后端部署在不同域名时,才需要 CORS 配置
:::
## 常见问题排查
### 问题 1:请求被 CORS 拦截
**现象**:浏览器控制台报 `Access-Control-Allow-Origin` 错误
**排查**:
```bash
# 1. 检查 CORS 中间件是否在最前面
grep -n "CorsMiddleware" application/settings.py
# 2. 检查 CORS 配置
grep -n "CORS_ALLOW" application/settings.py
# 3. 检查 .env 中的配置
cat .env | grep CORS
```
### 问题 2:OPTIONS 预检请求返回 403
**现象**:OPTIONS 请求被权限中间件拦截
**排查**:
```bash
# 检查 check_login 是否排除了 OPTIONS 请求
# middleware/login_middleware.py 中应放行 OPTIONS
```
### 问题 3:携带自定义 Header 失败
**现象**:自定义 Header(如 `X-Token`)被拒绝
**解决**:
```python
# 在 CORS_ALLOW_HEADERS 中添加自定义 Header
CORS_ALLOW_HEADERS = [
...
'x-token',
'custom-header',
]
```
### 问题 4:Cookie 未传递
**现象**:跨域请求中 Cookie 未携带
**解决**:
```python
# 1. 后端设置
CORS_ALLOW_CREDENTIALS = True
# 2. 前端设置(Axios)
axios.defaults.withCredentials = true
# 3. 注意:Allow-Credentials 为 true 时,Allow-Origin 不能为 *
```
## 安全建议
```
1. 生产环境禁止 CORS_ALLOW_ALL_ORIGINS = True
2. 只允许必要的域名和请求方法
3. 谨慎使用 CORS_ALLOW_CREDENTIALS = True
4. 定期审查 CORS 配置,移除不再使用的域名
5. 避免在 CORS_ALLOW_HEADERS 中使用通配符 *
```
## 总结
CORS 跨域配置具备以下特点:
```
1. django-cors-headers:成熟的 Django CORS 解决方案
2. 环境变量配置:通过 .env 灵活配置允许的源
3. 开发/生产分离:开发环境允许所有源,生产环境指定域名
4. 中间件顺序:必须在 CommonMiddleware 之前
5. 前端代理:开发环境通过 Vite 代理解决跨域
6. 预检处理:自动处理 OPTIONS 预检请求
7. 安全可控:支持域名白名单、正则匹配、凭证控制
```
@@ -0,0 +1,150 @@
:::tip 演示模式
演示模式用于在线体验环境,开启后所有写操作(新增、编辑、删除等)将被拦截,返回"演示环境,暂无操作权限"提示。通过 `DJANGO_DEMO` 环境变量控制。
:::
## 配置项
| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `DJANGO_DEMO` | `False` | 是否开启演示模式 |
```bash
# .env
# 开启演示模式
DJANGO_DEMO=True
# 关闭演示模式(生产环境)
DJANGO_DEMO=False
```
## 实现原理
演示模式采用**视图层检查**的方式实现,在每个写操作的视图方法开头判断配置:
```python
# config/env.py
import os
DJANGO_DEMO = (os.getenv('DJANGO_DEMO', 'False') == 'True')
```
视图中通过导入 `DJANGO_DEMO` 变量进行判断:
```python
# application/user/views.py
from config.env import DJANGO_DEMO
class UserAddView(PermissionRequired, View):
permission_required = ("sys:user:add",)
def post(self, request):
# 演示环境禁止操作
if DJANGO_DEMO:
return R.failed("演示环境,暂无操作权限")
result = services.add_user(request)
return result
```
:::warning 为什么不用装饰器?
演示模式没有使用装饰器实现,原因是:不同模块对"写操作"的定义略有差异(有的模块有导入/导出,有的有授权操作),在视图层直接判断更灵活,可以精确控制每个接口的行为。
:::
## 受保护的操作
演示模式拦截以下操作(写操作):
```
- 新增(add)
- 编辑(update)
- 删除(delete / batchDelete)
- 状态设置(status)
- 重置密码(resetPwd)
- 修改密码(updatePwd)
- 导入(import)
- 授权(auth / assignMenu)
- 保存(save)
```
不受影响的操作:
```
- 登录(login)
- 获取验证码(captcha)
- 查询列表(list/page)
- 查询详情(detail)
- 导出(export)
- 获取菜单(getMenus)
- 获取用户信息(getUser)
```
## 各模块演示模式配置
| 模块 | 拦截的视图 | 说明 |
|------|-----------|------|
| 用户管理 | add/update/delete/status/resetPwd | 新增/编辑/删除/状态/重置密码 |
| 角色管理 | add/update/delete/status/assignMenu | 新增/编辑/删除/状态/分配菜单 |
| 菜单管理 | add/update/delete | 新增/编辑/删除 |
| 部门管理 | add/update/delete | 新增/编辑/删除 |
| 字典管理 | add/update/delete | 新增/编辑/删除(含字典项) |
| 配置管理 | add/update/delete | 新增/编辑/删除(含配置项) |
| 文章管理 | add/update/delete/status | 新增/编辑/删除/状态 |
| 登录日志 | delete | 删除(保留查看能力) |
| 操作日志 | delete | 删除(保留查看能力) |
## 自定义演示模式提示
如需修改演示模式的提示信息,直接修改对应视图中的提示文本:
```python
# 自定义提示信息
if DJANGO_DEMO:
return R.failed("当前为演示环境,如需操作请联系管理员")
```
## 前端配合处理
前端可通过响应中的 `msg` 字段识别演示模式拦截,并展示相应提示:
```typescript
// 前端拦截器已统一处理 code=1 的响应,自动弹出 ElMessage.error(msg)
// 无需额外处理,演示模式的提示会自动显示
```
## 适用场景
```
1. 在线体验:让用户无需注册即可体验系统功能
2. 演示环境:销售或产品演示时防止误操作
3. 公开测试:开放测试环境但保护数据安全
4. 试用评估:潜在客户试用系统但不允许修改数据
```
## 最佳实践
```
1. 开发环境:DJANGO_DEMO=False,允许所有操作
2. 测试环境:DJANGO_DEMO=False,允许所有操作
3. 演示环境:DJANGO_DEMO=True,只读模式
4. 生产环境:DJANGO_DEMO=False,通过权限控制访问
```
:::warning 注意事项
演示模式和权限控制是两套独立的机制:
- 权限控制:决定用户**是否有权限**执行某操作
- 演示模式:决定**所有用户**是否可以执行写操作
两者同时生效,即使有写权限的用户,在演示模式下也会被拦截。
:::
## 总结
演示模式具备以下特点:
```
1. 环境变量控制:DJANGO_DEMO=True 开启,False 关闭
2. 写操作拦截:在视图中检查配置,拦截所有写操作
3. 只读体验:查询操作不受影响,用户可正常浏览数据
4. 简单实现:无需装饰器,在视图函数中直接判断即可
5. 灵活配置:可按模块精确控制拦截范围
6. 与权限独立:和 RBAC 权限体系互不影响
```
@@ -0,0 +1,156 @@
:::tip 本章概要
数据字典的完整使用流程,从新增字典类型到在代码中使用字典数据的实操指南。
:::
# 数据字典使用指南
数据字典是项目的基础配置功能,用于管理枚举类型的下拉选项。本章从「如何新增字典」到「如何在代码中使用」的完整实操流程。
## 数据字典结构
数据字典由两层组成:
```
字典类型(dict) 字典项(dict_item)
+-- sys_user_status +-- 1 -> 正常
+-- sys_user_gender +-- 2 -> 停用
+-- article_type +-- 3 -> 删除
+-- ...
```
| 层级 | 表 | 说明 |
|:----|:----|:----|
| 字典类型 | `django_dict` | 字典分类(如「用户状态」「文章类型」) |
| 字典项 | `django_dict_item` | 字典的具体选项(如「1-正常」「2-停用」) |
## 实操:新增一个字典类型
### 场景
需要为「培训方式」模块添加一个下拉选项:`1-线上 2-线下 3-混合`。
### 第 1 步:在后台添加字典类型
登录管理后台,进入「系统管理 -> 数据字典」:
1. 点击「新增」按钮
2. 填写信息:
| 字段 | 值 |
|:----|:----|
| 字典名称 | 培训方式 |
| 字典编码 | training_method |
| 状态 | 正常 |
3. 保存
### 第 2 步:添加字典项
在字典类型列表中点击「培训方式」,进入字典项管理:
1. 添加字典项:
| 字典值 | 字典标签 | 排序 |
|:----|:----|:----|
| 1 | 线上 | 1 |
| 2 | 线下 | 2 |
| 3 | 混合 | 3 |
2. 保存
## 在后端代码中使用
### 方式 1:通过字典编码获取字典项
```python
from application.dict_item import services as dict_item_services
# 获取字典项列表
items = dict_item_services.get_by_dict_code('training_method')
# 返回: [{"label": "线上", "value": "1"}, {"label": "线下", "value": "2"}, {"label": "混合", "value": "3"}]
```
### 方式 2:在 service 中获取字典值翻译
```python
# 在分页查询中为记录补充字典名称
def _enrich_records(records):
status_map = dict_item_services.get_dict_map('sys_status')
for record in records:
record['statusName'] = status_map.get(str(record.get('status')), '未知')
return records
```
## 在前端代码中使用
### 方式 1:字典下拉组件
```vue
<template>
<el-select v-model="form.trainingMethod" placeholder="请选择培训方式">
<el-option
v-for="item in dictOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useDictStore } from '@/store/dict';
const dictStore = useDictStore();
const dictOptions = ref([]);
onMounted(async () => {
dictOptions.value = await dictStore.loadDict('training_method');
});
</script>
```
### 方式 2:表格列显示字典文本
在 `columns.ts` 中使用 `render` 函数:
```typescript
import { h } from 'vue';
import { ElTag } from 'element-plus';
export const columns = [
{
label: '培训方式',
prop: 'trainingMethod',
render(record) {
const map = { 1: '线上', 2: '线下', 3: '混合' };
const typeMap = { 1: 'success', 2: 'warning', 3: 'info' };
return h(ElTag, { type: typeMap[record.row.trainingMethod] }, {
default: () => map[record.row.trainingMethod] || '-'
});
},
},
];
```
## 代码生成器与字典
代码生成器会自动识别字段注释中的枚举格式(`1-线上 2-线下 3-混合`),并:
1. 自动解析 `choices` 列表
2. 自动生成 `dict_code`(格式:`{table_name}_{field_name}`)
3. 前端自动生成下拉选择框
生成后需在后台手动创建对应的字典类型和字典项。
## 最佳实践
1. **字典编码命名**:使用 `模块_字段` 格式,如 `user_status`、`article_type`
2. **字典值使用整数**:便于排序和比较
3. **字典标签简洁**:控制在 4 个字以内,适合表格列显示
4. **状态字段统一**:`1-正常 2-停用` 全项目保持一致
5. **生成后补充字典**:代码生成器创建的 `dict_code` 需在后台手动创建对应数据
## 总结
数据字典通过「字典类型 + 字典项」两层结构管理枚举选项。后端通过字典编码获取选项列表,前端通过 useDictStore 获取下拉数据。代码生成器会自动识别注释中的枚举格式并生成对应配置。
@@ -0,0 +1,182 @@
:::tip 数据字典
数据字典用于管理系统中的枚举值和配置项,如状态、类型、性别等。通过字典编码统一管理,前端可通过 `useDictStore` 获取字典选项,避免硬编码。模块位于 `application/dict/`。
:::
## 模块结构
```
application/dict/
├── models.py # 字典模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
application/dict_item/
├── models.py # 字典项模型
├── forms.py # 表单验证
├── services.py # 业务逻辑层
├── views.py # 视图层
└── urls.py # 路由配置
```
## 字典模型
### 字典类型(Dict)
```python
# application/dict/models.py
class Dict(BaseModel):
"""字典模型类"""
class Meta:
db_table = 'django_dict'
name = models.CharField(max_length=150, verbose_name='字典名称')
code = models.CharField(max_length=150, verbose_name='字典编码')
sort = models.IntegerField(default=0, verbose_name='字典排序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='字典备注')
```
### 字典项(DictItem)
```python
# application/dict_item/models.py
class DictItem(BaseModel):
"""字典项模型类"""
class Meta:
db_table = 'django_dict_item'
name = models.CharField(max_length=150, verbose_name='字典项名称')
value = models.CharField(max_length=150, verbose_name='字典项值')
dict_id = models.IntegerField(default=0, verbose_name='字典ID')
sort = models.IntegerField(default=0, verbose_name='字典项顺序')
note = models.CharField(max_length=255, null=True, blank=True, verbose_name='字典项备注')
```
## API 接口
### 字典管理
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/dict/page` | GET | `sys:dict:page` | 字典分页列表 |
| `/dict/detail/<int:id>` | GET | `sys:dict:detail` | 字典详情 |
| `/dict/add` | POST | `sys:dict:add` | 新增字典 |
| `/dict/update` | PUT | `sys:dict:update` | 编辑字典 |
| `/dict/delete/<int:id>` | DELETE | `sys:dict:delete` | 删除字典 |
### 字典项管理
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/dict/item/page` | GET | `sys:dictItem:page` | 字典项分页列表 |
| `/dict/item/detail/<int:id>` | GET | `sys:dictItem:detail` | 字典项详情 |
| `/dict/item/getDictItemList/{code}` | GET | 需登录 | 按编码获取字典项 |
| `/dict/item/add` | POST | `sys:dictItem:add` | 新增字典项 |
| `/dict/item/update` | PUT | `sys:dictItem:update` | 编辑字典项 |
| `/dict/item/delete/<int:id>` | DELETE | `sys:dictItem:delete` | 删除字典项 |
## 后端获取字典
```python
# 通过字典编码获取字典项列表
from application.dict_item import services as dict_item_services
# 获取状态字典
status_list = dict_item_services.get_by_dict_code("sys_status")
# 返回: [{"label": "正常", "value": "1"}, {"label": "停用", "value": "2"}]
```
## 前端使用
前端通过 `useDictStore` 获取字典数据:
```typescript
import { useDictStore } from '@/store/dict'
const dictStore = useDictStore()
// 获取字典选项
const statusOptions = dictStore.getDict('sys_status')
// [{label: "正常", value: "1"}, {label: "停用", "value": "2"}]
```
### 在组件中使用
```vue
<template>
<!-- el-select 下拉选择 -->
<el-select v-model="form.status" placeholder="请选择状态">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- el-tag 标签展示 -->
<el-tag v-if="form.status === '1'">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
```
### 字典缓存
前端字典数据通过 Pinia Store 缓存,首次获取后缓存在内存中:
```typescript
// store/dict.ts
export const useDictStore = defineStore('dict', {
state: () => ({
dictMap: new Map<string, any[]>(),
}),
actions: {
async getDict(code: string) {
if (this.dictMap.has(code)) {
return this.dictMap.get(code);
}
const res = await getDictItemList(code);
this.dictMap.set(code, res);
return res;
},
// 清除缓存(字典数据变更时调用)
clearDict() {
this.dictMap.clear();
}
}
});
```
## 常用字典示例
| 字典编码 | 说明 | 字典项 |
|---------|------|--------|
| `sys_status` | 系统状态 | 正常(1)、停用(2) |
| `sys_gender` | 性别 | 男(1)、女(2) |
| `operation_type` | 操作类型 | 新增(1)、修改(2)、删除(3) 等 |
| `menu_type` | 菜单类型 | 菜单(0)、节点(1) |
## Redis 缓存机制
字典数据支持 Redis 缓存,减少数据库查询:
```
1. 首次查询:从数据库读取 → 写入 Redis 缓存 → 返回
2. 后续查询:从 Redis 缓存读取 → 返回
3. 数据变更:更新数据库 → 清除 Redis 缓存
4. 缓存 Key:dict:{code},如 dict:sys_status
```
## 总结
数据字典模块具备以下特点:
```
1. 两级结构:字典类型(分组)+ 字典项(具体值),清晰管理
2. 前端集成:useDictStore 统一管理,el-select 直接绑定
3. 值为字符串:字典项 value 统一为字符串类型
4. 避免硬编码:枚举值统一走字典,修改无需改代码
5. Redis 缓存:减少数据库查询,提升性能
6. 批量导入导出:支持 Excel 批量导入字典项
```
@@ -0,0 +1,130 @@
:::tip 全局异常与响应
定义了统一的响应格式和全局异常处理机制。所有 API 返回 `{code, data, msg, ok}` 标准格式。响应函数位于 `utils/R.py`。
:::
## 统一响应格式
`utils/R.py` 提供统一的响应生成函数:
```python
from utils import R
# 成功响应
R.ok(data={"id": 1}, msg="操作成功")
# {"code": 0, "data": {"id": 1}, "msg": "操作成功", "ok": true}
# 失败响应
R.failed(msg="参数错误")
# {"code": 1, "data": null, "msg": "参数错误", "ok": false}
# 带扩展参数的响应
R.ok(data=list, total=100)
# {"code": 0, "data": [...], "msg": "操作成功", "ok": true, "total": 100}
```
### 响应函数说明
| 函数 | 说明 | code | ok |
|------|------|------|------|
| `R.ok()` | 成功响应 | 0 | true |
| `R.failed()` | 失败响应 | 1 | false |
| `R.response()` | 通用响应 | 自定义 | 自定义 |
函数签名:
```python
def ok(data=None, msg="操作成功", code=0, **kwargs) -> JsonResponse:
"""生成成功响应,支持通过 kwargs 添加额外字段(如 total)"""
def failed(msg="操作失败", code=1, data=None, **kwargs) -> JsonResponse:
"""生成失败响应"""
def response(data=None, msg="操作成功", code=0, success=True, **kwargs) -> JsonResponse:
"""通用响应生成器(高级用法),允许完全自定义字段"""
```
### 分页响应
分页数据通过 `R.ok()` 传递:
```python
# application/level/services.py
def get_level_page(request):
# ... 分页查询逻辑 ...
return R.ok(data={
"records": records,
"total": total,
"size": size,
"current": current,
"pages": pages
})
```
## 状态码常量
```python
# utils/R.py
class Codes:
SUCCESS = 0
FAILED = 1
UNAUTHORIZED = 401 # 未认证
FORBIDDEN = 403 # 无权限
NOT_FOUND = 404 # 资源不存在
VALIDATE_ERROR = 422 # 验证错误
SERVER_ERROR = 500 # 服务器错误
```
## Services 层异常处理
在 service 层使用 try/except 捕获异常:
```python
# application/level/services.py
def add_level(request):
try:
data = parse_request_body(request)
form = LevelForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
# 业务逻辑...
Level.objects.create(**form.cleaned_data)
return R.ok(msg="添加成功")
except Exception as e:
logger.error(f"添加职级失败: {str(e)}")
return R.failed(msg="添加失败")
```
## 全局异常处理
Django 通过中间件处理全局异常:
```python
# middleware/exception_middleware.py
class ExceptionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
response = self.get_response(request)
return response
except Exception as e:
logger.error(f"系统异常: {str(e)}")
return R.failed(msg="服务器内部错误", code=500)
```
## 总结
全局异常与响应模块具备以下特点:
```
1. 统一格式:所有 API 返回 {code, data, msg, ok} 标准格式
2. R.ok()/R.failed():简洁的响应生成函数
3. Codes 常量:定义常用状态码
4. kwargs 扩展:支持通过 kwargs 添加额外字段
5. Services try/except:在 service 层捕获异常,返回友好提示
6. 全局异常:中间件捕获未处理异常,返回统一格式
```
@@ -0,0 +1,118 @@
:::tip 防重复提交
防重复提交是保障数据一致性的重要措施。通过前端防抖和后端唯一性校验双重保护,防止用户重复点击导致的数据重复提交。
:::
## 前端防抖
### useLockFn
前端提供 `useLockFn` 钩子函数,实现请求级别的防重复提交:
```javascript
import { useLockFn } from '@/utils/lock'
// 包装提交函数,防止重复点击
const handleSubmit = useLockFn(async () => {
await axios.post('/level/add', formData)
ElMessage.success('添加成功')
})
```
`useLockFn` 原理:
```javascript
function useLockFn(fn) {
let isLocked = false
return async (...args) => {
if (isLocked) return
isLocked = true
try {
return await fn(...args)
} finally {
isLocked = false
}
}
}
```
### 按钮 loading
ElementPlus 的按钮组件支持 `loading` 属性,请求期间禁用按钮:
```vue
<el-button
type="primary"
:loading="submitting"
@click="handleSubmit"
>
提交
</el-button>
<script setup>
const submitting = ref(false)
async function handleSubmit() {
submitting.value = true
try {
await axios.post('/level/add', formData)
ElMessage.success('添加成功')
} finally {
submitting.value = false
}
}
</script>
```
## 后端唯一性校验
### Django Form 唯一性
在 service 层查询数据库校验唯一性:
```python
# application/level/services.py
def add_level(request):
data = parse_request_body(request)
form = LevelForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
# 唯一性校验
name = data.get('name')
if Level.objects.filter(name=name, is_delete=False).exists():
return R.failed("职级名称不能重复")
# 保存数据...
return R.ok(msg="添加成功")
```
### 软删除 + 唯一索引
软删除场景下,唯一索引需要包含 `is_delete` 字段:
```sql
-- MySQL: 唯一索引包含 is_delete 字段
ALTER TABLE django_level ADD UNIQUE INDEX uk_name (name, is_delete);
```
## 最佳实践
```
1. 前端防抖:useLockFn 或按钮 loading,防止用户重复点击
2. 后端校验:service 层查询数据库校验唯一性
3. 数据库约束:唯一索引兜底,防止并发重复插入
4. 错误提示:重复提交时返回明确的错误提示
```
## 总结
防重复提交方案具备以下特点:
```
1. 前端防抖:useLockFn 函数级防抖,按钮 loading 状态控制
2. 后端校验:service 层查询数据库校验唯一性
3. 数据库约束:唯一索引兜底,防止并发重复插入
4. 双重保护:前端防抖 + 后端校验,确保数据一致性
5. 用户体验:重复点击时按钮禁用,返回明确提示
```
@@ -0,0 +1,334 @@
:::tip JWT认证
JWT(JSON Web Token)是系统的核心认证机制。用户登录成功后获取 Token,后续请求通过 `Authorization: Bearer <token>` 头携带 Token 进行身份验证。项目内置了 Token 生成、解析、刷新和黑名单管理功能,源码位于 `utils/jwt.py`。
:::
## 认证流程
```
1. 前端发送 POST /login/login(username + password + captcha)
2. 后端校验验证码、用户名密码
3. 登录成功 → create_token() 生成 JWT
4. 前端存储 Token,后续请求携带 Authorization: Bearer <token>
5. check_login 装饰器解析 Token → 验证有效性
6. 退出登录 → Token 加入 Redis 黑名单立即失效
```
## 配置项
| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `JWT_SALT` | 内置默认密钥 | JWT 签名密钥,建议至少32字节 |
| `DEFAULT_TIMEOUT_MINUTES` | `20` | Token 过期时间(分钟) |
:::warning 密钥安全
生产环境务必通过 `.env` 设置 `JWT_SALT` 为强随机密钥。生成方式:
```bash
python -c "import secrets; print(secrets.token_urlsafe(48))"
```
:::
## Token 生成
`utils/jwt.py` 中的 `create_token()` 函数负责生成 JWT:
```python
from utils.jwt import create_token
# 登录成功后生成 Token
access_token = create_token({
"userId": user.id,
"username": user.username,
"realname": user.realname or ''
})
```
函数签名与核心逻辑:
```python
def create_token(payload, timeout=None):
"""
生成JWT令牌
参数:
payload (dict): JWT载荷数据,通常包含userId、username等信息
timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES
返回:
str: 生成的JWT令牌字符串
"""
if timeout is None:
timeout = DEFAULT_TIMEOUT_MINUTES
payload_copy = payload.copy()
current_time = datetime.datetime.now(tz=datetime.timezone.utc)
payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout)
payload_copy['iat'] = current_time
headers = {"typ": "JWT", "alg": JWT_ALGORITHM}
token = jwt.encode(
payload=payload_copy,
key=JWT_SALT,
algorithm=JWT_ALGORITHM,
headers=headers
)
return token
```
Token 结构包含三部分:
```python
# Header
{
"typ": "JWT",
"alg": "HS256"
}
# Payload(自动添加 exp 和 iat)
{
"userId": 1,
"username": "admin",
"realname": "管理员",
"exp": "2026-09-05T12:00:00Z", # 过期时间
"iat": "2026-09-05T11:40:00Z" # 签发时间
}
```
## Token 解析验证
`parse_payload()` 函数验证 Token 签名和过期状态,返回标准格式结果:
```python
from utils.jwt import parse_payload
result = parse_payload(token)
if result['code'] == 0:
user_id = result['data']['userId']
username = result['data']['username']
else:
error_msg = result['msg']
# "token已失效,请重新登录" / "token认证失败" / "非法的token"
```
核心实现:
```python
def parse_payload(token):
"""解析验证JWT令牌,返回 {code, data, msg} 标准格式"""
result = {"code": 0, "data": None, "msg": "操作成功"}
if not token:
result['code'] = -1
result['msg'] = "token不能为空"
return result
# 检查Token是否在黑名单中
if is_token_blacklisted(token):
result['code'] = -1
result['msg'] = "token已失效,请重新登录"
return result
try:
verified_payload = jwt.decode(token, JWT_SALT, algorithms=[JWT_ALGORITHM])
result['data'] = verified_payload
except exceptions.ExpiredSignatureError:
result['code'] = -1
result['msg'] = "token已失效,请重新登录"
except exceptions.DecodeError:
result['code'] = -1
result['msg'] = "token认证失败,无效的令牌格式"
except exceptions.InvalidTokenError:
result['code'] = -1
result['msg'] = "非法的token,请检查令牌有效性"
return result
```
异常处理说明:
| 异常类型 | 提示信息 | 说明 |
|---------|---------|------|
| `ExpiredSignatureError` | token已失效,请重新登录 | Token 过期 |
| `DecodeError` | token认证失败,无效的令牌格式 | 格式错误或签名无效 |
| `InvalidTokenError` | 非法的token | 算法不匹配等 |
## Token 刷新
Token 支持无感刷新,刷新时会生成新令牌:
```python
from utils.jwt import refresh_token
result = refresh_token(old_token, extend_minutes=30)
if result['code'] == 0:
new_token = result['data']['access_token']
expires_in = result['data']['expires_in'] # 秒
```
刷新核心逻辑:
```python
def refresh_token(token, extend_minutes=None):
# 1. 验证原令牌有效性
result = parse_payload(token)
if result['code'] != 0:
return {"code": -1, "data": None, "msg": f"令牌无效,无法刷新: {result['msg']}"}
# 2. 移除JWT标准字段,保留业务数据
payload = result['data']
clean_payload = {k: v for k, v in payload.items() if k not in ['exp', 'iat', 'nbf']}
# 3. 生成新令牌
new_token = create_token(clean_payload, timeout=extend_minutes)
return {
"code": 0,
"data": {"access_token": new_token, "expires_in": extend_minutes * 60},
"msg": "令牌刷新成功"
}
```
## Token 黑名单
退出登录时将 Token 加入 Redis 黑名单,使其立即失效:
```python
from utils.jwt import add_token_to_blacklist, is_token_blacklisted
# 退出登录:加入黑名单
add_token_to_blacklist(token)
# 中间件校验:检查是否已注销
if is_token_blacklisted(token):
# 拒绝访问
```
黑名单实现细节:
```python
# 黑名单 Key 前缀
_TOKEN_BLACKLIST_PREFIX = 'token:blacklist:'
# SHA256 指纹存储,不将完整 Token 存入 Redis
def _token_fingerprint(token: str) -> str:
return hashlib.sha256(token.encode('utf-8')).hexdigest()
def add_token_to_blacklist(token: str) -> bool:
"""退出登录时调用,TTL 与 Token 剩余有效期一致"""
from django_redis import get_redis_connection
redis = get_redis_connection("default")
expiration = get_token_expiration(token)
ttl_seconds = int((expiration - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds())
fingerprint = _token_fingerprint(token)
key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}'
redis.set(key, '1', ex=ttl_seconds)
def is_token_blacklisted(token: str) -> bool:
"""检查 Token 是否已注销,Redis 不可用时 fail-open 放行"""
try:
from django_redis import get_redis_connection
redis = get_redis_connection("default")
fingerprint = _token_fingerprint(token)
key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}'
return redis.exists(key) > 0
except Exception as e:
# fail-open:Redis 故障时放行,避免全站不可用
_log_blacklist_check_alert(e)
return False
```
黑名单特性:
- 使用 SHA256 指纹存储,不将完整 Token 存入 Redis
- TTL 与 Token 剩余有效期一致,过期自动清理
- Redis 不可用时采用 fail-open 策略放行(避免全站不可用)
- 告警节流:Redis 故障期间首次失败记 ERROR,随后每 60 秒至多一条 ERROR
## 请求中的 Token 提取
```python
from utils.jwt import get_access_token, parse_token
# 从请求头提取 Token
success, token, msg = get_access_token(request)
# 完整解析(提取 + 验证)
result = parse_token(request)
# result['code'] == 0 表示成功
# result['data']['userId'], result['data']['username']
```
`get_access_token()` 支持 Bearer 前缀大小写不敏感(RFC 7235):
```python
def get_access_token(request):
"""从请求头中提取 access_token,返回 (success, token, message)"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
stripped = auth_header.strip()
if stripped.lower().startswith('bearer '):
access_token = stripped[7:].strip()
else:
access_token = stripped
return True, access_token, 'success'
```
## check_login 装饰器
`middleware/login_middleware.py` 中的 `check_login` 是全局认证装饰器,应用于所有需要登录的视图:
```python
from middleware.login_middleware import check_login
@method_decorator(check_login, name='dispatch')
class UserPageView(PermissionRequired, View):
def get(self, request):
# 只有已登录用户才能访问
...
```
认证成功后,业务代码通过 `utils/security.py` 获取当前用户信息:
```python
from utils.security import get_user_id, get_username, get_realname
user_id = get_user_id(request)
username = get_username(request)
realname = get_realname(request)
```
## 前端集成
前端登录后将 Token 存入 localStorage,每次请求自动携带:
```javascript
// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
// 响应拦截器:检测 Token 过期
axios.interceptors.response.use(response => {
if (response.data.code === 401) {
// Token 过期,跳转登录页
router.push('/login')
}
return response
})
```
## 总结
JWT 认证方案具备以下特点:
```
1. 安全性:HS256 算法 + 强密钥校验(建议至少32字节)
2. 黑名单管理:SHA256 指纹存储,TTL 自动清理,Redis 故障时 fail-open 降级
3. 全局统一:check_login 装饰器 + security 工具函数,业务代码零侵入
4. Bearer 标准:RFC 7235 规范,前缀大小写不敏感
5. Django 原生:基于 django-redis 连接池,与 Django ORM 无缝集成
```
@@ -0,0 +1,233 @@
:::tip 登录日志
登录日志记录每次登录尝试(成功/失败),包含登录账号、IP地址、浏览器信息、登录时间等。登录逻辑位于 `application/login/services.py`,日志记录位于 `application/login_log/` 模块。
:::
## 记录时机
登录日志在以下时机自动记录:
```
1. 登录成功 -> 记录成功日志(status=0, logType=1)
2. 登录失败(密码错误)-> 记录失败日志(status=1, logType=1)
3. 登录失败(用户不存在)-> 记录失败日志(status=1, logType=1)
4. 登录失败(验证码错误)-> 记录失败日志(status=1, logType=1)
5. 退出登录 -> 记录成功日志(status=0, logType=2)
```
## 日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `username` | str | 登录账号 |
| `logType` | int | 日志类型:1-登录 2-退出 |
| `source` | int | 登录来源:0-后台系统 1-手机站 2-客户端 |
| `method` | str | 请求方法 |
| `requestMethod` | str | HTTP 请求方法 |
| `url` | str | 请求路径 |
| `param` | str | 请求参数(截取前2000字符) |
| `result` | str | 响应结果(截取前2000字符) |
| `ip` | str | 客户端 IP 地址 |
| `location` | str | IP 地理位置(ip2region 解析) |
| `os` | str | 操作系统 |
| `browser` | str | 浏览器类型 |
| `consumeTime` | int | 请求耗时(毫秒) |
| `status` | int | 操作状态:0-成功 1-异常 |
| `error` | str | 错误信息 |
| `createUser` | str | 操作人 |
| `createTime` | datetime | 记录时间 |
## 记录实现
登录日志通过 `record_login_log()` 函数记录,自动提取客户端信息:
```python
# application/login_log/services.py
from utils.ip2region import get_ip_location
from utils.common import get_client_ip, parse_user_agent, format_params, format_result
def record_login_log(request, username, log_type, status, source=0,
error_msg=None, consume_time=0, result=None):
"""记录登录日志"""
# 获取客户端 IP
client_ip = get_client_ip(request)
# 获取地理位置信息(ip2region 直接返回字符串)
location_str = get_ip_location(client_ip)
# 解析 User-Agent 信息
user_agent = request.META.get('HTTP_USER_AGENT', '')
ua_info = parse_user_agent(user_agent)
# 构建日志数据
log_data = {
'username': username,
'log_type': log_type,
'source': source,
'method': request.method,
'request_method': request.method,
'url': request.path,
'ip': client_ip,
'location': location_str,
'os': ua_info['os'],
'browser': ua_info['browser'],
'consume_time': consume_time,
'status': status,
'error': error_msg,
'create_user': username,
'update_user': username,
}
LoginLog.objects.create(**log_data)
```
## IP 地理位置解析
框架集成 ip2region 库,自动将 IP 解析为地理位置:
```python
# utils/ip2region.py
from utils.ip2region import get_ip_location
location = get_ip_location("116.25.146.177")
# 返回: "广东省深圳市"
```
:::tip ip2region
ip2region 是离线 IP 地理位置解析库,数据文件存储在 `utils/ip2region.xdb`,无需网络请求,解析速度极快。支持 IPv4 地址,覆盖国内省市和运营商信息。
:::
## User-Agent 解析
自动从 User-Agent 字符串中提取操作系统和浏览器信息:
```python
# utils/common.py
def parse_user_agent(user_agent):
"""解析 User-Agent,返回操作系统和浏览器信息"""
# 示例返回值
return {
'os': 'Windows 10',
'browser': 'Chrome 120.0'
}
```
## 分页查询接口
```
GET /login/log/page?pageNo=1&pageSize=10&username=admin&type=1&status=0
```
### 查询参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `pageNo` | int | 页码(默认 1) |
| `pageSize` | int | 每页条数(默认 10) |
| `username` | str | 登录账号(模糊查询) |
| `type` | int | 日志类型:1-登录 2-退出 |
| `status` | int | 操作状态:0-成功 1-异常 |
| `startTime` | str | 开始时间 |
| `endTime` | str | 结束时间 |
### 响应示例
```json
{
"code": 0,
"data": {
"records": [
{
"id": 1,
"title": "用户登录",
"username": "admin",
"logType": 1,
"logTypeText": "登录",
"ip": "127.0.0.1",
"location": "本机地址",
"os": "Windows 10",
"browser": "Chrome 120.0",
"status": 0,
"statusText": "成功",
"createTime": "2026-09-08 10:30:00"
}
],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
},
"msg": "操作成功",
"ok": true
}
```
## 详情查询接口
```
GET /login/log/detail/<int:log_id>
```
详情接口额外返回 `param`(请求参数)和 `result`(响应结果)字段,参数会自动格式化为 JSON 便于前端展示。
## 删除接口
```
DELETE /login/log/delete/<str:log_id>
```
支持批量删除,多个 ID 用逗号分隔:`DELETE /login/log/delete/1,2,3`
:::warning 权限要求
- 分页查询:`sys:loginlog:page`
- 详情查询:`sys:loginlog:detail`
- 删除:`sys:loginlog:delete`
:::
## 与操作日志的区别
| 维度 | 登录日志 | 操作日志 |
|------|---------|---------|
| 记录内容 | 登录/退出行为 | 业务操作行为 |
| 记录方式 | 登录/退出时手动调用 | `@operation_log` 装饰器自动记录 |
| 触发时机 | 登录成功/失败、退出登录 | 任何标记了装饰器的视图方法 |
| 数据来源 | `application/login_log/` | `application/operation_log/` |
| 权限标识 | `sys:loginlog:*` | `sys:operationLog:*` |
## 安全审计场景
### 异常登录检测
通过查询登录日志,可识别以下异常场景:
```
1. 同一账号短时间内多次登录失败 -> 可能是暴力破解
2. 同一 IP 大量不同账号登录 -> 可能是撞库攻击
3. 异地登录(IP 地理位置变化)-> 可能是账号被盗
4. 非工作时间登录 -> 可能是异常行为
```
### 排查命令
```bash
# 查询最近 1 小时内登录失败的记录
GET /login/log/page?status=1&type=1&startTime=2026-09-08 09:00:00&endTime=2026-09-08 10:00:00
# 查询特定账号的登录记录
GET /login/log/page?username=admin
# 查询特定 IP 的登录记录
GET /login/log/page?ip=192.168.1.100
```
## 总结
登录日志模块具备以下特点:
```
1. 自动记录:登录成功/失败/退出自动记录,无需手动埋点
2. IP 解析:ip2region 离线解析,速度快、无网络依赖
3. UA 解析:自动提取浏览器和操作系统信息
4. 地理位置:自动解析 IP 所在地区
5. 安全审计:完整记录登录行为,支持异常登录检测
6. 批量管理:支持按条件筛选、批量删除、清空操作
```
@@ -0,0 +1,207 @@
:::tip 操作日志
操作日志通过 `@operation_log` 装饰器标记需要记录日志的视图方法,自动记录请求参数、响应结果、耗时、操作类型等信息。源码位于 `application/operation_log/decorators.py`。
:::
## 工作原理
```
1. 视图方法通过 @operation_log(title=..., log_type=LogType.XXX) 标记
2. 装饰器在方法执行前记录开始时间、提取请求参数
3. 执行原方法,获取响应
4. 检查响应是否成功(解析 JSON 中的 code 字段)
5. 记录操作日志到数据库(含参数、结果、耗时、状态)
6. 异常时记录错误信息并重新抛出
```
## 装饰器使用
```python
# application/level/views.py
from application.operation_log.decorators import operation_log
from application.operation_log.constants import LogType
class LevelAddView(PermissionRequired, View):
permission_required = ("sys:level:add",)
@operation_log(
title="职级管理-添加记录",
log_type=LogType.ADD
)
def post(self, request):
result = services.add_level(request)
return result
```
### 装饰器参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `title` | str | 操作标题,如"职级管理-添加职级" |
| `log_type` | int | 操作类型,参考 LogType 常量 |
| `source` | int | 操作来源,0-后台系统 1-手机站 2-客户端 3-其他 |
### LogType 常量
```python
# application/operation_log/constants.py
class LogType:
OTHER = 0 # 其它
ADD = 1 # 新增
UPDATE = 2 # 修改
DELETE = 3 # 删除
BATCH_DELETE = 4 # 批量删除
STATUS = 5 # 状态设置
RESET_PWD = 6 # 重置密码
UPDATE_PWD = 7 # 修改密码
IMPORT = 8 # 导入
EXPORT = 9 # 导出
QUERY = 10 # 查询
```
## 核心实现
```python
# application/operation_log/decorators.py
def operation_log(title: str, log_type: int = LogType.OTHER, source: int = 0):
def decorator(func):
@wraps(func)
def wrapper(self, request, *args, **kwargs):
# 记录开始时间
start_time = time.time()
# 保存请求参数用于日志记录
request.log_params = _extract_params(request)
try:
# 执行原方法
response = func(self, request, *args, **kwargs)
# 计算耗时(毫秒)
consume_time = int((time.time() - start_time) * 1000)
# 检查响应是否成功
is_success, error_msg = _check_response_success(response)
status = 0 if is_success else 1
# 记录操作日志
record_operation_log(
request=request,
title=title,
log_type=log_type,
source=source,
status=status,
error_msg=error_msg,
consume_time=consume_time,
result=response
)
return response
except Exception as e:
consume_time = int((time.time() - start_time) * 1000)
record_operation_log(
request=request,
title=title,
log_type=log_type,
source=source,
status=1,
error_msg=str(e),
consume_time=consume_time,
result=None
)
raise
return wrapper
return decorator
```
## 参数提取
装饰器自动提取请求参数,支持 GET/POST/PUT/DELETE/PATCH:
```python
def _extract_params(request):
"""提取请求参数,自动过滤敏感信息"""
params = {}
if request.method == 'GET':
params = request.GET.dict()
elif request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
try:
params = json.loads(request.body.decode('utf-8'))
except:
params = request.POST.dict()
# 过滤敏感信息
sensitive_keys = ['password', 'pwd', 'token', 'secret', 'access_token']
for key in sensitive_keys:
if key in params:
params[key] = '******'
return params
```
## 响应检查
装饰器自动检查响应是否成功:
```python
def _check_response_success(response):
"""检查响应是否成功"""
is_success = True
error_msg = None
try:
# 检查 HTTP 状态码
if hasattr(response, 'status_code') and response.status_code >= 400:
return False, f"HTTP {response.status_code}"
# 解析 JSON 响应内容
if hasattr(response, 'content') and response.content:
content = response.content.decode('utf-8')
data = json.loads(content)
if isinstance(data, dict):
code = data.get('code')
if code is not None and code != 0:
is_success = False
error_msg = data.get('msg', '操作失败')
except:
pass
return is_success, error_msg
```
## 日志字段
| 字段 | 说明 |
|------|------|
| `title` | 操作标题 |
| `log_type` | 操作类型 |
| `url` | 请求路径 |
| `method` | 请求方法 |
| `param` | 请求参数(截取前2000字符) |
| `result` | 响应结果(截取前2000字符) |
| `consume_time` | 耗时(毫秒) |
| `status` | 状态:0-成功 1-失败 |
| `error_msg` | 错误信息 |
| `create_user` | 操作人 |
## 查询接口
```
GET /operation/log/page?pageNo=1&pageSize=10
GET /operation/log/detail/<int:log_id>
```
## 总结
操作日志模块具备以下特点:
```
1. 装饰器模式:@operation_log 标记需要记录日志的视图方法
2. 自动记录:参数、结果、耗时、操作人等完整信息
3. 类型常量:LogType 定义标准操作类型
4. 敏感过滤:自动过滤密码、token 等敏感信息
5. 异常捕获:异常时记录错误信息并重新抛出
6. 响应检查:自动解析 JSON 响应判断操作是否成功
```
@@ -0,0 +1,262 @@
:::tip 请求限流
请求限流可通过中间件实现 IP 级别的限流,防止恶意请求和 DoS 攻击。项目中限流可通过 Nginx 反向代理或自定义中间件实现。
:::
## 限流策略
项目支持两种限流实现方式,可根据部署环境选择:
| 方式 | 适用场景 | 性能 | 精度 |
|------|---------|------|------|
| Nginx 限流 | 生产环境(推荐) | 高(不占用应用进程) | 中 |
| Django 中间件 | 开发/测试环境 | 中(占用应用进程) | 高 |
## 方式 1:Nginx 限流(推荐)
Nginx 限流在反向代理层实现,不占用 Django 进程资源,适合生产环境:
```nginx
# nginx.conf
http {
# 定义限流区域:按 IP 维度,每秒 10 个请求
# 10m 共享内存区域可存储约 16 万个 IP 地址
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
# 应用限流:突发允许 20 个请求排队,不延迟处理
limit_req zone=api_limit burst=20 nodelay;
# 限流时返回 429 状态码
limit_req_status 429;
# 限流日志级别
limit_req_log_level warn;
proxy_pass http://127.0.0.1:8000;
}
}
}
```
### Nginx 限流参数说明
| 参数 | 说明 | 示例 |
|------|------|------|
| `rate` | 每秒允许的请求数 | `10r/s`(每秒 10 个) |
| `burst` | 突发请求数(排队等待) | `burst=20` |
| `nodelay` | 突发请求不延迟处理 | 有此参数则立即处理 |
| `limit_req_status` | 限流时返回的状态码 | `429` |
| `limit_req_log_level` | 限流日志级别 | `warn` |
### 不同接口差异化限流
```nginx
http {
# API 接口限流:每秒 10 个请求
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 登录接口限流:每分钟 5 个请求(防暴力破解)
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
# 上传接口限流:每秒 2 个请求
limit_req_zone $binary_remote_addr zone=upload_limit:10m rate=2r/s;
server {
location /api/login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8000;
}
location /api/upload {
limit_req zone=upload_limit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8000;
}
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8000;
}
}
}
```
## 方式 2:Django 中间件限流
Django 中间件限流在应用层实现,支持更灵活的限流规则:
```python
# middleware/rate_limit_middleware.py
import time
from django.core.cache import cache
from django.http import JsonResponse
class RateLimitMiddleware:
"""基于缓存的 IP 限流中间件"""
def __init__(self, get_response):
self.get_response = get_response
self.rate_limit = 100 # 每分钟最大请求数
self.window = 60 # 窗口时间(秒)
def __call__(self, request):
# 获取客户端 IP
ip = self._get_client_ip(request)
cache_key = f'rate_limit:{ip}'
# 获取当前计数
current = cache.get(cache_key, 0)
if current >= self.rate_limit:
return JsonResponse({
'code': 1,
'msg': '请求过于频繁,请稍后再试',
'data': None,
'ok': False
}, status=429)
# 递增计数
cache.set(cache_key, current + 1, self.window)
response = self.get_response(request)
return response
def _get_client_ip(self, request):
"""获取客户端 IP,支持代理头"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
```
### 注册中间件
```python
# application/settings.py
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'middleware.rate_limit_middleware.RateLimitMiddleware', # 限流中间件
'django.middleware.common.CommonMiddleware',
...
]
```
### 按接口差异化限流
```python
class RateLimitMiddleware:
"""支持按接口差异化限流的中间件"""
# 接口限流配置
RATE_LIMITS = {
'/api/login': {'limit': 5, 'window': 60}, # 登录:每分钟 5 次
'/api/captcha': {'limit': 10, 'window': 60}, # 验证码:每分钟 10 次
'/api/upload': {'limit': 5, 'window': 60}, # 上传:每分钟 5 次
'default': {'limit': 100, 'window': 60}, # 默认:每分钟 100 次
}
def __call__(self, request):
ip = self._get_client_ip(request)
path = request.path
# 获取当前接口的限流配置
config = self.RATE_LIMITS.get(path, self.RATE_LIMITS['default'])
cache_key = f'rate_limit:{ip}:{path}'
current = cache.get(cache_key, 0)
if current >= config['limit']:
return JsonResponse({
'code': 1,
'msg': '请求过于频繁,请稍后再试',
'data': None,
'ok': False
}, status=429)
cache.set(cache_key, current + 1, config['window'])
return self.get_response(request)
```
## 配置项
| 配置项 | 默认值 | 说明 |
|---------|--------|------|
| `rate_limit` | `100` | 每分钟最大请求数 |
| `window` | `60` | 统计窗口时长(秒) |
:::warning 生产环境建议
生产环境建议使用 Nginx 限流,性能更好且不占用 Django 进程资源。Django 中间件限流适合开发环境和需要精细控制的场景。
:::
## 限流响应格式
当请求被限流时,返回 429 状态码:
```json
{
"code": 1,
"msg": "请求过于频繁,请稍后再试",
"data": null,
"ok": false
}
```
前端可通过 HTTP 状态码 429 识别限流,并提示用户稍后重试。
## 降级策略
```
1. Redis 不可用:直接放行,不阻塞请求
2. 限流计数异常:降级放行,避免全站不可用
3. 限流阈值配置错误:使用默认值兜底
```
:::tip fail-open 策略
限流采用 fail-open 策略:缓存不可用时放行所有请求,优先保证服务可用性。这是安全与可用性的权衡——宁可放过一些请求,也不能因为限流故障导致全站不可用。
:::
## 验证码防刷
验证码接口有独立的限流策略,按 IP 频率控制:
```python
# 验证码防刷:单 IP 每分钟最多获取 5 次验证码
def check_captcha_rate_limit(ip):
cache_key = f'captcha_rate:{ip}'
count = cache.get(cache_key, 0)
if count >= 5:
return False # 超过限制
cache.set(cache_key, count + 60)
return True
```
## 前端配合
前端可通过 Axios 拦截器处理 429 响应:
```typescript
// 响应拦截器
responseInterceptorsCatch: (error: any) => {
if (error.response?.status === 429) {
ElMessage.warning('请求过于频繁,请稍后再试');
return Promise.reject(error);
}
// ... 其他错误处理
}
```
## 总结
请求限流模块具备以下特点:
```
1. 多种实现:Nginx 限流(推荐)或 Django 中间件限流
2. IP 维度:按客户端 IP 限流,支持代理头
3. 滑动窗口:基于缓存的计数器实现
4. 降级策略:缓存故障时 fail-open 放行
5. 灵活配置:可调整限流阈值和窗口时间
6. 差异化限流:支持按接口独立配置限流策略
7. 验证码防刷:验证码接口有独立的频率控制
```
@@ -0,0 +1,143 @@
:::tip RBAC权限
采用基于角色的访问控制(RBAC)模型,通过用户-角色-菜单三层关系实现细粒度的权限管理。权限控制贯穿前后端:后端通过 `PermissionRequired` 混入类校验(`middleware/permission_middleware.py`),前端通过 `v-perm` 指令控制按钮显隐。
:::
## 权限模型
```
用户(User) --N:N-- 角色(Role) --N:N-- 菜单(Menu)
|
type=0: 菜单(页面路由)
type=1: 节点(权限按钮)
```
数据库表关系:
| 关联表 | 说明 |
|--------|------|
| `django_user_role` | 用户-角色关联表 |
| `django_role_menu` | 角色-菜单关联表 |
| `django_menu` | 菜单表(含权限节点) |
## 权限节点格式
权限节点采用 `sys:module:action` 三段式命名:
```
sys:user:list 用户列表
sys:user:add 用户新增
sys:user:update 用户编辑
sys:user:delete 用户删除
sys:user:resetPwd 重置密码
sys:role:list 角色列表
sys:role:add 角色新增
sys:menu:list 菜单列表
sys:dept:list 部门列表
sys:level:list 职级列表
sys:position:list 岗位列表
```
## 后端权限校验
### PermissionRequired 混入类
`middleware/permission_middleware.py` 中的 `PermissionRequired` 混入类用于基于类的视图权限校验:
```python
from middleware.permission_middleware import PermissionRequired
@method_decorator(check_login, name="get")
class UserPageView(PermissionRequired, View):
permission_required = ("sys:user:page",)
def get(self, request):
result = services.get_user_page(request)
return result
```
校验逻辑(完整实现):
```python
# middleware/permission_middleware.py
from django.contrib.auth.mixins import PermissionRequiredMixin
class PermissionRequired(PermissionRequiredMixin):
def has_permission(self):
permissions = self.get_permission_required()
user_id = get_user_id(self.request)
# 超级管理员自动放行
if user_id and user_id != 1:
from application.menu import services
permission_list = services.get_user_permissions(user_id)
for permission in permissions:
if permission not in permission_list:
return False
return True
def handle_no_permission(self):
return R.failed("暂无操作权限", 401)
```
:::tip 管理员免检
用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。
:::
### 权限数据查询
权限列表通过角色-菜单关联查询:
```python
# application/menu/services.py
def get_user_permissions(user_id):
"""获取用户的权限节点列表"""
if user_id == 1:
# 超管拥有全部权限
menu_list = Menu.objects.filter(
type=1, is_delete=False
)
else:
# 通过 user -> role -> menu 关联查询
menu_list = Menu.objects.filter(
type=1,
is_delete=False,
status=0,
rolemenu__role__userrole__user_id=user_id,
rolemenu__role__is_delete=False,
).distinct()
return [item.permission for item in menu_list]
```
## 前端权限控制
### v-perm 指令
前端使用自定义 `v-perm` 指令控制按钮显隐:
```html
<!-- 只有拥有 sys:user:add 权限的用户才能看到此按钮 -->
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
<!-- 多个权限,满足其一即可 -->
<el-button v-perm="['sys:user:update', 'sys:user:delete']">操作</el-button>
```
### 路由守卫
前端路由守卫根据用户菜单权限动态生成路由,无权限的页面不会出现在导航菜单中。
## 总结
RBAC 权限方案具备以下特点:
```
1. 三层模型:用户-角色-菜单,支持灵活的角色分配
2. 节点权限:sys:module:action 格式,细粒度控制每个操作
3. 前后端统一:后端 PermissionRequired + 前端 v-perm 指令
4. 管理员免检:userId=1 自动跳过权限校验
5. Django 原生:基于 Django PermissionRequiredMixin 扩展
6. 菜单路由:type=0 菜单生成前端路由,type=1 节点控制按钮权限
```
@@ -0,0 +1,182 @@
:::tip 参数验证
使用 Django ModelForm 进行请求参数验证。所有请求体参数通过 Django Form 定义,视图中调用 `form.is_valid()` 自动验证,验证失败通过 `regular.get_err()` 提取错误信息返回前端。
:::
## 基本用法
### 定义表单类
```python
# application/level/forms.py
# +======================================================================
# | 模块: 职级表单验证
# | 说明: 职级创建/编辑的请求数据校验
# +======================================================================
from django import forms
from application.level.models import Level
class LevelForm(forms.ModelForm):
"""职级创建/编辑表单"""
class Meta:
model = Level
fields = ['name', 'status', 'sort']
error_messages = {
'name': {
'required': '职级名称不能为空',
'max_length': '职级名称不能超过150个字符',
},
}
```
### 端点使用
```python
# application/level/views.py
class LevelAddView(PermissionRequired, View):
permission_required = ("sys:level:add",)
def post(self, request):
form = LevelForm(parse_request_body(request))
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
# 保存数据...
return R.ok(msg="添加成功")
```
:::tip 自动验证
`form.is_valid()` 自动完成参数验证。验证失败时通过 `regular.get_err(form)` 提取错误信息:
```json
{"code": 1, "data": null, "msg": "职级名称不能为空", "ok": false}
```
:::
## error_messages 自定义
Django Form 支持通过 `error_messages` 自定义中文错误提示:
```python
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ['username', 'realname', 'mobile']
error_messages = {
'username': {
'required': '用户名不能为空',
'max_length': '用户名不能超过30个字符',
},
'realname': {
'required': '姓名不能为空',
},
'mobile': {
'required': '手机号不能为空',
'invalid': '手机号格式不正确',
},
}
```
## regular.get_err() 错误提取
`utils/regular.py` 中的 `get_err()` 函数从 Form 对象中提取所有错误信息:
```python
# utils/regular.py
def get_err(form):
"""
获取表单错误文本
从Django表单对象中提取所有错误信息,并将其合并为单个字符串。
多个错误用'/'分隔。
使用示例:
form = UserForm(request.POST)
if not form.is_valid():
error_msg = get_err(form)
return R.failed(msg=error_msg)
"""
error_list = []
for item in form.errors.get_json_data().values():
error_list.append(item[0].get('message'))
err_str = '/'.join(error_list)
return err_str
```
错误格式示例:
```python
# 单个错误
"职级名称不能为空"
# 多个错误
"职级名称不能为空/排序不能为空"
```
## 字段约束
Django Form 提供丰富的字段约束:
| 约束 | 说明 | 示例 |
|------|------|------|
| `required` | 是否必填 | `forms.CharField(required=True)` |
| `max_length` | 字符串最大长度 | `forms.CharField(max_length=150)` |
| `min_length` | 字符串最小长度 | `forms.CharField(min_length=1)` |
| `min_value` | 最小值 | `forms.IntegerField(min_value=0)` |
| `max_value` | 最大值 | `forms.IntegerField(max_value=99999)` |
## 唯一性校验
在 service 层查询数据库校验唯一性:
```python
# application/level/services.py
def add_level(request):
data = parse_request_body(request)
form = LevelForm(data)
if not form.is_valid():
return R.failed(msg=regular.get_err(form))
# 唯一性校验
name = data.get('name')
if Level.objects.filter(name=name, is_delete=False).exists():
return R.failed("职级名称不能重复")
# 保存数据...
return R.ok(msg="添加成功")
```
## parse_request_body
项目提供 `parse_request_body()` 工具函数,统一处理请求体解析:
```python
# utils/common.py
def parse_request_body(request):
"""
解析请求体,支持 JSON 和表单数据
Args:
request: Django 请求对象
Returns:
dict: 请求参数字典
"""
try:
return json.loads(request.body.decode('utf-8'))
except:
return request.POST.dict()
```
## 总结
参数验证模块具备以下特点:
```
1. Django ModelForm:类型安全、自动解析请求体
2. 中文错误:error_messages 自定义中文提示
3. regular.get_err():统一提取错误信息,多个错误用 / 分隔
4. 唯一性校验:在 service 层查询数据库,确保字段唯一性
5. parse_request_body:统一处理 JSON 和表单数据
```