두 손끝의 창조자

Hex-encoded string must have an even number of characters 본문

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
                )
        );
    }
}

주요 변경 사항

  1. DelegatingPasswordEncoder 도입

    • 여러 PasswordEncoder를 지원하는 위임 패턴 사용
    • 비밀번호의 prefix에 따라 적절한 인코더 선택
  2. 인코더 매핑

    • {noop}NoOpPasswordEncoder: 기존 평문 비밀번호 지원
    • {pbkdf2}Pbkdf2PasswordEncoder: 새로운 비밀번호는 PBKDF2로 인코딩
  3. 기본 인코더 설정

    • "pbkdf2"를 기본 인코더로 설정하여, prefix가 없는 새 비밀번호는 자동으로 PBKDF2로 인코딩

동작 원리

DelegatingPasswordEncoder의 동작 방식

  1. 비밀번호 저장 시

    • prefix가 없으면 기본 인코더(pbkdf2) 사용
    • {pbkdf2}... 형식으로 저장
  2. 비밀번호 검증 시

    • {noop}adminNoOpPasswordEncoder로 검증
    • {pbkdf2}...Pbkdf2PasswordEncoder로 검증
    • prefix가 없으면 기본 인코더로 처리

예시

// 기존 비밀번호 (application.yml에서 로드)
password: "{noop}admin"  // NoOpPasswordEncoder로 검증

// 새로 생성되는 비밀번호
passwordEncoder.encode("newpassword")  
// → "{pbkdf2}..." 형식으로 저장, Pbkdf2PasswordEncoder로 검증

추가 고려사항

NoOpPasswordEncoder Deprecation 경고

NoOpPasswordEncoder는 보안상의 이유로 deprecated되었습니다. 하지만 기존 데이터와의 호환성을 위해 유지했습니다.

향후 마이그레이션 계획:

  1. 기존 {noop} 비밀번호를 사용자에게 비밀번호 변경 요청
  2. 또는 배치 작업으로 모든 비밀번호를 {pbkdf2} 형식으로 재인코딩

보안 권장사항

  • 프로덕션 환경에서는 NoOpPasswordEncoder 사용을 피해야 합니다
  • 가능한 한 모든 비밀번호를 Pbkdf2PasswordEncoder 또는 더 강력한 인코더로 마이그레이션
  • {noop} prefix는 개발/테스트 환경에서만 사용

참고 자료

요약

  • 문제: Pbkdf2PasswordEncoder{noop} prefix를 인식하지 못해 Hex 디코딩 오류 발생
  • 해결: DelegatingPasswordEncoder를 사용하여 여러 인코더 지원
  • 결과: 기존 {noop} 비밀번호와 새로운 {pbkdf2} 비밀번호 모두 정상 처리
반응형
Comments