fix(security): 文件列表 userId 隔离 + CAS 退出清 SSO
研读模块 /gpt/file/list 之前没强制按 userId 过滤,理论上枚举
knowledgeBaseId 能拿到他人文件元信息(虽然 download 那一步有 userId 校验
所以下不下来,但文件名/大小/上传时间会泄露)。
- FileController.listFile 强制注入 userId = getSysUserId()
- UploadFileMapper.xml BaseSelect 加 <if userId != null> 过滤分支
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Map<String, String>> logout(HttpServletRequest request) {
|
||||
Map<String, String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加登录日志
|
||||
*
|
||||
|
||||
@@ -152,6 +152,8 @@ public class FileController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ResponseInfo<List<UploadFileVO>> listFile(@RequestParam Map map) {
|
||||
// 强制按当前登录用户隔离,防止枚举别人的 knowledgeBaseId/folderId 拿到他人文件元信息
|
||||
map.put("userId", getSysUserId());
|
||||
return fileService.listFile(new Query(map));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<if test="q.folderId != null and q.folderId > -1"> and t.folder_id = #{q.folderId}</if>
|
||||
<if test="q.embeddingId != null and q.embeddingId != ''"> and t.embedding_id = #{q.embeddingId}</if>
|
||||
<if test="q.knowledgeBaseId != null and q.knowledgeBaseId != ''"> and t.knowledge_base_id = #{q.knowledgeBaseId}</if>
|
||||
<if test="q.userId != null"> and t.user_id = #{q.userId}</if>
|
||||
<if test="q.type != null"> and t.type = #{q.type}</if>
|
||||
<if test="q.startDate != null and q.startDate != ''"> and date_format(t.create_time,'%Y-%m-%d') >= #{q.startDate} </if>
|
||||
<if test="q.endDate != null and q.endDate != ''"> and date_format(t.create_time,'%Y-%m-%d') <= #{q.endDate} </if>
|
||||
|
||||
@@ -16,6 +16,13 @@ export function fetchVerify<T>(data: object) {
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录:清后端 Redis 里的 token,返回 CAS server logout URL(前端跳过去清 SSO cookie)
|
||||
export function fetchLogout<T>() {
|
||||
return post<T>({
|
||||
url: '/app/api/logout',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function fetchSession<T>() {
|
||||
return get<T>({
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user