사실 비밀번호 암호화는 인증과 깊이 관련이 있다.
아래와 같은 사진으로 회원가입, 로그인 등 인증처리가 필요한 view가 있는데 이런 요청에서 비밀번호는 필수이고 db에 들어가기 때문에 사용자의 신용정보로서 암호화가 필수이다.
평문을 알아볼 수 없게 바꿔 놓음
=> 단방향, 양방향의 개념이 있다.
단방향
- hash 방식, 단뱡향은 복호화가 안된다는 뜻!양방향
- 복호화 가능 (대칭키, 공개키 방식)✅ 패스워드 암호화 요구조건
즉, hash && salt를 쓰는 이유를 살펴보면 된다!
: 똑같은 문자열의 패스워드라도 hash 값이 다르게 할 수 있기 때문이다!
현재는 Bcrypt 암호화 알고리즘을 많이 사용하는 추세이다.
: hash, 단방향이기 때문에!
암복호화에 사용하는 키가 동일! -> private 키
장점: 속도가 빠르다.
단점: 키가 탈취될 염려가 있어 인터넷이라는 공간안에서 쓰기 힘들다.
대칭키 알고리즘 : DES, AES 등
암복호화에 사용하는 키가 2개! -> private(개인) 키 & public(공개) 키
public키: 모든 사람이 접근 가능
private키: 각 사용자만이 가지고 있는 키
예를 들어, A가 B에게 데이터를 보낸다고 할 때, A는 B의 공개키로 암호화한 데이터를 보내고 B는 본인의 개인키로 해당 암호화된 데이터를 복호화해서 보기 때문에 암호화된 데이터는 B의 공개키에 대응되는 개인키를 갖고 있는 B만이 볼 수 있게 되는 것이다.
1) B 공개키/개인키 쌍 생성
2) 공개키 공개(등록), 개인키는 본인이 소유
3) A가 B의 공개키를 받아옴
4) A가 B의 공개키를 사용해 데이터를 암호화
5) 암호화된 데이터를 B에게 전송
6) B는 암호화된 데이터를 B의 개인키로 복호화 (개인키는 B만 가지고 있기 때문에 B만 볼 수 있음)
중간 공격자가 B의 공개키를 얻는다고 해도 B의 개인키로만 복호화가 가능하기 때문에 기밀성을 제공하며 개인키를 가지고있는 수신자만이 암호화된 데이터를 복호화할 수 있으므로 일종의 인증기능도 제공한다
설정
// crypto
implementation group: 'org.springframework.security', name: 'spring-security-crypto', version: '5.7.3'
implementation group: 'commons-logging', name: 'commons-logging', version: '1.2'
Utils
public class EncodePasswordUtils {
/* 순환참조 안될려면 이렇게 */
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Service
@Override
@Transactional
public void register(BoardVO board) {
validateEntity(board);
// 비밀번호 암호화 처리
String encodedPassword = passwordEncoder().encode(board.getPassword());
board.setPassword(encodedPassword);
long result = boardMapper.registerWithSelectKey(board);
log.info("register result: {}", result);
if (board.getAttachList() != null && board.getAttachList().size() != 0) {
board.getAttachList().forEach(attach -> {
attach.setBoardId(board.getId());
attachMapper.insert(attach);
});
boardMapper.registerFileYN(board.getId());
}
}
설정
// BCrypt
implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.3m'
1) 인터페이스 주입 방식
public interface Encryptor {
String encrypt(String origin);
boolean isMatch(String origin, String hashed);
}
public class BCryptEncryptor implements Encryptor {
@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;
}
}
}
2) static 메서드 주입방식 : 내가 자주 사용하는 방식
public class Encryptor {
public static String encrypt(String origin) {
return BCrypt.hashpw(origin, BCrypt.gensalt());
}
public static boolean isMatch(String origin, String hashed) {
try {
return BCrypt.checkpw(origin, hashed);
} catch (Exception e) { // 여러 예외가 있다.
return false;
}
}
}
User
...
public static User of(SignUpRequest signUpRequest) {
return User.builder()
.email(signUpRequest.getEmail())
.name(signUpRequest.getName())
.nickname(signUpRequest.getNickname())
// User 엔티티에 패스워드 암호화하면 테스트시 편해짐!
.password(encrypt(signUpRequest.getPassword()))
.build();
}
Service
@Override
@Transactional(readOnly = true)
public LoginDto getByEmailAndPassword(String email, String password) {
// 파라미터 password가 hash값이다.
User user = userRepository.findByEmail(email)
.map(u -> Encryptor.isMatch(u.getPassword(), password) ? u : null)
.orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER));
log.info("getByEmailAndPassword user : {}", user);
return LoginDto.mapToDto(user);
}