이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.
이전 포스팅에서는 시큐리티의 간단한 개념과 SecurityConfig를 살펴보았습니다. 이번 포스팅에서는 PasswordEncoder
부터 이어서 살펴보겠습니다.
Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는데, 그것이 바로 PasswordEncoder
인터페이스와 구현체들입니다.
저번 포스팅에서 작성한 SecurityConfig
파일을 보면, 비밀번호 암호화 방식을 사용하기 위해 아래와 같은 암호화 방식을 사용하였습니다.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
스프링 시큐리티에서 제공하며 bcrypt 해싱 함수로 암호를 인코딩하는 BCryptPasswordEncoder
를 직접 불러서 사용하였습니다.
bcrypt는 암호화 할 때 해시 알고리즘인 SHA-256을 사용합니다.해시 알고리즘의 대표적인 특성은, '암호화는 가능하나 복호화는 불가능하다'입니다. 즉, DB에 암호화되어 있는것을 해독할 수는 없다는 이야기입니다.
가끔 어느 사이트에서 비밀번호를 잊어버린 상황에서, '비밀번호를 찾고싶은데 왜 재설정하라고 하지?'라는 의문이 든적이 있을 겁니다.
그 이유가 바로 복호화가 불가능 하기 때문에 기존 값을 대체할 새로운 값을 넣어야하기 때문입니다.
matches()
함수를 통해 진행합니다. 그러다 문득 이런생각이 들었습니다. 만약, 제가 사용하고 있는 이 인코딩 방식에 문제점이 생긴다면 어떻게 유연하게 대응해야 할까요?
스프링 시큐리티에는 여러 인코딩 알고리즘을 지원하고 있고, 그중 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경된 경우가 있습니다.
StandardPasswordEncoder
: SHA-256을 이용해 암호를 해시한다. (강도가 약한 해싱 알고리즘이기 때문에 지금은 많이 사용되지 않는다.)Pbkdf2PasswordEncoder
: PBKDF2를 이용한다.BCryptPasswordEncoder
: bcrypt 강력 해싱 함수로 암호를 인코딩한다NoOpPasswordEncoder
: 암호를 인코딩하지 않고 일반 텍스트로 유지(테스트 용도로만 사용한다.)SCryptPasswordEncoder
: scrypt 해싱 함수로 암호를 인코딩한다.현재 사용되는 알고리즘에서 취약성이 발견되어 다른 인코딩 알고리즘으로 변경하고자 할 때 대응하기 좋은 방법은 DelegatingPasswordEncoder
을 사용하는 것입니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final ObjectMapper objectMapper;
// 스프링 시큐리티 기능 비활성화
// @Bean
// public WebSecurityCustomizer configure() {
// return (web -> web.ignoring()
// .requestMatchers(toH2Console())
//// .requestMatchers("/static/**")
// );
// }
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http .csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/signup", "/", "/login").permitAll()
.anyRequest().authenticated())
// .formLogin(formLogin -> formLogin
// .loginPage("/login")
// .defaultSuccessUrl("/home"))
.logout((logout) -> logout
.logoutSuccessUrl("/login")
.invalidateHttpSession(true))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
//== 변경된 부분 ==//
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
//== 변경 끝 ==//
DelegatingPasswordEncoder
는 여러 인코딩 알고리즘을 사용할 수 있게 해주는 기능입니다. 저는 이제 이 방식을 사용하여 스프링 시큐리티를 구성해보겠습니다.
PasswordEncoderFactories.createDelegatingPasswordEncoder();
메서드를 통해 PasswordEncoder를 반환하도록 하였습니다.
PasswordEncoder
의 내부를 살펴보겠습니다.
encodingId
는 "bcrypt"입니다. 이는 BCryptPasswordEncoder
와 매핑됩니다.
DelegatingPasswordEncoder
을 살펴보면, 먼저 Prefix인 "{"와 Suffix인 "}"의 유무를 판단해줍니다.
그 후, passwordEncoderForEncode
는 "idForEncode"에 담긴 bcrypt
를 통해 받아온 BCryptPasswordEncoder
를 사용하는 DelegatingPasswordEncoder
를 사용한다는 것을 알 수 있습니다.
encode 메서드는 아래와 같습니다.
return 값을 보면, {bcrypt}BCryptPasswordEncoder로 암호화된 문자열
인 것을 알 수 있습니다.
참고로, 스프링 시큐리티 5.0이상부터는 암호화된 password에 {bcrypt}
와 같이 암호화 방식을 명시해주지 않으면 에러가 발생한다고 합니다.
BCryptPasswordEncoder
는 어떻게 암호화하는지도 코드를 통해 한번 살펴보겠습니다.
getSalt()
를 통해 임의의 문자열을 받아온 후, 아래 메서드를 호출합니다. (설명은 주석을 참고)
public static String hashpw(byte passwordb[], String salt) {
.......
if (salt == null) { //임의의 문자열인 salt가 null이라면 예외를 발생시킵니다.
throw new IllegalArgumentException("salt cannot be null");
}
int saltLength = salt.length();//랜덤 문자열의 길이를 구합니다.
if (saltLength < 28) {//임의의 문자열의 길이가 28글자보다 작을 경우 예외를 발생시킵니다.
//즉 BCryptPasswordEncoder에서 사용하는 salt는 28글자 이상의 임의의 문자열을 필요로 한다는것을 알 수 있습니다.
throw new IllegalArgumentException("Invalid salt");
}
......
}
출처: https://ttl-blog.tistory.com/268 [Shin._.Mallang:티스토리]
다음 포스팅에서는 필터를 커스텀해서 사용해보겠습니다. 그 전에 기본적인 로그인 과정을 한번 알아보겠습니다.
출처 및 참고