프로젝트/아카이뷰
[Spring boot] 유저 인증 처리
cks._.hong
2024. 1. 15. 20:37
유저 인증 처리 왜 궁금했을까❓
SSAFY 1학기에 Share Your Trip에서 유저를 인증하기 위해 이메일 인증 방식을 사용했다. 하지만, 악의적인 사용자가 비밀번호 변경 API를 알아낸 뒤 다른 사용자의 비밀번호를 변경하면 취약점이 발생할 것이라 생각이 들었다. 이를 해결하기 위해, 유저 인증 처리에 대해서 학습해보려고 한다.
기존 이메일 인증 로직
- 클라이언트가 비밀번호 변경을 위해 이메일 인증 요청
- 서버는 인증번호를 생성하고 해당 클라이언트의 이메일로 메일 전송
- 메일 전송 완료 후, 서버는 생성된 인증번호를 클라이언트에게 전송
- 클라이언트는 자체 저장소(Redux, Recoil 등)에 저장하고 사용자가 입력한 값을 검증
- 일치한다면 패스워드 변경 진행
// 아이디, 패스워드 찾기용 이메일 인증 요청
@GetMapping("/find-email")
public ResponseEntity<Object> findMailSend(@RequestParam("email") String email) {
int auth_number = mailService.findSendMail(email);
return SuccessResponse.createSuccess(SuccessCode.EMAIL_SUCCESS, auth_number);
}
// 인증번호 생성
public static int createNumber(){
return (int)(Math.random() * (90000)) + 100000;
}
// 이메일 전송
public int sendEmail(String mail) {
int authNumber = createNumber();
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, mail);
message.setSubject("이메일 인증");
String body = "";
body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
body += "<h1>" + authNumber + "</h1>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
e.printStackTrace();
}
javaMailSender.send(message);
return authNumber;
}
// 이메일 검증 및 이메일 전송 요청 함수 호출
public int findSendMail(String email){
repository.findByEmail(email).orElseThrow(
() -> new RestApiException(ErrorCode.USER_NOT_FOUND));
return sendEmail(email);
}
인증 로직의 문제점
- 악의적인 사용자가 다른 사용자의 이메일로 인증 이메일 전송 요청을 보내면 인증번호가 응답으로 전송받게 될 것이다.
- 이 과정에서 패킷 트레이서와 같은 스니핑 툴을 통해 인증번호를 획득하고 검증한다면 보안에 문제가 생길 것이다.
- 또한, 개발자 모드에서 Network 탭에 Payload를 통해서 인증번호를 획득할 수 있다는 문제점이 존재할 것이다.
해당 문제점은 서버에서 클라이언트에게 인증번호를 전송하지 않고 보관하고 있다가 클라이언트의 검증 요청을 처리하는 방법을 통해서 해결할 수 있을 것이다.
변경된 이메일 인증 로직
- 클라이언트가 비밀번호 변경을 위해 이메일 인증 요청
- 서버는 인증번호를 생성하고 해당 클라이언트의 이메일로 메일 전송
- 메일 전송 완료 후, 서버는 생성된 인증번호를 3분 Timeout을 걸어 Redis에 저장
- 클라이언트는 이메일을 통해 받은 인증번호를 작성한 후 검증 API 전송
- 서버는 Redis에서 클라이언트의 이메일을 이용해서 Redis에 저장된 인증번호 조회 및 검증 수행
- 클라이언트에 결과 전송
// 인증번호 생성
public static int createNumber(){
return (int)(Math.random() * (90000)) + 100000;
}
// 이메일 전송
public int sendEmail(String mail) {
int authNumber = createNumber();
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, mail);
message.setSubject("이메일 인증");
String body = "";
body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
body += "<h1>" + authNumber + "</h1>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
e.printStackTrace();
}
javaMailSender.send(message);
// redis 인증번호 저장
redisTemplate.opsForValue().set(
mail,
String.valueOf(authNumber),
180,
TimeUnit.SECONDS
);
return authNumber;
}
// 이메일 검증 및 이메일 전송 요청 함수 호출
public int findSendMail(String email){
repository.findByEmail(email).orElseThrow(
() -> new RestApiException(ErrorCode.USER_NOT_FOUND));
return sendEmail(email);
}
// controller 코드
@PostMapping("/check-auth")
public ResponseEntity<Object> checkAuth(@RequestBody MailDto.authRequestDto dto) {
mailService.checkAuth(dto);
return SuccessResponse.createSuccess(SuccessCode.AUTH_SUCCESS);
}
// Service 코드
public boolean checkAuth(MailDto.authRequestDto dto) {
String number = redisTemplate.opsForValue().get(dto.getEmail());
if(StringUtils.hasText(number) && number.equals(dto.getNumber())) {
return true;
}
throw new RestApiException(ErrorCode.INVALID_NUMBER);
}