本文档描述校园管理系统(hzdyjs_xg)前端 「统一认证登录」 按钮的完整业务逻辑,涵盖 CAS OAuth2.0 授权流程、前后端协作方式、用户映射规则及常见问题排查。
相关文档:
涉及源码:
| 模块 | 文件路径 |
|---|---|
| 前端登录页 | web/src/views/login.vue |
| 前端路由守卫 | web/src/permission.ts |
| 后端 CAS 代理 | backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java |
统一身份认证(CAS,Central Authentication Service)是学校提供的集中式登录平台。用户在校内 CAS 完成身份验证后,业务系统无需再校验密码,只需根据 CAS 返回的用户标识匹配本地账号,即可签发本系统的登录 Token。
| 维度 | 普通登录 | 统一认证登录 |
|---|---|---|
| 认证方式 | 用户名 + 密码(AES+RSA 加密) | CAS OAuth2.0 授权码模式 |
| 密码校验 | 后端校验 | 无,由 CAS 平台完成 |
| 用户识别 | 表单输入 username |
CAS profile 中的 CODE 字段 |
| Token 来源 | POST /auth/login |
GET /cas/accessToken |
| 租户 | 表单 tenantId |
固定 000000(忽略 CAS 返回的 TENANTID) |
| 登录后行为 | 路由守卫拉取用户信息 + 动态路由 | 相同 |
http://td.hzdyjsxy.cn:19081http://web.hzdyjsxy.cn:18081sequenceDiagram
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 + 动态路由
登录页 web/src/views/login.vue 提供两个并列按钮:
从单点登录门户自动认证(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()。
点击「统一认证登录」后,浏览器整页跳转到 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 时由后端携带。
用户在 CAS 登录成功后,浏览器被重定向到:
http://web.hzdyjsxy.cn:18081?code=ST-xxxxxxxx
login.vue 在 onMounted 时自动检测 URL 中的 code,支持三种提取方式:
route.query.code 获取redirect 参数中解析(如 redirect=/?code=ST-xxx)route.fullPath 中正则匹配回调路径说明:若用户先落到 / 且无 Token,permission.ts 路由守卫会将其重定向到 /login?redirect=...,code 会保留在 redirect 参数中,登录页仍能正确解析。
前端不直接请求 CAS(避免浏览器 CORS 限制),而是调用后端代理接口:
GET {VITE_APP_BASE_API}/cas/accessToken?code={code}
收到响应后,从以下字段之一提取 Token:
body.data.access_tokenbody.access_tokenbody.data.accessTokenbody.accessToken提取成功后:
setToken() 持久化 TokenuserStore.tokenredirect 参数指定页面或首页 /CAS 登录只写入 Token,不会调用 userStore.login()。进入系统后,permission.ts 路由守卫检测到「有 Token 但 roles 为空」时,会自动:
getInfo() 拉取用户信息与权限generateRoutes() 生成动态菜单路由因此,CAS 登录与普通登录在进入系统之后的行为完全一致。
| 项目 | 值 |
|---|---|
| 接口地址 | GET /cas/accessToken |
| 权限要求 | 无需认证(@SaIgnore) |
| Controller | CasController.java |
| 请求参数 | code(必填,CAS 授权码) |
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 格式,后端兼容两种解析方式:
{"access_token":"xxx","expires_in":3600}access_token=xxx&expires_in=3600GET http://td.hzdyjsxy.cn:19081/cas/oauth2.0/profile?access_token={access_token}
从返回 JSON 的 attributes 数组中解析:
| CAS 字段 | 用途 | 本系统映射 |
|---|---|---|
CODE |
用户唯一标识 | → sys_user.user_name |
TENANTID |
租户标识 | 当前被忽略,强制使用 000000 |
000000 上下文中,按 user_name = CODE 查询 sys_userloginService.buildLoginUser(user) 构建 LoginUsere5cd7e4891bf95d1d19206ce24a7b32e 配置超时等参数LoginHelper.login() 通过 Sa-Token 签发本系统 Token响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"expire_in": 7200,
"client_id": "e5cd7e4891bf95d1d19206ce24a7b32e"
}
}
| 场景 | 返回信息 |
|---|---|
| code 为空 | code 不能为空 |
| 无法解析 access_token | 无法解析 CAS access_token,原始响应: ... |
| profile 无用户信息 | CAS profile 未包含用户信息 |
| 本地用户不存在 | user.not.exists |
| 用户已禁用 | user.blocked |
| CAS 请求异常 | 请求 CAS accessToken 失败: ... |
client_id 和 client_secretredirect_uri 与代码一致attributes 中包含 CODE 字段td.hzdyjsxy.cn:19081 对后端服务器网络可达sys_user 表中存在与 CAS CODE 一致的 user_name 记录redirect_uri 与 CAS 注册地址一致CasController 中 CAS 地址、client 配置与 CAS 平台一致前端(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
http://td.hzdyjsxy.cn:19081client_id 是否已在 CAS 注册code 参数code 是否已被使用(授权码一次性有效,刷新页面会失败)/cas/accessToken 请求响应CODE 值与 sys_user.user_name 不一致sys_user.status 字段是否为正常状态getInfo() 失败,检查后端 /auth/getInfo 接口/cas/accessToken 代理application.yml,避免硬编码(后端 secret 仍待外置)CAS_CLIENT_SECRETTENANTID 的解析逻辑// 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;
};
// web/src/views/login.vue — onMounted
const code = extractCodeFromRoute(router.currentRoute.value);
if (code) {
fetchAccessToken(code);
}
// 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。