diff --git a/doc/web/docs/conf.d/http.conf b/doc/web/docs/conf.d/http.conf index ddf0d75..ae2fe22 100644 --- a/doc/web/docs/conf.d/http.conf +++ b/doc/web/docs/conf.d/http.conf @@ -26,4 +26,15 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location /ai-platform/ws/ { + proxy_pass http://ai-platform-server/ai-platform/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + } \ No newline at end of file diff --git a/doc/web/docs/conf.d/https.conf b/doc/web/docs/conf.d/https.conf index 751c24c..312af0e 100644 --- a/doc/web/docs/conf.d/https.conf +++ b/doc/web/docs/conf.d/https.conf @@ -41,4 +41,14 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + location /ai-platform/ws/ { + proxy_pass http://ai-platform-server/ai-platform/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } } \ No newline at end of file diff --git a/src/main/java/com/supervision/config/SecurityConfig.java b/src/main/java/com/supervision/config/SecurityConfig.java index ed39545..3f8902f 100644 --- a/src/main/java/com/supervision/config/SecurityConfig.java +++ b/src/main/java/com/supervision/config/SecurityConfig.java @@ -20,6 +20,8 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import java.util.Base64; @@ -28,22 +30,22 @@ import java.util.Base64; @RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final UserDetailsService userDetailsService; - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, + RequestMatcher[] permitAllRequestMatcher, + JwtAuthenticationFilter jwtAuthenticationFilter, + AuthenticationProvider authenticationProvider) throws Exception { http .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF .cors(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/login","/agent/streamChat","/livetalking/chatCallBack").permitAll() + .requestMatchers(permitAllRequestMatcher).permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - .authenticationProvider(authenticationProvider()) + .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 禁用 formLogin 和 httpBasic .formLogin(AbstractHttpConfigurer::disable) @@ -51,9 +53,17 @@ public class SecurityConfig { return http.build(); } - @Bean - public AuthenticationProvider authenticationProvider() { + public RequestMatcher[] permitAllRequestMatchers() { + return new RequestMatcher[] { + new AntPathRequestMatcher("/auth/login"), + new AntPathRequestMatcher("/agent/streamChat"), + new AntPathRequestMatcher("/livetalking/chatCallBack"), + new AntPathRequestMatcher("/ws/**") + }; + } + @Bean + public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { // 使用DaoAuthenticationProvider,并注入自定义的UserDetailsService和PasswordEncoder DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); // 从数据库读取用户进行认证 diff --git a/src/main/java/com/supervision/filter/JwtAuthenticationFilter.java b/src/main/java/com/supervision/filter/JwtAuthenticationFilter.java index f68ca84..0e3e03c 100644 --- a/src/main/java/com/supervision/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/supervision/filter/JwtAuthenticationFilter.java @@ -1,5 +1,7 @@ package com.supervision.filter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.supervision.dto.R; import com.supervision.util.JwtUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -7,14 +9,17 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; - import java.io.IOException; @@ -23,40 +28,88 @@ import java.io.IOException; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; + private final UserDetailsService userDetailsService; + private final ObjectMapper objectMapper; + + private final RequestMatcher[] permitAllRequestMatchers; @Override protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { + + // 1. 检查是否在白名单中 + if (isPermitAllRequest(request)) { + filterChain.doFilter(request, response); + return; + } + // 获取 Authorization 头 String authHeader = request.getHeader("Authorization"); - if (authHeader != null && authHeader.startsWith("Bearer ")) { - // 提取JWT Token(去掉前缀"Bearer ") - String token = authHeader.substring(7); - String username; + // 提取 token(去掉 "Bearer " 前缀) + String token = authHeader.substring(7); + String username; + + // 2:解析 token 失败(格式错误、签名无效、过期等) + try { + username = jwtUtils.getUsernameFromToken(token); + } catch (Exception e) { + writeTokenErrorResponse(response, "Token 无效或已过期,请重新登录"); + return; // 中断 + } + + // 3:成功解析出用户名,且当前 SecurityContext 未认证 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { try { - // 从JWT中解析用户名 - username = jwtUtils.getUsernameFromToken(token); - } catch (Exception e) { - // 如果JWT格式不正确或过期,直接放行(后续的过滤器会处理认证失败) - filterChain.doFilter(request, response); - return; - } - // 如果成功提取到用户名,并且当前没有已认证的用户 - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // 根据用户名从数据库加载用户信息 + // 从 UserDetailsService 加载用户信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); - // 验证Token的有效性(是否未过期) + + // 验证 Token 是否未过期(你可以根据需要添加更多验证,如登录时间) if (!jwtUtils.isTokenExpired(token)) { - // 将用户信息封装到Authentication对象中,标记为已认证 + // 创建认证对象 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - // 将Authentication对象放入SecurityContext,表示当前请求已通过认证 + + // 设置到 SecurityContext SecurityContextHolder.getContext().setAuthentication(authToken); + + // 认证成功!放行到 Controller + filterChain.doFilter(request, response); + return; + } else { + // Token 已过期 + writeTokenErrorResponse(response, "Token 已过期,请重新登录"); + return; } + + } catch (UsernameNotFoundException e) { + // 用户不存在 + writeTokenErrorResponse(response, "用户不存在或已被删除"); + return; + } catch (Exception e) { + // 其他加载异常 + writeTokenErrorResponse(response, "用户信息加载失败:" + e.getMessage()); + return; } } - filterChain.doFilter(request, response); + + // 特殊情况兜底:比如 token 解析成功但 userDetails 为 null,或已认证但不符合预期 + // 根据你的“核心逻辑”,只要没成功放行,就视为失败 + writeTokenErrorResponse(response, "认证失败,请重新登录"); + } + private void writeTokenErrorResponse(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpStatus.OK.value()); // 设置为 200 + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), R.fail(401, message)); } + private boolean isPermitAllRequest(HttpServletRequest request) { + for (RequestMatcher matcher : permitAllRequestMatchers) { + if (matcher.matches(request)) { + return true; + } + } + return false; + } } diff --git a/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java b/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java index b49df96..36df759 100644 --- a/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java +++ b/src/main/java/com/supervision/service/danmaku/DanmakuWebSocketHandler.java @@ -31,27 +31,32 @@ public class DanmakuWebSocketHandler extends TextWebSocketHandler { private final LivetalkingService livetalkingService; @Override - protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) { + protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws IOException { // 解析客户端消息 - DanmakuMessage danmaku = DanmakuMessage.fromJson(textMessage.getPayload()); - String roomId = getRoomIdFromSession(session); - danmaku.setRoomId(roomId); + try { + DanmakuMessage danmaku = DanmakuMessage.fromJson(textMessage.getPayload()); + String roomId = getRoomIdFromSession(session); + danmaku.setRoomId(roomId); - // 验证消息 - validateDanmaku(danmaku); + // 验证消息 + validateDanmaku(danmaku); - // 保存到日志 - String messageId = dialogueLogService.saveLog(danmaku); + // 保存到日志 + String messageId = dialogueLogService.saveLog(danmaku); - // 发布到消息中心 - DanmakuPublisher.getInstance().publish(danmaku); + // 发布到消息中心 + DanmakuPublisher.getInstance().publish(danmaku); - // 判断是否需要回复 - if (isQuestion(danmaku.getContent())) { - log.info("检测到问题: {}", danmaku.getContent()); - LivetalkingChatDTO livetalkingChatDTO = new LivetalkingChatDTO(danmaku); - livetalkingChatDTO.setMessageId(messageId); - livetalkingService.chat(livetalkingChatDTO); + // 判断是否需要回复 + if (isQuestion(danmaku.getContent())) { + log.info("检测到问题: {}", danmaku.getContent()); + LivetalkingChatDTO livetalkingChatDTO = new LivetalkingChatDTO(danmaku); + livetalkingChatDTO.setMessageId(messageId); + livetalkingService.chat(livetalkingChatDTO); + } + } catch (Exception e) { + log.info("处理弹幕消息失败: {}", e.getMessage()); + afterConnectionClosed(session, CloseStatus.BAD_DATA); } }