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