개발인생/Project

Spring Security 설정 완벽 분석 - 보안 설정 가이드

forri 2025. 3. 5. 18:12
프로젝트 코드 리뷰
package com.project.erpre.config;

import com.project.erpre.auth.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
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.Collections;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    public WebSecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }

    // 회원가입이 생략되어 평문 비밀번호를 사용하고 있으므로 BCryptPasswordEncoder 제거
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
//        configuration.setAllowedOriginPatterns(Collections.singletonList("http://localhost:8787")); // 웹소켓에서는 allowedOrigins 대신 allowedOriginPatterns 사용
        configuration.setAllowedOriginPatterns(Collections.singletonList("*")); // 웹소켓에서는 allowedOrigins 대신 allowedOriginPatterns 사용
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 모든 HTTP 메서드 명시적 허용
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); // 필요한 헤더 추가
        configuration.setAllowCredentials(true); // 쿠키 허용 // test 환경에서 false

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.cors().configurationSource(corsConfigurationSource()) // CORS 설정 활성화 및 직접 설정
                .and()
                .csrf()
                .ignoringAntMatchers("/talk/**", "/app/**", "/topic/**", "/ws/**", "/queue/**")
                    .disable()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // 세션 기반으로 설정
                .and()
                .authorizeRequests()
                    .antMatchers("/orderReport", "/employeeAttend", "/employeeSalary").hasAuthority("ROLE_SPECIAL_ACCESS") // 특정 페이지 접근 제한
                    .antMatchers("/android/api/**").permitAll()
                    .antMatchers("/**", "/api/**", "/talk/**", "/user/**", "/app/**", "/topic/**", "/ws/**", "/queue/**", "/uploads/**", "/profile-pictures/**", "/chat/**", "/Temp/**").permitAll()
                    .antMatchers("/",  "/static/**", "/bundle/**", "/img/**", "/css/**", "/fonts/**", "/index.html").permitAll()
                    .antMatchers("/api/login", "/login").permitAll() // 로그인 앤드포인트 허용 (현재 모든 페이지 접근 허용! 이거 나중에 바꿔야 함)
                    .antMatchers("/user/**", "/").hasAnyRole("Staff", "Admin", "Assistant Manager", "Executive", "Director", "Manager")
                    .antMatchers("/admin/**").hasRole("Admin")
                    .anyRequest().permitAll() // 인증 필요 없음
                .and()
                .formLogin() // 기본 로그인 폼 제공
                    .loginPage("/login")
                    .defaultSuccessUrl("/main", true)
                    .permitAll()
                .and()
                .logout() // 로그아웃 처리
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login")
                    .permitAll();
//                .and()
//                .anonymous().disable(); // 익명 사용자 비활성화


        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class).build();
    }
}

 

이 코드는 Spring Security를 사용하여 웹 애플리케이션의 보안 설정을 정의하는 설정 파일임. WebSecurityConfig 클래스는 사용자 인증 및 인가, CORS 설정, CSRF 보호 해제, 로그인 및 로그아웃 설정 등을 담당함.


내가 했던 부분은 아니지만 공부차원에서 코드 리뷰를 해보려고 한다🫡


1. 클래스 및 생성자

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    private final CustomUserDetailsService customUserDetailsService;

    public WebSecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }
}

설명

  • @Configuration : 이 클래스가 Spring 설정 파일임을 나타냅니다.
  • @EnableWebSecurity : Spring Security 기능을 활성화하는 어노테이션입니다.
  • CustomUserDetailsService customUserDetailsService :
    • CustomUserDetailsService는 사용자 인증을 처리하는 서비스입니다.
    • UserDetailsService 인터페이스를 구현한 클래스이며, DB에서 사용자 정보를 불러오는 역할을 합니다.

관련 개념

  • UserDetailsService는 Spring Security에서 사용자 정보를 관리하는 핵심 인터페이스입니다.
  • CustomUserDetailsService는 DB에서 사용자 계정을 조회하고 인증을 수행하는 역할을 합니다.

2. 비밀번호 인코더 설정

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

설명

  • @Bean : 이 메서드는 Spring Bean으로 등록되어 애플리케이션에서 재사용됩니다.
  • NoOpPasswordEncoder.getInstance() :
    • 평문(암호화되지 않은) 비밀번호를 사용합니다. 보안상 매우 위험하므로, 실무에서는 BCryptPasswordEncoder를 사용해야 합니다.

관련 개념

  • PasswordEncoder : 비밀번호를 해싱(암호화)하는 인터페이스
  • NoOpPasswordEncoder : 비밀번호를 암호화하지 않고 그대로 저장하는 방식 (안전하지 않음!)
  • BCryptPasswordEncoder : 보안성을 위해 가장 널리 사용되는 비밀번호 인코더

주의

실제 프로젝트에서는 NoOpPasswordEncoder를 절대 사용하면 안 됩니다. 반드시 BCryptPasswordEncoder를 사용하세요.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

3. CORS 설정 (Cross-Origin Resource Sharing)

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

