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) 으로 처리합니다.