认证后端 qxj-backend-admin
仓库定位
QXJ 的认证与管理服务端,基于 Django 4.2 + DRF,从 v2 重构而来(v3)。负责 SM2 密钥协商服务端、NSP-SM Token 签发、用户/角色/设备管理、时间规则与地理围栏、 验证码与安全审计日志。JWT 不使用 SimpleJWT,而是自定义 NSP-SM (SM2withSM3 非对称签名)Token,配合 SM2 四步握手、SM4-GCM 加密帧、 黑名单 / 撤销与设备状态校验。
默认端口 4607(甲方部署端口),API 前缀 /api/v3/,管理类接口 /api/v3/admin/...。当前分支 feature/admin-backend-sync-20261004; repo 工具管理(.git 为符号链接,git 操作走 WSL)。
技术栈明细
| 类别 | 依赖 |
|---|---|
| 框架 | Django 4.2.25、djangorestframework 3.15.2 |
| 认证 | djangorestframework-simplejwt 5.3.1(仅借用结构,签名算法自研 NSP-SM) |
| API 文档 | drf-spectacular 0.29.0(Swagger UI) |
| 过滤 / 跨域 | django-filter 24.3、django-cors-headers 4.4.0 |
| 缓存 | django-redis 5.4.0(JSONSerializer,禁 pickle,KEY_PREFIX=qxj) |
| 环境变量 | python-dotenv 1.0.0(两级 .env 加载) |
| 生产服务器 | gunicorn 23.0.0(原生支持 TLS) |
| 静态文件 | whitenoise(含 frontend_dist 一体化托管) |
| 开发辅助 | django-extensions、Werkzeug、pyOpenSSL(runserver_plus HTTPS) |
| 云服务 SDK | 阿里云验证码 2.0、阿里云 dypns(短信) |
| 本地依赖 | vendor/qxj_backend_sdk-0.1.0 离线 wheel |
| 数据库 | 默认 SQLite(db.sqlite3),DB_ENGINE=mysql 切 MySQL(utf8mb4) |
功能模块全列表(apps 下 14 个 app)
| App | 模型与职责 |
|---|---|
| users | User(AbstractUser):real_name/phone、status(0禁用/1激活/2冻结,同步 is_active)、expires_at(有效期兼年审)、failed_attempts、登录时段、allowed_ips(CIDR)、token_version(改密递增);含账户有效性/时段/IP 校验与令牌吊销 |
| auth | 无模型。登录/登出/刷新;sm2_jwt.py(NSP-SM 编解码)、authentication.py(BlacklistJWTAuthentication)、jti 黑名单 + sid 会话撤销 |
| keymgr | ServerKey:服务器唯一 SM2 密钥对,私钥 SM4-CBC + HMAC-SHA256 加密入库(iv16+密文+mac32),主密钥取环境变量 QXJ_KEY_ENC_KEY;SM2 四包协商服务、C 库封装、二维码导出 |
| devices | DeviceType / Device:唯一标识 eqp_unique_identifier、状态(0禁用/1激活/2挂失/3年审中)、年审字段、MAC/蓝牙/星闪地址;get_effective_device 贯穿握手与鉴权 |
| device_public_keys | DevicePublicKey:Pad 的 SM2 公钥(小写 hex),关联设备,软删除,active 唯一 |
| roles / user_roles | Role(继承 Group,role_type:0管理员/1普通/2访客);关联表 signals 自动同步 is_staff/is_superuser |
| geofences / role_geofences | 圆形围栏(经纬度、GCJ-02/WGS84/BD-09、半径);haversine 距离与围栏校验 |
| time_rules / role_time_rules | 单表:时段 + weekdays/month_days/months JSON、单双周、ALLOW/DENY、有效期;DENY 优先 |
| security_logs | SecurityLog:约 30 种 LogType、Result、Level、请求详情 JSON、敏感字段过滤;SecurityLogMiddleware 自动记录 |
| sms | 验证码(内网网关 gateway / 阿里云 dypns),手机+邮箱同一码、频率限制、纯文本/HTML 邮件模板(EMAIL_USE_HTML 开关) |
| captcha | 阿里云验证码 2.0 服务端校验、JSBridge 预验证票据 |
另有 common/(DRF 公共件:异常/过滤/分页/权限/限流)。
快速开始全流程
开发环境(WSL + conda qxj)
# 1. 进入 WSL 并激活 conda 环境(也可用 venv:python -m venv venv && pip install -r requirements.txt)
wsl -e bash -lc "source ~/miniconda3/etc/profile.d/conda.sh && conda activate qxj"
# 2. 安装依赖(含 vendor/ 离线 wheel)
cd /mnt/c/Users/etsuyou/Desktop/qxj-project/qxj-backend-admin
pip install -r requirements.txt
# 3. 数据库迁移(默认 SQLite;MySQL 需先在 .env 配 DB_ENGINE=mysql)
python manage.py migrate
# 4. 初始化数据(角色/超管/设备/围栏/时间规则,见下文「初始化数据」)
python tools/setup.py init
# 5. 开发启动(4607 端口,常开自动重载)
python manage.py runserver 0.0.0.0:46072
3
4
5
6
7
8
9
10
11
12
13
14
15
一键版(README 原始命令):
wsl -e bash -lc "source ~/miniconda3/etc/profile.d/conda.sh && conda activate qxj && cd /mnt/c/Users/etsuyou/Desktop/qxj-project/qxj-backend-admin && python manage.py runserver 0.0.0.0:4607"生产环境(gunicorn 直接 TLS)
# 前置:Redis 已启动;已生成 certs/server.crt + certs/server.key(tools/setup.py gen-cert)
DJANGO_SETTINGS_MODULE=config.settings.production gunicorn config.wsgi:application \
--bind 0.0.0.0:4607 \
--certfile certs/server.crt \
--keyfile certs/server.key \
--workers 3 \
--timeout 120 \
--access-logfile - --error-logfile -2
3
4
5
6
7
8
- 生产需
python manage.py migrate --settings=config.settings.production, 需要 Django Admin 界面再collectstatic; - 后台常驻用
nohup ... --pid logs/gunicorn.pid;平滑重启kill -HUP "$(cat logs/gunicorn.pid)"; - gunicorn 无热重载,改代码需 HUP/重启;开发 HTTPS 热重载用
runserver_plus; - 公网正式部署建议 Nginx 反代 + gunicorn(80→443 跳转、静态资源直出), 内网/Pad 联调用 gunicorn 直接 TLS 即可。详见仓内
docs/生产环境HTTPS部署-gunicorn.md。
测试
python manage.py test apps.users apps.devices默认账号与入口
| 入口 | 地址 |
|---|---|
| 前端管理端(一体化部署后) | http://127.0.0.1:4607/ |
| API 文档(Swagger) | http://127.0.0.1:4607/api/v3/docs/ |
| Django Admin | http://127.0.0.1:4607/admin/django/ |
| 路由总览 | http://127.0.0.1:4607/api/v3/ |
- 超管:
admin/Nsp123456!(另有超管nsp);开发环境WEB_LOGIN_SMS_BYPASS可免短信验证码登录; - 兜底密码可用环境变量
QXJ_INIT_ADMIN_PASSWORD覆盖(仅首次创建时设密)。
接口 URL 总览(均已落地)
| 模块 | 公开接口 | 管理接口 |
|---|---|---|
| 认证 | POST /api/v3/user/login/、POST /api/v3/user/logout/、POST /api/v3/token/refresh/ | — |
| 当前用户 | GET/PATCH /api/v3/users/me/ | — |
| SM2 握手 | POST /api/v3/keymgr/sm2/build_resp/、POST /api/v3/keymgr/sm2/build_token/(AllowAny,octet-stream 原始帧) | — |
| 用户 | — | /api/v3/admin/users/(含 {id}/set_password/) |
| 角色 | — | /api/v3/admin/roles/、/api/v3/admin/user_roles/ |
| 设备 | — | /api/v3/admin/devices/、/device-types/、/device-keys/(import_file/ multipart、{id}/deactivate/) |
| 围栏 | — | /api/v3/admin/geofences/、/role-geofences/ |
| 时间规则 | — | /api/v3/admin/time-rules/、/role-time-rules/ |
| 安全日志 | — | /api/v3/admin/security-logs/(只读+删除,statistics/、statistics/trend/?days=7、export/ CSV) |
| 密钥管理 | — | /api/v3/admin/keymgr/(服务器密钥轮换等) |
| 短信 / 邮件 | POST /api/v3/sms/send_code/ | — |
| 图形验证码 | /api/v3/captcha/... | — |
约定:字段命名全部 snake_case(旧 camelCase 已废弃);全局默认权限 IsAdmin,需普通登录的显式 IsAuthenticated,公开接口显式 AllowAny; 分页 {count,next,previous,results},page/page_size(≤100)。
目录结构
qxj-backend-admin/
├── apps/ # 14 个业务 app
│ ├── users/ # 用户(状态/有效期/失败次数/IP白名单/token_version)
│ ├── auth/ # NSP-SM 签发、BlacklistJWTAuthentication(无模型)
│ ├── keymgr/ # ServerKey(私钥加密入库)+ 握手服务 + 协议帧解析
│ ├── devices/ # 设备(eqp_unique_identifier,4 种状态)
│ ├── device_public_keys/ # 设备 SM2 公钥(active 唯一)
│ ├── roles/ # Role 继承 Group(role_type)
│ ├── user_roles/ # 用户-角色(signals 同步 staff/superuser)
│ ├── geofences/ # 地理围栏(中心点+半径,三种坐标系)
│ ├── role_geofences/ # 角色-围栏
│ ├── time_rules/ # 时间规则(ALLOW/DENY,单双周/跨夜)
│ ├── role_time_rules/ # 角色-时间规则
│ ├── security_logs/ # 安全日志(模型 + 中间件)
│ ├── sms/ # 短信 / 邮件同一验证码
│ └── captcha/ # 图形验证码
├── common/ # 公共组件(权限、分页器等)
├── config/ # 项目配置
│ ├── settings/ # base / development / production(两级 .env)
│ └── urls.py # 总路由
├── docker/ # 安全基线 Dockerfile + compose(见 docker/README.md)
├── certs/ # 自签名 CA / 证书
├── vendor/ # 离线 wheel(qxj-backend-sdk 等)
├── docs/ # 模块文档(建库、接口清单、密钥/证书、部署、审计整改等 11+ 份)
├── agent_docs/ # 开发过程文档(PROJECT_PLAN、API_MAPPING、交接文档等)
├── tools/ # setup.py(migrate/init/status/qrcode/gen-cert 等)
├── init/ # run_all.py 一键启动
├── frontend_dist/ # 前端 pnpm build:backend 产物(不入库,.gitignore)
├── tmp/ # 临时脚本(不上传 git)
├── manage.py
└── requirements.txt2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
前后端一体化部署
前端构建产物统一放在后端根目录的 frontend_dist/(构建生成,已加入 .gitignore)。在前端项目执行:
cd ../qxj-frontend-admin
pnpm install
pnpm build:backend2
3
随后按原方式启动 Django/Gunicorn:
- 访问后端端口根路径即可打开 Vue 管理端;
/api/v3/等接口仍按原路由工作;- 刷新前端 history 路由自动回退到
frontend_dist/index.html; - 后端通过 WhiteNoise 提供该目录静态资源,生产环境无需单独配置前端静态服务器; 如已有 Nginx,也可继续让 Nginx 代理/缓存静态资源;
- 生产前端 API 使用同源
/api/v3/,不需要修改主机 IP。
Docker / 离线 wheel / 证书
docker/
安全基线 Dockerfile + docker-compose(Django + Redis,Redis 密码 Nsp123456!),暴露 4607 / 6379:
cd docker
docker-compose up -d --build # 构建并后台启动
docker-compose logs -f web # 看 Django 日志
docker-compose down # 停止并删除容器(保留数据卷)2
3
4
- 首次启动自动:构建镜像 → 等 Redis → migrate → 启动 dev server;
- 源码挂载
/app,开发模式热更新;生产模式把.env.docker中DJANGO_SETTINGS_MODULE改为config.settings.production,gunicorn 4 worker × 2 thread 并自动 collectstatic; - 离线部署:
docker save qxj-backend:latest -o qxj-backend.tar/docker load -i;compose 构建前需先pnpm build:backend生成frontend_dist/,否则根路径提示先构建前端。
vendor/
离线 wheel 目录(qxj_backend_sdk-0.1.0 等),供无公网环境 pip install 使用。
certs/
自签名 CA / TLS 证书目录。python tools/setup.py gen-cert 自动生成自签证书 并探测宿主 IP 写入 SAN;gunicorn --certfile/--keyfile 直接引用 certs/server.crt / certs/server.key;正式部署换 CA 签发的 fullchain 证书即可。
配置项(config/settings 三份 + 环境变量)
两级 .env 加载
进程环境变量 > .env.<环境> > .env。模板:.env.example / .env.development.example / .env.production.example;gunicorn (wsgi.py 默认 production)启动时自动加载 .env.production 并覆盖 .env 同名项。
关键环境变量
| 变量 | 说明 |
|---|---|
SECRET_KEY | Django 密钥,生产必须随机长字符串(fail-closed) |
QXJ_KEY_ENC_KEY | SM2 私钥主密钥(64 hex)。优先读环境变量;未设置时非生产用 sha256('keymgr_master:' + SECRET_KEY) 派生;生产缺失 fail-closed。初始化服务器密钥后不可更改 |
REDIS_URL | Redis 连接(SM2 会话、token 黑名单、缓存、验证码都依赖) |
DB_ENGINE | 默认 SQLite;mysql 切 MySQL(utf8mb4,需 mysqlclient) |
USE_HTTPS | 生产开启 HSTS、安全 Cookie、SSL 重定向 |
ALLOWED_HOSTS | 逗号分隔;漏配返回 400 DisallowedHost |
CORS_ALLOWED_ORIGINS | 生产强制非空(无跨域也需占位) |
ACCESS_TOKEN_LIFETIME_HOURS | access 寿命,默认 12 小时 |
REFRESH_TOKEN_LIFETIME_DAYS | refresh 寿命,默认 7 天 |
ALLOW_JSON_LOGIN | JSON 明文登录开关(默认 false) |
ALLOW_DEVICE_LOGIN | 设备加密帧登录开关(默认 true) |
VERIFY_INNER_HMAC | 内层 HMAC 校验(base false / production true) |
ENABLE_DJANGO_ADMIN | Django Admin 入口开关 |
WEB_LOGIN_SMS_BYPASS | Web 登录免短信验证码(DEBUG 时生效) |
ALLOW_LOGIN_WITHOUT_PHONE | 允许无手机号用户登录 |
SKIP_TIME_RULE_CHECK / SKIP_GEOFENCE_CHECK | 跳过时间规则 / 围栏校验 |
QXJ_INIT_ADMIN_PASSWORD | 初始化超管密码覆盖 |
TRUSTED_PROXY_COUNT | 代理层数(限流 NUM_PROXIES 跟随) |
settings 要点
- CACHES:django-redis,JSONSerializer(禁 pickle),max_connections=50, KEY_PREFIX=
qxj、VERSION=1; - DRF:默认认证 BlacklistJWTAuthentication、默认权限 IsAdmin、分页 PAGE_SIZE=10;限流 anon 60/min、user 600/min、sms 5/min、captcha 20/min;
- production:fail-closed(SECRET_KEY / QXJ_KEY_ENC_KEY 缺失即拒绝启动)、 仅 JSONRenderer、SSL 跳转/HSTS/安全 Cookie、CORS 必须显式配置、日志落
logs/; - development:Swagger 开、CORS 全放、console 日志。
主密钥一致性
服务器 gunicorn 报「密钥协商失败」多为 QXJ_KEY_ENC_KEY 与建库时不一致 (runserver 与 gunicorn 走不同 settings 导致派生值不同),需对齐后重启。
核心链路
SM2 握手服务(AllowAny,octet-stream 原始帧)
| 接口 | 逻辑 |
|---|---|
POST /api/v3/keymgr/sm2/build_resp/ | INIT 帧 SHA256 指纹 SET NX 防重放 → 按帧内 IDA 查设备 + active DevicePublicKey(忽略请求体公钥)→ C 库 nsp_sm2_build_resp_packet(B 侧长期密钥 + 每次临时密钥对)→ 会话上下文 sm2:session:{uuid}(10 分钟)+ pending_ack 登记 |
POST /api/v3/keymgr/sm2/build_token/ | ACK 帧(仅 total_len+SA),遍历 pending 会话用 C 库验 SA 自证定位(会话级原子锁)→ nsp_sm2_build_token_packet → 发布 sm2:device_session_key:{device_id}(TTL 7 天) |
失败统一返回 HTTP 200 + 报警帧(0x8002/0x8003/0x8004),不回显内部细节。
NSP-SM Token(双层签名)
- 登录后
_make_tokens组装 payload(jti/exp/iat/token_type/sid/tv/custom),sm2_jwt.encode:header{"alg":"NSP-SM","typ":"JWT"},SM2withSM3 (先 SM3 摘要再 SM2 签名,r‖s 64B,签名随机数强制 CSPRNG); - 内层:按 device_id 取会话密钥,对
device_id(UTF-8) + user_id(>Q) + exp(>Q)算 SM3-HMAC(纯 Python RFC2104)放入 custom.hmac;验签要求 exp_s == 外层 exp, compare_digest 比对。
登录 / 刷新 / 登出
POST /api/v3/user/login/:按 Content-Type 分 JSON / SM4-GCM 信封两路; 加密登录密文 SHA256 SET NX 防重放;device_id 必须等于帧头 sender; 非超管校验时间规则与短信/邮箱验证码(设备登录校验地理围栏);POST /api/v3/token/refresh/:刷新不轮换 refresh;POST /api/v3/user/logout/:jti 黑名单 + sid 撤销。
访问控制(未使用 Casbin)
- DRF 默认 IsAdmin(is_staff 或 is_superuser),高敏操作 IsSuperUser, 公开接口显式 AllowAny;
- BlacklistJWTAuthentication:外层验签 → token_type=access → 黑名单/撤销 → 账户有效 → tv 版本一致 → 内层 HMAC(含设备有效性);
- 业务级限制:角色时间规则 + 角色地理围栏,登录时校验,超管豁免,DENY 优先。
tools 工具脚本
| 工具 | 用途 |
|---|---|
tools/setup.py | 统一 CLI:migrate、init(随机 SM2;--fixed 仅 DEBUG;--overwrite 轮换)、status、verify、qrcode(PNG+txt)、gen-cert(自签 TLS 自动探测宿主 IP)、gen-token、inspect |
tools/gen_token.py | 离线签发测试 token,支持 --verify 请求受保护接口 |
tools/register_device/import_keys.py | 扫 files/*.dat(device_id#时间戳#公钥),自动注册设备/类型并导入公钥;files/ 内现有 4 台真机 .dat |
tools/init/run_all.py | 初始化:migrate → 角色 → 管理员 → 设备 → 围栏 → 时间规则 |
初始化数据(tools/init)
- 角色 5 个:管理员、一般老师、一般学生、二室老师、二室学生;
- 超管 2 个:admin(zhouyueyang@iie.ac.cn,18013287438)、nsp(nsp@nsp.ac.cn); 测试用户:lzf(二室老师)、zyy/ysh(二室学生)、lwh(一般学生,无手机); 兜底密码
Nsp123456!; - 围栏 4 个:天安门(r2653m)、信工所大(r197m)/小(r87m)、宿舍(r100m),GCJ-02;
- 时间规则 10 条(4 条「周六轮换」默认停用,可用
tools.init.schedule toggle切换)。
文档现状
README 的模块进度表偏旧(认证/短信/密钥已完成仍标待做);docs/ 下有 11+ 份操作文档 (建库、接口清单、密钥/证书、部署、审计整改、测试报告复现等)与 agent_docs/ 交接文档 (PROJECT_PLAN、API_MAPPING、NEXT_AGENT_HANDOFF 等)。