From a5484259237951187f0087d11ca0e78dbad36392 Mon Sep 17 00:00:00 2001 From: liuguancen Date: Thu, 7 May 2026 20:34:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E6=96=87=E4=BB=B6=E5=88=97?= =?UTF-8?q?=E8=A1=A8=20userId=20=E9=9A=94=E7=A6=BB=20+=20CAS=20=E9=80=80?= =?UTF-8?q?=E5=87=BA=E6=B8=85=20SSO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 研读模块 /gpt/file/list 之前没强制按 userId 过滤,理论上枚举 knowledgeBaseId 能拿到他人文件元信息(虽然 download 那一步有 userId 校验 所以下不下来,但文件名/大小/上传时间会泄露)。 - FileController.listFile 强制注入 userId = getSysUserId() - UploadFileMapper.xml BaseSelect 加 过滤分支 CAS 退出登录之前只清了本地 JWT,没调 CAS server logout,导致: - 后端 Redis 里的 token 还在 - CAS server 的 SSO cookie 还在 → 再点"统一身份登录"立即静默登入 - 其他接 CAS 的系统也还能继续访问 新增 POST /app/api/logout: - 删 Redis 里 LOGIN_TOKEN_KEY:{userId}:{sessionId} - SecurityContextHolder.clearContext() - 返回 casLogoutUrl(${serverLogout}?service=前端 login 页) 前端 Operates.vue quit() 改 async:先调 logout 拿 casLogoutUrl, removeToken 后 window.location.href 跳过去,让 CAS 清 SSO cookie 再回 /login。 CasProperties 加回 serverLogout 字段(之前清理时删了,本次需要)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../chat/base/security/cas/CasProperties.java | 2 + .../gpt/controller/app/LoginController.java | 50 +++++++++++++++++++ .../gpt/controller/gpt/FileController.java | 2 + .../src/main/resources/application.yml | 1 + .../resources/mapper/gpt/UploadFileMapper.xml | 1 + chat_web_front/src/api/index.ts | 7 +++ chat_web_front/src/components/Operates.vue | 18 ++++++- 7 files changed, 79 insertions(+), 2 deletions(-) diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java index 7100881..ebc3c0c 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java @@ -18,6 +18,8 @@ public class CasProperties { private String serverHost; /** CAS 服务器登录入口,通常 ${serverHost}/login */ private String serverLogin; + /** CAS 服务器登出入口,通常 ${serverHost}/logout */ + private String serverLogout; /** 本应用的 CAS 回调路径(servlet path),通常 /cas/login */ private String appLogin; } diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/app/LoginController.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/app/LoginController.java index 951cb84..4dfbee4 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/app/LoginController.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/app/LoginController.java @@ -6,6 +6,8 @@ import com.inspur.llm.chat.base.constant.SysConfigConstants; import com.inspur.llm.chat.base.security.JwtTokenUtils; import com.inspur.llm.chat.base.security.Oauth2Token; import com.inspur.llm.chat.base.security.UserDetail; +import com.inspur.llm.chat.base.security.cas.CasProperties; +import com.inspur.llm.chat.base.security.cas.CasUrlBuilder; import com.inspur.llm.chat.base.util.RedisUtils; import com.inspur.llm.chat.base.validator.ValidatorUtil; import com.inspur.llm.chat.gpt.enums.ResponseEnum; @@ -27,6 +29,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + import java.util.Optional; import java.util.Set; @@ -47,6 +55,8 @@ public class LoginController { private IUserService userService; @Autowired private RedisUtils redisUtils; + @Autowired + private CasProperties casProperties; /** * 登录 @@ -101,6 +111,46 @@ public class LoginController { return ResponseInfo.success(token); } + /** + * 退出登录:删 Redis 里的 token + 返回 CAS server logout URL(若用户用 CAS 登录的) + * + * 前端拿到 casLogoutUrl 后 window.location.href 跳过去,CAS 服务器会清掉 SSO cookie + * 并跳回 service 参数指定的页面(我们的 /login)。这样下次再点"统一身份登录"会重新跑 + * CAS 认证流,不会因为 SSO cookie 还在而直接静默登入。 + */ + @PostMapping("/api/logout") + public ResponseInfo> logout(HttpServletRequest request) { + Map data = new HashMap<>(); + try { + UserDetail userDetail = JwtTokenUtils.getLoginUser(); + if (userDetail != null) { + // 删 Redis 里这个 session 的 token + String key = RedisConstants.LOGIN_TOKEN_KEY + + userDetail.getId() + StringPoolConstant.COLON + userDetail.getSessionId(); + redisUtils.del(key); + } + } catch (Exception e) { + // Redis 不可用不影响登出流程 + e.printStackTrace(); + } + SecurityContextHolder.clearContext(); + + // 拼 CAS server logout URL + // service 参数:让 CAS 退出后回跳到我们的前端 login 页(动态用 X-Forwarded-Host) + String backToFront; + try { + backToFront = URLEncoder.encode( + CasUrlBuilder.buildServiceUrl(request, "/metalinfo/#/login"), + StandardCharsets.UTF_8.name()); + } catch (Exception e) { + backToFront = ""; + } + String casLogoutUrl = casProperties.getServerLogout() + + (backToFront.isEmpty() ? "" : "?service=" + backToFront); + data.put("casLogoutUrl", casLogoutUrl); + return ResponseInfo.success(data); + } + /** * 添加登录日志 * diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/gpt/FileController.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/gpt/FileController.java index bc17394..03ffda6 100644 --- a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/gpt/FileController.java +++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/controller/gpt/FileController.java @@ -152,6 +152,8 @@ public class FileController extends BaseController { */ @GetMapping("/list") public ResponseInfo> listFile(@RequestParam Map map) { + // 强制按当前登录用户隔离,防止枚举别人的 knowledgeBaseId/folderId 拿到他人文件元信息 + map.put("userId", getSysUserId()); return fileService.listFile(new Query(map)); } diff --git a/chat_web_backend/src/main/resources/application.yml b/chat_web_backend/src/main/resources/application.yml index e1097a8..ae55e02 100644 --- a/chat_web_backend/src/main/resources/application.yml +++ b/chat_web_backend/src/main/resources/application.yml @@ -97,6 +97,7 @@ security: cas: server-host: http://192.168.203.20:8180 server-login: ${security.cas.server-host}/login + server-logout: ${security.cas.server-host}/logout app-login: /cas/login diff --git a/chat_web_backend/src/main/resources/mapper/gpt/UploadFileMapper.xml b/chat_web_backend/src/main/resources/mapper/gpt/UploadFileMapper.xml index 83b660f..66a0f8c 100644 --- a/chat_web_backend/src/main/resources/mapper/gpt/UploadFileMapper.xml +++ b/chat_web_backend/src/main/resources/mapper/gpt/UploadFileMapper.xml @@ -44,6 +44,7 @@ and t.folder_id = #{q.folderId} and t.embedding_id = #{q.embeddingId} and t.knowledge_base_id = #{q.knowledgeBaseId} + and t.user_id = #{q.userId} and t.type = #{q.type} and date_format(t.create_time,'%Y-%m-%d') >= #{q.startDate} and date_format(t.create_time,'%Y-%m-%d') <= #{q.endDate} diff --git a/chat_web_front/src/api/index.ts b/chat_web_front/src/api/index.ts index 4a846aa..55de47b 100644 --- a/chat_web_front/src/api/index.ts +++ b/chat_web_front/src/api/index.ts @@ -16,6 +16,13 @@ export function fetchVerify(data: object) { }) } +// 退出登录:清后端 Redis 里的 token,返回 CAS server logout URL(前端跳过去清 SSO cookie) +export function fetchLogout() { + return post({ + url: '/app/api/logout', + }) +} + // 获取用户信息 export function fetchSession() { return get({ diff --git a/chat_web_front/src/components/Operates.vue b/chat_web_front/src/components/Operates.vue index 5239d8c..53c5bde 100644 --- a/chat_web_front/src/components/Operates.vue +++ b/chat_web_front/src/components/Operates.vue @@ -57,6 +57,7 @@ import { useAuthStore } from "@/store"; import { reactive, watch, computed } from "vue"; import { useRouter, useRoute } from "vue-router"; import { User, Setting, SwitchButton, TopRight } from "@element-plus/icons-vue"; +import { fetchLogout } from "@/api"; const authStore = useAuthStore(); const isAdmin = computed(() => authStore.session?.admind === true); @@ -112,9 +113,22 @@ const openExtLink = (url: string) => { window.open(url, '_blank'); }; -const quit = () => { +const quit = async () => { + // 调后端 logout:清 Redis token + 拿 CAS 服务器登出 URL + // 拿到后跳过去清 CAS SSO cookie,否则下次"统一身份登录"会因为 cookie 还在直接静默登入 + let casLogoutUrl: string | null = null; + try { + const res: any = await fetchLogout(); + casLogoutUrl = res?.data?.casLogoutUrl || null; + } catch (e) { + console.warn("logout API 调用失败,本地清理后跳登录页", e); + } authStore.removeToken(); - router.push("/login"); + if (casLogoutUrl) { + window.location.href = casLogoutUrl; + } else { + router.push("/login"); + } }; const goProfile = () => {