이 장에서는 다음 내용을 다룹니다.
- PasswordEncoder 구현 및 작업
- Spring Security Crypto 모듈이 제공하는 도구 사용
3장에서는 Spring Security를 활용한 애플리케이션에서 사용자 관리 방법을 논의했지만, 이번 장에서는 비밀번호와 secret의 관리 방법에 대해 다룹니다. Spring Security는 비밀번호와 중요한 정보를 보호하기 위해 PasswordEncoder 인터페이스와 함께 다양한 도구를 제공합니다. 이러한 도구들은 비밀번호뿐만 아니라, API 키, 토큰, 암호화 키 등 민감한 데이터를 안전하게 관리할 수 있도록 돕습니다.
여기서 secret은 사용자의 인증에 중요한 정보를 의미하며, 비밀번호 외에도 애플리케이션에서 보호해야 할 중요한 데이터들을 포함합니다. 이들은 모두 암호화되어 저장되며, Spring Security는 이를 안전하게 관리하여 애플리케이션의 보안을 강화합니다.
4.1 PasswordEncoder 사용
3장에서 UserDetails 인터페이스와 그 구현 방식에 대해 다루었고, 2장에서 인증 및 권한 부여 과정에서 여러 actor들이 사용자의 정보를 처리하는 방식에 대해 배웠습니다. 이번 장에서는 PasswordEncoder에 대해 집중적으로 다루며, 이를 통해 Spring Security에서 사용자의 비밀번호를 어떻게 검증하고 암호화하는지 알아봅니다.
actors 및 user representation
이 과정에서 actor는 인증 및 권한 부여에서 사용자 정보를 처리하고 관리하는 다양한 컴포넌트를 의미합니다. 예를 들어, UserDetails, UserDetailsService, PasswordEncoder 등이 actor로 간주되며, 이들은 서로 협력하여 사용자의 인증과 권한을 처리합니다. user representation은 인증 및 권한 부여 과정에서 사용자의 아이디, 비밀번호, 권한 등의 정보를 나타내는 객체를 의미합니다. Spring Security에서는 UserDetails 인터페이스가 이러한 정보를 정의하고 사용합니다.
인증 과정에서 PasswordEncoder의 역할
그림 4.1에서 PasswordEncoder는 인증 과정 중 비밀번호를 검증하는 핵심적인 역할을 합니다. 일반적으로 비밀번호는 평문으로 저장되지 않으며, 이를 안전하게 처리하기 위해 암호화나 해시 처리가 필요합니다. Spring Security는 이를 위해 PasswordEncoder 인터페이스를 제공합니다.
4.1.1 PasswordEncoder 계약
PasswordEncoder 인터페이스는 비밀번호를 검증하는 두 가지 중요한 메소드를 정의합니다:
- encode(CharSequence rawPassword): 원본 비밀번호를 인코딩하여 반환합니다. 이 과정에서 비밀번호는 암호화되거나 해시됩니다.
- matches(CharSequence rawPassword, String encodedPassword): 인코딩된 비밀번호가 원본 비밀번호와 일치하는지 확인합니다.
또한, upgradeEncoding(CharSequence encodedPassword) 메소드가 기본적으로 false를 반환하며, 이를 true로 재정의하면 보안을 강화하기 위해 비밀번호를 다시 인코딩할 수 있습니다.
간단한 구현 예시
- PlainTextPasswordEncoder: 이 구현은 비밀번호를 인코딩하지 않고 그대로 반환합니다. matches() 메소드는 두 비밀번호가 동일한지 단순히 비교합니다.
public class PlainTextPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString(); // 비밀번호를 그대로 반환
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword); // 두 비밀번호가 같은지 비교
}
}
- SHA-512를 사용하는 PasswordEncoder: SHA-512 알고리즘을 사용하여 비밀번호를 해시합니다. encode() 메소드는 비밀번호를 해시하여 반환하고, matches() 메소드는 원본 비밀번호를 해시하여 인코딩된 비밀번호와 비교합니다.
public class Sha512PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return hashWithSHA512(rawPassword.toString()); // SHA-512로 해시한 비밀번호 반환
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String hashedPassword = encode(rawPassword); // 원본 비밀번호를 해시
return encodedPassword.equals(hashedPassword); // 인코딩된 비밀번호와 비교
}
}
- SHA-512 해시 메소드 구현:
private String hashWithSHA512(String input) {
StringBuilder result = new StringBuilder();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] digested = md.digest(input.getBytes());
for (int i = 0; i < digested.length; i++) {
result.append(Integer.toHexString(0xFF & digested[i])); // 해시 값을 16진수로 변환
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Bad algorithm");
}
return result.toString();
}
이 코드는 SHA-512 해시 알고리즘을 사용하여 입력값을 해시 처리한 결과를 반환합니다. matches() 메소드는 원본 비밀번호를 해시하여, 기존에 저장된 해시 값과 비교합니다.
결론
PasswordEncoder는 비밀번호를 안전하게 관리하는 데 필수적인 역할을 합니다. 이 장에서 다룬 두 가지 간단한 구현 예시—PlainTextPasswordEncoder와 SHA-512를 사용하는 PasswordEncoder—는 비밀번호 인코딩 및 검증 과정을 이해하는 데 도움이 됩니다. 이후에는 더 나은 방식으로 비밀번호를 처리하는 방법을 배울 수 있습니다.
4.1.3 제공된 PasswordEncoder 구현들 중에서 선택하기
Spring Security는 비밀번호 인코딩을 위한 여러 구현을 제공하며, 이러한 구현을 적절하게 선택하면 자신만의 비밀번호 인코더를 구현할 필요가 없습니다. 각 구현은 보안 수준과 성능이 다르므로, 애플리케이션에 맞는 구현을 선택하는 것이 중요합니다. 여기에서는 Spring Security가 제공하는 주요 PasswordEncoder 구현들을 살펴봅니다.
1. NoOpPasswordEncoder
- 특징: 비밀번호를 인코딩하지 않고 평문 그대로 반환합니다.
- 용도: 예제나 테스트 환경에서만 사용되며, 실제 운영 환경에서는 절대로 사용해서는 안 됩니다.
- 인스턴스 생성:
PasswordEncoder p = NoOpPasswordEncoder.getInstance();
2. StandardPasswordEncoder
- 특징: SHA-256 해싱 알고리즘을 사용하여 비밀번호를 해시합니다. 그러나 이 구현은 현재 사용되지 않으며, 더 이상 강력한 보안성을 제공하지 않기 때문에 새로운 애플리케이션에서는 사용하지 않는 것이 좋습니다.
- 인스턴스 생성:
PasswordEncoder p = new StandardPasswordEncoder(); PasswordEncoder p = new StandardPasswordEncoder("secret"); // secret을 시크릿 키로 사용
3. Pbkdf2PasswordEncoder
- 특징: PBKDF2 (Password-Based Key Derivation Function 2) 알고리즘을 사용하여 비밀번호를 해시합니다. 이 알고리즘은 비밀번호를 해시할 때 반복적인 연산을 수행하여 보안성을 높입니다.
- 인스턴스 생성:
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 16, 310000, Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
- secret: 시크릿 키
- 16: 해시의 길이
- 310000: 반복 횟수
PBKDF2의 인코딩 강도는 반복 횟수와 결과 길이에 영향을 받습니다. 반복 횟수가 많을수록 더 강력하지만, 성능에 영향을 미치므로 적절한 타협이 필요합니다.
4. BCryptPasswordEncoder
- 특징: bcrypt 해싱 알고리즘을 사용하여 비밀번호를 안전하게 해시합니다. 해싱 과정에서 로그 라운드(logarithmic rounds)를 사용하여 반복 횟수를 설정할 수 있습니다.
- 인스턴스 생성:
PasswordEncoder p = new BCryptPasswordEncoder(); PasswordEncoder p = new BCryptPasswordEncoder(4); // 강도 계수 설정 (4는 2^4 = 16번 반복)
- 4: 해싱 작업에 사용되는 반복 횟수(2^log rounds)
BCrypt는 비밀번호 해싱에 있어서 매우 강력한 알고리즘으로, 주로 비밀번호 보호에 널리 사용됩니다.
5. SCryptPasswordEncoder
- 특징: scrypt 해싱 알고리즘을 사용하여 비밀번호를 인코딩합니다. 이 알고리즘은 CPU와 메모리 자원을 동시에 사용하는 방식으로 보안성을 강화합니다.
- 인스턴스 생성:
PasswordEncoder p = new SCryptPasswordEncoder(); PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
- 16384: CPU 비용
- 8: 메모리 비용
- 1: 병렬화 수준
- 32: 키 길이
- 64: 솔트 길이
SCryptPasswordEncoder는 높은 보안성을 제공하지만, 성능 상의 부담이 크기 때문에 필요에 따라 적절한 파라미터를 설정해야 합니다.
해싱 알고리즘과 보안 고려 사항
각 해싱 알고리즘은 보안과 성능 측면에서 차이를 보입니다:
- bcrypt와 scrypt는 보안성이 매우 높으며, 비밀번호 해시를 생성하는 데 상당한 계산 자원을 사용하여 공격자가 무차별 대입 공격을 시도하는 것을 어렵게 만듭니다.
- PBKDF2는 높은 보안성을 제공하지만, 반복 횟수와 결과 길이에 따라 성능에 영향을 미칩니다.
- StandardPasswordEncoder는 더 이상 안전하지 않기 때문에 사용을 피해야 합니다.
- NoOpPasswordEncoder는 안전하지 않으며, 주로 예제나 테스트에서만 사용해야 합니다.
결론
각 구현은 보안 요구 사항과 성능의 균형을 맞추는 데 중요합니다. 대부분의 경우 BCryptPasswordEncoder나 SCryptPasswordEncoder와 같은 강력한 해싱 알고리즘을 사용하는 것이 좋으며, 애플리케이션의 성능 요구 사항에 따라 PBKDF2PasswordEncoder를 선택할 수 있습니다.
4.1.4 여러 인코딩 전략과 DelegatingPasswordEncoder
이 섹션에서는 애플리케이션에서 비밀번호 인코딩 전략이 변경될 때 여러 가지 인코딩 방법을 선택적으로 적용해야 하는 경우에 대해 다룹니다. 특히, 애플리케이션이 시간이 지나면서 비밀번호 인코딩 방식을 변경할 필요가 있을 때, 기존의 비밀번호는 그대로 두고 새로운 비밀번호는 다른 방식으로 인코딩하려는 경우에 유용합니다.
DelegatingPasswordEncoder 사용 이유
비밀번호 인코딩 알고리즘에 취약점이 발견되면, 새로운 사용자는 더 강력한 알고리즘(예: bcrypt)을 사용하고, 기존 사용자의 비밀번호는 기존 알고리즘으로 인코딩된 상태를 유지하고 싶을 수 있습니다. 이런 경우, DelegatingPasswordEncoder를 사용하여 서로 다른 인코딩 전략을 동시에 사용할 수 있습니다.
DelegatingPasswordEncoder는 다양한 PasswordEncoder 구현체를 내부적으로 관리하고, 비밀번호가 가진 접두어를 기준으로 적절한 인코더를 선택하여 위임하는 방식으로 동작합니다. 이 방법을 통해 여러 가지 인코딩 방식이 혼합된 환경에서도 일관된 방식으로 비밀번호를 처리할 수 있습니다.
예시
예를 들어, 애플리케이션에서 noop, bcrypt, scrypt와 같은 여러 가지 비밀번호 인코딩 방식을 사용할 때, 각각의 접두어를 기반으로 적절한 PasswordEncoder로 위임할 수 있습니다. 비밀번호가 {noop}으로 시작하면 NoOpPasswordEncoder를 사용하고, {bcrypt}으로 시작하면 BCryptPasswordEncoder로 위임하는 방식입니다.
다음은 DelegatingPasswordEncoder를 설정하는 방법에 대한 예시입니다:
@Configuration
public class ProjectConfig {
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
}
위 코드에서 DelegatingPasswordEncoder는 bcrypt를 기본 인코더로 사용하며, 접두어에 따라 다른 인코더로 위임합니다. 예를 들어, 비밀번호가 {bcrypt}로 시작하면 BCryptPasswordEncoder를 사용하고, {noop}으로 시작하면 NoOpPasswordEncoder를 사용합니다.
DelegatingPasswordEncoder의 편리함
DelegatingPasswordEncoder는 비밀번호 인코딩 전략을 선택할 때 유용하며, Spring Security에서는 PasswordEncoderFactories.createDelegatingPasswordEncoder() 메서드를 통해 기본적으로 bcrypt를 사용하는 DelegatingPasswordEncoder 인스턴스를 생성할 수 있습니다.
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
이렇게 하면 기본적으로 bcrypt를 사용하면서도 다양한 인코딩 방식들을 유연하게 처리할 수 있습니다.
인코딩, 암호화, 해싱의 차이
- 인코딩: 데이터를 다른 형식으로 변환하는 과정입니다. 예를 들어, 문자열을 뒤집는 것과 같습니다.
- 암호화: 키를 사용하여 데이터를 보호하는 방식으로, 키를 알고 있는 사람만 데이터를 복호화할 수 있습니다.
- 해싱: 단방향 변환으로, 입력 데이터를 복원할 수 없으며, 동일한 입력은 항상 동일한 출력을 생성합니다. 해싱은 주로 비밀번호 검증에 사용됩니다.
따라서, 비밀번호와 같은 민감한 데이터를 안전하게 처리하려면 적절한 해싱 기법을 사용하는 것이 중요합니다.
4.2 Spring Security Crypto 모듈에 대해 더 알아보기
이 섹션에서는 Spring Security의 암호화 관련 기능을 제공하는 **Spring Security Crypto 모듈(SSCM)**에 대해 논의합니다. Java는 기본적으로 암호화 및 복호화 함수, 키 생성 기능을 제공하지 않기 때문에, 이를 사용하려면 외부 라이브러리를 추가해야 합니다. Spring Security는 이러한 기능을 보다 쉽게 사용할 수 있도록 자체 솔루션을 제공하여 개발자가 불필요한 종속성을 줄이고 간편하게 암호화 작업을 처리할 수 있게 합니다.
4.2.1 키 생성기 사용하기
**키 생성기(Key Generators)**는 암호화나 해싱 알고리즘에 필요한 키를 생성하는 객체입니다. Spring Security에서는 별도의 종속성 추가 없이 사용할 수 있는 키 생성기를 제공하며, 이를 통해 효율적으로 암호화 관련 작업을 처리할 수 있습니다.
두 가지 주요 키 생성기 유형:
- StringKeyGenerator: 문자열 키를 생성합니다. 주로 해싱이나 암호화 알고리즘에서 사용할 솔트(salt) 값으로 사용됩니다.문자열 키 생성기를 사용하는 예:
- StringKeyGenerator keyGenerator = KeyGenerators.string(); String salt = keyGenerator.generateKey();
- public interface StringKeyGenerator { String generateKey(); }
- BytesKeyGenerator: 바이트 배열로 키를 생성합니다. 해싱 알고리즘에서 많이 사용되며, generateKey() 메서드를 통해 바이트 배열 키를 반환합니다.바이트 키 생성기 사용 예:만약 동일한 키 값을 여러 번 생성해야 한다면 KeyGenerators.shared() 메서드를 사용하여 동일한 키를 반환하는 BytesKeyGenerator를 생성할 수 있습니다.
- BytesKeyGenerator keyGenerator = KeyGenerators.shared(16); byte[] key1 = keyGenerator.generateKey(); byte[] key2 = keyGenerator.generateKey();
- BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(); byte[] key = keyGenerator.generateKey(); int keyLength = keyGenerator.getKeyLength();
- public interface BytesKeyGenerator { int getKeyLength(); byte[] generateKey(); }
4.2.2 암호화기 사용하기
**암호화기(Encryptors)**는 데이터를 암호화하거나 복호화하는 객체입니다. Spring Security에서는 암호화기와 관련된 두 가지 주요 인터페이스를 제공하며, 이를 통해 문자열 및 바이트 배열 데이터를 암호화할 수 있습니다.
1. TextEncryptor: 문자열 데이터를 암호화하고 복호화하는 인터페이스입니다.
public interface TextEncryptor {
String encrypt(String text);
String decrypt(String encryptedText);
}
TextEncryptor를 사용하는 예:
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
TextEncryptor encryptor = Encryptors.text(password, salt);
String encrypted = encryptor.encrypt(valueToEncrypt);
String decrypted = encryptor.decrypt(encrypted);
2. BytesEncryptor: 바이트 배열 데이터를 암호화하고 복호화하는 인터페이스입니다.
public interface BytesEncryptor {
byte[] encrypt(byte[] byteArray);
byte[] decrypt(byte[] encryptedByteArray);
}
BytesEncryptor를 사용하는 예:
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
BytesEncryptor encryptor = Encryptors.standard(password, salt);
byte[] encrypted = encryptor.encrypt(valueToEncrypt.getBytes());
byte[] decrypted = encryptor.decrypt(encrypted);
Encryptors.standard()와 Encryptors.stronger()는 암호화 강도를 조절할 수 있는 메서드로, 기본적으로 AES 256을 사용합니다. stronger()는 GCM 모드를 사용하는 암호화기를 생성하고, standard()는 CBC 모드를 사용합니다.
3. 쿼리 가능한 텍스트 암호화: 동일한 입력값에 대해 반복해서 암호화할 때, 항상 동일한 출력을 생성하려는 경우 **Encryptors.queryableText()**를 사용할 수 있습니다.
TextEncryptor encryptor = Encryptors.queryableText(password, salt);
String encrypted1 = encryptor.encrypt(valueToEncrypt);
String encrypted2 = encryptor.encrypt(valueToEncrypt);
queryableText()는 동일한 입력에 대해 동일한 암호화 결과를 생성하도록 보장합니다. 이 기능은 OAuth와 같은 환경에서 유용하게 사용됩니다.
요약
- PasswordEncoder: 비밀번호 처리의 핵심 책임을 맡고 있으며, 다양한 해싱 알고리즘을 제공합니다.
- Spring Security Crypto 모듈(SSCM): 키 생성기와 암호화기를 제공하여 암호화 및 해싱 작업을 쉽게 처리할 수 있게 합니다.
- 키 생성기는 암호화 알고리즘에 필요한 키를 생성하는 데 도움을 주는 객체로, StringKeyGenerator와 BytesKeyGenerator를 제공합니다.
- 암호화기는 데이터를 암호화하고 복호화하는 데 도움을 주며, TextEncryptor와 BytesEncryptor를 제공합니다.
Spring Security의 Crypto 모듈을 활용하면 암호화와 해싱 작업을 더욱 간편하게 처리할 수 있습니다.
'Spring Security in Action' 카테고리의 다른 글
8장 Configuring endpoint-level authorization: Applying restrictions (0) | 2024.12.01 |
---|---|
7장 Configuring endpoint-level authorization: Restricting access (0) | 2024.12.01 |
5장 A web app's security begins with filters (0) | 2024.12.01 |
3장 Managing users (0) | 2024.12.01 |
2장 Hello Spring Security (0) | 2024.12.01 |