설명

  • CORS(Cross-Origin Resource Sharing)는 다른 도메인에서 API 요청을 허용하는 기능입니다.
  • setAllowedOriginPatterns("*") : 모든 도메인에서 요청을 허용 (*).
  • setAllowedMethods(...) : GET, POST, PUT, DELETE, OPTIONS 요청을 허용.
  • setAllowedHeaders(...) : 특정 HTTP 헤더(Authorization, Cache-Control, Content-Type) 허용.
  • setAllowCredentials(true) : 쿠키 기반 인증을 허용.

관련 개념

  • CORS란? 웹 브라우저가 다른 도메인에서 요청을 보낼 때 보안 정책을 관리하는 기능
  • OPTIONS 메서드: CORS 요청 시 사전 검사 요청(Preflight Request) 역할을 합니다.

주의

실제 운영 환경에서는 특정 도메인만 허용하는 것이 보안상 안전합니다.

configuration.setAllowedOriginPatterns(Collections.singletonList("https://example.com"));

4. Spring Security 필터 체인 (SecurityFilterChain)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.cors().configurationSource(corsConfigurationSource()) // CORS 설정 적용
        .and()
        .csrf()
        .ignoringAntMatchers("/talk/**", "/app/**", "/topic/**", "/ws/**", "/queue/**")
            .disable()

설명

  • http.cors() : 위에서 설정한 CORS 정책을 적용합니다.
  • .csrf().disable() : CSRF 보호를 비활성화합니다.
  • .ignoringAntMatchers(...) :
    • 특정 경로(/talk/**, /app/**, /topic/**, /ws/**, /queue/**)에 대해서는 CSRF 보호를 해제합니다.

관련 개념

  • CSRF(Cross-Site Request Forgery) : 악성 웹사이트가 사용자의 세션을 도용하는 공격 기법
  • CSRF 보호 해제 시 주의할 점: 보안이 필요한 POST 요청에서는 CSRF 보호를 유지하는 것이 안전.

5. 세션 설정

.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)

설명

  • SessionCreationPolicy.ALWAYS : 항상 세션을 생성하여 사용자 정보를 저장합니다.

관련 개념

  • SessionCreationPolicy.ALWAYS : 항상 세션을 생성 (기본값)
  • SessionCreationPolicy.STATELESS : 세션을 사용하지 않음 (JWT 기반 인증에서 사용)

6. 접근 권한 설정 (인가 - Authorization)

.authorizeRequests()
    .antMatchers("/orderReport", "/employeeAttend", "/employeeSalary").hasAuthority("ROLE_SPECIAL_ACCESS")
    .antMatchers("/android/api/**").permitAll()
    .antMatchers("/api/login", "/login").permitAll()
    .antMatchers("/user/**", "/").hasAnyRole("Staff", "Admin", "Assistant Manager", "Executive", "Director", "Manager")
    .antMatchers("/admin/**").hasRole("Admin")
    .anyRequest().permitAll()

설명

  • .hasAuthority("ROLE_SPECIAL_ACCESS") : 특정 페이지 접근 제한 (예: ROLE_SPECIAL_ACCESS 권한을 가진 사용자만 접근 가능)
  • .antMatchers("/android/api/**").permitAll() : Android API는 누구나 접근 가능.
  • .hasAnyRole(...) : 특정 역할(Role)을 가진 사용자만 접근 가능.
  • .hasRole("Admin") : 관리자만 접근 가능.

관련 개념

  • 인증(Authentication): 사용자가 로그인했는지 확인 (예: ID/PW 입력)
  • 인가(Authorization): 사용자가 특정 기능을 사용할 수 있는지 확인 (예: 관리자 권한 체크)

7. 로그인 & 로그아웃 설정

.formLogin()
    .loginPage("/login")
    .defaultSuccessUrl("/main", true)
    .permitAll()

 

loginPage("/login") : 로그인 페이지 경로 설정

defaultSuccessUrl("/main", true) : 로그인 성공 시 이동할 페이지

.logout()
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login")
    .permitAll();

 

logoutUrl("/logout") : 로그아웃 요청 경로

logoutSuccessUrl("/login") : 로그아웃 후 이동할 페이지

관련 개념

  • Spring Security는 기본적으로 /login과 /logout 엔드포인트를 제공함
  • 커스텀 로그인 페이지를 사용하려면 .loginPage("/custom-login") 으로 변경 가능

8. 인증 관리자 설정

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    return http.getSharedObject(AuthenticationManagerBuilder.class).build();
}

설명

AuthenticationManager는 사용자 인증을 처리하는 핵심 객체입니다.


결론

이 설정 파일은 Spring Security를 사용하여 인증 및 인가, CORS 설정, CSRF 보호, 로그인/로그아웃 기능을 정의합니다.

 

🚨 배포 전에 반드시 확인해야 할 보안 설정:

  1. NoOpPasswordEncoder 대신 BCryptPasswordEncoder 사용
  2. permitAll()을 제거하고 적절한 권한 설정 적용
  3. SessionCreationPolicy 설정을 환경에 맞게 조정 (예: JWT 사용 시 STATELESS로 변경)