PasswordEncoder 인터페이스는 암호가 안전하게 저장되도록 단방향 변환을 수행해준다. 보통 SHA-256 같은 단방향 해시를 통해 암호를 실행한 후 저장한다. 그러나 레인보우 테이블 같은 조회 테이블때문에 암호의 보안을 더 강화해야 했다.
레인보우 테이블이란 암호화 해시 함수의 출력을 캐싱하기 위해 미리 계산된 테이블을 말한다. 제한된 문자 집합으로 구성된 특정 길이까지 키 파생 함수를 복구하는데 사용된다. 무차별 대입 공격보다 더 적은 처리 시간을 가지지만 많은 저장 공간을 사용한다.
따라서 솔티드 암호가 권장되었다. 비밀번호를 사용하는 대신 임의의 추가 바이트를 덧붙여 해시 함수를 통해 해시를 생성한다. 따라서 레인보우 테이블이 효과적이지 않게 되었다. 스프링은 따라서 각 시스템에 맞는 성능을 제공하기 위해 여러 구현체들을 두고 있다.
스프링 시큐리티 5.0 이전엔 기본 암호 인코더로 NoOpPasswordEncoder를 사용했으나 여러 문제가 있었다.
따라서 5.0 이후엔 DelegatingPasswordEncoder을 통해 해결했다.
DelegatingPasswordEncoder는 다음과 같은 기능을 제공해 해결한다.
- 권장 사항을 적용한 암호가 인코딩 됐는지 확인한다.
- 최신 형식의 유효성 검사 허용
- 인코딩 업그레이드 허용
쉽게 인코더를 생성할 수 있다.
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
DelegatingPasswordEncoder의 저장형식은 {id}encodedPassword으로 {id}에 원하는 방식을 택하면 된다.
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
구현은 일반적으로 BCryptPasswordEncoder을 사용한다. 공격에 대한 방어력을 높이기 위해 bcrypt는 상대적으로 속도가 느리다.
기본 강도는 10을 사용하고 커스텀할 수 있다.
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
기본적으로 prefix가 없을 경우 bcrypt가 적용되나 커스텀이 가능하다.
configure의 AuthenticationManagerBuilder를 통해 구현해 커스텀이 가능하다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}user").roles("USER").and()
.withUser("admin").password("{noop}admin").roles("ADMIN");
}
아래는 구현 가능한 구현체들의 일부이다.
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
'스프링 부트 > 시큐리티' 카테고리의 다른 글
[스프링 시큐리티] 주요 Security Filter들 정리 (0) | 2022.05.24 |
---|---|
[스프링 시큐리티] FilterChainProxy (0) | 2022.05.23 |
[스프링 시큐리티] SecurityContextHolder (0) | 2022.05.23 |
[스프링 시큐리티] AuthenticationManager, AccessDecisionManager (0) | 2022.05.23 |
댓글