checkpoint: 重写 langchain agent 前的快照

包含未合入 main 的工作:
- CAS 单点登录接入 (CasSecurityConfig + 4 个 CAS 类)
- LoginController 改 POST + body, 修 URL 密码暴露
- chat_test.py 修 texts→text typo
- 前端外部系统侧边栏 + login API POST
- gateway-nginx.conf

下一步: 从这个分支拉 feat/langgraph-rewrite 做 langchain-chat agent 重写
回滚点: git checkout backup/pre-langgraph

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 14:58:53 +08:00
parent f8e8017434
commit 911f7adee6
17 changed files with 647 additions and 10 deletions

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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<CasAssertionAuthenticationToken> 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;
}
}

View File

@@ -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.telfallback
* 4) 都没找到 → 抛 UsernameNotFoundException
*/
@Slf4j
@Service
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@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.<SysUser>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.<User>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.<User>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);
}
}

View File

@@ -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<Oauth2Token> login(LoginCommand command) {
@PostMapping("/api/oauth/token")
public ResponseInfo<Oauth2Token> login(@RequestBody LoginCommand command) {
UsernamePasswordAuthenticationToken authenticationToken;
if (UserTypeEnum.TEL.getValue().equals(command.getLoginType())) {
UserVO user = userService.loginByTel(command.getTel(), command.getPassword(), command.getCode()).getData();

View File

@@ -70,6 +70,11 @@ public class SysUser extends BaseEntity {
*/
private String email;
/**
* CAS 单点登录用户名(钢研统一身份)
*/
private String casUsername;
/**
* 性别 0->女;1->男
*/

View File

@@ -74,6 +74,11 @@ public class User extends BaseEntity {
*/
private String unionid;
/**
* CAS 单点登录用户名(钢研统一身份)
*/
private String casUsername;
/**
* 登录ip
*/

View File

@@ -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