우와한 개발자

[Spring Security] Spring Security 구현 - FilterChain, UserDetailsService, PasswordEncoder, CSRF/CORS

by 우와한개발자

1. pom.xml : 라이브러리 의존성

  • Thymeleaf 사용시 Thymeleaf에서 sec 네임 스페이스 사용하려면 org.thymeleaf.extras 의존성 추가
<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Thymeleaf + Security 통합 (sec 네임스페이스 사용 시) -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

 

2. SecurityConfig

1) JavaConfig방식 Bean 등록

  • @Configuration : 설정 클래스 선언
  • @EnableWebSecurity : Spring Security 활성화
@Configuration
@EnableWebSecurity
public class SecurityConfig {
	...
}

 

2) SecurityFilterChain Bean 등록

  • Spring Security 5 버전까진는 WebSecurityConfigurerAdapter 클래스를 상속하여 구현
  • SpringSecurity 6 버전부터는 SecurityFilterChain Bean을 직접 등록(@EnableWebSecurity 사용)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http
        // URL별 접근 권한 설정
        .authorizeHttpRequests(auth -> auth

            // 정적 리소스(css, js, 이미지)는 로그인 없이 누구나 접근 가능
            .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()

            // 로그인 페이지, 회원가입 페이지는 인증 없이 접근 가능
            .requestMatchers("/member/login", "/member/insert").permitAll()

            // 게시판 관련 URL은 USER 또는 ADMIN 권한이 있어야 접근 가능
            .requestMatchers("/board/**").hasAnyRole("USER", "ADMIN")

            // 파일 관련 URL은 ADMIN 권한이 있어야 접근 가능
            .requestMatchers("/file/**").hasRole("ADMIN")

            // 위에서 허용하지 않은 나머지 모든 요청은 인증된 사용자만 접근 가능
            .anyRequest().authenticated()
        )

        // 폼 로그인 방식 설정
        .formLogin(form -> form

            // 커스텀 로그인 페이지 URL
            .loginPage("/member/login")

            // 로그인 성공 시 이동할 기본 페이지
            .defaultSuccessUrl("/")

            // 로그인 관련 기능은 누구나 접근 가능
            .permitAll()
        )

        // 로그아웃 설정
        .logout(logout -> logout

            // 로그아웃 요청 URL
            .logoutUrl("/member/logout")

            // 로그아웃 성공 후 이동할 페이지
            .logoutSuccessUrl("/member/login")

            // 로그아웃 시 기존 세션 무효화
            .invalidateHttpSession(true)
        )

        // CSRF 보호 기능 비활성화
        // (실습/테스트 단계에서는 간단히 꺼두기도 하지만,
        // 세션 기반 폼 로그인에서는 보통 활성화 상태로 사용하는 것이 안전함)
        .csrf(csrf -> csrf.disable());

    return http.build();
}

 

(1) HttpSecurity 객체 주요 메서드

메서드 설명
authorizeHttpRequests() URL별 접근 권한을 설정
requestMatchers() 특정 URL 패턴을 지정
permitAll() 모든 사용자 접근 허용
authenticated() 인증된 사용자만 접근 허용
hasRole() 특정 권한을 가진 사용자만 접근 허용
hasAnyRole() 여러 권한 중 하나라도 가지면 접근 허용
formLogin() 폼 로그인 방식을 설정
loginPage() 커스텀 로그인 페이지 URL을 지정
defaultSuccessUrl() 로그인 성공 후 이동할 URL을 지정
logout() 로그아웃 관련 설정
logoutUrl() 로그아웃 요청 URL을 지정
logoutSuccessUrl() 로그아웃 성공 후 이동할 URL을 지정
invalidateHttpSession(true) 로그아웃 시 기존 세션을 무효화
csrf() CSRF 보호 설정
cors() CORS 설정 적용
sessionManagement() 세션 생성 정책, 동시 로그인 수 등 세션 관련 설정

 

(2) 권한(Role) 처리

  • Spring Security는 기본적으로 ROLE_ prefix를 사용
  • hasRole() 과 roles() 는 ROLE_ 을 자동으로 붙여주므로 DB에는 USER 만 저장
  • hasAuthority() 는 ROLE_ 을 직접 명시해야 하므로 혼용 시 주의 필요
구분 사용 값 실제 비교 값
DB 저장 USER -
roles("USER") USER ROLE_USER 자동 변환
hasRole("USER") USER ROLE_USER 자동 변환
hasAuthority("ROLE_USER") ROLE_USER 직접 명시 ROLE_USER
// hasRole() - ROLE_ 자동 추가
.requestMatchers("/board/**").hasRole("USER")       // 내부적으로 ROLE_USER와 비교

// hasAuthority() - ROLE_ 직접 명시
.requestMatchers("/board/**").hasAuthority("ROLE_USER")  // ROLE_USER 그대로 비교​

 

3. PasswordEncoder Bean 등록

  • 반드시 암호화하여 저장
  • Spring Security는 BCryptPasswordEncoder 사용 권장
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
// 비밀번호 저장 시 (회원가입)
String encodedPassword = passwordEncoder.encode(rawPassword);

// 비밀번호 검증 시 (로그인)
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

 

4. UserDetailsService 구현

  • Spring Security 인증 흐름에서 UserDetailsService는 DB에서 사용자 정보를 불러오는 핵심 인터페이스로 반드시 직접 구현해야함
  • UserDetails를 반환하면, AuthenticationProvider가 입력된 비밀번호와 DB의 비밀번호를 PasswordEncoder.matches()로 비교함.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));

        return User.builder()
                .username(member.getUsername())
                .password(member.getPassword())     // 이미 BCrypt 인코딩된 비밀번호
                .roles(member.getRole())  // "USER" → 내부적으로 "ROLE_USER"로 처리
                .build();
    }
}

 

5. CSRF  vs.  XSS  vs. CORS 비교

구현 CSRF XSS CORS
전체 명칭 Cross-Site Request Forgery Cross-Site Scripting Cross-Origin Resource Sharing
공격 주체 다른 사이트가 사용자 권한을 도용 공격자가 악성 스크립트를 삽입 브라우저의 동일 출처 정책(Same-Origin Policy)에서 비롯된 제한
피해 유형 인증된 사용자가 의도하지 않은 요청을 서버에 보냄 스크립트가 실행되어 쿠키, 세션 탈취 등 가능 보안 정책이 아니라 다른 도메인 간 자원 공유를 제어하는 메커니즘
방어 방법 CSRF Token, SameSite Cookie 입력값 이스케이프, CSP 헤더 서버에서 허용 Origin 설정 (Access-Control-Allow-Origin)
Spring 설정 .csrf(csrf -> csrf.disable()) or Token 사용 입력값 검증, Thymeleaf 기본 이스케이프 .cors() + CorsConfigurationSource Bean
JWT 환경 세션 미사용 → CSRF 위협 낮음 → disable 가능 HttpOnly 쿠키 사용 시 XSS에 강함 프론트/백 분리 시 반드시 CORS 설정 필요
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("<http://localhost:3000>")); // 허용할 프론트엔드 도메인
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}
// SecurityConfig의 filterChain 내부에 추가
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

 

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기