[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()));
'프레임워크 > Spring' 카테고리의 다른 글
블로그의 정보
우와한개발자 님의 블로그
우와한개발자