From 0f946a359238ae386443d1c4d442c42497771108 Mon Sep 17 00:00:00 2001 From: gitee Date: Wed, 30 Jul 2025 16:57:19 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E6=9D=83=E9=99=90=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A6=82=E6=9E=9C=E6=9D=83?= =?UTF-8?q?=E9=99=90=E8=AE=A4=E8=AF=81=E4=B8=8D=E9=80=9A=E8=BF=87=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=9C=A8=E8=BF=87=E6=BB=A4=E5=99=A8=E4=B8=AD=E8=BF=94?= =?UTF-8?q?=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/SecurityConfig.java | 25 +++-- .../filter/JwtAuthenticationFilter.java | 95 ++++++++++++++----- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/supervision/livedigitalavatarmanage/config/SecurityConfig.java b/src/main/java/com/supervision/livedigitalavatarmanage/config/SecurityConfig.java index a08acf2..afccc25 100644 --- a/src/main/java/com/supervision/livedigitalavatarmanage/config/SecurityConfig.java +++ b/src/main/java/com/supervision/livedigitalavatarmanage/config/SecurityConfig.java @@ -20,7 +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; @@ -29,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","/ollama/generate").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) @@ -52,9 +53,17 @@ public class SecurityConfig { return http.build(); } + @Bean + public RequestMatcher[] permitAllRequestMatchers() { + return new RequestMatcher[]{ + new AntPathRequestMatcher("/auth/login"), + new AntPathRequestMatcher("/ollama/generate") + }; + } + @Bean - public AuthenticationProvider authenticationProvider() { + public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { // 使用DaoAuthenticationProvider,并注入自定义的UserDetailsService和PasswordEncoder DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); // 从数据库读取用户进行认证 diff --git a/src/main/java/com/supervision/livedigitalavatarmanage/filter/JwtAuthenticationFilter.java b/src/main/java/com/supervision/livedigitalavatarmanage/filter/JwtAuthenticationFilter.java index f84e39f..7ce6e1e 100644 --- a/src/main/java/com/supervision/livedigitalavatarmanage/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/supervision/livedigitalavatarmanage/filter/JwtAuthenticationFilter.java @@ -1,19 +1,23 @@ package com.supervision.livedigitalavatarmanage.filter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.supervision.livedigitalavatarmanage.dto.R; import com.supervision.livedigitalavatarmanage.dto.UserDetail; import com.supervision.livedigitalavatarmanage.util.JwtUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; import java.io.IOException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @Component @@ -21,42 +25,87 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS 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 { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + // 1. 检查是否在白名单中 + if (isPermitAllRequest(request)) { + filterChain.doFilter(request, response); + return; + } + String authHeader = request.getHeader("Authorization"); - if (authHeader != null && authHeader.startsWith("Bearer ")) { - // 提取JWT Token(去掉前缀"Bearer ") - String token = authHeader.substring(7); - String username; + //2:根本没有 Authorization 头 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + writeTokenErrorResponse(response, "用户未登录,请登录"); + return; // 直接返回,不放行 + } + + String token = authHeader.substring(7); // 去掉 "Bearer " + String username; + // 3:解析 Token 失败(格式错误、签名无效、过期等) + try { + username = jwtUtils.getUsernameFromToken(token); + } catch (Exception e) { + writeTokenErrorResponse(response, "Token 无效或已过期,请重新登录"); + return; + } + + // 4:解析出用户名,但 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) { - // 根据用户名从数据库加载用户信息 UserDetail userDetails = (UserDetail) userDetailsService.loadUserByUsername(username); - // 验证登录时间是否一致,如果不一致说明登录已过期 - // 验证Token的有效性(是否未过期) + + // 验证 Token 是否仍然有效(时间戳、登录时间等) if (jwtUtils.validateLoginTime(token, userDetails.getLastLoginDate()) && !jwtUtils.isTokenExpired(token)) { - // 将用户信息封装到Authentication对象中,标记为已认证 + + //认证成功:设置 SecurityContext UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - // 将Authentication对象放入SecurityContext,表示当前请求已通过认证 SecurityContextHolder.getContext().setAuthentication(authToken); + + //成功后放行,进入 Controller + filterChain.doFilter(request, response); + return; + + } else { + //Token 过期 或 登录态不一致 + writeTokenErrorResponse(response, "登录已过期,请重新登录"); + return; } + + } catch (UsernameNotFoundException e) { + writeTokenErrorResponse(response, "用户不存在"); + return; + } catch (Exception e) { + writeTokenErrorResponse(response, "用户认证异常:" + e.getMessage()); + return; } } - filterChain.doFilter(request, response); + 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; + } }