1. 회원가입에 관한 기능 구현
- 이메일 인증이 완료가 되었을 경우에는 클라이언트의 모든 정보를 정상적으로 DB에 저장해야 한다.
@Transactional
public Enum<? extends IResult> register(UserEntity user, EmailAuthEntity emailAuth) {
EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(
emailAuth.getEmail(),
emailAuth.getCode(),
emailAuth.getSalt());
// email 해싱에서는 numeric (6)이 들어간 메서드를 사용하지 않는다.
user.setPassword(CryptoUtils.hashSha512(user.getPassword()));
if (existingEmailAuth == null || !existingEmailAuth.getIsExpired()) {
return RegisterResult.EMAIL_NOT_VERIFIED;
}
if (this.memberMapper.insertUser(user) == 0) {
return CommonResult.FAILURE;
}
// insertUser(user)에서 이미 insert작업은 실행이된다. if문보다 아래에 박혀있을 경우 insert작업이 실행이 되고 1이므로 DB에는 이미 값이 박힌다.
// 그리고 나서 그 아래에서 암호화가 진행되고 SUCCESS는 반환된다.
return CommonResult.SUCCESS;
}
- 위 로직은 Register에 관한 Service 로직이다.
- EmailAuthEntity타입의 새로운 객체를 생성하여 이메일 인증이 완료된 email, code, salt 3가지의 정보를 existingEmailAuth에 담는다.
- 이때 user가 입력하는 password는 암호화 처리가 되어진 password로 DB에 저장되어야 한다.(setPassword)를 통해 user의 password를 암호화한 비밀번호로 업데이트 한다는 뜻이다.
- existingEmailAuth의 값이 null이거나 만료시간(getIsExpired)이 false라는 뜻은 이메일 인증이 정상적으로 이루어지지 않았다는 것을 나타내기 때문에 이메일 인증이 완료되지 않았다는 Result값을 반환해준다.
Service 로직 작성중 중요한 사실 한가지가 있다.
- inserUser메서드가 사용되는 시점보다 암호화 처리를 하는 로직이 위에 있어야 정상적으로 실행이 된다.
- 기본적으로 DB에 insert가 되는 시점보다 비밀번호가 암호화가 되고난 후 그 암호화된 값이 DB에 insert가 되어야 한다.
2. 회원가입에 관한 XML 및 DB에 insert하기
- Service에서 사용한 insertUser메서드이다.
- int타입으로 지정한 이유는 Service에서 insert된 값이 0인지 1인지를 구분하기 위함 즉, 레코드수로 insert가 정상적으로 이루어 졌는지 확인하기 위해 사용되어졌다.
- 매개변수로는 UserEntity의 모든 값을 매개변수로 받는다(클라이언트의 모든 정보가 insert되어야 하기 때문)
정상적인 이메일 인증이 완료되고 모든 로직에서 오류가 없다면!
DB에 모든 값이 정상적으로 들어간 것을 확인 할 수 있다.(특히 비밀번호는 암호화 되어 들어간 것이 Point)
3. 비밀번호 재설정 관련 기능 구현
@RequestMapping(value = "recoverPasswordEmail", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String postRecoverPasswordEmail(EmailAuthEntity emailAuth) {
Enum<?> result = this.memberService.recoverPasswordCheck(emailAuth);
JSONObject responseObject = new JSONObject();
responseObject.put("result", result.name().toLowerCase());
if (result == CommonResult.SUCCESS) {
responseObject.put("code", emailAuth.getCode());
responseObject.put("salt", emailAuth.getSalt());
}
return responseObject.toString();
}
@RequestMapping(value = "recoverPasswordEmail", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
@ResponseBody
public ModelAndView getRecoverPasswordEmail(EmailAuthEntity emailAuth) {
Enum<?> result = this.memberService.recoverPasswordAuth(emailAuth);
ModelAndView modelAndView = new ModelAndView("member/recoverPasswordEmail");
modelAndView.addObject("result", result.name());
return modelAndView;
}
- 비밀번호를 재설정 해주기 위해서는 우선 클라이언트의 이메일이 인증이 되었을때 비밀번호 재설정이 가능하도록 설정해 주어야 한다.
- 회원가입시 이메일 인증과 비슷하지만 recoverPasswordCheck()메서드를 통해 email인증이 완료된 값들을 result에 담는다.
- 이때 result의 값이 SUCCESS일때
@RequestMapping(value = "recoverPassword", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView getRecoverPassword() {
ModelAndView modelAndView = new ModelAndView("member/recoverPassword");
return modelAndView;
}
@RequestMapping(value = "recoverPassword", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String postRecoverPassword(EmailAuthEntity emailAuth) throws MessagingException {
Enum<? extends IResult> result = this.memberService.recoverPasswordSend(emailAuth);
JSONObject responseObject = new JSONObject();
responseObject.put("result", result.name().toLowerCase());
if (result == CommonResult.SUCCESS) {
responseObject.put("index", emailAuth.getIndex());
}
return responseObject.toString();
}
@RequestMapping(value = "recoverPassword", method = RequestMethod.PATCH, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String patchRecoverPassword(EmailAuthEntity emailAuth, UserEntity user) {
Enum<?> result = this.memberService.recoverPassword(emailAuth, user);
JSONObject responseObject = new JSONObject();
responseObject.put("result", result.name().toLowerCase());
return responseObject.toString();
}
- 비밀번호 재설정에서 가장 중요한것은 GET, POST, PATCH 요청방식이 다 이루어 진다는 것이다.
- 특히 POST 요청방식에서 result가 SUCCESS일경우 responseObject(Json객체)에 put메서드를 통해 EmailAuthEntity의 getIndex()한 값을 넣어주어야 하는데 자세한 설명은 아래에서 하도록 한다.
form['emailSend'].addEventListener('click', () => {
Warning.hide();
if (form['email'].value === '') {
Warning.show('이메일을 입력해 주세요.');
form['email'].focus();
return;
}
Cover.show('계정을 확인하고 있습니다.\n잠시만 기다려 주세요.');
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('email', form['email'].value);
xhr.open('POST', './recoverPassword')
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
Cover.hide();
if (xhr.status >= 200 && xhr.status < 300) {
const responseObject = JSON.parse(xhr.responseText);
switch (responseObject['result']) {
case 'success':
emailAuthIndex = responseObject['index'];
form['email'].setAttribute('disabled', 'disabled');
form['emailSend'].setAttribute('disabled', 'disabled');
form['password'].focus();
form['password'].select();
break;
default:
Warning.show('해당 이메일을 사용하는 계정을 찾을 수 없습니다.');
form['email'].focus();
form['email'].select();
}
} else {
Warning.show('서버와 통신하지 못하였습니다. 잠시 후 다시 시도해 주세요.');
}
}
};
xhr.send(formData);
});
- 입력한 이메일 input칸에 관한 이메일 확인메일을 보내는 JS코드이다.
이메일 인증을 언제할지는 클라이언트가 정하기 때문에 브라우저에서는 계속해서 요청을 보내야 한다. 즉 계속 이메일 인증을 하였는가? 에대한 요청을 보내어 OK를 하였을 경우를 다음 단계로 넘어가도록 로직을 짜야한다.
let emailAuthIndex = null;
setInterval(() => {
if (emailAuthIndex === null) {
return;
}
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('index', emailAuthIndex);
xhr.open('POST', './recoverPasswordEmail');
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
const responseObject = JSON.parse(xhr.responseText);
console.log(responseObject);
switch (responseObject['result']) {
case 'success':
form['code'].value = responseObject['code'];
form['salt'].value = responseObject['salt'];
form.querySelector('[rel="messageRow"]').classList.remove('visible');
form.querySelector('[rel="passwordRow"]').classList.add('visible');
emailAuthIndex = null;
break;
default:
}
}
}
}
xhr.send(formData);
}, 1000);
- let emailAuthIndex 는 기본값이 null이므로 1초마다 반복될시 default값으로 빠지기 때문에 result값을 아래와 같이 failure로 계속해서 확인 요청을 보낸다.(클라이언트가 인증확인을 누르기 전까지 1초단위로 계속 요청함.)
- 왼쪽 자료처럼 계속해서 result결과값을 failure 즉 요청에 대한 인증확인을 누르기 전 상태를 유지하다가 클라이언트가 인증을 완료하는 그순간 오른쪽 자료 처럼 result 값이 success로 변하고 code, salt 의 값도 찍히게 된다.
- 1초마다 요청을 보내는 index값과 인증완료를 눌렀을때의 index값을 비교하여 failure값과 success 결과에 따라 값을 보여주는 것이 달라진다.
- 이메일이 정상적으로 이루어졌을 경우 message-row <tr>태그가 생기면서 새로운 비밀번호를 설정하는 칸이 visible이 됨과 동시에 보여지게 된다.
form['recover'].addEventListener('click', () => {
Warning.hide();
if (form['password'].value === '') {
Warning.show('비밀번호를 입력해 주세요.');
form['password'].focus();
return;
}
if(form['password'].value === form['passwordCheck'].value) {
form.querySelector('[rel="messageRow"]').classList.remove('visible');
} else {
Warning.show('비밀번호가 일치하지 않습니다. 다시 입력해주세요.');
return;
}
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('email', form['email'].value);
formData.append('code', form['code'].value);
formData.append('salt', form['salt'].value);
formData.append('password', form['password'].value);
xhr.open('PATCH', './recoverPassword')
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
Cover.hide();
if (xhr.status >= 200 && xhr.status < 300) {
const responseObject = JSON.parse(xhr.responseText);
switch (responseObject['result']) {
case 'success':
alert('비밀번호 변경 성공!!!');
window.location.href = 'login';
break;
default:
Warning.show('비밀번호가 서로 일치하지 않습니다.');
form['password'].focus();
form['password'].select();
}
} else {
Warning.show('서버와 통신하지 못하였습니다. 잠시 후 다시 시도해 주세요.');
}
}
};
xhr.send(formData);
});
- 새로운 비밀번호를 설정하는 코드인데 주의해야할 사항이 몇가지 있다.
- 새로설정하는 비밀번호와 다시한번 입력한 비밀번호가 일치하지 않는 경우
- PATCH 요청방식을 통해 새롭게 변경된 비밀번호를 업데이트 해주어야 함
- form.append메서드를 통해 위에서 인증된 code, salt, email, password 네가지를 같이 send()해주어야함
- [1],[2]번은 쉽게 이해가 되었으나 [3]번이 조금 헷갈렸다. 기존 코드 작성시 email, password만을 append하였는데 오류가 발생하였다. 그 이유는 1초단위로 index값을 통한 result:success 의 결과값도 같이 받아주어야 하기 때문이다. success 결과값으로 console창에 찍힌 값은 result, code, salt였기 때문에 email을 비롯한 모든 값이 인증이 되었다는 것을 완료 시켜 주어야 한다. 아래와 같이 Response값에 email, code, salt, password 4개의 값이 응답으로 돌아가야 하기 때문에 append에 4가지를 추가해주어야 한다. (매우 중요!!)
@Transactional // 앞에꺼 Insert하는거 취소시킨다는 내용
public Enum<? extends IResult> recoverPasswordSend(EmailAuthEntity emailAuth) throws MessagingException {
UserEntity existingUser = this.memberMapper.selectUserByEmail(emailAuth.getEmail());
if (existingUser == null) {
return CommonResult.FAILURE;
}
String authCode = RandomStringUtils.randomNumeric(6);
String authSalt = String.format("%s%s%f%f", authCode,
emailAuth.getEmail(),
Math.random(),
Math.random());
authSalt = CryptoUtils.hashSha512(authSalt);
Date createdOn = new Date(); // 현재일시
Date expiresOn = DateUtils.addMinutes(createdOn, 5); //5분 미래
emailAuth.setCode(authCode);
emailAuth.setSalt(authSalt);
emailAuth.setCreatedOn(createdOn);
emailAuth.setExpiredOn(expiresOn);
emailAuth.setIsExpired(false);
if (this.memberMapper.insertEmailAuth(emailAuth) == 0) {
return CommonResult.FAILURE;
}
Context context = new Context();
context.setVariable("email", emailAuth.getEmail());
context.setVariable("code", emailAuth.getCode());
context.setVariable("salt", emailAuth.getSalt());
String text = this.templateEngine.process("member/recoverPasswordEmailAuth", context);
MimeMessage mail = this.mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mail, "UTF-8");
helper.setFrom("ja513698@gmail.com");
helper.setTo(emailAuth.getEmail());
helper.setSubject("[스터디] 비밀번호 재설정 인증 링크");
helper.setText(text, true);
this.mailSender.send(mail);
return CommonResult.SUCCESS;
}
- 위 로직은 실제 이메일을 input칸에 입력시 클라이언트의 email로 이메일 인증코드를 전송할 수 있도록 작성한 로직이다.
- JavaMailSender를 통해 JavaMailSender 인터페이스를 상속받아 본문에 HTML 메일을 작성할 수 있도록 기능이 추가된 인터페이스 이다.
- MailSender는 SimpleMailMessage를 정의해 텍스트 메일을 발송 할 수 있는 반면, JavaMailSender에선 MimeMessage를 정의해 본문이 HTML로 이루어진 메일을 발송할 수 있다.(HTML로 발송하기 위해서는 JavaMailSender를 사용하여 개발자가 원하는 페이지로 이메일 인증메일을 발송 할 수 있도록 해준다.)
- 타임리프를 사용중이라면 타임리프에서 제공하는 TemplateEngine 클래스의 process()메소드를 이용하면 된다.
- 이때, 타임리프에서 사용하는 파라미터값을 제공하기 위해 스프링의 Model과 비슷한 역할을 하는 Context 객체에 뷰페이지에서 사용하는 파라미터를 정의하여 타임리프가 사용된 HTML파일과 Context객체를 인자로 주면 알아서 HTML로 변환해주며, 이 변환된 문자열을 MimeMessage의 setText()메소드 인자로 할당하면 HTML 메일이 보내진다.
- 값세팅으로 인한 코드 가독성이 떨어질 수 있으므로 유틸 클래스로 관리하는 것이 효율 적이다.
@Transactional
public Enum<? extends IResult> recoverPasswordCheck(EmailAuthEntity emailAuth) {
EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByIndex(emailAuth.getIndex());
if (existingEmailAuth == null || !existingEmailAuth.getIsExpired()) {
return CommonResult.FAILURE;
}
emailAuth.setCode(existingEmailAuth.getCode());
emailAuth.setSalt(existingEmailAuth.getSalt());
return CommonResult.SUCCESS;
}
@Transactional
public Enum<? extends IResult> recoverPasswordAuth(EmailAuthEntity emailAuth) {
EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(emailAuth.getEmail(), emailAuth.getCode(), emailAuth.getSalt());
// 전달인자의 순서는 memberMapper에서 메서드에서 정의해놓은값과 순서에 맞게 배치한다.
if (existingEmailAuth == null) {
return CommonResult.FAILURE;
}
if (existingEmailAuth.getExpiredOn().compareTo(new Date()) < 0) {
return CommonResult.FAILURE;
}
existingEmailAuth.setIsExpired(true);
if (this.memberMapper.updateEmailAuth(existingEmailAuth) == 0) {
return CommonResult.FAILURE;
}
return CommonResult.SUCCESS;
}
public Enum<? extends IResult> recoverPassword(EmailAuthEntity emailAuth, UserEntity user) {
EmailAuthEntity existingEmailAuth = this.memberMapper.selectEmailAuthByEmailCodeSalt(emailAuth.getEmail(), emailAuth.getCode(), emailAuth.getSalt());
System.out.println(existingEmailAuth);
if (existingEmailAuth == null || !existingEmailAuth.getIsExpired()) {
return CommonResult.FAILURE;
}
UserEntity existingUser = this.memberMapper.selectUserByEmail(existingEmailAuth.getEmail());
//EmailAuth에 있는 이메일로 UserTable에 동일한 이메일이 포함되어있는 레코드를 UserEntity에 담는다.
existingUser.setPassword(CryptoUtils.hashSha512(user.getPassword()));
System.out.println(existingUser.getPassword());
// 입력을 하고있는 비밀번호는 UserEntity의 user가 담고있다.
//
if (this.memberMapper.updateUser(existingUser) == 0) {
return CommonResult.FAILURE;
}
return CommonResult.SUCCESS;
}
- recoverPasswordCheck는 emailAuthIndex를 DB에서 getIndex()한 값을 existingEmailAuth객체에 담는다.
- existingEmailAuth의 값이 null이거나 isExpired의 값이 false 라면 Failure를 반환한다.
- index요청에 대한 응답이 되었다면 emailAuth의 (input(code,salt)의) value값을 인증번호 발송과 함께 받은 code, salt값으로 세팅한다.
- recoverPasswordAuth는 Mapper의 selectEmailAuthByEmailCodeSalt메서드를 통해 DB 쿼리에서 email, code, salt 3가지가 다 인증이 완료되어진 값(getEmail())이 existingEmailAuth객체에 담긴다.
- null이거나 expiredOn값이 음수(즉 만료가 되었다.)일 경우 Failure를 반환한다. if조건문에 걸리지 않았을 경우 IsExpired값을 true로 세팅해준다.(updateUser()메서드가 실행되기 전에 작성하여야 update된 isExpired값이 저장된다.) 그리고 최종적으로 SUCCESS값을 반환한다.
- recoverPassword메서드에서는 2가지의 객체(emailAuth관련 객체, user관련 객체)에 값을 담아야 한다(중요한 포인트)
- EmailAuthEntity타입을 가지는 emailAuth객체에 인증이 완료된 클라이언트가 작성한 email값을 담아준다.(code, email, salt 3가지의 값 비교가 이루어진)
- UserEntity타입을 가지는 existingUser객체에는 위에서 이메일 인증이 완료된 email값을 담은 객체가 된다. selectUserByEmail()메서드를 통해 위에서 입력한 클라이언트의 email과 일치한다면 existingUser의 setPassword를 통해 클라이언트가 입력한 비밀번호(Hashing이 되어진 새로운 비밀번)를 업데이트 해준다.
최종적으로 SUCCESS를 반환함과 동시에 비밀번호 변경하기를 누르고 alert('비밀번호 변경 완료')가
뜨면 DB에서 새로 설정한 비밀번호가 Hashing이 되어 update가 정상적으로 구현이 되면 성공이다.
'SpringBoot' 카테고리의 다른 글
SpringBoot 게시판 기능구현 2 (0) | 2022.11.21 |
---|---|
SpringBoot 게시판 기능구현 1 (0) | 2022.11.20 |
SpringBoot 로그인 기능 구현, session값을 통한 게시판 접근 (0) | 2022.11.15 |
SpringBoot 회원가입 기능구현 3 (0) | 2022.11.13 |
SpringBoot 회원가입 기능구현 2 (1) | 2022.11.11 |