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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -70,6 +70,11 @@ public class SysUser extends BaseEntity {
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* CAS 单点登录用户名(钢研统一身份)
|
||||
*/
|
||||
private String casUsername;
|
||||
|
||||
/**
|
||||
* 性别 0->女;1->男
|
||||
*/
|
||||
|
||||
@@ -74,6 +74,11 @@ public class User extends BaseEntity {
|
||||
*/
|
||||
private String unionid;
|
||||
|
||||
/**
|
||||
* CAS 单点登录用户名(钢研统一身份)
|
||||
*/
|
||||
private String casUsername;
|
||||
|
||||
/**
|
||||
* 登录ip
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user