# 统一身份认证接入说明 ## 文档说明 本文档描述校园管理系统(hzdyjs_xg)前端 **「统一认证登录」** 按钮的完整业务逻辑,涵盖 CAS OAuth2.0 授权流程、前后端协作方式、用户映射规则及常见问题排查。 **相关文档**: - [用户认证登录API](./API/用户认证登录API.md) — 普通账号密码登录接口 - [API文档基础规范](./API/API文档基础规范.md) — 通用 API 规范与 Token 认证方式 **涉及源码**: | 模块 | 文件路径 | |------|----------| | 前端登录页 | `web/src/views/login.vue` | | 前端路由守卫 | `web/src/permission.ts` | | 后端 CAS 代理 | `backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java` | --- ## 1. 概述 ### 1.1 什么是统一身份认证 统一身份认证(CAS,Central Authentication Service)是学校提供的集中式登录平台。用户在校内 CAS 完成身份验证后,业务系统无需再校验密码,只需根据 CAS 返回的用户标识匹配本地账号,即可签发本系统的登录 Token。 ### 1.2 与普通登录的区别 | 维度 | 普通登录 | 统一认证登录 | |------|----------|--------------| | 认证方式 | 用户名 + 密码(AES+RSA 加密) | CAS OAuth2.0 授权码模式 | | 密码校验 | 后端校验 | 无,由 CAS 平台完成 | | 用户识别 | 表单输入 `username` | CAS profile 中的 `CODE` 字段 | | Token 来源 | `POST /auth/login` | `GET /cas/accessToken` | | 租户 | 表单 `tenantId` | 固定 `000000`(忽略 CAS 返回的 TENANTID) | | 登录后行为 | 路由守卫拉取用户信息 + 动态路由 | 相同 | ### 1.3 协议与模式 - **协议**:OAuth 2.0 授权码模式(Authorization Code) - **CAS 服务地址**:`http://td.hzdyjsxy.cn:19081` - **前端回调地址**:`http://web.hzdyjsxy.cn:18081` --- ## 2. 整体流程 ```mermaid sequenceDiagram participant User as 用户浏览器 participant Web as 前端 (web) participant CAS as 统一认证 CAS
td.hzdyjsxy.cn:19081 participant Backend as 后端 (ruoyi-admin) participant DB as 本地用户库 sys_user User->>Web: 点击「统一认证登录」 Web->>CAS: 跳转 /cas/oauth2.0/authorize
?client_id=...&redirect_uri=... User->>CAS: 输入校内统一账号密码 CAS->>Web: 302 回调 redirect_uri?code=ST-xxx Web->>Web: login.vue 检测到 code Web->>Backend: GET /cas/accessToken?code=ST-xxx Backend->>CAS: 用 code 换 access_token Backend->>CAS: 用 access_token 拉 profile CAS-->>Backend: 返回 attributes(CODE, TENANTID...) Backend->>DB: 按 CODE 查 sys_user.user_name Backend->>Backend: Sa-Token 生成本系统 token Backend-->>Web: { access_token: "..." } Web->>Web: setToken + 跳转首页 Web->>Backend: 路由守卫拉 getInfo + 动态路由 ``` --- ## 3. 前端实现 ### 3.1 入口:统一认证登录按钮 登录页 `web/src/views/login.vue` 提供两个并列按钮: - **登录** — 普通账号密码登录 - **统一认证登录** — 跳转 CAS 授权页 **从单点登录门户自动认证**(2026-06-25 新增): 当用户从学校单点登录门户跳转到本系统登录页时,**无需再点击「统一认证登录」**,页面会自动跳转 CAS 完成认证。直接打开 `/login`(无 `redirect`)仍可使用账号密码登录。 单点门户典型入口 URL(**无需** `sso`、`from` 等额外参数): ``` http://web.hzdyjsxy.cn:18081/login?redirect=/index ``` 识别「来自 SSO / 外部入口」的条件(满足任一即可): | 条件 | 说明 | |------|------| | URL 带 `redirect=/index` 且 Referer **不是**本站 | 单点门户固定链接;本地未登录为 `redirect=/` 不会触发 | | Referer 来自 `td.hzdyjsxy.cn`、`hzdyjsxy.cn` | 从统一认证门户点击链接触发 | | URL 参数 `sso=1` 或 `from=sso` / `from=cas` | 可选,门户显式携带时同样生效 | **仅生产域名 `web.hzdyjsxy.cn` 启用自动跳转**;本地 `localhost` 开发不会误触发 CAS 跳转到生产环境。 **不自动跳转**的场景:用户在本系统内操作导致会话过期,被重定向到 `login?redirect=...` 且 Referer 为 `web.hzdyjsxy.cn` 时,保留账号密码登录。 处理顺序:`onMounted` 时若 URL 已有 `code` 则直接换 Token;否则若判定为外部/SSO 入口则自动 `handleUnifiedLogin()`。 ### 3.2 跳转 CAS 授权页 点击「统一认证登录」后,浏览器整页跳转到 CAS 授权地址: ``` GET http://td.hzdyjsxy.cn:19081/cas/oauth2.0/authorize ?response_type=code &client_id=0i7tcT5tULqvmmbbV8 &redirect_uri=http%3A%2F%2Fweb.hzdyjsxy.cn%3A18081 ``` **当前写死的配置项**(`login.vue`): | 配置项 | 值 | 说明 | |--------|-----|------| | `CAS_BASE` | `http://td.hzdyjsxy.cn:19081` | CAS 服务器地址 | | `CAS_CLIENT_ID` | `0i7tcT5tULqvmmbbV8` | OAuth2 客户端 ID | | `CAS_CLIENT_SECRET` | `68c4f7641523452aaf5ef6d0432869f7` | 客户端密钥(仅后端使用,前端不应暴露) | | `FRONTEND_REDIRECT_URI` | `http://web.hzdyjsxy.cn:18081` | CAS 授权成功后的回调地址 | > **注意**:`client_secret` 在前端代码中有定义但未在跳转 URL 中使用;实际换 token 时由后端携带。 ### 3.3 CAS 回调与 code 提取 用户在 CAS 登录成功后,浏览器被重定向到: ``` http://web.hzdyjsxy.cn:18081?code=ST-xxxxxxxx ``` `login.vue` 在 `onMounted` 时自动检测 URL 中的 `code`,支持三种提取方式: 1. 直接从 `route.query.code` 获取 2. 从 `redirect` 参数中解析(如 `redirect=/?code=ST-xxx`) 3. 从 `route.fullPath` 中正则匹配 **回调路径说明**:若用户先落到 `/` 且无 Token,`permission.ts` 路由守卫会将其重定向到 `/login?redirect=...`,`code` 会保留在 `redirect` 参数中,登录页仍能正确解析。 ### 3.4 通过后端代理换取 Token 前端**不直接**请求 CAS(避免浏览器 CORS 限制),而是调用后端代理接口: ``` GET {VITE_APP_BASE_API}/cas/accessToken?code={code} ``` 收到响应后,从以下字段之一提取 Token: - `body.data.access_token` - `body.access_token` - `body.data.accessToken` - `body.accessToken` 提取成功后: 1. 调用 `setToken()` 持久化 Token 2. 更新 `userStore.token` 3. 跳转到 `redirect` 参数指定页面或首页 `/` ### 3.5 登录后的权限加载 CAS 登录只写入 Token,**不会**调用 `userStore.login()`。进入系统后,`permission.ts` 路由守卫检测到「有 Token 但 roles 为空」时,会自动: 1. 调用 `getInfo()` 拉取用户信息与权限 2. 调用 `generateRoutes()` 生成动态菜单路由 3. 进入目标页面 因此,CAS 登录与普通登录在**进入系统之后的行为完全一致**。 --- ## 4. 后端实现 ### 4.1 接口信息 | 项目 | 值 | |------|-----| | 接口地址 | `GET /cas/accessToken` | | 权限要求 | 无需认证(`@SaIgnore`) | | Controller | `CasController.java` | | 请求参数 | `code`(必填,CAS 授权码) | ### 4.2 处理步骤 #### 步骤 1:用 code 换取 CAS access_token ``` GET http://td.hzdyjsxy.cn:19081/cas/oauth2.0/accessToken ?client_id=0i7tcT5tULqvmmbbV8 &client_secret=68c4f7641523452aaf5ef6d0432869f7 &code={code} &redirect_uri=http%3A%2F%2Fweb.hzdyjsxy.cn%3A18081 ``` CAS 可能返回 JSON 或 query-string 格式,后端兼容两种解析方式: - JSON:`{"access_token":"xxx","expires_in":3600}` - Query-string:`access_token=xxx&expires_in=3600` #### 步骤 2:用 access_token 获取用户信息 ``` GET http://td.hzdyjsxy.cn:19081/cas/oauth2.0/profile?access_token={access_token} ``` 从返回 JSON 的 `attributes` 数组中解析: | CAS 字段 | 用途 | 本系统映射 | |----------|------|------------| | `CODE` | 用户唯一标识 | → `sys_user.user_name` | | `TENANTID` | 租户标识 | **当前被忽略**,强制使用 `000000` | #### 步骤 3:匹配本地用户并签发 Token 1. 在租户 `000000` 上下文中,按 `user_name = CODE` 查询 `sys_user` 2. 校验用户存在且状态非禁用 3. 调用 `loginService.buildLoginUser(user)` 构建 `LoginUser` 4. 使用默认客户端 `e5cd7e4891bf95d1d19206ce24a7b32e` 配置超时等参数 5. 调用 `LoginHelper.login()` 通过 Sa-Token 签发本系统 Token #### 步骤 4:返回登录结果 **响应示例**: ```json { "code": 200, "msg": "操作成功", "data": { "access_token": "eyJhbGciOiJIUzI1NiJ9...", "expire_in": 7200, "client_id": "e5cd7e4891bf95d1d19206ce24a7b32e" } } ``` ### 4.3 错误处理 | 场景 | 返回信息 | |------|----------| | code 为空 | `code 不能为空` | | 无法解析 access_token | `无法解析 CAS access_token,原始响应: ...` | | profile 无用户信息 | `CAS profile 未包含用户信息` | | 本地用户不存在 | `user.not.exists` | | 用户已禁用 | `user.blocked` | | CAS 请求异常 | `请求 CAS accessToken 失败: ...` | --- ## 5. 配置清单 ### 5.1 CAS 平台侧(需运维/信息中心配置) - [ ] 注册 OAuth2 应用,获取 `client_id` 和 `client_secret` - [ ] 配置授权回调地址 `redirect_uri` 与代码一致 - [ ] 确认 profile 接口返回 `attributes` 中包含 `CODE` 字段 - [ ] 确认 CAS 服务器 `td.hzdyjsxy.cn:19081` 对后端服务器网络可达 ### 5.2 本系统侧 - [ ] `sys_user` 表中存在与 CAS `CODE` 一致的 `user_name` 记录 - [ ] 用户状态为正常(非禁用) - [ ] 前端 `redirect_uri` 与 CAS 注册地址一致 - [ ] 后端 `CasController` 中 CAS 地址、client 配置与 CAS 平台一致 - [ ] 生产环境部署时,将硬编码 IP 改为实际域名/地址 ### 5.3 当前硬编码配置汇总 **前端**(`web/src/views/login.vue`): ``` CAS_BASE = http://td.hzdyjsxy.cn:19081 CAS_CLIENT_ID = 0i7tcT5tULqvmmbbV8 FRONTEND_REDIRECT_URI = http://web.hzdyjsxy.cn:18081 ``` **后端**(`CasController.java`): ``` CAS_BASE = http://td.hzdyjsxy.cn:19081 CLIENT_ID = 0i7tcT5tULqvmmbbV8 CLIENT_SECRET = 68c4f7641523452aaf5ef6d0432869f7 REDIRECT_URI = http://web.hzdyjsxy.cn:18081 DEFAULT_CLIENT_ID = e5cd7e4891bf95d1d19206ce24a7b32e DEFAULT_TENANT_ID = 000000 ``` --- ## 6. 常见问题排查 ### 6.1 点击按钮后无法跳转 CAS - 检查浏览器能否访问 `http://td.hzdyjsxy.cn:19081` - 检查 CAS 平台是否正常运行 - 检查 `client_id` 是否已在 CAS 注册 ### 6.2 回调后停留在登录页 / 登录失败 - 检查 URL 是否携带 `code` 参数 - 检查 `code` 是否已被使用(授权码一次性有效,刷新页面会失败) - 打开浏览器控制台查看 `/cas/accessToken` 请求响应 ### 6.3 提示「用户不存在」 - CAS profile 中 `CODE` 值与 `sys_user.user_name` 不一致 - 需在系统管理中预先创建对应用户,或建立 CAS 用户与本地用户的同步机制 ### 6.4 提示「用户已禁用」 - 检查 `sys_user.status` 字段是否为正常状态 ### 6.5 登录成功但无菜单 / 权限 - Token 已写入但 `getInfo()` 失败,检查后端 `/auth/getInfo` 接口 - 检查用户是否已分配角色和菜单权限 ### 6.6 CORS 相关 - 前端不应直接请求 CAS,必须通过后端 `/cas/accessToken` 代理 - 若后端也报连接失败,检查后端服务器到 CAS 的网络连通性 --- ## 7. 后续优化建议 1. **配置外置化**:将 CAS 地址、client_id、client_secret、redirect_uri 移至 `application.yml`,避免硬编码(后端 secret 仍待外置) 2. **密钥安全**:~~前端不应保留 `CAS_CLIENT_SECRET`~~(已删除;换票仅走后端;CAS 相关日志已脱敏,禁止打印 secret/token/profile) 3. **租户映射**:如需支持多租户,可恢复对 CAS `TENANTID` 的解析逻辑 4. **用户自动注册**:CAS 用户首次登录时自动创建本地账号(需评估权限与安全策略) 5. **移动端接入**:mobile 工程可参考相同 OAuth2 流程实现统一认证登录 6. **退出登录**:考虑对接 CAS 单点登出(SLO)接口 --- ## 8. 附录:关键代码索引 ### 8.1 前端 — 跳转 CAS ```typescript // web/src/views/login.vue const handleUnifiedLogin = () => { const authorizeUrl = `${CAS_BASE}/cas/oauth2.0/authorize?response_type=code&client_id=${CAS_CLIENT_ID}&redirect_uri=${FRONTEND_REDIRECT_URI_ENCODED}`; window.location.href = authorizeUrl; }; ``` ### 8.2 前端 — 回调处理 ```typescript // web/src/views/login.vue — onMounted const code = extractCodeFromRoute(router.currentRoute.value); if (code) { fetchAccessToken(code); } ``` ### 8.3 后端 — 用户映射与登录 ```java // CasController.java LoginUser loginUser = TenantHelper.dynamic(finalTenantId, () -> { SysUserVo user = userMapper.selectVoOne( new LambdaQueryWrapper().eq(SysUser::getUserName, finalUsername)); // 校验存在性、状态... return loginService.buildLoginUser(user); }); LoginHelper.login(loginUser, model); ``` --- **文档版本**:v1.1 **创建日期**:2026-06-10 **更新日期**:2026-06-25 — 新增从 SSO 门户进入登录页时自动统一认证 **维护说明**:CAS 配置变更或登录逻辑调整时,请同步更新本文档及 [用户认证登录API](./API/用户认证登录API.md)。