Spring Security 정복하기(1)
Spring Security 기본 개념 및 비밀번호 암호화
많은 개발자들이 스프링부트를 사용하면 로그인 및 인가처리를 구현할때 스프링 시큐리티를 이용할 것이다.
필자또한 스프링 시큐리티를 이용해서 기능을 구현해본 적이 있다.
허나 스프링 시큐리티의 내부 동작방식을 알지 못해서 오류가나면 해결하지도 못했고 기능확장 또한 어려웠다.
이번에 샘플 코드를 작성해보며 동작방식과 원리에 대해서 파악해보며 스프링 시큐리티를 잘 다루는 개발자가 되어보자.
(모든 내용은 스프링시큐리티 6.3.2 버전의 공식문서와 다른 블로그를 참조하여 작성할 예정이다)
스프링 시큐리티와 기본 개념들
공식문서에서 설명하는 스프링 시큐리티가 뭔지에 대하여 번역하여 적어보았다.
Spring Security는 인증, 인가, 그리고 일반적인 공격에 대한 보호를 제공하는 프레임워크입니다. 명령형 및 리액티브 애플리케이션 모두를 안전하게 보호할 수 있는 일류 지원을 제공하며, Spring 기반 애플리케이션 보안의 사실상 표준(de-facto standard)으로 자리 잡고 있습니다.
- 여기서 일반적인 공격이란
common attacks라고 표현되어 있으며 아래 3가지를 의미한다.- CSRF
- HTTP Headers
- HTTP Requests
- 인증(Authentication) : 사용자가 누구인지 확인하는 과정
- 인가(Authorization) : 작업을 수행할 수 있는 권한 확인
요약하자면 스프링 시큐리티는 스프링부트에서 인증, 인가, 보안처리에 관련된 사실상 표준이다.
인증
인증은 특정 리소스에 접근하는 사용자의 신원을 확인하는 방법이다. 우리가 알다시피 사용자를 인증하는 일반적인 방법은 사용자이름과 비밀번호를 요구하는 것(로그인)이다. 로그인이 수행되면 그때서야 권한을 부여할 수 있다.
비밀번호 보안은 어떻게 처리하지?
비밀번호는 유출되어서는 안 되는 아주 중요한 데이터이다.
스프링 시큐리티에서는 PasswordEncoder 라는 인터페이스를 제공한다.
- 비밀번호를 안전하게 저장하기 위해 단방향 암호화를 수행한다.
- 일반적으로 비밀번호를 암호화하여 저장하며, 인증 시 사용자가 입력한 비밀번호와 비교하는 데 사용된다.
비밀번호 저장 방식의 역사
- 초기 : 비밀번호 평문 저장.
이후 SQL 인젝션 으로 털림- 이후 : SHA-256같은 단방향 해시함수를 이용해서 비밀번호 해시 후 저장.
레인보우 테이블이라는 해시 역산 표에 의해 무력화- 그 이후 : salt를 사용하여 비밀번호를 해시하여 레인보우 테이블 무력화
- 현재 : 하드웨어의 발전으로 SHA-256같은 해시함수도 안전하지 않다고 판단됨.
bcrypt, PBKDF2, argon2 등 적응형 단방향 함수(시스템자원을 많이 사용)를 사용받도록 권장
DelegatingPasswordEncoder
- 많은 응용 프로그램이 쉽게 마이그레이션할 수 없는 이전의 암호 인코딩 방식을 사용하고 있다.
- 비밀번호 저장에 대한 보안 모범 사례는 계속 변화한다.
- Spring Security는 프레임워크로서 호환성을 유지해야 하기 때문에 자주 변경할 수 없다.
즉, 보안 모범사례가 계속해서 바뀌어가고 인코딩 알고리즘을 쉽게 바꿀수가 없다는 내용이다.
이러한 문제들을 해결하기 위해 Spring Security는 DelegatingPasswordEncoder를 도입했다. 이 클래스는 다음과 같은 기능을 제공한다.
- 다양한 인코딩 알고리즘 관리:
DelegatingPasswordEncoder는 다양한 인코딩 방식을 처리할 수 있으며, 이는 레거시 시스템과 최신 보안 표준 간의 호환성을 유지하는 데 유용하다. - 미래 대비: 새로운 인코딩 방식이 도입될 때, 기존의 비밀번호를 재인코딩하지 않아도 새로운 방식으로 쉽게 전환할 수 있다.
결론적으로, DelegatingPasswordEncoder는 다양한 비밀번호 인코딩 방식을 일관되게 관리하며, 미래의 보안 요구 사항에도 대비할 수 있도록 설계된 유연한 도구이다.
간단히 샘플코드만 작성해보고 비밀번호 암호화에 대한 부분을 마치자.
// 디폴트 DelegatingPasswordEncoder 생성
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String encoded = passwordEncoder.encode("hi");
System.out.println(encoded);
//출력
{bcrypt}$2a$10$oVQGp0qQgZrHmpye0o0uq.lXVx4L.JnFIe126H1k63Y7sqHGPr1wO
- DelegatingPassowordEncoder의 디폴트 알고리즘은 bcrypt이다.
- 앞에 접두사로 알고리즘의 식별자를 포함한다.
- 단방향 해싱 및 적응형 단방향 함수이기 때문에 비밀번호를 크랙하는 시간 및 비용이 매우큼
String idForEncode = "pbkdf2";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
String rawPassword = "myPassword";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Encoded password: " + encodedPassword);
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("Password matches: " + matches);
// 출력
Encoded password: {pbkdf2}1d7180a616eeddd8c3dc78d8416af6e2dc2f1fef3aa87ce6be6724189b95045749d330b902c07e31a808d85b648db96f
Password matches: true
id는 어떤PasswordEncoder를 사용해야 하는지에 대한 식별자이다.- 비밀번호 매칭은 {id}와 생성자에서 제공된 PasswordEncoder의 id 매핑에 기반한다. 즉, DelegatingPasswordEncoder 생성자 첫번째 Argument에 매칭되지 않는 id를 제공하면 안된다.
- 이 경우 IllegalArgumentException이 발생한다.
여기까지가 비밀번호 암호화에 대한 공식문서의 내용이다.
강의나 책을보았을땐 BCryptPasswordEncoder를 Bean으로 주입받아 사용할때가 많았는데 위 DelegatingPasswordEncoder를 사용하면 유연성과 추후 변경될 수 있는 인코딩방식으로 마이그레이션의 편리함을 챙겨갈 수 있을것 같다.
Appendix
“Have I Been Pwned”는 해커들이 유출시킨 대규모 데이터베이스에 저장된 비밀번호를 수집하고, 이를 바탕으로 비밀번호 유출 여부를 확인해주는 서비스다.
Spring Security에서는 이 서비스를 통합하여 비밀번호 유출 여부를 확인할 수 있도록 도와준다.
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
- 단순히
CompromisedPasswodChecker를 빈으로 등록해주면 된다. - 이 설정이 적용된 경우 Spring Security가 자동으로 “Have I Been Pwned” API를 호출하여 비밀번호가 유출된 적이 있는지를 확인한다.
- 만약 비밀번호가 유출된 것으로 확인되면
CompromisedPasswordException이 발생한다. 컨트롤러 어드바이스에서 비밀번호를 재설정하게 하는 등 적절히 처리하면 될것이다.