2024. 2. 25. 22:47ㆍSpring Security
이 장에서는 다음 내용을 다룹니다.
- PasswordEncoder 구현 및 작업
- Spring Security Crypto 모듈이 제공하는 도구 사용
3장에서는 Spring Security로 구현된 애플리케이션에서 사용자 관리를 논의했습니다. 그렇다면 비밀번호는 어떨까요? 인증 흐름에서 중요한 부분임이 분명합니다. 이 장에서는 Spring Security로 구현된 애플리케이션에서 비밀번호와 secret을 관리하는 방법을 배울 것입니다. PasswordEncoder 계약과 비밀번호 관리를 위해 Spring Security Crypto 모듈(SSCM)이 제공하는 도구에 대해 논의할 것입니다.
위 설명에서 secret은 애플리케이션에서 사용자의 인증과 데이터 보안을 위해 관리해야 하는 중요한 정보나 키를 뜻합니다. 이 secret은 비밀번호와 같이 사용자의 개인 정보나 중요한 데이터 접근을 보호하기 위해 암호화되어 저장됩니다. secret은 비밀번호뿐만 아니라 API 키, 토큰, 암호화 키 등의 민감한 정보도 포함될 수 있으며, Spring Security에서 이러한 정보는 안전하게 암호화하고 저장하여 보안을 강화하는 역할을 합니다.
4.1 Using password encoders
3장에서, 여러분은 UserDetails 인터페이스가 무엇인지, 그리고 그 구현을 사용하는 여러 가지 방법에 대해 명확하게 이해하게 되었습니다. 하지만 2장에서 배운 것처럼, 다른 actor들이 인증 및 권한 부여 과정에서 user representation을 관리합니다. 또한 UserDetailsService와 PasswordEncoder와 같은 일부에는 디폴트 값이 있다는 것을 배웠습니다. 이제 디폴트 값을 재정의할 수 있다는 것을 알게 되었습니다. 이러한 빈들과 그것들을 구현하는 방법에 대한 깊은 이해를 계속하면서, 이 섹션에서는 PasswordEncoder를 분석합니다. 그림 4.1은 PasswordEncoder가 인증 과정에서 어디에 속하는지 상기시켜 줍니다.
위 설명에서 actors는 인증과 권한 부여 과정에서 사용자 정보를 처리하고 관리하는 다양한 구성 요소나 객체들을 의미합니다. 예를 들어, UserDetails, UserDetailsService, PasswordEncoder와 같은 컴포넌트들이 actor로 간주될 수 있습니다. 이들은 각자 특정 역할을 수행하며, 서로 상호작용하여 사용자의 인증과 권한을 관리하는 데 중요한 역할을 합니다.
위 설명에서 user representation은 인증 및 권한 부여 과정에서 사용자의 정보를 나타내는 객체나 구조를 의미합니다. 이 user representation은 사용자의 아이디, 비밀번호, 권한 등의 정보를 포함하며, Spring Security에서는 UserDetails 인터페이스를 통해 이러한 사용자 정보를 정의합니다. 인증 및 권한 부여 과정에서 이 정보를 사용하여 사용자의 신원을 확인하고, 접근 권한을 제어하게 됩니다.
그림 4.1 Spring Security 인증 과정. AuthenticationProvider는 인증 과정에서 사용자의 비밀번호를 검증하기 위해 PasswordEncoder를 사용합니다.
일반적으로 시스템이 비밀번호를 평문으로 관리하지 않기 때문에, 이들은 대개 읽기와 훔치기가 더 어렵게 변환되는 종류의 처리를 거칩니다. 이것을 위해 Spring Security는 별도의 인터페이스를 정의합니다. 이 섹션에서 쉽게 설명하기 위해 PasswordEncoder 구현과 관련된 다양한 코드 예시를 제공합니다. 이러한 인터페이스를 이해하기 시작한 후, 프로젝트 내에서 우리 자신의 구현을 작성할 것입니다. 그런 다음 4.1.3 절에서 Spring Security가 제공하는 PasswordEncoder의 가장 잘 알려지고 널리 사용되는 구현 리스트를 제공할 것입니다.
4.1.1 The PasswordEncoder contract
이 섹션에서는 PasswordEncoder 인터페이스의 정의에 대해 논의합니다. 이 인터페이스를 구현하여 Spring Security에 사용자의 비밀번호를 어떻게 검증할지 알려줍니다. 인증 과정에서 PasswordEncoder는 비밀번호가 유효한지 여부를 결정합니다. 모든 시스템은 어떤 방식으로든 비밀번호를 인코딩하여 저장합니다. 비밀번호를 해시하여 저장하는 것이 바람직하여 누군가가 비밀번호를 읽을 수 있는 가능성이 없습니다. PasswordEncoder는 비밀번호를 인코딩할 수도 있습니다. 인터페이스가 선언하는 encode() 및 matches() 메소드는 실제로 그 책임의 정의입니다. 이 두 가지는 서로 강하게 연결되어 있기 때문에 같은 인터페이스의 일부입니다. 애플리케이션이 비밀번호를 인코딩하는 방식은 비밀번호를 검증하는 방식과 관련이 있습니다. 먼저 PasswordEncoder 인터페이스의 내용을 검토해 봅시다:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
이 인터페이스는 두 개의 추상 메소드와 하나의 디폴트 구현 메소드를 정의합니다. 추상 메소드인 encode()와 matches()는 PasswordEncoder 구현과 관련하여 가장 자주 언급되는 메소드입니다.
encode(CharSequence rawPassword) 메소드의 목적은 제공된 문자열의 인코딩을 리턴하는 것입니다. Spring Security 기능 측면에서, 이는 주어진 비밀번호에 대한 암호화 또는 해시를 제공하는 데 사용됩니다. 이후에 matches(CharSequence rawPassword, String encodedPassword) 메소드를 사용하여 인코딩된 문자열이 원본 비밀번호와 일치하는지 확인할 수 있습니다. 인증 과정에서 matches() 메소드를 사용하여 제공된 비밀번호를 알려진 자격증명 세트와 비교합니다. 세 번째 메소드인 upgradeEncoding(CharSequence encodedPassword)는 계약에서 기본적으로 false로 설정됩니다. 이를 true를 반환하도록 재정의하면, 인코딩된 비밀번호가 보안을 강화하기 위해 다시 인코딩됩니다.
일부 경우에는 인코딩된 비밀번호를 다시 인코딩함으로써 결과에서 평문 비밀번호를 얻기가 더 어려워질 수 있습니다. 일반적으로, 이는 개인적으로 좋아하지 않는 어떤 종류의 모호성입니다. 하지만 프레임워크는 여러분의 경우에 적용된다고 생각한다면 이 가능성을 제공합니다.
Listing 4.1 The simplest implementation of a PasswordEncoder
public class PlainTextPasswordEncoder
implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString(); #A
}
@Override
public boolean matches(
CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword); #B
}
}
#A We don’t change the password, just return it as is.
#B Checks if the two strings are equal
인코딩의 결과는 항상 비밀번호와 동일합니다. 따라서 이들이 일치하는지 확인하기 위해서는 equals()를 사용하여 문자열을 비교하기만 하면 됩니다. 해싱 알고리즘 SHA-512를 사용하는 PasswordEncoder의 간단한 구현 예는 다음과 같습니다.
Listing 4.2 Implementing a PasswordEncoder that uses SHA-512
public class Sha512PasswordEncoder
implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return hashWithSHA512(rawPassword.toString());
}
@Override
public boolean matches(
CharSequence rawPassword, String encodedPassword) {
String hashedPassword = encode(rawPassword);
return encodedPassword.equals(hashedPassword);
}
// Omitted code
}
리스팅 4.2에서, 우리는 제공된 문자열 값을 SHA-512로 해시하는 메소드를 사용합니다. 리스팅 4.2에서 이 메소드의 구현은 생략했지만, 리스팅 4.3에서 찾을 수 있습니다. 이 메소드는 encode() 메소드에서 호출되며, 이제 입력에 대한 해시 값을 반환합니다. 입력에 대해 해시를 검증하기 위해, matches() 메소드는 입력된 원본 비밀번호를 해시하고, 검증을 수행하는 해시와 동등성을 비교합니다.
Listing 4.3 The implementation of the method to hash the input with 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]));
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Bad algorithm");
}
return result.toString();
}
다음 섹션에서 이 작업을 수행하는 더 나은 방법을 배우게 될 것이므로, 지금은 이 코드에 너무 신경 쓰지 마세요.
4.1.3 Choosing from the provided PasswordEncoder implementations( 제공된 PasswordEncoder 구현들 중에서 선택하기)
PasswordEncoder를 어떻게 구현하는지 아는 것이 유용하지만, Spring Security가 이미 여러분에게 유리한 구현을 제공하고 있다는 것도 알아야 합니다. 이 중 하나가 여러분의 애플리케이션과 일치한다면, 다시 작성할 필요가 없습니다. 이 섹션에서는 Spring Security가 제공하는 PasswordEncoder 구현 옵션에 대해 논의합니다. 이들은 다음과 같습니다.
- NoOpPasswordEncoder는 비밀번호를 인코딩하지 않고 평문 상태로 유지합니다. 이 구현은 예제용으로만 사용합니다. 비밀번호를 해시하지 않기 때문에, 실제 환경에서는 절대 사용해서는 안 됩니다.
- StandardPasswordEncoder는 비밀번호를 해시하기 위해 SHA-256을 사용합니다. 이 구현은 현재 사용되지 않으며, 새로운 구현을 위해 사용해서는 안 됩니다. 사용되지 않는 이유는 더 이상 충분히 강력하다고 여겨지지 않는 해싱 알고리즘을 사용하기 때문입니다. 그러나 여러분은 여전히 기존 애플리케이션에서 이 구현을 사용하는 것을 발견할 수 있습니다. 가능하다면, 기존 앱에서 이를 발견하면 더 강력한 다른 비밀번호 인코더로 변경해야 합니다.
- Pbkdf2PasswordEncoder는 비밀번호 기반 키 파생 함수 2(PBKDF2)를 사용합니다.
- BCryptPasswordEncoder는 강력한 해싱 함수인 bcrypt를 사용하여 비밀번호를 인코딩합니다.
- SCryptPasswordEncoder는 scrypt 해싱 함수를 사용하여 비밀번호를 인코딩합니다.
해싱과 이러한 알고리즘에 대해 더 알고 싶다면, David Wong의 "Real-World Cryptography" (Manning, 2021) 2장에서 좋은 토론을 찾을 수 있습니다. 여기 링크가 있습니다:
https://livebook.manning.com/book/real-world-cryptography/chapter-2/.
이제 이러한 유형의 PasswordEncoder 구현 인스턴스를 생성하는 몇 가지 예제를 살펴보겠습니다. NoOpPasswordEncoder는 비밀번호를 인코딩하지 않습니다. 이것은 우리의 예제 4.1에 있는 PlainTextPasswordEncoder와 유사한 구현을 가지고 있습니다. 이러한 이유로, 우리는 이 비밀번호 인코더를 이론적인 예제와 함께만 사용합니다. 또한, NoOpPasswordEncoder 클래스는 싱글톤으로 설계되었습니다. 클래스 외부에서 직접 생성자를 호출할 수 없지만, NoOpPasswordEncoder.getInstance() 메소드를 사용하여 다음과 같이 클래스의 인스턴스를 얻을 수 있습니다:
PasswordEncoder p = NoOpPasswordEncoder.getInstance();
Spring Security에서 제공하는 StandardPasswordEncoder 구현은 SHA-256을 사용하여 비밀번호를 해시합니다. StandardPasswordEncoder에는 해싱 과정에서 사용되는 시크리트를 제공할 수 있습니다. 이 시크리트의 값을 생성자의 파라미터를 통해 설정합니다. 아규먼트 없는 생성자를 호출하기로 선택한다면, 구현은 키의 값으로 빈 문자열을 사용합니다. 그러나 StandardPasswordEncoder는 현재 사용되지 않으며, 새로운 구현에 이를 사용하는 것을 추천하지 않습니다. 여전히 이를 사용하는 오래된 애플리케이션 또는 레거시 코드를 찾을 수 있기 때문에, 이에 대해 알고 있어야 합니다. 다음 코드 스니펫은 이 비밀번호 인코더의 인스턴스를 생성하는 방법을 보여줍니다:
PasswordEncoder p = new StandardPasswordEncoder();
PasswordEncoder p = new StandardPasswordEncoder("secret");
Spring Security가 제공하는 또 다른 옵션은 비밀번호 인코딩에 PBKDF2를 사용하는 Pbkdf2PasswordEncoder 구현입니다. Pbkdf2PasswordEncoder의 인스턴스를 생성하기 위해 다음 옵션이 있습니다:
PasswordEncoder p =
New Pbkdf2PasswordEncoder(“secret”, 16, 310000,
Pbkdf2PasswordEncoder.
SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
Spring Security에서 제공하는 또 다른 옵션은 비밀번호 인코딩을 위해 PBKDF2를 사용하는 Pbkdf2PasswordEncoder 구현입니다. Pbkdf2PasswordEncoder의 인스턴스를 생성하기 위해 다음과 같은 옵션들이 있습니다:
- PBKDF2WithHmacSHA1
- PBKDF2WithHmacSHA256
- PBKDF2WithHmacSHA512
PBKDF2는 인코딩 과정에서 지정된 횟수만큼 HMAC을 수행하는 비교적 쉬운 느린 해시 함수입니다. 마지막 호출에서 받는 세 가지 파라미터는 인코딩 과정에 사용되는 키 값, 비밀번호 인코딩에 사용되는 반복 횟수, 그리고 해시의 크기입니다. 두 번째와 세 번째 파라미터는 결과의 강도에 영향을 미칠 수 있습니다. 더 많은 또는 더 적은 반복을 선택할 수 있으며, 결과의 길이도 선택할 수 있습니다. 해시가 길수록 비밀번호가 더 강력해집니다. 그러나 이러한 값은 성능에 영향을 미치므로 반복 횟수가 많을수록 애플리케이션이 더 많은 리소스를 소비합니다. 해시를 생성하는 데 소비되는 리소스와 인코딩의 필요한 강도 사이에서 현명한 타협을 해야 합니다.
참고: 이 책에서는 더 알고 싶을 만한 여러 암호화 개념을 언급합니다. HMAC 및 기타 암호화 관련 세부 사항에 대한 유용한 정보를 원한다면 David Wong의 Real-World Cryptography (Manning, 2020)를 추천합니다. 해당 책의 3장에서 HMAC에 대해 자세히 다루고 있습니다. 책은 [여기](https://livebook.manning.com/book/real-world-cryptography/chapter-3/)에서 찾아볼 수 있습니다.
Pbkdf2PasswordEncoder 구현에서 두 번째 또는 세 번째 값을 지정하지 않으면 디폴트값으로 반복 횟수는 185000회, 결과의 길이는 256으로 설정됩니다. 반복 횟수와 결과 길이에 대해 사용자 지정 값을 설정하려면, 파라미터가 없는 Pbkdf2PasswordEncoder() 생성자 또는 비밀 값만을 파라미터로 받는 Pbkdf2PasswordEncoder("secret") 생성자 중 하나를 선택할 수 있습니다.
Spring Security에서 제공하는 또 다른 훌륭한 옵션은 bcrypt 강력 해싱 함수를 사용하여 비밀번호를 인코딩하는 BCryptPasswordEncoder입니다. 파라미터가 없는 생성자를 호출하여 BCryptPasswordEncoder를 인스턴스화할 수 있습니다. 또한, 인코딩 과정에서 사용되는 로그 라운드(logarithmic rounds)를 나타내는 강도 계수를 지정할 수도 있습니다. 추가로, 인코딩에 사용되는 SecureRandom 인스턴스를 변경할 수도 있습니다.
PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4);
SecureRandom s = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, s);
log rounds 값은 해싱 작업에 사용되는 반복 횟수에 영향을 미칩니다. 사용되는 반복 횟수는 2^log rounds입니다. 반복 횟수 계산에서 log rounds 값은 4에서 31 사이여야 합니다. 이는 이전 코드 스니펫에서처럼 두 번째 또는 세 번째 오버로드된 생성자 중 하나를 호출하여 지정할 수 있습니다.
마지막으로 소개할 옵션은 SCryptPasswordEncoder입니다(그림 4.2). 이 비밀번호 인코더는 scrypt 해싱 함수를 사용합니다. ScryptPasswordEncoder의 경우, 인스턴스를 생성하는 데 두 가지 옵션이 있습니다:
PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
위 예제의 값들은 파라미터가 없는 생성자를 호출하여 인스턴스를 생성할 때 사용되는 값들입니다.
Salt는 비밀번호와 같은 민감한 데이터를 암호화할 때 보안을 강화하기 위해 추가하는 임의의 데이터입니다. 솔트를 사용하면 동일한 비밀번호라도 솔트 값에 따라 다른 해시값이 생성되므로, 동일한 비밀번호를 가진 사용자들의 해시가 달라지게 됩니다. 이는 무차별 대입 공격(brute-force attack)이나 사전 공격(dictionary attack)을 방어하는 데 효과적입니다. 솔트는 주로 비밀번호를 해시화할 때 함께 저장되며, 해시 복원 시에도 솔트가 필요합니다.
4.1.4 Multiple encoding strategies with DelegatingPasswordEncoder
이 섹션에서는 인증 흐름에서 비밀번호를 일치시키기 위해 다양한 구현을 적용해야 하는 경우를 논의합니다. 또한, 애플리케이션에서 PasswordEncoder로 작동하는 유용한 도구를 적용하는 방법도 배우게 됩니다. 이 도구는 자체 구현을 가지는 대신, PasswordEncoder 인터페이스를 구현하는 다른 객체들에 위임합니다.
일부 애플리케이션에서는 다양한 비밀번호 인코더를 가지고 특정 구성에 따라 선택하는 것이 유용할 수 있습니다. 실제 애플리케이션에서 DelegatingPasswordEncoder를 사용하게 되는 일반적인 시나리오는 애플리케이션의 특정 버전부터 인코딩 알고리즘이 변경될 때입니다. 현재 사용 중인 알고리즘에 취약점이 발견되어 새로 등록된 사용자에게는 알고리즘을 변경하고 싶지만, 기존의 자격 증명에는 변경을 적용하지 않으려는 경우를 상상해 보십시오. 이 경우 여러 종류의 해시가 생기게 됩니다. 이런 경우를 어떻게 관리할 수 있을까요? 이 시나리오에 대한 유일한 접근 방식은 아니지만, 좋은 선택 중 하나는 DelegatingPasswordEncoder 객체를 사용하는 것입니다.
DelegatingPasswordEncoder는 PasswordEncoder 인터페이스의 구현으로, 자체 인코딩 알고리즘을 구현하는 대신 동일한 계약을 구현하는 다른 인스턴스에 위임합니다. 해시는 해당 해시를 정의하는 데 사용된 알고리즘 이름을 접두어로 시작합니다. DelegatingPasswordEncoder는 비밀번호의 접두어를 기준으로 적절한 PasswordEncoder 구현에 위임합니다.
복잡해 보이지만, 예제를 통해 살펴보면 쉽게 이해할 수 있습니다. 그림 4.3은 PasswordEncoder 인스턴스 간의 관계를 보여줍니다. DelegatingPasswordEncoder는 위임할 PasswordEncoder 구현 목록을 가지고 있으며, 각 인스턴스를 맵에 저장합니다. NoOpPasswordEncoder는 키 noop에 할당되고, BCryptPasswordEncoder 구현은 키 bcrypt에 할당됩니다. 비밀번호가 {noop} 접두어를 가지고 있으면 DelegatingPasswordEncoder는 NoOpPasswordEncoder 구현에 작업을 위임합니다. 접두어가 {bcrypt}이면 작업은 그림 4.4에서 보여준 것처럼 BCryptPasswordEncoder 구현에 위임됩니다.
다음으로, DelegatingPasswordEncoder를 정의하는 방법을 알아보겠습니다. 먼저 원하는 PasswordEncoder 구현 인스턴스들을 모아 컬렉션을 만들고, 이를 DelegatingPasswordEncoder에 함께 넣습니다. 다음 예시와 같이 설정할 수 있습니다.
Listing 4.4 Creating an instance of DelegatingPasswordEncoder
@Configuration
public class ProjectConfig {
// Omitted code
@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는 PasswordEncoder처럼 작동하는 도구에 불과하여, 여러 구현 중 하나를 선택해야 할 때 사용할 수 있습니다. 리스팅 4.4에서 선언된 DelegatingPasswordEncoder 인스턴스는 NoOpPasswordEncoder, BCryptPasswordEncoder, 그리고 SCryptPasswordEncoder에 대한 참조를 포함하고 있으며, 디폴트 값은 BCryptPasswordEncoder 구현으로 위임됩니다. 해시의 접두어를 기반으로 DelegatingPasswordEncoder는 비밀번호를 일치시키기 위해 올바른 PasswordEncoder 구현을 사용합니다. 이 접두어는 인코더 맵에서 사용할 비밀번호 인코더를 식별하는 키 역할을 합니다. 접두어가 없는 경우, DelegatingPasswordEncoder는 기본 인코더를 사용합니다. 기본 PasswordEncoder는 DelegatingPasswordEncoder 인스턴스를 생성할 때 첫 번째 파라미터로 제공된 인코더입니다. 리스팅 4.4의 코드에서 디폴트 PasswordEncoder는 bcrypt입니다.
참고: 중괄호는 해시 접두어의 일부이며, 키 이름을 둘러싸야 합니다. 예를 들어, 제공된 해시가 {noop}12345라면, DelegatingPasswordEncoder는 접두어 noop에 대해 등록한 NoOpPasswordEncoder로 작업을 위임합니다. 다시 말하지만, 접두어에서 중괄호는 필수입니다.
해시가 다음 코드 스니펫과 같이 보이면, 비밀번호 인코더는 접두어 {bcrypt}에 할당된 BCryptPasswordEncoder입니다. 또한, 접두어가 전혀 없을 때도 애플리케이션은 이를 디폴트 구현으로 정의했기 때문에 BCryptPasswordEncoder로 작업을 위임하게 됩니다.
{bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG
편의를 위해 Spring Security는 모든 표준 PasswordEncoder 구현을 포함하는 맵을 가진 DelegatingPasswordEncoder를 생성하는 방법을 제공합니다. PasswordEncoderFactories 클래스는 createDelegatingPasswordEncoder()라는 정적 메서드를 제공하며, bcrypt를 디폴트 인코더로 사용하는 DelegatingPasswordEncoder 구현을 반환합니다.
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
Encoding vs. encrypting vs. hashing
이전 섹션에서 인코딩, 암호화, 해싱이라는 용어를 자주 사용했습니다. 여기서 이 용어들과 책에서 사용하는 방식을 간략히 설명하고자 합니다.
인코딩은 주어진 입력의 변환을 의미합니다. 예를 들어, 문자열을 뒤집는 함수 x가 있다고 할 때, 함수 x -> y를 ABCD에 적용하면 DCBA가 됩니다. 암호화는 특정한 유형의 인코딩으로, 출력을 얻기 위해 입력 값과 키 모두를 제공합니다. 이 키를 사용하여 누가 함수(출력에서 입력을 얻는)를 되돌릴 수 있는지 결정할 수 있습니다. 암호화를 함수로 표현하는 가장 간단한 형태는 다음과 같습니다:
(x, k) −> y
여기서 x는 입력, k는 키, y는 암호화 결과입니다. 이 방식에서 키를 알고 있는 사람은 알려진 함수를 사용하여 출력에서 입력을 얻을 수 있습니다:(y, k) −> x. 이 역함수를 복호화라고 합니다. 암호화와 복호화에 동일한 키가 사용된다면 이를 대칭 키라고 부릅니다.
암호화와 복호화에 두 개의 다른 키가 사용된다면 예를 들어 (x, k1) −> y와 (y, k2) −> x, 이는 비대칭 키로 암호화되었다고 합니다. 이때 (k1, k2)를 키 쌍이라고 부르며, 암호화에 사용되는 키 k1을 공개 키(public key), 복호화에 사용되는 키 k2를 개인 키(private key)라고 합니다. 이 방식에서 개인 키의 소유자만 데이터를 복호화할 수 있습니다.
Hashing은 특정한 유형의 인코딩으로, 함수가 단방향입니다. 즉, 해싱 함수의 출력 y에서 입력 x를 되돌릴 수 없습니다. 그러나 출력 y가 입력 x에 해당하는지를 확인할 수 있는 방법이 항상 있어야 합니다. 따라서 해싱은 인코딩과 매칭을 위한 함수 쌍으로 이해할 수 있습니다. 해싱이 x -> y라면, 매칭 함수 (x, y) -> boolean도 있어야 합니다.
때때로 해싱 함수는 입력에 추가되는 임의의 값을 사용할 수도 있습니다:(x, k) −> y. 이 값을 솔트라고 부릅니다. 솔트는 함수를 더 강력하게 만들어, 출력에서 입력을 얻기 위해 역함수를 적용하는 난이도를 높여줍니다.
지금까지 이 책에서 논의하고 적용한 인터페이스를 요약하면, 테이블 4.1은 각 구성 요소를 간략히 설명합니다.
Interface | Contract |
UserDetails | Spring Security에서 인식하는 User에 대한 표현 |
GrantedAuthority | User가 수행할 수 있는 애플리케이션 내의 허용된 작업을 정의합니다 (예: 읽기, 쓰기, 삭제 등). |
UserDetailsService | User 이름으로 UserDetails를 조회하는 데 사용되는 객체를 나타냅니다. |
UserDetailsManager | UserDetailsService의 더 구체적인 인터페이스입니다. User 이름으로 User를 조회할 뿐만 아니라, User 컬렉션 또는 특정 User를 수정하는 데도 사용할 수 있습니다. |
PasswordEncoder | 비밀번호가 어떻게 암호화 또는 해시되는지, 그리고 주어진 인코딩된 문자열이 평문 비밀번호와 일치하는지 확인하는 방법을 지정합니다. |
4.2 More about the Spring Security Crypto module4.2 More about the Spring Security Crypto module
이 섹션에서는 암호화를 다루는 Spring Security의 일부인 Spring Security Crypto 모듈(SSCM)에 대해 논의합니다. Java 언어는 기본적으로 암호화와 복호화 함수 및 키 생성을 제공하지 않으므로, 이러한 기능에 보다 접근하기 쉬운 방법을 제공하는 종속성을 추가할 때 개발자에게 제약을 줍니다.
개발을 더욱 쉽게 하기 위해 Spring Security는 별도의 라이브러리를 사용할 필요 없이 종속성을 줄일 수 있는 자체 솔루션도 제공합니다. 비밀번호 인코더 또한 이전 섹션에서 따로 다루었지만, SSCM의 일부입니다. 이 섹션에서는 SSCM이 암호화와 관련하여 제공하는 다른 옵션에 대해 논의합니다. SSCM의 두 가지 필수 기능을 사용하는 예제도 살펴보겠습니다.
- Key generators : 해싱 및 암호화 알고리즘을 위한 키를 생성하는 데 사용되는 객체.
- Encryptors : 데이터를 암호화 및 복호화를 위한 객체
4.2.1 Using key generators
이 섹션에서는 키 생성기에 대해 논의합니다. 키 생성기는 일반적으로 암호화 또는 해싱 알고리즘에 필요한 특정 유형의 키를 생성하는 객체입니다. Spring Security에서 제공하는 키 생성기 구현은 유용한 도구들로, 별도의 종속성을 추가하는 대신 이 구현들을 사용하는 것이 좋습니다. 따라서 이 도구들을 익히는 것을 추천합니다. 키 생성기를 생성하고 적용하는 코드 예제를 살펴보겠습니다.
두 가지 주요 키 생성기 유형을 나타내는 두 개의 인터페이스가 있습니다: BytesKeyGenerator와 StringKeyGenerator. 이들은 KeyGenerators라는 팩토리 클래스를 이용하여 직접 생성할 수 있습니다. 문자열 키를 얻기 위해 StringKeyGenerator 인터페이스를 사용하여 문자열 키 생성기를 사용할 수 있으며, 보통 해싱 또는 암호화 알고리즘을 위한 솔트 값으로 사용됩니다. StringKeyGenerator 인터페이스의 정의는 다음 코드 스니펫에서 확인할 수 있습니다:
public interface StringKeyGenerator {
String generateKey();
}
생성기는 키 값을 나타내는 문자열을 반환하는 generateKey() 메서드만을 가지고 있습니다. 다음 코드 스니펫은 StringKeyGenerator 인스턴스를 얻고 솔트 값을 얻는 예제입니다:
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();
생성기는 8바이트 키를 생성하고 이를 16진수 문자열로 인코딩합니다. 메서드는 이 작업의 결과를 문자열로 반환합니다. 키 생성기를 설명하는 두 번째 인터페이스는 BytesKeyGenerator이며, 다음과 같이 정의됩니다:
public interface BytesKeyGenerator {
int getKeyLength();
byte[] generateKey();
}
바이트 배열로 키를 반환하는 generateKey() 메서드 외에, BytesKeyGenerator 인터페이스는 바이트 단위의 키 길이를 반환하는 또 다른 메서드를 정의합니다. ByteKeyGenerator는 8바이트 길이의 키를 생성합니다:
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte [] key = keyGenerator.generateKey();
int keyLength = keyGenerator.getKeyLength();
이전 코드 스니펫에서 키 생성기는 8바이트 길이의 키를 생성합니다. 다른 키 길이를 지정하려면 KeyGenerators.secureRandom() 메서드에 원하는 값을 제공하여 키 생성기 인스턴스를 얻을 때 이를 지정할 수 있습니다:
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16);
KeyGenerators.secureRandom() 메서드로 생성된 BytesKeyGenerator에 의해 생성된 키는 generateKey() 메서드의 각 호출마다 고유합니다. 경우에 따라 동일한 키 생성기에 대한 각 호출에서 동일한 키 값을 반환하는 구현을 선호할 수 있습니다. 이 경우 KeyGenerators.shared(int length)서드를 사용하여 BytesKeyGenerator를 생성할 수 있습니다. 다음 코드 스니펫에서 `key1`과 `key2`는 동일한 값을 가집니다:
BytesKeyGenerator keyGenerator = KeyGenerators.shared(16);
byte [] key1 = keyGenerator.generateKey();
byte [] key2 = keyGenerator.generateKey();
4.2.2 Using encryptors for encryption and decryption operations
이 섹션에서는 Spring Security에서 제공하는 암호화기 구현을 코드 예제와 함께 적용합니다. 암호화기(encryptor)는 암호화 알고리즘을 구현하는 객체입니다. 보안과 관련하여, 암호화와 복호화는 일반적인 작업이므로 애플리케이션 내에서 필요할 수 있습니다.
시스템의 구성 요소 간에 데이터를 전송하거나 데이터를 저장할 때 종종 암호화가 필요합니다. 암호화기가 제공하는 작업은 암호화와 복호화입니다. SSCM에는 두 가지 유형의 암호화기가 정의되어 있습니다: BytesEncryptor와 TextEncryptor. 이들은 유사한 책임을 가지지만, 서로 다른 데이터 유형을 처리합니다. TextEncryptor는 문자열 데이터를 관리하며, 메서드는 문자열을 입력으로 받고 문자열을 출력으로 반환합니다. 인터페이스 정의는 다음과 같습니다:
public interface TextEncryptor {
String encrypt(String text);
String decrypt(String encryptedText);
}
BytesEncryptor는 더 일반적이며, 입력 데이터를 바이트 배열로 제공합니다:
public interface BytesEncryptor {
byte[] encrypt(byte[] byteArray);
byte[] decrypt(byte[] encryptedByteArray)
}
암호화기를 구축하고 사용하는 옵션에 대해 알아보겠습니다. 팩토리 클래스 Encryptors는 여러 가지 가능성을 제공합니다. BytesEncryptor의 경우 Encryptors.standard() 또는 Encryptors.stronger() 메서드를 다음과 같이 사용할 수 있습니다:
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
BytesEncryptor e = Encryptors.standard(password, salt);
byte [] encrypted = e.encrypt(valueToEncrypt.getBytes());
byte [] decrypted = e.decrypt(encrypted);
백그라운드에서, 표준 바이트 암호화기는 입력을 암호화하기 위해 256바이트 AES 암호화를 사용합니다. 더 강력한 바이트 암호화기 인스턴스를 생성하려면 Encryptors.stronger() 메서드를 호출할 수 있습니다:
BytesEncryptor e = Encryptors.stronger(password, salt);
차이는 미세하며 백그라운드에서 발생하는데, 256비트 AES 암호화가 운영 모드로 Galois/Counter Mode(GCM)를 사용하는 반면, 표준 모드는 더 약한 방식으로 간주되는 사이퍼 블록 체이닝(CBC)을 사용합니다.
TextEncryptor는 세 가지 주요 유형이 있습니다. Encryptors.text(), Encryptors.delux(), Encryptors.queryableText() 메서드를 호출하여 이 세 가지 유형을 생성할 수 있습니다. 암호화기를 생성하는 이러한 메서드 외에도 값을 암호화하지 않는 더미 TextEncryptor를 반환하는 메서드도 있습니다. 더미 TextEncryptor는 데모 예제나 암호화에 소요되는 시간을 생략하여 애플리케이션의 성능을 테스트하려는 경우에 사용할 수 있습니다. 이 작동하지 않는(no-op) 암호화기를 반환하는 메서드는 Encryptors.noOpText()입니다. 다음 코드 스니펫은 TextEncryptor를 사용하는 예제를 보여줍니다. 암호화기를 호출했지만 예제에서 encrypted와 valueToEncrypt는 동일합니다:
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.noOpText();
String encrypted = e.encrypt(valueToEncrypt);
Encryptors.text() 암호화기는 암호화 작업을 관리하기 위해 Encryptors.standard() 메서드를 사용하며, Encryptors.delux() 메서드는 다음과 같이 Encryptors.stronger() 인스턴스를 사용합니다:
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.text(password, salt);
String encrypted = e.encrypt(valueToEncrypt);
String decrypted = e.decrypt(encrypted);
Encryptors.text() 및 Encryptors.delux()의 경우, 동일한 입력에 대해 encrypt() 메서드를 반복적으로 호출하면 서로 다른 출력을 생성합니다. 이는 암호화 과정에서 생성된 무작위 초기화 벡터 때문입니다. 실제로는 OAuth API 키의 경우처럼 이러한 일이 발생하지 않기를 원하는 경우도 있습니다. OAuth 2에 대해서는 12장에서 15장까지 더 논의할 것입니다. 이러한 종류의 입력을 쿼리 가능한 텍스트라고 하며, 이 상황에서는 Encryptors.queryableText() 인스턴스를 사용할 수 있습니다. 이 암호화기는 동일한 입력에 대해 연속적인 암호화 작업이 동일한 출력을 생성하도록 보장합니다. 다음 예제에서 encrypted1 변수의 값은 encrypted2 변수의 값과 동일합니다:
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.queryableText(password, salt);
String encrypted1 = e.encrypt(valueToEncrypt);
String encrypted2 = e.encrypt(valueToEncrypt);
요약
- PasswordEncoder는 인증 로직에서 비밀번호를 처리하는 가장 중요한 책임 중 하나를 가집니다.
- Spring Security는 해싱 알고리즘에 대한 여러 대안을 제공하여 구현을 선택 사항으로 만듭니다.
- Spring Security Crypto 모듈(SSCM)은 키 생성기와 암호화기의 다양한 구현 대안을 제공합니다.
- 키 생성기는 암호화 알고리즘에 사용되는 키를 생성하는 데 도움을 주는 유틸리티 객체입니다.
- 암호화기는 데이터의 암호화와 복호화를 적용하는 데 도움을 주는 유틸리티 객체입니다.
'Spring Security' 카테고리의 다른 글
ch06 Implementing authenticaiton (0) | 2024.02.26 |
---|---|
ch05 A web app's security begins with filters (0) | 2024.02.25 |
ch03 Managing users (0) | 2024.02.25 |
Ch02 Hello Spring Security (0) | 2024.02.25 |
CORS : Non-Simple Request / Simple Request (0) | 2023.06.20 |