개요
안녕하세요 빈코입니다. 오늘은 Java 프로젝트에서 회원가입 및 로그인 시 중요한 정보인 비밀번호를 해시화하는 방법에 대해 자세히 알아보려고 합니다. 만약 사용자가 회원가입 및 로그인 시 비밀번호를 입력했을 때, 사용자가 입력한 텍스트 그대로 서버로 통신하면 정보가 유출될 가능성이 높습니다. 그렇기 때문에, 해당 기능 작업 시에 중요한 정보들은 꼭 Hash 화해서 서버로 통신하는데 대표적인 방법으로 'SHA-256' 방법을 많이 사용하곤 합니다. 좀 더 자세한 내용은 하단에서 이어가 볼게요😁
회원가입 및 로그인📙
로그인 시 비밀번호 검증을 위해 보통 사용자가 입력한 비밀번호를 해시를 떠서 사용하는 것이 일반적입니다. 보안상 암호화된 비밀번호를 데이터베이스에서 꺼내와 복호화하여 비교하는 대신, 입력한 비밀번호를 해시화하여 저장된 해시와 비교하는 것이 좋습니다.
조금 더 쉽게 풀어서 설명하자면, 한 사용자가 회원가입을 할 때 입력한 비밀번호를 SHA-256를 이용하여 해시화하고, 이 해시 값을 데이터베이스에 저장합니다. 그리고 해당 사용자가 로그인할 때 입력한 비밀번호를 다시 SHA-256으로 해시화하고, 데이터베이스에서 꺼내온 해당 사용자의 비밀번호 해시값을 비교하는 방식으로 진행됩니다.
여기서 특정 텍스트의 해시 값은 항상 똑같기 때문에 보안을 강화하기 위해 salt라는 무작위 수도 함께 붙여서 저장합니다.
결론적으로는, 입력한 비밀번호의 해시와 salt를 데이터베이스에 각각 저장하고 추후 로그인 시 입력한 비밀번호와 데이터베이스에서 가져온 salt 값을 이용하여 해시화한 것이 미리 저장된 데이터베이스의 해시 값과 동일하다면 같은 비밀번호이기 때문에 로그인이 가능하게 됩니다😃
아래 코드 예시는 해시화 하는 함수와 무작위 수 salt를 생성하는 Util 클래스인 PasswordUtil, 회원가입을 진행하는 registerUser() 함수, 로그인을 진행하는 loginUser() 순서대로 진행됩니다.
PasswordUtil 생성📘
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class PasswordUtil {
// SHA-256 해시 생성
public static String hashPassword(String password, String salt) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt.getBytes()); // 솔트 추가
byte[] hashedPassword = md.digest(password.getBytes());
return bytesToHex(hashedPassword);
}
// 랜덤한 솔트 생성
public static String generateSalt() {
SecureRandom sr = new SecureRandom();
byte[] salt = new byte[16];
sr.nextBytes(salt);
return bytesToHex(salt);
}
// 바이트 배열을 16진수 문자열로 변환
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
hashPassword() 메서드는 파라미터로 넘겨받은 비밀번호와 salt를 사용하여 SHA-256 알고리즘으로 해시 값을 생성합니다. 첫 번째로, SHA-256 알고리즘을 사용하기 위해 MessageDigest 객체를 생성하고, 이후에 전달받은 salt 값을 바이트 배열로 변환 후에 해시 계산에 포함시킵니다.
md.digest(password.getBytes())로 비밀번호를 바이트 배열로 변환해 해시 연산을 수행하고, 그 결과를 hashedPassword에 저장합니다. 마지막으로 bytesToHex(hashedPassword)를 사용하여 해시 결과를 16진수 문자열로 변환해 반환합니다.
generateSalt() 메서드는 무작위 salt를 생성하는 메서드입니다. SecureRandom을 이용하여 보안이 강화된 난수 생성기를 초기화한 후에, salt를 저장할 바이트 배열을 생성합니다. 이후에 sr.nextBytes(salt)를 사용하여 배열을 무작위 바이트로 채운 후 해당 바이트 배열을 16진수 문자열로 변환하기 위해 bytesToHex(salt) 메서드를 사용합니다.
마지막으로 bytesToHex() 메서드는 바이트 배열을 16진수 문자열로 반환하는 메서드입니다. 결과를 누적할 문자열 빌더인 StringBuilder를 생성 후에 반복문을 통해 각 바이트를 접근하고, 각 바이트를 2자리 16진수로 변환하여 sb에 추가합니다. 모두 추가가 되었다면 해당 문자열을 반환합니다.
회원가입 진행📒
public void registerUser(String username, String password) throws NoSuchAlgorithmException {
String salt = PasswordUtil.generateSalt();
String hashedPassword = PasswordUtil.hashPassword(password, salt);
// DB Query 예시 username, hashedPassword, salt 저장
String sql = "INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)";
}
회원가입은 간단합니다. 사용자가 회원가입 폼을 이용하여 registerUser() 메서드에 username과 password를 넘겨준다면, 첫 번째로 이전에 만든 PasswordUtil 클래스의 generateSalt() 메서드를 이용하여 무작위 salt를 생성하고, hashPassword() 메서드에 비밀번호인 password와 무작위 값 salt를 넘겨주면 해당 사용자의 비밀번호 해시가 생성됩니다.
이후에, 쿼리를 이용하여 데이터베이스에 해시와 salt 값을 insert 합니다.
로그인 진행📗
public boolean loginUser(String username, String inputPassword) throws NoSuchAlgorithmException {
// DB에서 username에 해당하는 password_hash와 salt 가져오기
String sql = "SELECT password_hash, salt FROM users WHERE username = ?";
String storedHash = ""; // DB에서 가져온 비밀번호 해시
String storedSalt = ""; // DB에서 가져온 솔트 값
// 입력 비밀번호를 저장된 솔트로 해시화
String hashedInputPassword = PasswordUtil.hashPassword(inputPassword, storedSalt);
// 저장된 해시와 입력된 비밀번호의 해시 비교
return storedHash.equals(hashedInputPassword);
}
로그인은 개요에서 설명한 바와 같이 사용자가 로그인 폼을 이용하여 username과 password를 입력한다면(여기선 inputPassword), 사용자 명을 이용하여 DB에서 사용자 명이 일치하는 로우의 해시와 salt값을 가져옵니다.
이후에 사용자가 입력한 비밀번호인 inputPassword와 미리 DB에 저장된 해당 로우의 salt 값을 이용하여 해시화를 진행합니다. 마지막으로, DB에 저장된 해시와 입력된 비밀번호의 해시를 비교한 값을 return 합니다.
만약, 해시 값이 동일하다면 로그인을 성공한 것이고 그렇지 않다면 실패한 것이겠죠?
마치며
지금까지 회원가입 및 로그인 시 비밀번호를 해시값으로 비교하는 방법에 대해 알아보았습니다. 생각보다 간단하지만, 해당 로직이 없을 경우에 사용자들의 비밀번호가 노출되기 쉽기 때문에 프로젝트 진행 시에는 꼭 적용해야 할 것 같네요😊
'TIL' 카테고리의 다른 글
PostgerSQL Upsert 쿼리 개념 및 대용량 속도 차이 예제 (0) | 2024.11.23 |
---|---|
Java 파일 업로드 구현하기 및 Progress Bar 설정 (3) | 2024.11.16 |
[JS] find,some,filter,map 등의 고차 함수 활용하기 (0) | 2024.10.11 |
js Shallow Copy(얕은 복사)와 Deep Copy(깊은 복사)의 차이 (0) | 2024.10.07 |
Java IP 유효성 검사 및 여러 유용한 함수 모음 (1) | 2024.09.24 |