Files
hzdyjs/docs/开发文档/统一身份认证接入说明.md
T
2026-06-26 15:30:24 +08:00

384 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 统一身份认证接入说明
## 文档说明
本文档描述校园管理系统(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<br/>td.hzdyjsxy.cn:19081
participant Backend as 后端 (ruoyi-admin)
participant DB as 本地用户库 sys_user
User->>Web: 点击「统一认证登录」
Web->>CAS: 跳转 /cas/oauth2.0/authorize<br/>?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`,避免硬编码
2. **密钥安全**:前端不应保留 `CAS_CLIENT_SECRET`(当前未使用但存在代码中,建议删除)
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<SysUser>().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)。