package com.mydreamjob.config; import com.mydreamjob.auth.JwtAuthenticationFilter; import com.mydreamjob.audit.AuditService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpMethod; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; 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.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.List; /** * Spring Security 配置 */ @Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableJpaAuditing public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final AuditService auditService; @Value("${app.cors.allowed-origins:http://localhost:5173,http://localhost:3000}") private String allowedOrigins; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setContentType("application/json;charset=UTF-8"); response.setStatus(401); response.getWriter().write( "{\"code\":401,\"message\":\"not authenticated\",\"timestamp\":\"\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setContentType("application/json;charset=UTF-8"); response.setStatus(403); response.getWriter().write( "{\"code\":403,\"message\":\"access denied\",\"timestamp\":\"\"}"); })) .authorizeHttpRequests(auth -> auth // Actuator 健康检查 .requestMatchers("/actuator/health/**").permitAll() .requestMatchers("/actuator/info").permitAll() // Swagger UI .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**").permitAll() // 健康检查 API .requestMatchers("/health/**").permitAll() .requestMatchers("/health").permitAll() // 认证相关 .requestMatchers("/auth/sms/send").permitAll() .requestMatchers("/auth/login").permitAll() .requestMatchers("/auth/register").permitAll() // 当前用户 .requestMatchers("/auth/me").authenticated() // 行业技能词云(需登录,仅 STUDENT,Controller 层校验角色) .requestMatchers(HttpMethod.GET, "/skills/**").authenticated() // 企业资料(需登录) .requestMatchers("/companies/me/**").authenticated() // 企业公开岗位列表(匿名 GET) .requestMatchers(HttpMethod.GET, "/companies/*/jobs").permitAll() // 企业公开主页(匿名 GET) .requestMatchers(HttpMethod.GET, "/companies/*").permitAll() // 管理后台(需登录+ADMIN角色) .requestMatchers("/admin/**").authenticated() // 权重模板(登录即可) .requestMatchers(HttpMethod.GET, "/review-weight-templates").authenticated() // 个人权重配置(需登录,仅 STUDENT,Service 层校验) .requestMatchers(HttpMethod.GET, "/profile/me/review-weights").authenticated() .requestMatchers(HttpMethod.PUT, "/profile/me/review-weights").authenticated() // 求职者评分卡(需登录,仅 STUDENT,Service 层校验) .requestMatchers(HttpMethod.GET, "/profile/me/candidate-score-card").authenticated() .requestMatchers(HttpMethod.GET, "/profile/me/candidate-score-trend").authenticated() .requestMatchers(HttpMethod.GET, "/profile/me/candidate-percentile").authenticated() // 个人资料(需登录) .requestMatchers("/profile/me").authenticated() // 岗位评价查询、详情、评分汇总、岗位对比(公开) .requestMatchers(HttpMethod.GET, "/jobs/compare").permitAll() .requestMatchers(HttpMethod.GET, "/jobs/*/reviews/summary").permitAll() .requestMatchers(HttpMethod.GET, "/jobs/*/reviews/*").permitAll() .requestMatchers(HttpMethod.GET, "/jobs/*/reviews").permitAll() // 岗位收藏和评价接口(需登录) .requestMatchers("/jobs/*/favorite").authenticated() .requestMatchers("/jobs/favorites").authenticated() .requestMatchers("/jobs/*/reviews/**").authenticated() // 企业回复评价(公开查询,写操作需登录) .requestMatchers(HttpMethod.GET, "/reviews/*/reply").permitAll() .requestMatchers("/reviews/*/reply").authenticated() // 个性化评分(需登录,仅 STUDENT,Service 层校验) .requestMatchers(HttpMethod.GET, "/jobs/*/personalized-review-score").authenticated() // 岗位公开查询 .requestMatchers("/jobs/public/**").permitAll() .requestMatchers("/jobs").permitAll() // 岗位搜索与筛选(公开) .requestMatchers(HttpMethod.GET, "/jobs/search").permitAll() .requestMatchers(HttpMethod.GET, "/jobs/filters").permitAll() // 岗位管理(需登录) .requestMatchers("/jobs/**").authenticated() // 投递(需登录) .requestMatchers("/applications/**").authenticated() .requestMatchers("/company/applications").authenticated() // 企业候选人看板(需登录,COMPANY 角色,Service 层校验) .requestMatchers("/company/candidates/**").authenticated() // 企业评分运营看板(需登录,COMPANY/ADMIN 角色,Service 层校验) .requestMatchers("/company/scoring/dashboard/**").authenticated() // 面试(需登录) .requestMatchers("/interviews/**").authenticated() // 入职清单与公司知识(需登录;细粒度角色/所有权在 Service 校验) .requestMatchers("/onboarding/**").authenticated() // 薪资计算器(需登录;仅 STUDENT 在 Service 校验) .requestMatchers("/salary-calculator/**").authenticated() // API 信息入口 .requestMatchers("/").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of(allowedOrigins.split(","))); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-Id")); config.setExposedHeaders(List.of("X-Request-Id")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }