DMS 系统 SSO 单点登录完整技术方案
前后端集成实现详解
| 项目名称 |
DMS 环境监测系统 |
软件版本 |
V3.1.5 |
| 模块名称 |
OAuth2.0 单点登录集成(前端 + 后端) |
| 文档版本 |
V2.0(完整版) |
文档日期 |
2026-04-20 |
| 技术栈 |
前端:Vue 3 + Vue Router + Axios + Ant Design Vue
后端:Django 4.x + Django REST Framework + MySQL + JWT |
1. 方案概述
本方案详细描述了 DMS 系统如何与 C大脑门户实现 OAuth2.0 单点登录(SSO)集成的前端实现细节。采用业界标准的 OAuth2.0 授权码模式(Authorization Code Grant),确保身份认证的安全性、标准化和可维护性。
核心特点:
- 动态配置:所有 SSO 参数从后端接口动态获取,无需重启前端服务
- 智能路由:根据用户类型(SSO/传统)自动选择登录方式
- 无感续期:Token 过期后 SSO 用户自动跳转至认证中心重新认证
- 双重兼容:保留传统账号密码登录,作为应急备份通道
- 安全机制:支持 State 参数防 CSRF 攻击,密钥数据库存储
2. 整体架构设计
2.1 技术架构图
用户访问 DMS
→
路由守卫检测
→
未登录?
→
跳转 C大脑认证
C大脑返回 Code
→
前端回调页面
→
调用后端接口
→
获取 JWT Token
保存 Token
→
标记 SSO 用户
→
跳转首页
2.2 核心文件清单
| 文件路径 |
功能说明 |
关键职责 |
| src/config/axios-config.js |
Axios 拦截器配置 |
Token 自动注入、401 错误处理、SSO 重定向逻辑 |
| src/config/setting.js |
全局配置文件 |
白名单路由、Token 存取方法、用户信息管理 |
| src/router/index.js |
路由配置与守卫 |
OAuth2 回调路由注册、登录状态判断、动态路由加载 |
| src/views/station/oauth/callback.vue |
OAuth2 回调页面 |
接收 Code、调用后端登录接口、保存 Token、标记 SSO 用户 |
| src/views/login/login.vue |
传统登录页面 |
账号密码登录入口(应急备份) |
3. 核心流程详解
3.1 首次 SSO 登录流程
步骤 1:用户访问受保护路由
当用户直接访问 DMS 系统或 Token 过期时,触发路由守卫逻辑。
// src/router/index.js - 路由守卫 router.beforeEach((to, from, next) => { NProgress.start(); updateTitle(to); // 判断是否登录 if (setting.takeToken()) { // 已登录,检查动态路由 if (!store.state.user.menus) { store.dispatch('user/getMenus').then(({menus, home}) => { router.addRoute({ path: '/', component: EleLayout, redirect: setting.homePath || home, children: menuToRoutes(menus, (component) => import('@/views' + component)) }); next({...to, replace: true}); }).catch(() => { next(); }); } else { next(); } } else if (setting.whiteList.includes(to.path)) { // 白名单路由,直接放行 next(); } else if (to.path === '/oauth2/callback/') { // 【关键】OAuth2 回调页,直接放行让页面处理 Code next(); } else if (to.query.code) { // 其他页面带了 code,跳转到回调页 next('/oauth2/callback/'); } else { // 未登录且无 code,跳转到传统登录页 next({path: '/login', query: to.path === '/' ? {} : {from: to.path}}); } });
步骤 2:判断是否需要 SSO 登录
在传统登录页面或业务逻辑中,可以主动触发 SSO 登录。
注意:当前版本中,用户需要手动点击"SSO 登录"按钮或通过特定入口触发。未来可以优化为自动检测并跳转。
步骤 3:构建 OAuth2 授权 URL
调用 redirectToSSO() 函数,动态从后端获取配置并构造授权 URL。
// src/config/axios-config.js - redirectToSSO 函数 async function redirectToSSO() { // 清除本地标记和 Token await store.dispatch('user/removeToken'); localStorage.removeItem('is_sso_user'); try { // 【动态获取】从后端接口获取最新的 SSO 配置 const res = await axios.get('/cbrain/detail'); if (res.data.code === 0 && res.data.data) { const config = res.data.data; // 直接使用数据库中配置的完整授权地址 const authorizeUrl = config.authorize_url; const redirectUri = config.redirect_uri; // 构造 OAuth2 参数字符串 const paramsStr = `response_type=code&client_id=${config.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid,userinfo`; // 【严格匹配客户文档】将参数拼在 # 后面 let finalAuthUrl = ''; if (authorizeUrl.includes('?')) { // 如果地址已有查询参数使用&连接 finalAuthUrl = `${authorizeUrl}&${paramsStr}`; } else { // 如果地址没有查询参数使用?连接 finalAuthUrl = `${authorizeUrl}?${paramsStr}`; } console.log('正在重定向至 SSO 认证中心:', finalAuthUrl); window.location.href = finalAuthUrl; } else { console.error('获取 SSO 配置失败,回退到密码登录'); location.replace('/login'); } } catch (error) { console.error('重定向至 SSO 提供商异常:', error); location.replace('/login'); } }
URL 拼接逻辑说明:
- 场景 1:authorize_url =
http://example.com/path?param1=value1 → 使用 & 连接
- 场景 2:authorize_url =
http://example.com/path → 使用 ? 连接
- 这种设计确保了无论管理员如何配置 URL,都能生成合法的 OAuth2 授权链接
步骤 4:浏览器跳转至 C大脑认证中心
执行 window.location.href = finalAuthUrl,浏览器跳转到 C大脑登录页面。
// 最终生成的 URL 示例 http://10.230.8.92/brain-platform/#/Oauth?response_type=code&client_id=b3240ffe2ed640e09b38c5b968a96d02&redirect_uri=http%3A%2F%2F192.168.0.75%2Foauth2%2Fcallback%2F&scope=openid,userinfo
步骤 5:用户在 C大脑完成认证
用户在 C大脑门户输入账号密码(或已登录),C大脑验证通过后,携带 code 参数重定向回 DMS 的回调地址。
// C大脑重定向回 DMS https://192.168.0.75/oauth2/callback/?code=wzxk2LhzaGsMrrYND9fK7NKLkWLLP8BSElD27Zme3urgiyfTP4BdozaZaRgP
3.2 OAuth2 回调处理流程
步骤 6:回调页面接收 Code
Vue Router 匹配到 /oauth2/callback/ 路由,加载回调组件。
<!-- src/views/station/oauth/callback.vue --> <template> <div class="oauth-callback-container"> <div class="loading-box"> <a-spin tip="正在验证身份..." /> </div> </div> </template> <script> export default { name: 'OAuthCallback', async created() { console.log('=== OAuth 回调页面已加载 ==='); console.log('当前完整 URL:', window.location.href); // 【双重保险】优先用 Vue Router,如果拿不到再尝试原始解析 let code = this.$route.query.code; if (!code) { const urlParams = new URLSearchParams(window.location.search); code = urlParams.get('code'); } console.log('最终获取到的 Code:', code); if (!code) { this.$message.error('授权失败:缺少必要参数 (Code)'); console.error('错误原因:URL 中未检测到 code 参数'); return; } try { // 从后端接口获取最新的 SSO 配置 const configRes = await this.$http.get('/cbrain/detail'); if (configRes.data.code !== 0 || !configRes.data.data) { this.$message.error('获取 SSO 配置失败'); return; } const cbrainConfig = configRes.data.data; // 从 redirect_uri 中提取后端 API 路径 const redirectUri = cbrainConfig.redirect_uri; let callbackPath = new URL(redirectUri).pathname; // 【关键】确保路径以斜杠结尾,避免 Django APPEND_SLASH 报错 if (!callbackPath.endsWith('/')) { callbackPath += '/'; } // 调用后端接口完成登录(使用 axios baseURL + 相对路径) const res = await this.$http.post(callbackPath, { code }); if (res.data.code === 0) { // 保存 Token (注意:Store 需要传入对象格式) this.$store.commit('user/SET_TOKEN', { token: res.data.data.token, remember: true }); // 保存用户信息 if (res.data.data.userInfo) { this.$store.commit('user/SET', { key: 'user', value: res.data.data.userInfo }); } // 【关键】标记当前为 SSO 登录用户,用于 Token 过期后的自动重定向 localStorage.setItem('is_sso_user', 'true'); this.$message.success('登录成功'); // 跳转到首页 this.$router.push('/'); } else { // 处理业务错误(如:不在白名单中) this.$message.error(res.data.msg || '登录失败'); } } catch (error) { console.error('OAuth 登录异常:', error); this.$message.error('网络异常或登录服务不可用'); } } } </script>
关键技术点:
- 双重 Code 提取:优先使用 Vue Router 的
this.$route.query.code,失败时使用原生 URLSearchParams 解析,提高兼容性
- 动态回调路径:从数据库配置的
redirect_uri 中提取 pathname,确保与后端路由一致
- 斜杠处理:强制确保回调路径以
/ 结尾,避免 Django 的 APPEND_SLASH 机制导致 301 重定向丢失 POST 数据
- SSO 标记:设置
localStorage.setItem('is_sso_user', 'true'),这是后续智能续期的关键
3.3 Token 自动续期流程
步骤 7:Token 过期检测与智能重定向
当用户访问受保护接口时,如果 Token 过期(后端返回 401),Axios 响应拦截器会捕获并判断用户类型。
// src/config/axios-config.js - 响应拦截器 axios.interceptors.response.use((res) => { // 登录过期处理 if (res.data.code === 401) { if (res.config.url === setting.menuUrl) { goLogin(); } else { Modal.destroyAll(); // 【方案一】智能重定向:判断是否为 SSO 用户 const isSsoUser = localStorage.getItem('is_sso_user') === 'true'; if (isSsoUser) { // SSO 用户:直接跳回 SSO 提供商重新认证(无感续期) Modal.info({ title: '身份认证过期', content: '请重新进行单点登录认证', okText: '确定', onOk: () => { redirectToSSO(); } }); } else { // 传统用户:跳去密码登录页 Modal.info({ title: '系统提示', content: '登录状态已过期, 请退出重新登录!', okText: '重新登录', onOk: () => { goLogin(true); } }); } } return Promise.reject(new Error(res.data.msg)); } // token自动续期 const access_token = res.headers['x-new-token']; if (access_token) { setting.cacheToken(access_token); } return res; }, (error) => { return Promise.reject(error); });
智能续期优势:
- SSO 用户:点击"确定"后自动跳转至 C大脑,由于 C大脑会话仍然有效,用户无需再次输入密码,实现"无感续期"
- 传统用户:跳转到本地登录页,需要重新输入账号密码
- 用户体验:大幅减少 SSO 用户的重复登录操作,提升满意度
4. 配置管理
4.1 前端配置文件
4.1.1 白名单路由配置
// src/config/setting.js export default { // 不需要登录的路由(OAuth2 回调路径需与数据库配置一致) whiteList: ['/login', '/forget', '/oauth2/callback/'], // token存储的名称 tokenStoreName: 'access_token', // 用户信息存储的名称 userStoreName: 'user', // ... 其他配置 }
重要说明:
/oauth2/callback/ 必须加入白名单,否则路由守卫会拦截并重定向到登录页,导致无法接收 Code 参数。
4.1.2 Token 管理方法
// src/config/setting.js - Token 存取方法 export default { /** * 获取缓存的token的方法 * @returns {string} */ takeToken() { let token = localStorage.getItem(this.tokenStoreName); if (!token) { token = sessionStorage.getItem(this.tokenStoreName); } return token; }, /** * 缓存token的方法 * @param token * @param remember 是否永久存储 */ cacheToken(token, remember) { localStorage.removeItem(this.tokenStoreName); sessionStorage.removeItem(this.tokenStoreName); if (token) { if (remember) { localStorage.setItem(this.tokenStoreName, token); } else { sessionStorage.setItem(this.tokenStoreName, token); } } } }
4.2 后端配置接口
前端通过 GET /cbrain/detail 接口动态获取 SSO 配置,该接口返回以下字段:
| 字段名 |
类型 |
说明 |
示例值 |
| authorize_url |
String |
C大脑授权地址(完整 URL) |
http://10.230.8.92/brain-platform/#/Oauth |
| token_url |
String |
Token 交换接口地址 |
http://.../cbrain-auth/oauth2/token |
| userinfo_url |
String |
用户信息接口地址 |
http://.../cbrain-auth/oauth2/userInfo |
| client_id |
String |
应用标识 |
b3240ffe2ed640e09b38c5b968a96d02 |
| client_secret |
String |
应用密钥(仅后端使用) |
不返回给前端 |
| redirect_uri |
String |
回调地址(完整 URL) |
https://192.168.0.75/oauth2/callback/ |
| is_active |
Boolean |
SSO 功能是否启用 |
true |
安全提醒:
client_secret 绝对不能暴露给前端!前端只需要 authorize_url、client_id 和 redirect_uri 即可构建授权 URL。Token 交换必须在后端完成。
5. 关键技术点
5.1 路由守卫策略
| 场景 |
条件判断 |
处理方式 |
| 已登录且有菜单 |
setting.takeToken() 且 store.state.user.menus 存在 |
直接放行 next() |
| 已登录但无菜单 |
setting.takeToken() 但 !store.state.user.menus |
获取菜单并动态添加路由,然后 next({...to, replace: true}) |
| 访问白名单 |
setting.whiteList.includes(to.path) |
直接放行 next() |
| OAuth2 回调页 |
to.path === '/oauth2/callback/' |
直接放行 next(),让页面自己处理 Code |
| 其他页面带 Code |
to.query.code 存在 |
重定向到回调页 next('/oauth2/callback/') |
| 未登录且无 Code |
以上条件都不满足 |
跳转到传统登录页 next({path: '/login', ...}) |
5.2 URL 参数编码
在构建 OAuth2 授权 URL 时,redirect_uri 必须进行 URL 编码:
// 正确做法 const redirectUri = config.redirect_uri; // https://192.168.0.75/oauth2/callback/ const paramsStr = `redirect_uri=${encodeURIComponent(redirectUri)}`; // 结果:redirect_uri=https%3A%2F%2F192.168.0.75%2Foauth2%2Fcallback%2F // 错误做法(会导致参数解析失败) const paramsStr = `redirect_uri=${redirectUri}`; // 结果:redirect_uri=https://192.168.0.75/oauth2/callback/ ❌
5.3 LocalStorage vs SessionStorage
| 存储方式 |
生命周期 |
适用场景 |
SSO 中的使用 |
| localStorage |
永久存储,除非手动清除 |
"记住我"功能 |
存储 is_sso_user 标记、长期有效的 Token |
| sessionStorage |
浏览器关闭后自动清除 |
临时会话 |
短期 Token(未勾选"记住我"时) |
5.4 回调路径斜杠处理
常见陷阱:
Django 默认开启 APPEND_SLASH,如果回调路径不以 / 结尾,会发生 301 重定向,导致 POST 请求变成 GET 请求,Code 参数丢失!
// 解决方案:强制确保路径以斜杠结尾 let callbackPath = new URL(redirectUri).pathname; if (!callbackPath.endsWith('/')) { callbackPath += '/'; } // 例如:/oauth2/callback → /oauth2/callback/
6. 异常处理机制
6.1 前端异常场景
| 异常场景 |
触发条件 |
处理方式 |
用户提示 |
| 缺少 Code 参数 |
回调 URL 中没有 code 参数 |
显示错误消息,停留在回调页 |
"授权失败:缺少必要参数 (Code)" |
| 获取配置失败 |
/cbrain/detail 接口返回错误或网络异常 |
回退到传统登录页 |
控制台输出错误日志 |
| 后端登录失败 |
后端返回 code !== 0 |
显示后端返回的错误消息 |
例如:"工号 XXX 未在 DMS 授权名单中" |
| 网络异常 |
调用后端接口时发生网络错误 |
捕获异常并提示 |
"网络异常或登录服务不可用" |
| Token 过期 |
后端返回 401 状态码 |
弹出模态框,根据用户类型选择重定向方式 |
SSO 用户:"身份认证过期";传统用户:"登录状态已过期" |
6.2 降级策略
多层降级保障:
- 第一层:SSO 配置获取失败 → 回退到传统登录页
- 第二层:SSO 登录失败 → 用户仍可使用账号密码登录
- 第三层:Token 续期失败 → 提示用户重新登录
7. 安全性设计
7.1 安全措施清单
| 安全措施 |
实现方式 |
防护目标 |
| 密钥隔离 |
client_secret 仅存储在后端数据库,不暴露给前端 |
防止密钥泄露 |
| HTTPS 传输 |
生产环境强制使用 HTTPS 协议 |
防止中间人攻击 |
| 一次性 Code |
OAuth2 Code 只能使用一次,使用后失效 |
防止重放攻击 |
| Token 自动刷新 |
后端返回 x-new-token Header,前端自动更新 |
延长会话有效期 |
| CSRF 防护 |
支持 State 参数校验(需在授权 URL 中添加) |
防止跨站请求伪造 |
| 白名单机制 |
后端校验用户工号是否在本地数据库中存在 |
防止未授权访问 |
待优化项:
当前代码中尚未实现 State 参数的生成和校验。建议在 redirectToSSO() 函数中添加随机 State 参数,并在回调时验证,以增强 CSRF 防护能力。
8. 测试建议
8.1 功能测试用例
| 测试场景 |
测试步骤 |
预期结果 |
| 正常 SSO 登录 |
1. 清除本地 Token
2. 触发 SSO 登录
3. 在 C大脑完成认证 |
成功跳转回 DMS,显示首页,LocalStorage 中有 is_sso_user=true |
| Token 过期续期 |
1. 模拟 Token 过期(后端返回 401)
2. 点击"确定"重新认证 |
跳转至 C大脑,自动完成认证(无感续期),返回 DMS 后恢复正常 |
| 白名单拦截 |
使用未在 DMS 注册的工号登录 C大脑 |
后端返回错误,前端显示"工号 XXX 未在 DMS 授权名单中" |
| 配置获取失败 |
模拟 /cbrain/detail 接口返回 500 错误 |
前端回退到传统登录页,控制台输出错误日志 |
| 缺少 Code 参数 |
直接访问 /oauth2/callback/(不带 code) |
显示"授权失败:缺少必要参数 (Code)" |
| 传统登录兼容 |
使用账号密码登录 |
登录成功,is_sso_user 不被设置或为 false |
8.2 兼容性测试
- 浏览器兼容:Chrome、Firefox、Edge、Safari 最新版本
- URL 格式兼容:测试带参数和不带参数的
authorize_url 配置
- 回调路径兼容:测试带斜杠和不带斜杠的
redirect_uri 配置
9. 部署注意事项
9.1 环境变量配置
# .env.production VUE_APP_API_BASE_URL=https://dms.samc.intra/api VUE_APP_SYSTEM_NAME=DMS 环境监测系统 BASE_URL=/
9.2 Nginx 配置要点
# Nginx 配置示例 location /oauth2/callback/ { # 确保回调路径正确代理到后端 proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # Vue Router History 模式必需 }
关键配置:
Vue Router 使用 History 模式时,Nginx 必须配置 try_files $uri $uri/ /index.html;,否则刷新页面会出现 404 错误。
9.3 数据库配置检查
确保 application_cbrain 表中的配置正确:
- authorize_url:必须是完整的 C大脑授权地址,包含协议和路径
- redirect_uri:必须与 C大脑后台配置的回调地址完全一致(包括协议、域名、端口、路径)
- client_id:从 C大脑运维团队获取的正确应用 ID
- is_active:设置为
true 启用 SSO 功能
10. 常见问题排查
10.1 Code 参数丢失
问题现象:回调页面显示"缺少必要参数 (Code)"
可能原因:
- Nginx 配置错误,导致 URL 参数被截断
- Django
APPEND_SLASH 导致 301 重定向,POST 变 GET
- C大脑配置的
redirect_uri 与实际不一致
解决方案:
- 检查浏览器地址栏,确认 URL 中包含
?code=xxx
- 检查 Nginx 日志,确认没有发生意外的重定向
- 确保回调路径以
/ 结尾
10.2 CORS 跨域错误
问题现象:控制台显示 CORS 错误,无法调用后端接口
解决方案:
- 确保后端 Django 配置了正确的
CORS_ALLOWED_ORIGINS
- 开发环境下,检查
vue.config.js 中的代理配置
- 生产环境下,确保前后端同源或通过 Nginx 统一入口
10.3 Token 无法保存
问题现象:登录成功后刷新页面,又回到登录页
可能原因:
- 浏览器禁用了 Cookie 或 LocalStorage
- Token 存储逻辑有误
- 路由守卫判断条件错误
解决方案:
- 打开浏览器开发者工具,检查 Application → Local Storage 中是否有
access_token
- 检查 setting.cacheToken() 方法是否正确调用
- 检查 setting.takeToken() 是否能正确读取 Token
11. 后端技术方案详解
11.1 后端架构概览
DMS 系统的 SSO 后端采用 Django 4.x 框架,基于 OAuth2.0 授权码模式实现与 C大脑门户的单点登录集成。后端主要负责配置管理、Token 交换、用户匹配和会话建立等核心功能。
11.1.1 核心模块结构
| 模块名称 |
文件路径 |
职责说明 |
| OAuth2 应用 |
application/oauth2/ |
处理 OAuth2 回调、Token 交换、用户匹配等核心认证逻辑 |
| CBrain 配置应用 |
application/cbrain/ |
管理 SSO 配置参数(Client ID、Secret、URLs 等) |
| User 模型扩展 |
application/user/models.py |
扩展用户表字段,支持 SSO 用户标识和审计 |
| JWT 工具 |
utils/jwts.py |
生成和验证本地 JWT Token |
| 事件日志 |
utils/event_logger.py |
记录 SSO 登录事件到系统日志表 |
11.1.2 后端技术栈
- Web 框架:Django 4.x + Django REST Framework
- 数据库:MySQL 8.0+
- HTTP 客户端:Requests 库(用于调用 C大脑 API)
- 认证机制:JWT (JSON Web Token)
- 日志系统:Python logging + 自定义事件日志
- 部署方式:Waitress (Windows) / Gunicorn (Linux) + Nginx
11.2 数据库设计
11.2.1 CBrain 配置表(application_cbrain)
该表存储 SSO 集成的所有配置参数,支持动态修改而无需重启服务。
| 字段名 |
类型 |
必填 |
默认值 |
说明 |
| id |
Integer |
是 |
1 |
主键,固定为 1(单例配置) |
| sso_base_url |
Varchar(255) |
是 |
https://cbrain.comac.intra |
SSO 提供商的基础域名 |
| authorize_url |
Varchar(500) |
是 |
完整 URL |
OAuth2 授权地址(完整路径) |
| token_url |
Varchar(500) |
是 |
完整 URL |
Token 交换接口地址 |
| userinfo_url |
Varchar(500) |
是 |
完整 URL |
用户信息接口地址 |
| client_id |
Varchar(100) |
是 |
b3240ffe... |
OAuth2 应用 ID |
| client_secret |
Varchar(255) |
是 |
qDyfReC6... |
OAuth2 应用密钥(敏感信息) |
| redirect_uri |
Varchar(500) |
是 |
完整 URL |
OAuth2 回调地址(必须与 C大脑后台配置一致) |
| is_active |
Boolean |
否 |
False |
保留字段,当前架构下 SSO 始终启用 |
| updated_at |
DateTime |
自动 |
auto_now |
最后更新时间(自动更新) |
| updated_by |
Varchar(50) |
否 |
NULL |
最后修改配置的用户名 |
设计亮点:
- 单例模式:通过
get_or_create(id=1) 确保只有一条配置记录
- 动态加载:每次请求都从数据库读取最新配置,修改后立即生效
- 完整 URL:所有端点 URL 都是完整路径,避免拼接错误
- 审计追踪:记录更新时间和更新人,便于问题追溯
11.2.2 User 用户表扩展字段
为了支持 SSO 集成,在原有的 application_user 表中新增了以下字段:
| 字段名 |
类型 |
索引 |
说明 |
| source |
Varchar(20) |
是 |
用户来源标识
可选值:local(本地创建)、sso(SSO单点登录)、import(批量导入)、api(API同步)、other(其他)
默认值:local |
| cbrain_user_id |
Varchar(50) |
是 |
C大脑工号(如:AI0074)
用于匹配 C大脑返回的 userId
可为空,SSO 登录后自动填充 |
| oauth_openid |
Varchar(100) |
否 |
C大脑用户的唯一标识 OpenID
用于后续可能的用户信息同步
可为空,SSO 登录后自动填充 |
| last_sso_login |
DateTime |
否 |
记录用户最后一次通过 C大脑 SSO 登录的时间
用于审计和用户行为分析
可为空 |
迁移注意:
这些字段通过 Django Migration 文件 0002_user_cbrain_user_id_user_last_sso_login_and_more.py 自动添加,无需手动执行 SQL。
11.3 核心业务流程
11.3.1 配置管理流程
(1)获取配置接口
接口地址:GET /api/cbrain/detail
权限要求:无需登录(公开接口,供前端构建授权 URL 使用)
# application/cbrain/views.py class CBrainDetailView(View): def get(self, request): # 调用查询参数详情方法 data = services.CBrainDetail() # 返回结果 return R.ok(data=data)
# application/cbrain/services.py def CBrainDetail(): """查询 SSO/OAuth2 配置详情""" cbrain_obj = CBrain.objects.filter(id=1).first() if not cbrain_obj: return None data = { 'sso_base_url': cbrain_obj.sso_base_url, 'authorize_url': cbrain_obj.authorize_url, 'token_url': cbrain_obj.token_url, 'userinfo_url': cbrain_obj.userinfo_url, 'client_id': cbrain_obj.client_id, 'client_secret': cbrain_obj.client_secret, # ⚠️ 注意:生产环境应脱敏 'redirect_uri': cbrain_obj.redirect_uri, 'is_active': True, # SSO 始终启用(双轨制架构) 'updated_at': cbrain_obj.updated_at.strftime('%Y-%m-%d %H:%M:%S') if cbrain_obj.updated_at else '', 'updated_by': cbrain_obj.updated_by or '', } return data
安全提醒:
当前实现中,client_secret 会返回给前端!虽然前端不使用该字段,但存在安全风险。
建议优化:在 CBrainDetail() 函数中移除或脱敏 client_secret 字段,仅在后端内部使用。
(2)更新配置接口
接口地址:PUT /api/cbrain/update
权限要求:需要登录 + 权限标识 sys:cbrain:update
# application/cbrain/views.py @method_decorator(check_login, name='put') class CBrainUpdateView(PermissionRequired, View): permission_required = ('sys:cbrain:update',) def put(self, request): if IS_DEMO_MODE: return R.failed("演示环境,暂无操作权限") result = services.CBrainUpdate(request) return result
请求体示例:
{ "form": { "sso_base_url": "https://cbrain.comac.intra", "authorize_url": "https://cbrain.comac.intra/brain-platform/#/Oauth", "token_url": "https://cbrain.comac.intra/cbrain-gateway/cbrain-auth-server/cbrain-auth/oauth2/token", "userinfo_url": "https://cbrain.comac.intra/cbrain-gateway/cbrain-auth-server/cbrain-auth/oauth2/userInfo", "client_id": "b3240ffe2ed640e09b38c5b968a96d02", "client_secret": "qDyfReC6OdonsGfgy2RVdyAB", "redirect_uri": "https://dms.samc.intra/api/oauth2/callback/", "is_active": true }, "signInfo": { "username": "admin", "comment": "更新生产环境 SSO 配置" } }
11.3.2 OAuth2 回调处理流程
(1)回调接口定义
接口地址:POST /api/oauth2/callback/
权限要求:无需登录(白名单路由)
# application/oauth2/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.OAuth2CallbackView.as_view()), # 注意:末尾有斜杠 ]
(2)回调视图实现
# application/oauth2/views.py class OAuth2CallbackView(View): """ C大脑 OAuth2 登录回调视图 接收前端传来的 code,换取 token 并生成本地 JWT 仅支持 POST 方法(由前端主动调用) """ def post(self, request): try: # 1. 解析请求参数 data = json.loads(request.body) code = data.get('code') if not code: logger.warning("OAuth2 回调缺少 code 参数") return R.failed('缺少授权码') logger.info(f"收到 OAuth2 回调请求,code: {code[:20]}...") # 2. 用 Code 换取 Access Token token_data = OAuth2Service.exchange_code_for_token(code) if not token_data: logger.error("换取 Token 失败,可能是 Code 已失效或配置错误") return R.failed('认证服务响应异常或 Code 已失效') access_token = token_data.get('access_token') if not access_token: return R.failed('获取 Access Token 失败') # 3. 本地白名单校验与登录处理(兼容标准流程和特殊流程) user = OAuth2Service.handle_local_login(token_data) # 4. 生成 DMS 系统的本地 JWT jwt_token = create_token({'userId': user.id}) logger.info(f"用户 {user.username} 通过 C大脑 SSO 登录成功") return R.ok(data={ 'token': jwt_token, 'userInfo': { 'id': user.id, 'realname': user.realname, } }) except PermissionError as e: logger.warning(f"SSO 登录被拒绝: {str(e)}") return R.failed(str(e)) except Exception as e: logger.exception(f"SSO 登录处理发生未知错误: {e}") return R.failed(f'登录处理失败: {str(e)}')
(3)Token 交换服务(核心逻辑)
这是整个 SSO 流程中最关键的部分,实现了双重兼容机制:
# application/oauth2/services.py - exchange_code_for_token 方法 @staticmethod def exchange_code_for_token(code): """用 Code 换取 Access Token(兼容标准和特殊流程)""" try: config = OAuth2Service._get_config() # 准备 Token 交换参数 token_params = { 'grant_type': 'authorization_code', 'client_id': config['client_id'], 'client_secret': config['client_secret'], 'code': code, 'redirect_uri': config['redirect_uri'] } # 【方案 A:标准 OAuth2.0 规范】POST 请求 + Form Body logger.info("[方案 A] 正在尝试标准 OAuth2.0 方式 code 换取 Token (POST Body)") resp = requests.post(config['token_url'], data=token_params, timeout=10) logger.info(f"[方案 A] 响应状态码: {resp.status_code}") # 【兼容性检查】如果标准方式返回 400/405,尝试【方案 B:客户特定实现】 if resp.status_code in [400, 405]: logger.warning("[方案 A] 失败,自动切换至 [方案 B] 兼容模式 (POST with URL Params)...") from urllib.parse import urlencode full_url_b = f"{config['token_url']}?{urlencode(token_params)}" logger.info(f"[方案 B] 目标 URL (含参): {full_url_b}") resp = requests.post(config['token_url'], params=token_params, timeout=10) logger.info(f"[方案 B] 响应状态码: {resp.status_code}") if resp.status_code == 200: result = resp.json() logger.info(f"C大脑 Token 接口原始响应数据: {result}") # 提取关键字段 access_token = result.get('access_token') user_id = result.get('userId') # C大脑特殊字段 openid = result.get('openid') if not access_token: logger.error(f"C大脑响应中缺少 access_token: {result}") return None # 返回标准化的字典 token_data = { 'access_token': access_token, 'userId': user_id, # 可能为 None(标准流程) 'openid': openid } # 【新增兼容性】如果 Token 响应中没有 userId,尝试调用 UserInfo 接口获取 if not user_id: logger.info("Token 响应中未包含 userId,尝试调用 UserInfo 接口获取用户信息...") user_info = OAuth2Service.get_user_info(access_token) if user_info: logger.info(f"成功从 UserInfo 接口获取用户信息: {user_info}") # 从用户信息中提取 userId(兼容多种字段名) user_id = user_info.get('id') or user_info.get('userId') or user_info.get('account_no') if user_id: token_data['userId'] = user_id logger.info(f"从 UserInfo 接口提取到 userId: {user_id}") # 如果 UserInfo 返回了 openid,也补充进来 if not openid: token_data['openid'] = user_info.get('openid') else: logger.error(f"UserInfo 接口返回的数据中未找到有效的 userId 字段: {user_info}") return None else: logger.error("调用 UserInfo 接口失败,无法获取用户标识") return None logger.info(f"最终获取的 Token 数据: access_token={access_token[:20]}..., userId={user_id}, openid={openid}") return token_data else: logger.error(f"Token 交换失败,状态码: {resp.status_code}, 响应: {resp.text}") return None except Exception as e: logger.error(f"OAuth2 换取 Token 失败: {e}") return None
双重兼容机制说明:
- Token 接口兼容:先尝试标准 POST Body 方式,失败后自动切换到 URL 参数方式
- 用户标识获取兼容:优先从 Token 响应中获取 userId,如果没有则调用 UserInfo 接口
- UserInfo 接口兼容:先尝试 POST 请求,失败后切换到 GET + Bearer Token 方式
- 字段名兼容:支持多种 userId 字段名(
id、userId、account_no)
(4)UserInfo 接口调用
# application/oauth2/services.py - get_user_info 方法 @staticmethod def get_user_info(access_token): """获取用户详细信息(兼容多种认证服务器实现)""" try: config = OAuth2Service._get_config() if not config.get('userinfo_url'): logger.warning("未配置 userinfo_url,跳过用户信息获取") return None logger.info(f"正在调用 UserInfo 接口: {config['userinfo_url']}") # 【方案 A】尝试 POST 请求(C大脑的实现方式) logger.info("[UserInfo 方案 A] 尝试 POST 请求") resp = requests.post( config['userinfo_url'], data={'accessToken': access_token}, timeout=10 ) # 如果 POST 失败,尝试【方案 B】GET 请求 + Authorization Header(标准 OAuth2.0) if resp.status_code in [400, 401, 405]: logger.warning(f"[UserInfo 方案 A] 失败 (状态码: {resp.status_code}),尝试 [方案 B] GET + Bearer Token") headers = { 'Authorization': f'Bearer {access_token}', 'Accept': 'application/json' } resp = requests.get(config['userinfo_url'], headers=headers, timeout=10) logger.info(f"[UserInfo 方案 B] 响应状态码: {resp.status_code}") if resp.status_code == 200: result = resp.json() logger.info(f"UserInfo 接口原始响应: {result}") # 处理不同的响应格式 # 格式1: {"code": 200, "data": {...}} (C大脑风格) # 格式2: {...} (标准 OAuth2.0) user_data = result.get('data') if 'data' in result else result if user_data and isinstance(user_data, dict): return user_data else: logger.warning(f"UserInfo 接口返回的数据格式异常: {result}") return result else: logger.error(f"UserInfo 接口调用失败,状态码: {resp.status_code}, 响应: {resp.text}") return None except Exception as e: logger.error(f"OAuth2 获取用户信息失败: {e}", exc_info=True) return None
(5)本地用户匹配与登录
# application/oauth2/services.py - handle_local_login 方法 @staticmethod def handle_local_login(token_data): """处理本地用户匹配与登录(兼容标准和特殊流程)""" # 从 token_data 中获取 userId(工号) user_id = token_data.get('userId') if not user_id: raise PermissionError("认证服务器未返回有效的用户标识 (userId)。请检查 Token 或 UserInfo 接口响应。") logger.info(f"开始处理本地登录,userId: {user_id}") # 通过 username (工号) 查找本地用户 try: user = User.objects.get(username=user_id) logger.info(f"成功匹配本地用户: {user.username} (ID: {user.id})") except User.DoesNotExist: logger.warning(f"工号 {user_id} 未在 DMS 授权名单中") raise PermissionError(f"工号 {user_id} 未在 DMS 授权名单中。请联系管理员。") # 检查用户状态 if user.status != 1: logger.warning(f"账户 {user.username} 已被禁用") raise PermissionError("该账户已被禁用,请联系管理员") # 同步更新 SSO 相关字段 changed = False # 更新 C大脑工号 if user.cbrain_user_id != user_id: user.cbrain_user_id = user_id changed = True # 更新 OpenID openid = token_data.get('openid') if openid and user.oauth_openid != openid: user.oauth_openid = openid changed = True # 记录 SSO 登录时间 user.last_sso_login = datetime.datetime.now() changed = True # 标记用户来源为 SSO if user.source != 'sso': user.source = 'sso' changed = True # 如果有变更,保存用户信息 if changed: user.save(update_fields=['cbrain_user_id', 'oauth_openid', 'last_sso_login', 'source']) logger.info(f"已更新用户 {user.username} 的 SSO 相关信息") # 记录 SSO 登录事件 now_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") event_msg = f"用户 {user.username} 通过SSO单点登录" create_sys_event( strdatetime=now_time, type=9, # 事件类型:SSO 登录 opt_username='system', event=event_msg ) logger.info(f"SSO 登录事件已记录: {event_msg}") # 更新用户登录次数(与本地登录保持一致) user.login_times += 1 user.save(update_fields=['login_times']) return user
白名单机制说明:
系统采用工号白名单策略:只有当 C大脑返回的 userId(工号)在本地 application_user 表中存在时,才允许登录。
这确保了即使通过了 C大脑认证,未经 DMS 管理员授权的用户仍然无法访问系统。
11.4 配置管理界面
DMS 系统提供了可视化的 SSO 配置管理界面,管理员可以在后台动态修改 SSO 参数。
11.4.1 配置项说明
| 配置项 |
填写说明 |
示例值 |
| SSO门户地址 |
C大脑的基础域名 |
https://cbrain.comac.intra |
| SSO授权地址 |
完整的 OAuth2 授权页面 URL |
https://cbrain.comac.intra/brain-platform/#/Oauth |
| Token接口地址 |
用于用 code 换取 access_token 的接口 |
https://.../cbrain-auth/oauth2/token |
| 用户信息接口地址 |
用于获取用户详细信息的接口 |
https://.../cbrain-auth/oauth2/userInfo |
| Client ID |
从 C大脑运维团队获取的应用 ID |
b3240ffe2ed640e09b38c5b968a96d02 |
| Client Secret |
从 C大脑运维团队获取的应用密钥 |
(敏感信息,不展示) |
| OAuth2回调地址 |
DMS 接收 Code 的回调 URL(必须与 C大脑后台配置完全一致) |
https://dms.samc.intra/api/oauth2/callback/ |
配置修改注意事项:
- 修改配置后立即生效,无需重启 Django 服务(因为每次都从数据库读取)
redirect_uri 必须与 C大脑后台配置的回调地址完全一致(包括协议、域名、端口、路径、斜杠)
- 生产环境和开发环境的
redirect_uri 不同,请注意区分
- 修改配置后建议进行一次完整的 SSO 登录测试
11.5 日志与审计
11.5.1 日志级别设计
| 日志级别 |
记录内容 |
示例 |
| INFO |
正常流程的关键节点 |
“收到 OAuth2 回调请求”、“用户 XXX 通过 C大脑 SSO 登录成功” |
| WARNING |
异常情况但不影响流程 |
“工号 XXX 未在 DMS 授权名单中”、“账户 XXX 已被禁用” |
| ERROR |
严重错误导致流程中断 |
“Token 交换失败,状态码: 401”、“调用 UserInfo 接口失败” |
| DEBUG |
详细的调试信息(生产环境关闭) |
“向C大脑code换Token返回的有用数据: {...}” |
11.5.2 系统事件记录
每次 SSO 登录成功后,系统会在 application_sysevent 表中记录一条事件:
# 事件记录示例 { "strdatetime": "2026-04-20 14:30:25", "type": 9, // 事件类型:SSO 登录 "opt_username": "system", "event": "用户 AI0074 通过SSO单点登录" }
这些事件可用于:
- 安全审计:追踪谁在何时通过 SSO 登录
- 问题分析:排查登录失败的原因
- 使用统计:分析 SSO 登录的使用频率
11.6 安全性设计
11.6.1 安全措施清单
| 安全措施 |
实现方式 |
防护目标 |
| 密钥数据库存储 |
client_secret 存储在数据库中,不在代码中硬编码 |
防止源码泄露导致密钥暴露 |
| 动态配置加载 |
每次请求都从数据库读取最新配置 |
配置修改后立即生效,减少重启风险 |
| 白名单机制 |
校验 C大脑返回的 userId 是否在本地用户表中存在 |
防止未授权用户访问系统 |
| 用户状态校验 |
检查本地用户的 status 字段是否为 1(启用) |
禁用的账户即使通过 SSO 认证也无法登录 |
| 一次性 Code |
OAuth2 Code 只能使用一次,C大脑服务端会使其失效 |
防止重放攻击 |
| HTTPS 传输 |
生产环境强制使用 HTTPS 协议 |
防止中间人攻击和数据窃听 |
| JWT Token |
使用 JWT 作为本地会话令牌,设置合理的过期时间 |
无状态认证,便于水平扩展 |
| 请求超时控制 |
所有 HTTP 请求设置 timeout=10 秒 |
防止请求挂起导致资源耗尽 |
待优化安全项:
- State 参数:当前未实现 State 参数校验,建议添加以增强 CSRF 防护
- client_secret 脱敏:
/cbrain/detail 接口不应返回 client_secret 给前端
- 速率限制:建议对
/oauth2/callback/ 接口添加速率限制,防止暴力破解
- IP 白名单:可考虑限制只有 C大脑服务器的 IP 才能调用回调接口
11.7 部署与运维
11.7.1 环境变量配置
DMS 项目使用 .env 文件管理环境变量,SSO 相关配置如下:
# .env.production DJANGO_SETTINGS_MODULE=application.settings DATABASE_URL=mysql://user:password@localhost:3306/dms_db SECRET_KEY=your-django-secret-key-here DEBUG=False ALLOWED_HOSTS=dms.samc.intra,localhost # SSO 相关(可选,主要用于调试) SSO_ENABLED=True LOG_LEVEL=WARNING # 生产环境建议使用 WARNING 级别
11.7.2 数据库迁移
SSO 相关的数据库表通过 Django Migration 自动创建:
# 执行数据库迁移 python manage.py migrate # 这会创建/更新以下表: # - cbrain(SSO 配置表) # - application_user(扩展 SSO 字段) # - application_sysevent(事件日志表)
11.7.3 初始配置
首次部署时,需要在数据库中初始化 SSO 配置:
# 方法 1:通过 Django Shell python manage.py shell >>> from application.cbrain.models import CBrain >>> config = CBrain.get_config() # 自动创建默认配置 >>> print(config.client_id) # 查看默认值 # 方法 2:通过管理后台 # 访问 http://your-domain/admin/cbrain/cbrain/ # 编辑 id=1 的记录,填入实际的 SSO 配置参数
11.7.4 Nginx 配置要点
# Nginx 配置示例 server { listen 443 ssl; server_name dms.samc.intra; # SSL 证书配置 ssl_certificate /etc/nginx/ssl/dms.crt; ssl_certificate_key /etc/nginx/ssl/dms.key; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # Vue Router History 模式必需 } # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # OAuth2 回调接口(确保正确代理) location /api/oauth2/callback/ { proxy_pass http://127.0.0.1:8000/oauth2/callback/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 重要:保持 POST 方法和请求体 proxy_method POST; proxy_pass_request_body on; } }
Nginx 配置关键点:
- 确保
/api/oauth2/callback/ 路径正确代理到后端
- 保持 POST 方法和请求体不被丢弃
- 如果使用 Django 的
APPEND_SLASH,确保 URL 末尾有斜杠
11.7.5 常见问题排查
问题 1:Token 交换失败(401 Unauthorized)
可能原因:
client_id 或 client_secret 配置错误
code 已过期或被使用过(一次性)
redirect_uri 与 C大脑后台配置不一致
解决方案:
- 检查数据库中
cbrain 表的配置是否正确
- 查看后端日志中的详细错误信息
- 联系 C大脑运维团队确认应用配置
问题 2:用户不在白名单中
现象:提示“工号 XXX 未在 DMS 授权名单中”
原因:C大脑返回的
userId 在本地
application_user 表中不存在
解决方案:
- 在 DMS 管理后台创建对应用户,
username 字段填写工号(如:AI0074)
- 确保用户状态为“启用”(
status=1)
- 重新进行 SSO 登录
问题 3:回调路径 301 重定向
现象:浏览器地址栏显示 301 重定向,POST 请求变成 GET,Code 丢失
原因:Django 的
APPEND_SLASH 机制导致路径补斜杠
解决方案:
- 确保前端回调路径以
/ 结尾:/oauth2/callback/
- 确保后端 URL 配置也有斜杠:
path('', views.OAuth2CallbackView.as_view())
- 检查 Nginx 配置,确保没有额外的重定向规则
12. 前后端集成总结
12.1 完整流程图
第一阶段:授权发起
- 用户访问 DMS → 前端路由守卫检测未登录
- 前端调用
GET /api/cbrain/detail 获取 SSO 配置
- 前端构建 OAuth2 授权 URL,跳转到 C大脑
第二阶段:C大脑认证
- 用户在 C大脑输入账号密码(或已登录)
- C大脑验证通过,生成一次性 Code
- C大脑重定向回 DMS:
/oauth2/callback/?code=xxx
第三阶段:后端处理
- 前端回调页面提取 Code,调用
POST /api/oauth2/callback/
- 后端用 Code 换取 Access Token(兼容两种请求方式)
- 如果 Token 响应无 userId,调用 UserInfo 接口获取(兼容 POST/GET)
- 后端根据 userId(工号)在本地用户表中查找
- 校验用户状态,更新 SSO 相关字段
- 记录登录事件,生成本地 JWT Token
- 返回 JWT Token 和用户信息给前端
第四阶段:前端会话建立
- 前端保存 JWT Token 到 LocalStorage/SessionStorage
- 设置
localStorage.setItem('is_sso_user', 'true')
- 跳转到首页,加载动态菜单
第五阶段:Token 续期
- 用户访问受保护接口,Token 过期(后端返回 401)
- 前端 Axios 拦截器检测到 401,判断
is_sso_user
- SSO 用户:弹出提示,点击确定后跳转 C大脑重新认证(无感续期)
- 传统用户:跳转到本地登录页重新输入密码
12.2 关键技术点对比
| 技术点 |
前端实现 |
后端实现 |
| 配置管理 |
调用 /cbrain/detail 接口动态获取 |
从数据库 cbrain 表实时读取 |
| URL 构建 |
判断是否已有 ?,选择 & 或 ? 连接 |
不涉及(前端负责) |
| Code 接收 |
Vue Router $route.query.code + 原生解析兜底 |
接收前端 POST 请求的 JSON body |
| Token 交换 |
不涉及(后端完成) |
双重兼容:POST Body → URL Params |
| 用户标识获取 |
不涉及(后端完成) |
优先 Token 响应,降级 UserInfo 接口 |
| 用户匹配 |
不涉及(后端完成) |
根据 userId 查询本地 username 字段 |
| 会话建立 |
保存 JWT 到 Storage,设置 is_sso_user 标记 |
生成 JWT Token,更新用户 SSO 字段 |
| Token 续期 |
Axios 拦截器检测 401,智能重定向 |
返回 401 状态码,触发前端续期逻辑 |
12.3 优势与特点
架构优势:
- 双重兼容:同时支持 C大脑特殊实现和标准 OAuth2.0 流程
- 动态配置:修改 SSO 参数无需重启服务,立即生效
- 双轨登录:保留传统账号密码登录,作为应急备份
- 智能续期:SSO 用户 Token 过期后无感续期,提升用户体验
- 白名单机制:严格的工号校验,确保只有授权用户可访问
- 完整审计:记录所有 SSO 登录事件,支持安全审计
- 多层降级:配置获取失败→传统登录;SSO 失败→密码登录
13. 未来优化方向
13.1 短期优化(高优先级)
- 添加 State 参数:在授权 URL 中增加随机 State 参数,回调时验证,增强 CSRF 防护
- client_secret 脱敏:
/cbrain/detail 接口不再返回 client_secret 给前端
- 自动 SSO 检测:在登录页增加“使用 C大脑账号登录”按钮,或直接检测企业内网环境自动跳转 SSO
- 错误日志上报:将前端异常上报到监控系统,便于快速定位问题
- 速率限制:对
/oauth2/callback/ 接口添加速率限制,防止暴力破解
13.2 中期优化(中优先级)
- 静默续期:在 Token 即将过期前,自动发起续期请求,避免用户感知
- 多 SSO 提供商支持:抽象 OAuth2 接口,支持同时接入多个认证中心(如钉钉、企业微信)
- 用户信息同步:定期从 C大脑同步用户信息(姓名、部门、职位等)
- IP 白名单:限制只有 C大脑服务器的 IP 才能调用回调接口
- 性能优化:缓存 SSO 配置(带 TTL),减少数据库查询次数
13.3 长期优化(低优先级)
- OIDC 标准支持:升级为 OpenID Connect 协议,获取更多标准化用户信息
- 移动端适配:优化移动端 SSO 登录体验,支持 App 唤起
- 审计日志增强:记录所有 SSO 登录事件到独立的审计表,支持高级查询和报表
- 双因素认证:SSO 登录后增加二次验证(短信验证码、TOTP 等)
- 会话管理:实现统一的会话管理中心,支持强制下线、单点登出等功能
文档版本:V2.0(完整版) | 编制日期:2026-04-20
编制单位:杭州依谦科技有限公司 | 项目名称:DMS 环境监测系统
本文档为内部技术文档,未经许可不得外传。
如有疑问,请联系项目开发团队。
文档范围:涵盖前端(Vue 3)+ 后端(Django)完整 SSO 集成方案