diff --git a/chat_web_backend/pom.xml b/chat_web_backend/pom.xml
index edd9eb0..1698a34 100644
--- a/chat_web_backend/pom.xml
+++ b/chat_web_backend/pom.xml
@@ -191,6 +191,17 @@
spring-boot-starter-security
+
+
+ org.springframework.security
+ spring-security-cas
+
+
+ org.jasig.cas.client
+ cas-client-core
+ 3.5.0
+
+
org.springframework.boot
spring-boot-starter-data-jdbc
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/config/WebSecurityConfig.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/config/WebSecurityConfig.java
index dc69971..4874328 100644
--- a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/config/WebSecurityConfig.java
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/config/WebSecurityConfig.java
@@ -7,6 +7,7 @@ import com.inspur.llm.chat.base.security.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@@ -27,6 +28,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
* Copyright Ⓒ 2024 Inspur Corporation Limited All rights reserved.
*/
@Configuration
+@Order(100)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java
new file mode 100644
index 0000000..3511607
--- /dev/null
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasLoginSuccessHandler.java
@@ -0,0 +1,37 @@
+package com.inspur.llm.chat.base.security.cas;
+
+import com.inspur.llm.chat.base.security.JwtTokenUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * CAS 登录成功 → 生成 JWT → 重定向前端登录页(带 cas_token)
+ */
+@Slf4j
+@Component
+public class CasLoginSuccessHandler implements AuthenticationSuccessHandler {
+
+ @Autowired
+ private CasProperties casProperties;
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+ log.info("CAS 登录成功: {}", authentication.getName());
+ String token = JwtTokenUtils.generateToken(authentication);
+ String redirect = casProperties.getFrontEndUrl()
+ + "/#/login?cas_token=" + URLEncoder.encode(token, StandardCharsets.UTF_8.name());
+ response.sendRedirect(redirect);
+ }
+}
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
new file mode 100644
index 0000000..5ae1f96
--- /dev/null
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasProperties.java
@@ -0,0 +1,24 @@
+package com.inspur.llm.chat.base.security.cas;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * CAS 单点登录配置(绑定 application.yml 中的 security.cas.*)
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "security.cas")
+public class CasProperties {
+ private boolean enabled = false;
+ private String serverHost;
+ private String serverLogin;
+ private String serverLogout;
+ private String appLogin;
+ private String appLogout;
+ private String appKey;
+ private String appSecret;
+ private boolean httpsFlag = false;
+ private String frontEndUrl;
+}
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java
new file mode 100644
index 0000000..79f8d91
--- /dev/null
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasSecurityConfig.java
@@ -0,0 +1,101 @@
+package com.inspur.llm.chat.base.security.cas;
+
+import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
+import org.springframework.security.cas.authentication.CasAuthenticationProvider;
+import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
+import org.springframework.security.cas.web.CasAuthenticationFilter;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+
+/**
+ * CAS 单点登录 Security 配置(仅处理 /cas/** 路径,与 WebSecurityConfig 共存)。
+ *
+ * 流程:
+ * /cas/login(无 ticket) → 触发 CasEntryPoint → 302 到 CAS 登录页
+ * /cas/login?ticket=xxx → CasAuthenticationFilter 验证 → 成功 → CasLoginSuccessHandler 生成 JWT → 302 前端
+ */
+@Configuration
+@Order(1)
+public class CasSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private CasProperties casProperties;
+
+ @Autowired
+ private AuthenticationUserDetailsService casUserDetailsService;
+
+ @Autowired
+ private AuthenticationSuccessHandler casLoginSuccessHandler;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .antMatcher("/cas/**")
+ .csrf().disable()
+ .anonymous().disable()
+ .authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .exceptionHandling()
+ .authenticationEntryPoint(casAuthenticationEntryPoint())
+ .and()
+ .addFilter(casAuthenticationFilter());
+ http.headers().frameOptions().disable();
+ }
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) {
+ auth.authenticationProvider(casAuthenticationProvider());
+ }
+
+ @Bean
+ public ServiceProperties casServiceProperties() {
+ ServiceProperties sp = new ServiceProperties();
+ sp.setService(casProperties.getFrontEndUrl() + "/chat_web_backend" + casProperties.getAppLogin());
+ sp.setSendRenew(false);
+ sp.setAuthenticateAllArtifacts(true);
+ return sp;
+ }
+
+ @Bean
+ public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
+ CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint();
+ ep.setLoginUrl(casProperties.getServerLogin());
+ ep.setServiceProperties(casServiceProperties());
+ return ep;
+ }
+
+ @Bean
+ public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
+ CasAuthenticationFilter filter = new CasAuthenticationFilter();
+ filter.setAuthenticationManager(authenticationManager());
+ filter.setFilterProcessesUrl(casProperties.getAppLogin());
+ filter.setAuthenticationSuccessHandler(casLoginSuccessHandler);
+ filter.setServiceProperties(casServiceProperties());
+ // 仅在 URL 含 ticket 时才尝试认证;无 ticket → 让 ExceptionTranslationFilter 触发 EntryPoint 重定向 CAS
+ final String appLogin = casProperties.getAppLogin();
+ filter.setRequiresAuthenticationRequestMatcher(req ->
+ appLogin.equals(req.getServletPath()) && req.getParameter("ticket") != null
+ );
+ return filter;
+ }
+
+ @Bean
+ public CasAuthenticationProvider casAuthenticationProvider() {
+ CasAuthenticationProvider provider = new CasAuthenticationProvider();
+ provider.setAuthenticationUserDetailsService(casUserDetailsService);
+ provider.setServiceProperties(casServiceProperties());
+ provider.setTicketValidator(new Cas20ServiceTicketValidator(casProperties.getServerHost()));
+ provider.setKey("casAuthProviderKey");
+ return provider;
+ }
+}
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java
new file mode 100644
index 0000000..b4a1677
--- /dev/null
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/base/security/cas/CasUserDetailsService.java
@@ -0,0 +1,74 @@
+package com.inspur.llm.chat.base.security.cas;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.inspur.llm.chat.base.security.UserDetail;
+import com.inspur.llm.chat.gpt.mapper.SysUserMapper;
+import com.inspur.llm.chat.gpt.mapper.UserMapper;
+import com.inspur.llm.chat.gpt.pojo.entity.SysUser;
+import com.inspur.llm.chat.gpt.pojo.entity.User;
+import com.inspur.llm.chat.gpt.pojo.vo.SysUserVO;
+import com.inspur.llm.chat.gpt.pojo.vo.UserVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
+import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+
+/**
+ * 把 CAS 返回的 principal 映射到本地用户:
+ * 1) sys_user.cas_username
+ * 2) gpt_user.cas_username
+ * 3) gpt_user.tel(fallback)
+ * 4) 都没找到 → 抛 UsernameNotFoundException
+ */
+@Slf4j
+@Service
+public class CasUserDetailsService implements AuthenticationUserDetailsService {
+
+ @Autowired
+ private SysUserMapper sysUserMapper;
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Override
+ public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) {
+ String casPrincipal = token.getName();
+ log.info("CAS 用户加载: principal={}", casPrincipal);
+
+ SysUser sysUser = sysUserMapper.selectOne(
+ Wrappers.lambdaQuery().eq(SysUser::getCasUsername, casPrincipal).last("limit 1"));
+ if (sysUser != null) {
+ log.info("CAS 命中 sys_user id={}", sysUser.getId());
+ SysUserVO vo = new SysUserVO();
+ BeanUtils.copyProperties(sysUser, vo);
+ return new UserDetail(vo, new HashSet<>());
+ }
+
+ User user = userMapper.selectOne(
+ Wrappers.lambdaQuery().eq(User::getCasUsername, casPrincipal).last("limit 1"));
+ if (user != null) {
+ log.info("CAS 命中 gpt_user (cas_username) id={}", user.getId());
+ UserVO vo = new UserVO();
+ BeanUtils.copyProperties(user, vo);
+ return new UserDetail(vo, new HashSet<>());
+ }
+
+ user = userMapper.selectOne(
+ Wrappers.lambdaQuery().eq(User::getTel, casPrincipal).last("limit 1"));
+ if (user != null) {
+ log.info("CAS 命中 gpt_user (tel fallback) id={}", user.getId());
+ UserVO vo = new UserVO();
+ BeanUtils.copyProperties(user, vo);
+ return new UserDetail(vo, new HashSet<>());
+ }
+
+ log.warn("CAS 用户未在战知库中找到: {}", casPrincipal);
+ throw new UsernameNotFoundException("CAS 用户未绑定战知账号: " + casPrincipal);
+ }
+}
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 33755dd..951cb84 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
@@ -22,7 +22,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -54,8 +55,8 @@ public class LoginController {
* @date: 2024/1/9
* @version: 1.0.0
*/
- @GetMapping("/api/oauth/token")
- public ResponseInfo login(LoginCommand command) {
+ @PostMapping("/api/oauth/token")
+ public ResponseInfo login(@RequestBody LoginCommand command) {
UsernamePasswordAuthenticationToken authenticationToken;
if (UserTypeEnum.TEL.getValue().equals(command.getLoginType())) {
UserVO user = userService.loginByTel(command.getTel(), command.getPassword(), command.getCode()).getData();
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/SysUser.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/SysUser.java
index 122b0ab..e02ccbc 100644
--- a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/SysUser.java
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/SysUser.java
@@ -70,6 +70,11 @@ public class SysUser extends BaseEntity {
*/
private String email;
+ /**
+ * CAS 单点登录用户名(钢研统一身份)
+ */
+ private String casUsername;
+
/**
* 性别 0->女;1->男
*/
diff --git a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/User.java b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/User.java
index c17c176..6c4122a 100644
--- a/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/User.java
+++ b/chat_web_backend/src/main/java/com/inspur/llm/chat/gpt/pojo/entity/User.java
@@ -74,6 +74,11 @@ public class User extends BaseEntity {
*/
private String unionid;
+ /**
+ * CAS 单点登录用户名(钢研统一身份)
+ */
+ private String casUsername;
+
/**
* 登录ip
*/
diff --git a/chat_web_backend/src/main/resources/application.yml b/chat_web_backend/src/main/resources/application.yml
index 0b5b5ca..3f2100d 100644
--- a/chat_web_backend/src/main/resources/application.yml
+++ b/chat_web_backend/src/main/resources/application.yml
@@ -92,4 +92,17 @@ xss:
# 词向量模型名称
embedding-model-name: bge_m3
+# CAS 单点登录配置
+security:
+ cas:
+ enabled: true
+ 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
+ app-logout: /cas/logout
+ app-key: 8UyEQdr6L6FfAH3D
+ app-secret: Ely1U6Uv2Nf8APQSfZdg1epSbQ6ORKwr
+ front-end-url: http://192.168.203.8:3000
+
diff --git a/chat_web_front/src/api/index.ts b/chat_web_front/src/api/index.ts
index fb85922..4a846aa 100644
--- a/chat_web_front/src/api/index.ts
+++ b/chat_web_front/src/api/index.ts
@@ -8,9 +8,9 @@ export function fetchChatConfig() {
})
}
-// 登录
+// 登录(POST + body,避免密码出现在 URL/access log)
export function fetchVerify(data: object) {
- return get({
+ return post({
url: '/app/api/oauth/token',
data,
})
diff --git a/chat_web_front/src/views/login/index.vue b/chat_web_front/src/views/login/index.vue
index d136bbe..04ba1e2 100644
--- a/chat_web_front/src/views/login/index.vue
+++ b/chat_web_front/src/views/login/index.vue
@@ -56,6 +56,14 @@
{{ loginTip }}
+
+
+ 通过统一身份登录
+
+
@@ -139,6 +147,22 @@ const handleVerify = async () => {
}
};
+// 跳后端 CAS 入口;后端 302 跳 CAS 服务器
+const goCasLogin = () => {
+ window.location.href = `/chat_web_backend/cas/login`;
+};
+
+// 处理 CAS 回调:URL 含 cas_token 参数 → 存 token 后跳首页
+onMounted(() => {
+ const params = new URLSearchParams(window.location.search);
+ const casToken = params.get("cas_token");
+ if (casToken) {
+ authStore.setToken(casToken);
+ window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split("?")[0]);
+ router.push("/chat");
+ }
+});
+