BCrypt는 비밀번호를 안전하게 저장하고 검증하기 위한 강력한 해시 함수입니다. 이 글에서는 Spring Boot와 BCrypt를 사용하여 회원가입과 로그인을 구현하는 방법에 대해 알아보겠습니다!
BCrypt는 Blowfish 암호화 알고리즘을 기반으로 한 해시 함수로, 주로 비밀번호를 안전하게 저장하고 검증하기 위해 사용됩니다. BCrypt는 여러 가지 이점을 가지고 있습니다
안전한 해시 함수: BCrypt는 강력한 해시 함수로, 해시된 비밀번호는 반복적인 해시 함수 적용과 솔트(salt)를 사용하여 보안을 강화합니다.
솔트 사용: BCrypt는 각각의 해시에 임의의 솔트 값을 추가하여, 동일한 비밀번호라도 다른 해시 값을 생성합니다. 이는 무차별 대입 공격(brute-force attack)을 방지하는 데 도움이 됩니다.
반복적 해싱: BCrypt는 내부적으로 비밀번호 해싱을 반복하여 수행합니다. 이는 고성능 하드웨어에 대한 저항력을 높이며, 비밀번호 추측 공격을 어렵게 만듭니다.
업데이트 가능한 솔트 및 비용 매개 변수: BCrypt는 해시 함수의 복잡성을 쉽게 조정할 수 있는 매개 변수를 제공합니다. 비용 매개 변수(cost parameter)를 조절하여 계산 시간을 증가시킬 수 있어, 공격자가 비밀번호를 추측하기 어렵게 만듭니다.
String password = "mypassword";
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
System.out.println("BCrypt 해시된 비밀번호: " + hashedPassword);
위 예제에서 BCrypt.hashpw
메서드는 주어진 비밀번호를 BCrypt 해시로 변환하고, BCrypt.gensalt()
는 랜덤 솔트 값을 생성하여 비밀번호에 추가합니다.
String inputPassword = "mypassword";
String hashedPasswordFromDatabase = "$2a$10$TrgEuF0ifvyzQQ6FZ3xtOuX1Q.k7Bif5S9T4s8p7VUpkj0fj2m9l6"; // 예시 해시된 비밀번호
if (BCrypt.checkpw(inputPassword, hashedPasswordFromDatabase)) {
System.out.println("비밀번호 일치");
} else {
System.out.println("비밀번호 불일치");
}
위 예제에서 BCrypt.checkpw
메서드는 사용자가 입력한 비밀번호(inputPassword
)와 데이터베이스에서 가져온 해시된 비밀번호(hashedPasswordFromDatabase
)를 비교하여 일치 여부를 판단합니다.
BCrypt는 비밀번호 보안을 강화하기 위한 강력하고 안전한 방법으로, 해시 함수의 솔트와 반복 적용을 통해 보안성을 높이며, 무차별 대입 공격에 대한 저항력을 강화합니다. Java에서는 org.mindrot.jbcrypt.BCrypt
클래스를 사용하여 BCrypt를 쉽게 적용할 수 있습니다!
그러면 이제부터 BCrypt를 사용하여 회원가입과 로그인을 구현하는 방법에 대해 알아보겠습니다!
@Component
public class BCryptEncryptor implements Encryptor {
// 원본 비밀번호를 BCrypt 해시로 암호화하는 메서드
@Override
public String encrypt(String origin) {
return BCrypt.hashpw(origin, BCrypt.gensalt());
}
// 원본 비밀번호와 해시된 비밀번호가 일치하는지 확인하는 메서드
@Override
public boolean isMatch(String origin, String hashed) {
try {
return BCrypt.checkpw(origin, hashed);
} catch (Exception e) {
return false;
}
}
}
BCryptEncryptor
클래스는 Encryptor
인터페이스를 구현하여 BCrypt를 사용한 비밀번호 암호화 기능을 제공합니다.encrypt
메서드는 origin
이라는 원본 비밀번호를 받아 BCrypt 해시로 암호화합니다. BCrypt.hashpw(origin, BCrypt.gensalt())
를 사용하여 해시값을 생성합니다.isMatch
메서드는 origin
과 hashed
라는 두 개의 인자를 받아, 원본 비밀번호와 BCrypt 해시값이 일치하는지 검사합니다. BCrypt.checkpw(origin, hashed)
를 사용하여 일치 여부를 확인하며, 예외가 발생할 경우 false
를 반환합니다.@Service
public class UserService {
private final Encryptor encryptor;
private final UserRepository userRepository;
private final HttpSession session;
@Autowired
public UserService(Encryptor encryptor, UserRepository userRepository, HttpSession session) {
this.encryptor = encryptor;
this.userRepository = userRepository;
this.session = session;
}
@Transactional
public User createUser(User user) {
// 이메일 중복 확인
userRepository.findByEmail(user.getEmail())
.ifPresent(u -> {
throw new RuntimeException("User with the same email already exists");
});
// 유저 아이디 중복 확인
userRepository.findByUserId(user.getUserId())
.ifPresent(u -> {
throw new DuplicateKeyException("User with the same user ID already exists");
});
// 비밀번호를 BCrypt를 이용하여 암호화
user.setPassword(encryptor.encrypt(user.getPassword()));
// 데이터베이스에 저장
return userRepository.save(user);
}
}
UserService
클래스는 회원가입 관련 비즈니스 로직을 담당합니다.UserService(...)
는 Encryptor
, UserRepository
, HttpSession
등을 주입받습니다.createUser
메서드는 User
객체를 받아서 다음을 수행합니다:encryptor.encrypt(user.getPassword())
를 사용하여 사용자의 비밀번호를 BCrypt를 이용하여 안전하게 암호화합니다.userRepository.save(user)
를 호출하여 데이터베이스에 사용자 정보를 저장합니다.@Service
@Slf4j
@RequiredArgsConstructor
public class LoginService {
private final static String LOGIN_SESSION_KEY = "USER_ID";
private final UserService userService;
@Transactional
public void signUp(SignUpReq signUpReq, HttpSession session) {
try {
User user = new User(
signUpReq.getUserId(),
signUpReq.getName(),
signUpReq.getEmail(),
signUpReq.getGrade(),
signUpReq.getPassword()
);
userService.createUser(user);
session.setAttribute(LOGIN_SESSION_KEY, user.getId());
} catch (Exception e) {
log.error("Error during user sign-up", e);
throw new RuntimeException("Error during user sign-up", e);
}
}
public ResponseEntity<LoginResponse> login(LoginReq loginReq, HttpSession session) {
Optional<User> user = userService.findUserByUserIdAndPassword(loginReq.getUserId(), loginReq.getPassword());
if (user.isPresent()) {
session.setAttribute(LOGIN_SESSION_KEY, user.get().getId());
UserDetailsDTO userDetails = new UserDetailsDTO(user.get().getName(), user.get().getGrade());
LoginResponse response = new LoginResponse("로그인 되었습니다.", userDetails);
return ResponseEntity.ok(response);
} else {
LoginResponse response = new LoginResponse("로그인 되지 않았습니다.");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
}
public void logout(HttpSession session) {
session.removeAttribute(LOGIN_SESSION_KEY);
}
}
LoginService
클래스는 로그인 및 회원가입 관련 비즈니스 로직을 담당합니다.LoginService(...)
는 UserService
를 주입받습니다.signUp
메서드는 SignUpReq
객체를 받아서 다음을 수행합니다:User
객체를 생성하고, userService.createUser(user)
를 호출하여 회원가입을 처리합니다. 성공적으로 회원가입이 완료되면 세션에 사용자 아이디를 저장합니다.login
메서드는 LoginReq
객체를 받아서 다음을 수행합니다:userService.findUserByUserIdAndPassword(loginReq.getUserId(), loginReq.getPassword())
를 호출하여 사용자를 인증합니다.UserDetailsDTO
객체를 생성하여 로그인 응답을 반환합니다.UNAUTHORIZED
상태 코드와 함께 로그인 실패 메시지를 반환합니다.logout
메서드는 세션에서 LOGIN_SESSION_KEY
에 해당하는 사용자 정보를 삭제하여 로그아웃을 처리합니다.먼저 postman을 이용해서 회원가입을 해보겠습니다!
회원가입 성공 후 DB값을 보니 비밀번호 암호화가 잘 된 것을 확인할 수 있습니다.
그 후 다시 로그인을 해보면 로그인이 잘 되는 것을 확인할 수 있습니다!
Encryptor 인터페이스는 사용자 정의 인터페이스인가요?