🔐
Spring Security 기본 설정, 커스텀
June 24, 2024
Spring Security 기본 설정, 커스텀
기본 필터체인
SpringBootWebSecurityConfiguration.class
을 확인하면 다음과 같이 기본 필터체인이 등록되었음을 확인할 수 있다.
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
SecurityFilterChainConfiguration() {
}
@Bean
@Order(2147483642)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> {
((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
});
http.formLogin(Customizer.withDefaults());
http.httpBasic(Customizer.withDefaults());
return (SecurityFilterChain)http.build();
}
}
-
http.authorizeHttpRequests
: 이 구문은 해당 필터체인의 인가 규칙을 정의한다.((TypeCasting)requests.anyRequest()).authenticated()
: 모든 요청에 대해 인증이 필요하다고 설정한다.- ⚠️ 최신 스프링 시큐리티에서는 TypeCasting 없이 사용할 수 있도록 개선됐다. ->
requests -> {requests.anyRequest().authenticated()}
-
http.formLogin
: 폼 기반 로그인을 설정한다. 프론트엔드와 백엔드가 나뉘어 http로 유저네임과 비밀번호를 전송하는 경우에는 사용하지 않는다. -
http.httpBasic
: http 기본 인증을 설정한다. 이 설정은 http 헤더를 통해 인증을 수행할 수 있도록 한다. -> 이는 JWT의 그것과 다르다. 자세한 내용은 아래 참조.HTTP Basic?
HTTP Basic 인증 과정
- 클라이언트 요청:
- 클라이언트는 보호된 리소스에 접근하기 위해 서버에 HTTP 요청을 보냅니다. 이때 클라이언트는 아직 인증되지 않은 상태입니다.
- 서버의 인증 요구:
- 서버는 클라이언트가 인증되지 않았음을 인지하고, 401 Unauthorized 상태 코드와 함께 WWW-Authenticate: Basic 헤더를 포함한 응답을 반환합니다. 이 응답은 클라이언트에게 사용자명과 비밀번호를 제공하라고 요청합니다.
- 클라이언트의 인증 정보 제공:
- 클라이언트는 사용자명과 비밀번호를 username:password 형식의 문자열로 결합한 후, Base64로 인코딩합니다.
- 클라이언트는 인코딩된 자격 증명을 Authorization: Basic {encoded_credentials} 헤더에 포함하여 서버에 다시 요청을 보냅니다. (이 부분은 Bearer를 사용하는 JWT와 대조적)
- 서버의 인증 정보 검증:
- 서버는 요청 헤더에서 인코딩된 자격 증명을 추출하고, 이를 디코딩하여 사용자명과 비밀번호를 확인합니다.
- 서버는 이 정보를 바탕으로 사용자가 유효한지 검증합니다.
- 응답:
- 자격 증명이 유효한 경우, 서버는 요청된 리소스에 접근을 허용하고, 정상적인 응답을 반환합니다.
- 자격 증명이 유효하지 않은 경우, 서버는 다시 401 Unauthorized 응답을 반환합니다.
HTTP Basic 인증의 장점과 단점
장점:
- 구현이 매우 간단합니다.
- 별도의 세션 관리가 필요 없습니다.
- HTTP 프로토콜 표준의 일부로, 대부분의 HTTP 클라이언트와 서버가 지원합니다.
단점:
- 자격 증명이 Base64로 인코딩되어 전송되지만, 이는 단순한 인코딩일 뿐 암호화가 아닙니다. 따라서 네트워크 상에서 쉽게 탈취될 수 있습니다.
- HTTPS를 사용하지 않으면 자격 증명이 평문으로 전송되는 것과 같습니다.
- 매 요청마다 자격 증명을 포함하여 전송해야 하므로, 비효율적입니다.
- 클라이언트 요청:
-
http.build
: HttpSecurity 객체 http를 빌드하여 SecurityFilterChain으로 반환한다. 최신 버전에서는 앞의 타입 캐스팅을 생략할 수 있다.
기본 필터체인 (최신)
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
커스텀 필터체인 등록하기
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
ProjectSecurityConfig
클래스를 지정하고 @Configuration
어노테이션을 달아주었다. 그 안에 빈으로 커스텀 필처 체인을 등록했는데, 이는 아까 본 기본 필터체인을 그대로 복붙한 뒤 @Order
어노테이션만 제거해준 것이다.
이제부터 시리즈의 포스팅에서는 이 커스텀 필터체인을 가지고 놀아볼 것이다. 이렇게 커스텀 필터체인을 등록해주면, 기존에 동작하던 기본 필터체인은 더 이상 동작하지 않게 된다.
허용 URL과 차단 URL 설정하기
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(requests ->
requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
.requestMatchers("/notices", "/contact").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.httpBasic(withDefaults());
return http.build();
}
}
- Spring Security 6.1 버전 이후로 람다 DSL 스타일이 권장되어 위와 같이 설정했다.
"/myAccount", "/myBalance", "/myLoans", "/myCards"
경로에 대해서는 인증을 요구한다."/notices", "/contact"
경로에 대해서는 기본적으로 허용한다.anyRequest()
구문을 통해 명시되지 않은 다른 모든 경로는 차단한다.
인메모리 유저 추가하기
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("12345")
.authorities("admin")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
- 권한에 따라 여러 유저가 필요할 수도 있다. 그럴 때는 Security Config 클래스 내부에 위와 같이 유저 정보를 추가해주면 된다.
- 위 내용이 빈으로 등록되어
admin
과user
두 개의 계정이 생성된다. - 이 방법은 PasswordEncoder로
DefaultPasswordEncoder
를 사용하여, 운영 환경에는 적합하지 않다.
다른 방법
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User
.withUsername("admin")
.password("12345")
.authorities("admin")
.build();
UserDetails user = User
.withUsername("user")
.password("12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
- 계정을 만들 때 따로 패스워드 인코더를 지정하는 대신
NoOpPasswordEncoder
의 싱글톤 인스턴스를 빈으로 등록하는 방법