[Spring] Spring Security를 이용한 로그인 구현 (스프링부트 3.X 버전) [2] - PassWordEncoder란?

Paek·2024년 1월 26일
1

Spring프로젝트

목록 보기
5/9
post-thumbnail
post-custom-banner

이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.

이전 포스팅에서는 시큐리티의 간단한 개념과 SecurityConfig를 살펴보았습니다. 이번 포스팅에서는 PasswordEncoder 부터 이어서 살펴보겠습니다.

PasswordEncoder

Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는데, 그것이 바로 PasswordEncoder 인터페이스와 구현체들입니다.

  • encode() : 비밀번호를 암호화(단방향)
  • matches() : 암호화된 비밀번호와 암호화되지 않은 비밀번호가 일치하는지 비교
  • upgradeEncoding() : 인코딩된 암호화를 다시 한번 인코딩 할 때 사용 (true일 경우 다시 인코딩, default=false)

저번 포스팅에서 작성한 SecurityConfig 파일을 보면, 비밀번호 암호화 방식을 사용하기 위해 아래와 같은 암호화 방식을 사용하였습니다.

@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

스프링 시큐리티에서 제공하며 bcrypt 해싱 함수로 암호를 인코딩하는 BCryptPasswordEncoder를 직접 불러서 사용하였습니다.


참고 : bcrypt?

bcrypt는 암호화 할 때 해시 알고리즘인 SHA-256을 사용합니다.해시 알고리즘의 대표적인 특성은, '암호화는 가능하나 복호화는 불가능하다'입니다. 즉, DB에 암호화되어 있는것을 해독할 수는 없다는 이야기입니다.

가끔 어느 사이트에서 비밀번호를 잊어버린 상황에서, '비밀번호를 찾고싶은데 왜 재설정하라고 하지?'라는 의문이 든적이 있을 겁니다.

그 이유가 바로 복호화가 불가능 하기 때문에 기존 값을 대체할 새로운 값을 넣어야하기 때문입니다.

  • 그렇다면 비교는 어떻게 할까요?
    -> 이 부분은 정확히 알기는 어렵지만, DB 저장 시 솔트 로직이라는 것을 같이 넣어서 비교하는것으로 알고있습니다.(정확한 방식은 찾아보시길 바랍니다..) 저희는 이것을 스프링에서 제공하는 matches() 함수를 통해 진행합니다.

그러다 문득 이런생각이 들었습니다. 만약, 제가 사용하고 있는 이 인코딩 방식에 문제점이 생긴다면 어떻게 유연하게 대응해야 할까요?

스프링 시큐리티에는 여러 인코딩 알고리즘을 지원하고 있고, 그중 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경된 경우가 있습니다.

참고 : PasswordEncoder가 제공하는 구현 클래스

  • StandardPasswordEncoder : SHA-256을 이용해 암호를 해시한다. (강도가 약한 해싱 알고리즘이기 때문에 지금은 많이 사용되지 않는다.)
  • Pbkdf2PasswordEncoder : PBKDF2를 이용한다.
  • BCryptPasswordEncoder : bcrypt 강력 해싱 함수로 암호를 인코딩한다
  • NoOpPasswordEncoder : 암호를 인코딩하지 않고 일반 텍스트로 유지(테스트 용도로만 사용한다.)
  • SCryptPasswordEncoder : scrypt 해싱 함수로 암호를 인코딩한다.

현재 사용되는 알고리즘에서 취약성이 발견되어 다른 인코딩 알고리즘으로 변경하고자 할 때 대응하기 좋은 방법은 DelegatingPasswordEncoder을 사용하는 것입니다.

수정된 SecurityConfig

@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를 사용한다는 것을 알 수 있습니다.

DelegatingPasswordEncoder

encode 메서드는 아래와 같습니다.

return 값을 보면, {bcrypt}BCryptPasswordEncoder로 암호화된 문자열인 것을 알 수 있습니다.

참고로, 스프링 시큐리티 5.0이상부터는 암호화된 password에 {bcrypt}와 같이 암호화 방식을 명시해주지 않으면 에러가 발생한다고 합니다.

BCryptPasswordEncoder

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:티스토리]

다음 포스팅에서는 필터를 커스텀해서 사용해보겠습니다. 그 전에 기본적인 로그인 과정을 한번 알아보겠습니다.


출처 및 참고

profile
티스토리로 이전했습니다. https://100cblog.tistory.com/
post-custom-banner

0개의 댓글