BCryptPasswordEncoder.encode를 이용해서 로그인 로직을 구현했을 때 로그인이 실패하는 이슈 발생
오류 발생
멤버 테이블에 사용자의 비밀번호는 암호화된 상태로 저장되어 있다. 로그인 프로세스에서 사용자가 입력한 이메일에 해당하는 멤버를 찾아, 그 멤버의 비밀번호와 사용자가 입력한 비밀번호를 암호화한 값이 일치하는지를 비교한다. 이 과정에서 사용된 암호화 클래스는 BCryptPasswordEncoder이다.
하지만, 사용자가 정확한 비밀번호를 입력해도 로그인이 실패하는 문제가 발생했다. 이는 로그인 로직의 비밀번호 확인 방식에 오류가 있었기 때문이다.
아래는 기존 코드다.
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(loginDomain.getPassword());
boolean isValidPw = member.verifyPassword(encode);
/**
* 사용자 비밀번호 확인 메소드
* @param inputPw 사용자가 입력한 비밀번호를 암호화 한 데이터
* @return true/false
*/
public boolean verifyPassword(String inputPw) {
return ObjectUtils.nullSafeEquals(inputPw, this.password);
}
문제의 원인
문제의 핵심은 BCryptPasswordEncoder가 비밀번호를 암호화할 때 마다 다른 'salt' 값을 사용한다는 데 있다. 'salt'는 암호화 과정에 추가되는 무작위 데이터로, 같은 비밀번호라도 매번 다른 암호화 결과를 생성한다. 이 방식은 보안을 강화하지만, encode 메소드로 사용자의 비밀번호를 암호화한 후, 이를 데이터베이스에 저장된 암호화된 비밀번호와 직접 비교하는 것은 적절하지 않다.
그 결과, 로그인 프로세스가 항상 실패로 이어졌다.
해결 방법
이 문제의 해결책은 BCryptPasswordEncoder의 matches 메소드를 활용하는 것이다. 이 메소드는 사용자가 입력한 원본 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호를 올바르게 비교할 수 있도록 설계되었다.
matches 메소드는 원본 비밀번호를 암호화하면서 데이터베이스에 저장된 암호화된 비밀번호에 포함된 'salt' 값을 사용한다. 이를 통해 동일한 비밀번호가 입력되었을 때만 true를 반환하며, 로그인 프로세스에서 정확한 비밀번호 검증이 가능하게 된다.
boolean isValidPw = passwordEncoder.matches(loginDomain.getPassword(), member.getPassword());
이 코드는 로그인 로직을 올바르게 처리할 수 있게 해 주며, 앞서 발생했던 로그인 실패 문제를 해결한다. matches 메소드의 사용으로, 사용자가 입력한 원본 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호가 일치하는지 정확하게 확인할 수 있게 되었다. 이는 보안적인 측면에서도 안전하며, 사용자의 올바른 비밀번호 입력 시 로그인이 정상적으로 이루어지도록 보장한다.
encode, matches 코드 설명
다음 코드는 BCryptPasswordEncoder의 동작 방식을 이해하기 위해 작성되었다. 이 코드는 두 가지 주요 기능을 시연한다:
암호화 과정의 변동성
사용자가 입력한 비밀번호 '1234'를 BCryptPasswordEncoder를 사용해 여러 번 암호화한다. 이 과정에서 각 암호화 작업마다 다른 결과가 생성되는 것을 볼 수 있다. 이는 BCryptPasswordEncoder가 각 비밀번호 암호화 시 매번 다른 'salt' 값을 사용하기 때문이다. 이로 인해 동일한 원본 데이터라도 매번 다른 암호화 결과가 나온다.
matches 메소드를 통한 정확한 비교
matches 메소드는 사용자가 입력한 원본 비밀번호와 데이터베이스에 이미 저장된 암호화된 비밀번호를 비교한다. 이 예시에서는 '1234'라는 사용자 입력값과 이에 해당하는 데이터베이스에 저장된 암호화된 비밀번호를 비교한다. matches 메소드는 암호화된 비밀번호에 포함된 'salt' 값을 사용하여 입력된 원본 비밀번호를 암호화하고, 이를 저장된 암호화된 비밀번호와 비교하여 일치 여부를 판단한다. 이 경우, 두 값이 일치하므로 true를 반환한다.
💡 다시 한번 말해보자면,
passwordEncoder.matches() 메서드는 두 개의 매개변수를 받는다. 첫 번째 매개변수는 사용자가 입력한 원본 비밀번호이고, 두 번째 매개변수는 데이터베이스에 저장된 암호화된 비밀번호이다.
이 메서드는 사용자가 입력한 비밀번호를 내부적으로 같은 해시 함수와 솔트를 사용하여 암호화한다. 그리고 이렇게 암호화된 비밀번호를 데이터베이스에 저장된 암호화된 비밀번호와 비교한다.
따라서, 실제 비교는 "1234"와 "1234"를 직접 비교하는 것이 아니라, 사용자가 입력한 "1234"를 암호화하여 생성된 해시 값과, 데이터베이스에 저장된 암호화된 비밀번호의 해시 값을 비교하는 것이다.
이 코드는 BCryptPasswordEncoder의 암호화 과정이 매번 다른 결과를 생성하더라도, matches 메소드를 사용하면 올바른 원본 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호가 일치하는지 정확하게 검증할 수 있음을 보여준다. 이는 로그인 로직에서 정확하고 안전한 비밀번호 검증을 가능하게 한다.
public class MainTwo {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String userInput = "1234";
String storedPassword = "$2a$10$SSktcC5m.iTGjE3CfoMtIeKaGGpOJ6tGGIMgpYgJS3DdlTef1Z00m";
for (int i = 0; i < 3; i++) {
String encodedPassword = passwordEncoder.encode(userInput);
System.out.println("Encoded password " + (i + 1) + ": " + encodedPassword);
}
boolean isPasswordMatch = passwordEncoder.matches(userInput, storedPassword);
System.out.println("Password match: " + isPasswordMatch);
}
}
출력값
🔻 XSS 공격에 대해서 궁금하다면? 🔻
XSS 공격 방어: 입력 데이터 이스케이프, CSP, HTTPOnly 설정
이 포스트는 Team chillwave에서 사이드 프로젝트를 하던 중 적용했던 부분을 다시 공부하면서 기록한 것입니다.
시간이 괜찮다면 팀원 '개발자의 서랍'님의 블로그도 한번 봐주세요 :)
'Spring > Spring 트러블 슈팅' 카테고리의 다른 글
QueryDSL에서 Projections.constructor 사용해서 SQL 함수 사용하기 (0) | 2024.02.01 |
---|---|
스칼라 서브쿼리(Scalar Subquery)에서 Limit절 오류 해결 (0) | 2024.01.25 |
QueryDSL에서 NPE 해결하기 - 서브쿼리와 외부 조인의 활용 (2) | 2024.01.16 |
단위테스트에서 Static 메소드 Mock 주입 문제 해결 방법 (5) | 2023.12.25 |
스프링 시큐리티: 커스텀 에러 처리 실패 원인 분석 및 해결 방법 (0) | 2023.12.23 |