package kr.itn.itnhub.config; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.http.HttpStatus; @Configuration public class SecurityConfig { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 관리자 1계정. 값은 환경변수로만 들어온다. * 운영에서는 APP_ADMIN_PASSWORD에 충분히 긴 무작위 문자열을 넣는다. */ @Bean UserDetailsService userDetailsService(AdminProperties admin, PasswordEncoder encoder) { return new InMemoryUserDetailsManager( User.withUsername(admin.username()) .password(encoder.encode(admin.password())) .roles("ADMIN") .build()); } @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // SPA가 읽어서 X-XSRF-TOKEN 헤더로 되돌려 보낸다 .csrf(csrf -> csrf.csrfTokenRepository( CookieCsrfTokenRepository.withHttpOnlyFalse())) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() .requestMatchers("/api/**").authenticated() .anyRequest().permitAll()) .formLogin(form -> form .loginProcessingUrl("/api/auth/login") .successHandler((req, res, a) -> res.setStatus(HttpServletResponse.SC_OK)) .failureHandler((req, res, e) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED))) .logout(logout -> logout .logoutUrl("/api/auth/logout") .logoutSuccessHandler((req, res, a) -> res.setStatus(HttpServletResponse.SC_NO_CONTENT))) // API는 로그인 페이지로 리다이렉트하지 않고 401을 준다 .exceptionHandling(ex -> ex.authenticationEntryPoint( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))); return http.build(); } }