修复WebSocket身份认证问题
- 添加WebSocketAuthInterceptor处理token认证 - 修改WebSocket连接逻辑,支持token传递 - 统一用户身份识别,确保登录用户使用USER类型 - 修复前端环境变量配置,统一WebSocket URL - 添加Token测试页面用于验证功能 - 更新聊天消息处理逻辑,正确识别用户身份 解决了登录用户发送消息时同时保存GUEST和USER两种类型数据的问题
This commit is contained in:
@@ -24,9 +24,9 @@ public class EmotionSimpleApplication {
|
||||
System.out.println("🎉 情感博物馆服务启动成功!");
|
||||
System.out.println("📋 服务信息:");
|
||||
System.out.println(" - 服务名称: emotion-single");
|
||||
System.out.println(" - 服务端口: 8080");
|
||||
System.out.println(" - 服务端口: 19089");
|
||||
System.out.println(" - 环境配置: " + System.getProperty("spring.profiles.active"));
|
||||
System.out.println(" - API文档: http://localhost:8080/api/health");
|
||||
System.out.println(" - API文档: http://localhost:19089/api/health");
|
||||
System.out.println("========================================");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,18 +39,14 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF
|
||||
.csrf().disable()
|
||||
.csrf(csrf -> csrf.disable())
|
||||
|
||||
// 配置CORS
|
||||
.cors().configurationSource(corsConfigurationSource())
|
||||
|
||||
.and()
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
|
||||
// 配置会话管理
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
|
||||
.and()
|
||||
.sessionManagement(management -> management
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
@@ -58,10 +54,10 @@ public class SecurityConfig {
|
||||
.anyRequest().permitAll())
|
||||
|
||||
// 禁用默认登录页面
|
||||
.formLogin().disable()
|
||||
.formLogin(login -> login.disable())
|
||||
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic().disable();
|
||||
.httpBasic(basic -> basic.disable());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.WebSocketAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
@@ -16,6 +19,9 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Autowired
|
||||
private WebSocketAuthInterceptor webSocketAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// 启用简单消息代理,并设置消息代理的前缀
|
||||
@@ -39,4 +45,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
// 添加WebSocket认证拦截器
|
||||
registration.interceptors(webSocketAuthInterceptor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import com.emotion.service.AuthService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* WebSocket认证拦截器
|
||||
* 用于在WebSocket连接时验证用户身份
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
// 处理WebSocket连接时的认证
|
||||
handleAuthentication(accessor);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理WebSocket连接认证
|
||||
*/
|
||||
private void handleAuthentication(StompHeaderAccessor accessor) {
|
||||
try {
|
||||
// 从连接头中获取token
|
||||
String authHeader = accessor.getFirstNativeHeader("Authorization");
|
||||
String userId = accessor.getFirstNativeHeader("X-User-Id");
|
||||
|
||||
log.info("WebSocket连接认证: authHeader={}, userId={}",
|
||||
authHeader != null ? "Bearer ***" : null, userId);
|
||||
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
// 验证token
|
||||
if (authService.validateToken(token)) {
|
||||
String tokenUserId = authService.getUserIdFromToken(token);
|
||||
String username = authService.getUsernameFromToken(token);
|
||||
|
||||
log.info("WebSocket token验证成功: userId={}, username={}", tokenUserId, username);
|
||||
|
||||
// 创建认证对象
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
tokenUserId,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
);
|
||||
|
||||
// 设置用户认证信息
|
||||
accessor.setUser(authentication);
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", tokenUserId);
|
||||
accessor.getSessionAttributes().put("username", username);
|
||||
accessor.getSessionAttributes().put("authenticated", true);
|
||||
|
||||
} else {
|
||||
log.warn("WebSocket token验证失败: token无效");
|
||||
// token无效,但不阻止连接,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
} else {
|
||||
log.info("WebSocket连接无token,作为访客处理: userId={}", userId);
|
||||
// 无token,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket认证处理失败", e);
|
||||
// 认证失败,作为访客处理
|
||||
handleGuestUser(accessor, accessor.getFirstNativeHeader("X-User-Id"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理访客用户
|
||||
*/
|
||||
private void handleGuestUser(StompHeaderAccessor accessor, String userId) {
|
||||
String guestId = StringUtils.hasText(userId) ? userId : "guest_" + System.currentTimeMillis();
|
||||
|
||||
log.info("设置访客用户: guestId={}", guestId);
|
||||
|
||||
// 创建访客认证对象
|
||||
Authentication guestAuth = new UsernamePasswordAuthenticationToken(
|
||||
guestId,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_GUEST"))
|
||||
);
|
||||
|
||||
accessor.setUser(guestAuth);
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", guestId);
|
||||
accessor.getSessionAttributes().put("username", guestId);
|
||||
accessor.getSessionAttributes().put("authenticated", false);
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,41 @@ public class WebSocketService {
|
||||
*/
|
||||
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
log.info("处理聊天消息: {}", request);
|
||||
|
||||
log.info("处理聊天消息: request={}, sessionId={}, principal={}", request, sessionId, principal);
|
||||
|
||||
// 验证请求参数
|
||||
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
|
||||
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 确定用户身份和类型
|
||||
String userId = request.getSenderId();
|
||||
WebSocketMessage.SenderType senderType = WebSocketMessage.SenderType.GUEST;
|
||||
|
||||
if (principal != null) {
|
||||
userId = principal.getName();
|
||||
// 如果用户ID不是以guest_开头,说明是认证用户
|
||||
if (!userId.startsWith("guest_")) {
|
||||
senderType = WebSocketMessage.SenderType.USER;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新请求中的用户信息
|
||||
request.setSenderId(userId);
|
||||
request.setSenderType(senderType == WebSocketMessage.SenderType.USER ? ChatRequest.SenderType.USER
|
||||
: ChatRequest.SenderType.GUEST);
|
||||
|
||||
log.info("确定用户身份: userId={}, senderType={}", userId, senderType);
|
||||
|
||||
// 构建用户消息
|
||||
WebSocketMessage userMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(request.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(request.getContent())
|
||||
.senderId(request.getSenderId())
|
||||
.senderType(WebSocketMessage.SenderType.valueOf(request.getSenderType().name()))
|
||||
.senderId(userId)
|
||||
.senderType(senderType)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
@@ -89,14 +108,22 @@ public class WebSocketService {
|
||||
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = request.getUserId();
|
||||
if (userId == null && principal != null) {
|
||||
boolean isAuthenticated = false;
|
||||
|
||||
// 优先从Principal获取认证用户信息
|
||||
if (principal != null) {
|
||||
userId = principal.getName();
|
||||
// 检查是否是认证用户(不是访客)
|
||||
isAuthenticated = !userId.startsWith("guest_");
|
||||
}
|
||||
|
||||
// 如果还没有userId,生成访客ID
|
||||
if (userId == null) {
|
||||
userId = "guest_" + sessionId;
|
||||
}
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
|
||||
userId, sessionId, isAuthenticated);
|
||||
|
||||
// 记录在线用户
|
||||
onlineUsers.put(sessionId, userId);
|
||||
|
||||
Reference in New Issue
Block a user