Spring
Hex-encoded string must have an even number of characters
codinglog
2026. 1. 13. 10:34
반응형
Spring Security PasswordEncoder Hex 디코딩 오류 해결
문제 상황
로그인 시 다음과 같은 오류가 발생했습니다:
java.lang.IllegalArgumentException: Hex-encoded string must have an even number of characters
at org.springframework.security.crypto.codec.Hex.decode(Hex.java:51)
at org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.decode(Pbkdf2PasswordEncoder.java:221)
at org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.matchesNonNull(Pbkdf2PasswordEncoder.java:212)원인 분석
1. 설정 파일의 비밀번호 형식
application.yml에 다음과 같이 {noop} prefix를 가진 비밀번호가 설정되어 있었습니다:
idp:
users:
- username: "admin"
password: "{noop}admin"
- username: "user"
password: "{noop}user"
2. PasswordEncoder 설정
PasswordEncoderConfig에서 Pbkdf2PasswordEncoder만 사용하도록 설정되어 있었습니다:
@Bean
Pbkdf2PasswordEncoder testPasswordEncoder() {
return new Pbkdf2PasswordEncoder(
"",
SALT_LENGTH,
ITERATIONS,
PBKDF2WithHmacSHA256
);
}
3. 문제점
{noop}prefix는NoOpPasswordEncoder를 의미하는 Spring Security의 표준 형식입니다- 하지만
Pbkdf2PasswordEncoder는 이 prefix를 인식하지 못하고,{noop}admin문자열을 Hex로 디코딩하려고 시도 - 결과적으로 "Hex-encoded string must have an even number of characters" 오류 발생
해결 방법
DelegatingPasswordEncoder를 사용하여 여러 인코더를 지원하도록 수정했습니다.
수정된 코드
package cothe.oidcidp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import java.util.Map;
import static org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256;
@Configuration
public class PasswordEncoderConfig {
public static final int SALT_LENGTH = 16;
public static final int ITERATIONS = 10;
@Bean
public PasswordEncoder passwordEncoder() {
Pbkdf2PasswordEncoder pbkdf2Encoder = new Pbkdf2PasswordEncoder(
"",
SALT_LENGTH,
ITERATIONS,
PBKDF2WithHmacSHA256
);
return new DelegatingPasswordEncoder(
"pbkdf2",
Map.of(
"noop", NoOpPasswordEncoder.getInstance(),
"pbkdf2", pbkdf2Encoder
)
);
}
}
주요 변경 사항
DelegatingPasswordEncoder 도입
- 여러 PasswordEncoder를 지원하는 위임 패턴 사용
- 비밀번호의 prefix에 따라 적절한 인코더 선택
인코더 매핑
{noop}→NoOpPasswordEncoder: 기존 평문 비밀번호 지원{pbkdf2}→Pbkdf2PasswordEncoder: 새로운 비밀번호는 PBKDF2로 인코딩
기본 인코더 설정
"pbkdf2"를 기본 인코더로 설정하여, prefix가 없는 새 비밀번호는 자동으로 PBKDF2로 인코딩
동작 원리
DelegatingPasswordEncoder의 동작 방식
비밀번호 저장 시
- prefix가 없으면 기본 인코더(
pbkdf2) 사용 {pbkdf2}...형식으로 저장
- prefix가 없으면 기본 인코더(
비밀번호 검증 시
{noop}admin→NoOpPasswordEncoder로 검증{pbkdf2}...→Pbkdf2PasswordEncoder로 검증- prefix가 없으면 기본 인코더로 처리
예시
// 기존 비밀번호 (application.yml에서 로드)
password: "{noop}admin" // NoOpPasswordEncoder로 검증
// 새로 생성되는 비밀번호
passwordEncoder.encode("newpassword")
// → "{pbkdf2}..." 형식으로 저장, Pbkdf2PasswordEncoder로 검증
추가 고려사항
NoOpPasswordEncoder Deprecation 경고
NoOpPasswordEncoder는 보안상의 이유로 deprecated되었습니다. 하지만 기존 데이터와의 호환성을 위해 유지했습니다.
향후 마이그레이션 계획:
- 기존
{noop}비밀번호를 사용자에게 비밀번호 변경 요청 - 또는 배치 작업으로 모든 비밀번호를
{pbkdf2}형식으로 재인코딩
보안 권장사항
- 프로덕션 환경에서는
NoOpPasswordEncoder사용을 피해야 합니다 - 가능한 한 모든 비밀번호를
Pbkdf2PasswordEncoder또는 더 강력한 인코더로 마이그레이션 {noop}prefix는 개발/테스트 환경에서만 사용
참고 자료
요약
- 문제:
Pbkdf2PasswordEncoder가{noop}prefix를 인식하지 못해 Hex 디코딩 오류 발생 - 해결:
DelegatingPasswordEncoder를 사용하여 여러 인코더 지원 - 결과: 기존
{noop}비밀번호와 새로운{pbkdf2}비밀번호 모두 정상 처리
반응형