From 73717d6e6f22fa0ca6d4864fc08ec64a7acf7222 Mon Sep 17 00:00:00 2001 From: ovo Date: Fri, 6 Dec 2024 21:23:13 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guwan/backend/config/SecurityConfig.java | 42 +++++--- .../backend/constant/SecurityConstants.java | 33 ++++++ .../security/JwtAuthenticationFilter.java | 102 ++++++++++-------- 3 files changed, 121 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/guwan/backend/constant/SecurityConstants.java diff --git a/src/main/java/com/guwan/backend/config/SecurityConfig.java b/src/main/java/com/guwan/backend/config/SecurityConfig.java index ef24003..2b71ca0 100644 --- a/src/main/java/com/guwan/backend/config/SecurityConfig.java +++ b/src/main/java/com/guwan/backend/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.guwan.backend.config; +import com.guwan.backend.constant.SecurityConstants; import com.guwan.backend.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -15,6 +16,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +/** + * Spring Security 安全配置类 + * 配置安全相关的全局策略 + */ @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -22,34 +27,43 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; + /** + * 配置安全过滤链 + * 定义了系统的安全策略,包括: + * 1. CSRF 和 CORS 配置 + * 2. Session 管理策略 + * 3. 请求授权规则 + * 4. JWT 过滤器配置 + */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) - .cors(AbstractHttpConfigurer::disable) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF保护 + .cors(AbstractHttpConfigurer::disable) // 禁用CORS保护 + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 使用无状态会话 .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/demo/**", - "/api/user/register", - "/api/user/login", - "/api/user/register/email", - "/api/user/register/phone", - "/api/user/email/code", - "/api/user/phone/code" - ).permitAll() - .anyRequest().authenticated() + .requestMatchers(SecurityConstants.WHITE_LIST.toArray(new String[0])).permitAll() // 配置API白名单 + .requestMatchers(SecurityConstants.STATIC_RESOURCES.toArray(new String[0])).permitAll() // 配置静态资源白名单 + .anyRequest().authenticated() // 其他所有请求都需要认证 ) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器 return http.build(); } + /** + * 认证管理器 + */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } + /** + * 密码编码器 + * 使用 BCrypt 加密算法 + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/com/guwan/backend/constant/SecurityConstants.java b/src/main/java/com/guwan/backend/constant/SecurityConstants.java new file mode 100644 index 0000000..6cc0ee2 --- /dev/null +++ b/src/main/java/com/guwan/backend/constant/SecurityConstants.java @@ -0,0 +1,33 @@ +package com.guwan.backend.constant; + +import java.util.List; + +/** + * 安全相关常量配置 + */ +public class SecurityConstants { + + /** + * API接口白名单 + * 这些路径可以直接访问,不需要认证 + */ + public static final List WHITE_LIST = List.of( + "/demo/**", // 测试接口 + "/api/user/register", // 用户注册 + "/api/user/login", // 用户登录 + "/api/user/register/email", // 邮箱注册 + "/api/user/register/phone", // 手机号注册 + "/api/user/email/code", // 获取邮箱验证码 + "/api/user/phone/code" // 获取手机验证码 + ); + + /** + * 静态资源白名单 + * 这些路径用于访问静态资源,不需要认证 + */ + public static final List STATIC_RESOURCES = List.of( + "/static/**", // 静态资源目录 + "/public/**", // 公共资源目录 + "/error" // 错误页面 + ); +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/security/JwtAuthenticationFilter.java b/src/main/java/com/guwan/backend/security/JwtAuthenticationFilter.java index 3871e45..94bebc8 100644 --- a/src/main/java/com/guwan/backend/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/guwan/backend/security/JwtAuthenticationFilter.java @@ -2,6 +2,7 @@ package com.guwan.backend.security; import com.fasterxml.jackson.databind.ObjectMapper; import com.guwan.backend.common.Result; +import com.guwan.backend.constant.SecurityConstants; import com.guwan.backend.util.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -18,9 +19,11 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.Arrays; -import java.util.List; +/** + * JWT认证过滤器 + * 负责处理JWT token的验证和用户认证 + */ @Slf4j @Component @RequiredArgsConstructor @@ -30,29 +33,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsServiceImpl userDetailsService; private final ObjectMapper objectMapper; - // 不需要验证的路径 - private static final List PERMIT_PATHS = Arrays.asList( - "/demo/**", - "/api/user/register", - "/api/user/login", - "/api/user/register/email", - "/api/user/register/phone", - "/api/user/email/code", - "/api/user/phone/code" - ); - + /** + * 过滤器主要逻辑 + * 1. 检查是否是白名单路径 + * 2. 验证JWT token + * 3. 设置认证信息 + */ @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - - // 检查是否是允许的路径 + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { String path = request.getServletPath(); + + // 如果是白名单路径,直接放行 if (isPermitPath(path)) { chain.doFilter(request, response); return; } try { + // 验证token String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { handleAuthenticationError(response, "未登录"); @@ -65,16 +65,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return; } - Long userId = jwtUtil.getUserIdFromToken(jwt); - if (SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = userDetailsService.loadUserById(userId); - - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - SecurityContextHolder.getContext().setAuthentication(authentication); - } + // 设置认证信息 + setAuthentication(request, jwt); chain.doFilter(request, response); } catch (Exception e) { @@ -83,15 +75,50 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } + /** + * 检查请求路径是否在白名单中 + */ private boolean isPermitPath(String path) { - return PERMIT_PATHS.stream().anyMatch(pattern -> - pattern.endsWith("/**") - ? path.startsWith(pattern.substring(0, pattern.length() - 3)) - : path.equals(pattern) - ); + return SecurityConstants.WHITE_LIST.stream() + .anyMatch(pattern -> matchPath(path, pattern)) || + SecurityConstants.STATIC_RESOURCES.stream() + .anyMatch(pattern -> matchPath(path, pattern)); } - private void handleAuthenticationError(HttpServletResponse response, String message) throws IOException { + /** + * 路径匹配逻辑 + * 支持通配符 /** 匹配 + */ + private boolean matchPath(String path, String pattern) { + return pattern.endsWith("/**") + ? path.startsWith(pattern.substring(0, pattern.length() - 3)) + : path.equals(pattern); + } + + /** + * 设置认证信息到 SecurityContext + */ + private void setAuthentication(HttpServletRequest request, String jwt) { + Long userId = jwtUtil.getUserIdFromToken(jwt); + if (SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserById(userId); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + /** + * 处理认证错误 + * 返回统一的错误响应格式 + */ + private void handleAuthenticationError(HttpServletResponse response, String message) + throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); @@ -99,13 +126,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { Result result = Result.error(message); response.getWriter().write(objectMapper.writeValueAsString(result)); } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String path = request.getServletPath(); - // 静态资源不过滤 - return path.startsWith("/static/") || - path.startsWith("/public/") || - path.startsWith("/error"); - } } \ No newline at end of file