728x90
반응형
이메일 인증을 하는 이유
: 가짜 이메일을 통해, 무분별한 가입을 막기 위함
-> 토큰값을 이용해, 전송한 토큰과 일치 여부를 확인하는 것이 필요하다.
(1) 이메일 확인 오류 -> 토큰값 불일치, 이메일 주소 불일치 등 보안을 위해 응답정보제공 최소화
(2) 이메일 확인 성공 -> 몇번째 유저인지 리턴
소셜 인증을 사용하는 방법도 존재 -> api를 이용
localhost:8080/check-email-token?token=4cfa2712-354b-403f-8d18-53fdca98d788&email=your@gamil.com을 이용해 이메일 인증 확인 메일 체크
checked-email
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo</title>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<style>
.container {
max-width: 100%;
}
</style>
</head>
<body class="bg-light">
<!-- 네비바 만들기-->
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<!-- 배너-->
<a class="navbar-brand" href="/" th:href="@{/}">
<!--@는 경로설정인데 이 경우엔 root경로 -->
<img src="/images/logo_sm.png" width="30" height="30">
</a>
<!-- 네비게이션 아이템 검색 창-->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent">
<span class="navbar-toggler-icon"> </span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<!-- 스터디 찾기 버튼 href="#" th:href="은 타임리프 동작 X와 동작시-->
<form th:action="@{/search/study}" class="form-inline" method="get" action="#">
<input class="form-control mr-sm-2" name="keyword" placeholder="스터디 찾기" type="search" />
</form>
</li>
</ul>
<!-- 로그인, 가입 버튼 href="#" th:href="은 타임리프 동작 X와 동작시-->
<ul class="navbar-nav justify-content-end">
<li class="nav-item">
<a class="nav-link" href="#" th:href="@{/login}">로그인</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" th:href="@{/signup}">가입</a>
</li>
</ul>
</div>
</nav>
<div class="py-5 text-center" th:if="${error}">
<p class="lead">데모 이메일 확인</p>
<div class="alert alert-danger" role="alert">
이메일 확인 링크가 정확하지 않습니다.
</div>
</div>
<div class="py-5 text-center" th:if="${error == null}">
<p class="lead">데모 이메일 확인</p>
<h2>
이메일을 확인했습니다. <span th:text="${numberOfUser}">10</span>번째 회원,
<span th:text="${nickname}">***</span>님 가입을 축하합니다.
</h2>
<small class="text-info">이제부터 가입할 때 사용한 이메일 또는 닉네임과 패스트워드로 로그인 할 수 있습니다.</small>
</div>
</body>
</html>
AccountService
package com.demo.account;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.demo.domain.Account;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
// 이메일 발송 관련 빈 의존성 주입
private final JavaMailSender javaMailSender;
//패스워드 인코딩
private final PasswordEncoder passwordEncoder;
//DB와 싱크를 위해 필수
@Transactional
public void processNewAccount(SignUpForm signUpForm) {
// 1. 회원정보 저장
// 2. 이메일 토큰 만들기
// 3. 이메일 전송
Account newAccount = saveNewAccount(signUpForm);
// 회원가입 이메일 발송 -> starter-mail 이용 -> mailsender // 일단 가짜 객체에 저장해 콘솔에 출력
// 계정인증을 위한 토큰값 생성 -> 유효 아이디를 이용해 랜덤 생성
newAccount.generateEmailCheckToken();
//emailCehckToken=null
//저장 후 토큰 생성 -> 함수안에서만 트랜잭션이 존재하므로, DB에 싱크오류 ->@Transactional 필요
//상태 종료시 DB에 싱크를 해주기 위함
sendSignUpConfirmEmail(newAccount);
// TODO 회원 가입 처리 valid에 안걸리면 회원가입
}
private Account saveNewAccount(SignUpForm signUpForm) {
// form을 이용해 account에 저장
Account account = Account.builder().email(signUpForm.getEmail()).nickname(signUpForm.getNickname())
//.password(signUpForm.getPassword()) // TODO encoding (해시 암호화) 필요함
.password(passwordEncoder.encode(signUpForm.getPassword())) // 패스워드 인코딩 수행
// .emailVerified(false) //검증은 아직 이루어지지 않았으므로 null이 들어가 있어서 불필요
.studyCreatedByWeb(true).studyEnrollmentResultByWeb(true).studyUpdatedByWeb(true)
// 웹으로 알림을 알리는 옵션은 켜둔 상태로 저장 -> 나머지 기본값들은 모두 false
.build();
// 해당 account 생성 -> repository에 저장
//Account newAccount = accountRepository.save(account);
//return newAccount;
return accountRepository.save(account);
}
private void sendSignUpConfirmEmail(Account newAccount) {
// 관련 객체 정의 후 -> 빈 정의 후 발송
SimpleMailMessage mailMessage = new SimpleMailMessage();
// 받는 주소
mailMessage.setTo(newAccount.getEmail());
// 제목 작성
mailMessage.setSubject("Demo, 회원 가입 인증");
// 본문 작성 -> 가입인증 링크 만들어 제공 -> 토큰값을 생성해 가입인증 진행
mailMessage.setText("/check-email-token?token=" + newAccount.getEmailCheckToken() +
"&email=" + newAccount.getEmail());
// 메일 발송 ->
javaMailSender.send(mailMessage);
// 만약에 발송을 하지않는다면, test에서 오류를 내뿜게된다.
}
}
AccountController
package com.demo.account;
import java.time.LocalDateTime;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import com.demo.domain.Account;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class AccountController {
private final SignUpFormValidator signUpFormValidator;
//계정 리포지토리와 메일 샌더는 서비스로 이전
private final AccountService accountService;
//이메일 인증 메일 발송을 위한, accountrepository 사용 -> 리파지토리를 도메인으로 여겨서 자유롭게 사용
private final AccountRepository accountRepository;
@InitBinder("signUpForm")
public void initBinder(WebDataBinder webDataBinder) {
webDataBinder.addValidators(signUpFormValidator);
}
// sign-up 페이지에 연결된다면
@GetMapping("/sign-up")
public String signUpForm(Model model) {
// model.addAttribute(new SignUpForm()); 생략 가능
model.addAttribute("signUpForm", new SignUpForm());
return "account/sign-up";
}
// 스프링부트 자동설정에 의해
// templates에 존재하는 view인 account/sign-up을 리턴한다.
@PostMapping("/sign-up") // 복합객체는 본디 ModelAttribute로 받지만, 생략가능
public String signUpSubmit(@Valid @ModelAttribute SignUpForm signUpForm, Errors errors) {
if (errors.hasErrors()) {
return "account/sign-up";
}
// SignUpForm 도메인에 Valid를 위한 처리 필요
// @InitBinder를 이용해 대체
// signUpFormValidator.validate(signUpForm, errors);
// if (errors.hasErrors()) {
// return "account/sign-up";
// }
// -> 자동으로 SignUpForm 검증을 한다.
//이메일 보내는 작업을 컨트롤러가 모르도록 서비스 쪽으로 이전 -> 서비스에서 private으로 수행
accountService.processNewAccount(signUpForm);
return "redirect:/";
}
//이메일 인증 확인 -> 이메일, 토큰 값 둘다 일치해야함 -> 화면에 전달할 모델도 파라미터로 전송
@GetMapping("/check-email-token")
public String checkEmailToken(String token, String email, Model model) {
Account account = accountRepository.findByEmail(email);
String view = "account/checked-email";
//같은 링크를 리턴하지만, 담은 정보는 각각 다르다.
//해당 뷰에서 error를 담아서 왔을 경우와 setEmailVerified가 true를 담아서 왔을 때를 구분해서 처리
//리파지토리를 도메인 계층으로 생각해, 레이어계층에서의 리파지토리 참조를 자유롭게 한다.
//반대로, 레이어 계층인, 서비스나 컨트롤러를 도메인 계층에서 참조하는 것은 지양한다.
//1단계에서 이메일의 존재여부를 판단
//2단계에서는 해당하는 이메일의 토큰값 동일 여부를 확인한다.
if(account==null) {
//해당 이메일이 존재하지 않을 때 오류 발생
//정보제공을 줄이기 위해 -> 서버단에서만 wrong.email이라는 것을 명시하고, 리턴은 하지않는다.
model.addAttribute("error", "wrong.email");
return view;
}
//토큰이 같지 않다면
if(!account.getEmailCheckToken().equals(token)){
model.addAttribute("error", "wrong.token");
return view;
//84bc0858-c403-4097-94b5-fede148da852
//84bc0858-c403-4097-94b5-fede148da852
}
//해당 이메일 검증을 참으로 설정하고, 가입 정보(시간정보)와 몇번째 회원인지 리턴한다.
account.setEmailVerified(true);
account.setJoinedAt(LocalDateTime.now());
model.addAttribute("numberOfUser", accountRepository.count());
model.addAttribute("nickname", account.getNickname());
return view;
}
}
728x90
반응형
'Server Programming > Spring Boot Full-Stack Programming' 카테고리의 다른 글
[스프링 풀스택 클론 코딩 - 회원가입] (1-10) 회원 가입 인증 메일 확인 테스트 및 리팩토링 (0) | 2022.08.26 |
---|---|
[스프링 풀스택 클론 코딩] 회원가입 이메일 인증 정상/오류 시 분기 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩 - 회원가입] (1-8) 회원가입 패스워드 인코딩 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩 - 회원가입] (1-7) 회원가입 리팩토링 및 테스트 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩]목업 테스트 mokito 이용하기 (0) | 2022.08.26 |