轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

176 lines
11 KiB

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
// S03-C 整改:业务端口统一放行 /actuator/**。
// Actuator 端点仍只注册在独立管理端口(默认 127.0.0.1),业务端口上
// /actuator/** 全部落入 MVC 的不存在资源处理,由 GlobalExceptionHandler 的
// NoResourceFoundException 精确映射为 404 —— 避免匿名请求先被认证层
// 拦截成 401(未放行路径)或落入通用异常映射成 500(放行路径)。
// permitAll 不代表暴露指标,只保证所有 /actuator/** 都走到同一 404 语义;
// 业务端口不得新增任何 Actuator Controller 或端点映射。
.requestMatchers("/actuator/**").permitAll()
// Swagger UI
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**").permitAll()
// 健康检查 API
.requestMatchers("/health/**").permitAll()
.requestMatchers("/health").permitAll()
// 平台维护的岗位推荐分类字典(公开只读)
.requestMatchers(HttpMethod.GET, "/job-recommendation-taxonomies").permitAll()
// 认证相关
.requestMatchers("/auth/sms/send").permitAll()
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/auth/register").permitAll()
// S02:刷新允许匿名进入 Controller;是否成功仍取决于 Refresh Token 校验。
// permitAll 只表示可进入,不代表 Token 有效。
.requestMatchers("/auth/refresh").permitAll()
// 当前用户
.requestMatchers("/auth/me").authenticated()
// S02:登出必须已认证
.requestMatchers("/auth/logout").authenticated()
// 企业资料(需登录)
.requestMatchers("/companies/me/**").authenticated()
// V41 学生人才发现授权与企业人才发现推荐(角色和归属由 Service 校验)
.requestMatchers("/profile/me/talent-discovery").authenticated()
.requestMatchers("/companies/me/jobs/*/talent-recommendations").authenticated()
// V42 企业人才邀约与学生决策(角色和租户归属由 Service 二次校验)
.requestMatchers("/companies/me/jobs/*/talent-invitations/**").authenticated()
.requestMatchers("/companies/me/talent-invitations/**").authenticated()
.requestMatchers("/profile/me/talent-invitations/**").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()
// 个性化评分(需登录,仅 STUDENT,Service 层校验)
.requestMatchers(HttpMethod.GET, "/jobs/*/personalized-review-score").authenticated()
// 岗位公开查询
.requestMatchers("/jobs/public/**").permitAll()
// 仅公开岗位列表为匿名可读;写操作必须命中下方的 /jobs/** → authenticated
.requestMatchers(HttpMethod.GET, "/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()
// RIASEC 职业兴趣测评(需登录;类型/题目任意登录用户,答卷仅 STUDENT 由 Service 层校验)
.requestMatchers("/assessments/**").authenticated()
// 四维能力测评(需登录;类型任意登录用户,答卷仅 STUDENT 本人由 Service 层校验)
.requestMatchers("/abilities/**").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();
}
}