SpringSecurity
SpringSecurity [PasswordEncoder]
성찬우
2022. 6. 23. 01:31
가장 흔하게 쓰이는 SpringSecurity 기능 중 하나인
비밀번호 숨기기다.
기본적으로 PasswordEncoder를 사용하려면 @Bean 등록을 하고 사용을 하게 되는데

정말 간단하게 @Bean 등록이 가능하다.
이후 필요한 클래스에서
@Autowired PasswordEncoder passwordEncoder
또는
@RequiredArgsConstructor
public class 클래스명 {
private final PasswordEncoder passwordEncoder;
}
알아서 편한대로 가져다 사용하면 된다.
PasswordEncoder의 내부이다 대충 설명을 하자면

String encode(CharSequence rawPassword);
raw한 암호를 인코딩합니다. 8바이트 이상의 무작위 생성된 솔트와 결합된 SHA-1 이상의 해시를 제공.
솔트(salt)란 해시함수를 돌리기 전에 원문에 임의의 문자열을 덧붙이는 것을 말한다.
단어 뜻 그대로 원문에 임의의 문자열을 붙인다는 의미의 소금친다(salting) 는 것이다.
boolean matches(CharSequence rawPassword, String encodedPassword);
저장소에서 가져온 [인코딩된 암호]가 [인코딩된 후 제출된 raw 암호] 와 일치하는지 확인함.
암호 일치 - true, 불일치 - false 반환
default boolean upgradeEncoding(String encodedPassword) { return false;}
보안을 강화하기 위해서 인코딩된 암호를 또한번 인코딩 해야 할 경우 true를 반환하며 아닐경우 false를 반환
( default: false)
그리고 이 인터페이스를 구현 클래스들은 다음과 같습니다.
" -----?----- " 해시 함수를 사용해 비밀번호를 암호화 합니다.
BcryptPasswordEncoder : "BCrypt"
Argon2PasswordEncoder : "Argon2"
Pbkdf2PasswordEncoder : "PBKDF2"
SCryptPasswordEncoder : "SCrypt"
위 4개의 클래스들을 이용해 인코딩을 하며 매번 랜덤한 salt와 함께 인코딩을 합니다.
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
저는 보통 이런 식으로 사용하고 있습니다.
내부로 들어가보면 사용 방법은 비슷합니다.
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
private String getSalt() {
if (this.random != null) {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
}
return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
}
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
기본적인 PasswordEncoder로 돌아와서 사용 방법은 매우 간단합니다.
password를 저장하는 부분에서 간단하게 .
password = passwordEncoder.encode(password);
해주면 해당 password는 인코딩 되며 password를 담고있는 Entity 객체 또는 DTO 객체를
repository.save(객체); 해주면 DB에 인코딩된 password가 저장됩니다.
그리고
public boolean bad(SignupRequestDto requestDto, User user) {
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
return false;
}
return true;
}
과 같이 Dto로 받아온 정보와 현재 User 의 정보가 일치하는지 아닌지 확인하고
matches의 true / false 로 판별해 낼수 있습니다.
하지만 저는 보통 이러한 과정을 UserDetailsService 의 오버라이드 매소드인
loadUserByUsername(String username) 으로 처리합니다.