지난 숙련 주차에서 비밀번호 암호화에 대해서 알게 되었고, 직접 과제 코드에 적용해 보았다.
추가로 과제 발제 때 튜터님께서 말씀하셨던 것처럼 비밀번호 복호화 는 필요없는 이유 에 대해서도 생각해보았다.
: 어떠한 평문을 알아볼 수 없도록 암호문으로 바꾸는 것을 말한다.
만약 데이터가 유출이 되더라도! 비밀번호나 혹은 중요한 정보가 담겨있는 어떠한 것들을 해석하지 못하는 데에 의의가 있다.
➡️ 따라서 무조건 암호화하여 저장한다.
: 단방향 암호화는 주로 비밀번호 저장과 같은 경우에 사용되어, 원본 데이터를 복원할 수 없는 방식으로 변환된다.
해시 함수를 사용
: 임의의 길이를 가진 데이터를 입력받아 고정된 길이의 값, 즉 해시값을 출력하는 함수이다.
: 해시값은 입력 데이터로부터 유도되기 때문에 동일한 입력은 항상 동일한 해시값을 갖게 된다.
대표적인 해시 함수로는 SHA-256, bcrypt, scrypt, PBKDF2 등이 존재한다.
역상 저항성 (preimage resistance)
: 어떤 해시 함수가 특정한 값을 출력하는 입력값을 찾기 어려움을 의미한다.
-> 해시값에서 원본 데이터로의 역변환이 얼마나 어려운 지에 대한 척도
: 양방향 암호화는 말 그대로 복원할 수 있는 암호화이다.
대칭 키 암호화
: 동일한 키가 데이터를 암호화하고 해독하는 데 사용된다.
: 대표적인 대칭 키 알고리즘으로는 AES (Advanced Encryption Standard) 가 존재한다.
주로 데이터를 안전하게 전송하거나 저장하기 위해 사용된다.
-> 예를 들어, 데이터를 HTTPS를 통해 전송하는 경우에 양방향 암호화가 필요!
위에서 말했듯이, 단방향 암호화에서는 해시 함수를 사용한다.
그래서 같은 비밀번호가 입력된다면??? 똑같이 암호화가 된다는 것..
하지만 Bcrypt는 같은 비밀번호를 암호화하더라는 해시 값이 매번 다르게 도출되는 방식을 사용한다.
: 실제 비밀번호 이외에 추가적으로 랜덤한 데이터 값을 더해 해시 값을 계산하는 방법이다.
➡️ 비밀번호의 복잡도를 키워서 보안이 높아진다!
사용자 비밀번호 -> 솔트 값 생성 -> 해싱
비밀번호 길이도 더욱 더 길어지고, 따라서 크래킹하는데 시간이 늘어나기 때문에 해킹이 어려워진다.
그렇다면... 비밀번호가 확인 로직에서도 다른 솔트 값을 부여하면 어떻게 비교할까???
: Spring Security 프레임워크에서 제공하는 클래스이다.
-> 사용자가 제출된 비밀번호와 암호화되어 저장된 비밀번호의 일치 여부를 확인하는 메서드가 제공된다.
👏🏻👏🏻👏🏻 정말 똑똑하다..
또한, BcryptPasswordEncoder는 Bcrypt의 로그 라운드라고 하는 강도(strength)를 설정할 수 있는데, 강도가 클 수록 암호를 해시하기 위해 더 많은 작업을 수행해야한다.
: 기본값 = 10 / 4~31 사이의 값을 설정할 수 있음
그럼, 제공하는 메서드 3가지를 살펴보자
: 비밀번호를 암호화해주는 메서드이다.
해싱 과정에서 무작위로 생성한 salt가 포함되어, 같은 비밀번호를 인코딩해도 매번 다른 결과값이 반환된다.
: 제출된 인코딩하지 않은 비밀번호와 저장소에 있는 인코딩된 비밀번호가 일치하는지 확인하는 메서드이다.
-> 일치하면 true, 일치하지 않으면 false 반환
저장된 비밀번호 자체를 디코딩하는 것은 아니다!!!
: 더 나은 보안을 위하여 인코딩된 비밀번호를 다시 인코딩해야 하는 경우 true 반환, 그렇지 않으면 false를 반환하는 메서드이다.
-> 디폴트값으로 항상 false를 반환하므로, 필요할 때 오버라이딩하여 인코딩된 비밀번호의 안정성을 체크하는 로직을 구현할 수 있다.
1. build.gradle 에 의존성 추가
implementation 'at.favre.lib:bcrypt:0.10.2'
2. config 패키지에 아래의 클래스 추가
import at.favre.lib.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
앞서 말했던 encode 메서드와 matches 메서드를 사용하였다.
3. authService 에 적용
@Transactional
public UserResponseDto signup(String email, String username, String password) {
userRepository.findByEmail(email).ifPresent(user -> {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 존재하는 이메일입니다.");
});
// 인코딩 적용!!
String encodedPassword = passwordEncoder.encode(password);
User user = new User(email, username, encodedPassword);
User createdUser = userRepository.save(user);
return new UserResponseDto(createdUser.getUserId(), createdUser.getUsername());
}
public LoginResponseDto login(LoginRequestDto requestDto) {
User findUser = userRepository.findUserByEmailOrElseThrow(requestDto.getEmail());
// matches 함수를 통해 비밀번호 비교하여 로그인!!!
if(!passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())){
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}
return new LoginResponseDto(findUser.getUserId());
}
튜터님께서 말씀하셨던 비밀번호 복호화의 필요여부에 대한 내용이다.
우선 우리는 스프링에서 제공하는 Bcrypt를 사용하기 때문에, 애초에 복호화 과정이 불가능하다. 😅
과제에서는 단순한 회원가입/로그인 로직만 구현하였기 때문에 복호화 과정이 필요가 없다.
-> 비밀번호 찾기 를 구현하고 싶다면?
: 사실 우리가 사용하는 웹사이트들을 생각해보면, 비밀번호 찾기를 한다고 비밀번호를 다시 알려주진 않는다...
다시 알려주는 과정이 있다면 애초에 비밀번호 암호화가 소용 없는 게 아닐까???
-> 사용자 인증 (이메일 혹은 아이디를 통한 인증 코드 전송 or 비밀번호 재설정 링크 전송) 과정만 구현한다면, 비밀번호는 언제든 새로 설정하면 되기 때문이다!!!!!