오늘은 뮤신사 프로젝트에서 신규 회원이 회원 가입을 할 수 있게 하는 API를 작성했습니다.
새 회원을 DB에 저장하려면 당연히 회원의 비밀번호도 함께 저장을 해야하는데요.
이 포스트에서는 회원의 비밀번호를 어떻게 안전하게 DB에 담을 수 있는지 그 방법을 구현 위주로 소개해드리고자 합니다.
물론 비밀번호는 DB에 저장되어야 하는 것이 맞지만, 그냥 쌩 비밀번호 문자열을 DB에 저장하는 것은 아주아주아주 위험합니다.
그냥 쌩 문자열 비밀번호를 DB에 저장하게 되면 관리자 또는 개발자는 회원의 비밀번호를 모두 볼 수 있게 되어 회원의 계정이 보호되지 않을 뿐더러 서버가 해킹 당해서 DB에 있는 값을 해커가 조회할 수 있게되면 모든 회원의 비밀번호가 그대로 털리게 되기 때문입니다.
따라서 그냥 저장하면 안되고 몇가지 과정을 거쳐야합니다.
바로 salt 문자열을 패스워드에 concat한 후 해시 함수을 이용해 해시 값으로 바꾸어 저장하는 것인데요.
아래에서 바로 그 구현에 대해 알아보겠습니다.
salt 문자열은 password를 해싱하기 전에 앞에 더해주는 임의의 문자열입니다.
꼭 salt를 더해주는 이유는, 레인보우 공격을 막기 위해서인데요.
레인보우 공격이란 특정 조건, 길이를 만족하는 거의 모든 문자열을 마구잡이로 해싱하여 저장해놓은 레인보우 테이블에 있는 해시값과 유저 DB에 저장된 비밀번호 해시값을 비교하여 비밀번호를 복호화(?)해버리는 공격방법입니다.
제가 해싱할때 사용한 SHA-256 알고리즘은 문자열이 조금만 바뀌어도 해시값이 완전히 바뀌어버리기 때문에, salt라는 임의의 문자열을 붙여서 해싱해주면 레인보우 공격을 막을 수 있게 됩니다.
salt 문자열은 나중에 유저가 로그인 할때 패스워드가 맞는지 확인하기 위해서 고정된 문자열이어야 합니다.
하지만 마치 API Key처럼 외부에 노출되면 안됩니다.
따라서 아래와 같이 properties 파일에 값을 저장해두었습니다.
application-jwt.properties
저 사진은 예시이고 지금은 다른 걸로 바꾸었습니다.ㅎㅎ
그리고 솔트 값을 써야하는 JwtService.java
에 다음과 같이 @Value
어노테이션으로 값을 넣어주었습니다.
이제 클라이언트에서 온 요청에서 Password 값을 가져와서 salt 값을 더해주고 해싱해주어야합니다.
저는 해당 과정을 getEncryptedPassword
메소드로 분리하였습니다.
/**
* @param password 유저 비밀번호
* @return salt+password를 SHA-256으로 암호화 한 값
*/
private String getEncryptedPassword(String password) {
String saltedPassword = passwordSalt + password;
String encryptedPassword = null;
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
byte[] bytes = saltedPassword.getBytes(StandardCharsets.UTF_8);
md.update(bytes);
encryptedPassword = Base64.getEncoder().encodeToString(md.digest());
} catch(NoSuchAlgorithmException e) {
throw new IllegalArgumentException();
}
return encryptedPassword;
}
MessageDigest
객체를 통해 SHA-256
해시 알고리즘으로 해싱해주었습니다.
참고로 아래는 SHA-256
의 간단한 설명에 대해 정리하였습니다.
SHA-256이란 SHA(Secure Hash Algorithm) 알고리즘의 한 종류로서 어떤 값을 입력하더라도 256 비트로의 고정된 결과값을 출력합니다. 또한 일반적으로 입력값이 조금만 변동하여도 출력값이 완전히 달라지기 때문에 출력값을 토대로 입력값을 유추하는 것은 거의 불가능합니다.
이름에 내포되어 있듯이 2^256만큼의 경우의 수를 만들 수 있습니다. 아주 아주 작은 확률로 같은 다른 문자열을 넣었을 때 같은 해시값이 출력되는 충돌이 발생하기도 하지만, 개인용 컴퓨터로 무차별 대입을 수행해 해시 충돌 사례를 찾으려고 할 때 많은 시간이 소요될 정도로 큰 숫자이므로 충돌로부터 비교적 안정하다고 평가됩니다.
이제 다 왔습니다.
DB에 회원 정보를 저장해주기만 하면 됩니다.
이 때 중요한 것은 getEncryptedPassword
메소드를 통해 비밀번호를 반드시 해싱하여 저장해주어야 한다는 것입니다.
// TODO: sign-in -> DB에 회원 정보를 저장
public void signIn(SignInRequestDto signInRequestDto) {
String encryptedPassword = getEncryptedPassword(signInRequestDto.getPassword());
log.info("암호화된 패스워드: " + getEncryptedPassword(signInRequestDto.getPassword()));
log.info("length: " + encryptedPassword.length());
Member member = new Member.Builder()
.mewsinsaId(signInRequestDto.getMewsinsaId())
.password(encryptedPassword)
.name(signInRequestDto.getName())
.nickname(signInRequestDto.getNickname())
.email(signInRequestDto.getEmail())
.phone(signInRequestDto.getPhone())
.profileImage(signInRequestDto.getProfileImage())
.tierId(signInRequestDto.getTierId())
.isAdmin(signInRequestDto.getAdmin())
.points(signInRequestDto.getPoints())
.build();
try {
memberRepository.addMember(member);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
로그 내용
나중에 회원이 로그인하기 위해 POST
요청 바디에 아이디, 패스워드를 넣어 보내면, 그 패스워드를 같은 과정으로 해싱해줍니다.
그리고 DB에 있는 패스워드 해시값와 일치하면 인증에 성공하도록 처리해주면 됩니다.
Error: 1406-22001: Data too long for column 'password' at row 1
저는 처음에 password 칼럼의 데이터가 너무 길다는 내용에 에러를받았습니다.
저 방식대로 해싱을 하면 문자열의 길이는 항상 44가 나오게 됩니다.
password
칼럼의 길이를 44 이상으로 바꾸어주면 됩니다.
MariaDB 기준 바꾸어주는 쿼리는 아래와 같습니다.
ALTER TABLE member MODIFY password VARCHAR(44) NOT NULL;