로그인 구현에서 이어지는 실습
implementation 'org.springframework.boot:spring-boot-starter-mail'
위의 라이브러리를 추가해준다.
spring:
mail:
host: smtp.gmail.com
port: 587
username: 구글아이디@gmail.com
password: 구글에서 발급받은 password
properties:
mail:
smtp:
starttls:
enable: true
required: true
auth: true
timeout: 5000 #5초
starttls는 http와 https의 차이와 비슷한 설정으로, 설정하지 않고 넘어가면 메일이 스팸함으로 넘어갈 수도 있다.

GMail로 들어가서 설정에 들어간 다음 IMAP 사용을 체크해준다. 지금 설정하는 메일 주소를 통해서 추후 가입 인증 메일이 발송된다.

그다음 구글 계정 관리로 들어가서 보안 > 2단계 인증을 설정한다.


2단계 인증 설정을 완료했으면 상단 검색창에 앱 비밀번호를 검색한 후 비밀번호를 발급받는다. 앱 이름은 아무거나 적어주면 된다.

생성된 비밀번호를 띄어쓰기 없이 application.yaml에 적어주면 기본 설정은 끝난다.

signup() 함수에 sendEmail() 함수를 추가, 가입을 하면 아래와 같은 이메일이 온 것을 볼 수 있다.



가입할 때 이메일을 통해 인증이 되었는지 아닌지 구분하기 위해 User 엔티티에 enabled를 추가한다.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class EmailVerify {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
@ManyToOne
@JoinColumn(name="user_idx")
private User user;
private String uuid;
}
사용자가 가입을 완료한 후 인증받을 이메일과 서버에서 생성한 토큰을 비교할 수 있도록 EmailVerify 엔티티를 만들어 user와 uuid(인증을 위한 토큰)을 저장한다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping("/verify")
public void verify(String uuid) {
userService.verify(uuid);
}
}
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JavaMailSender mailSender;
private final EmailVerifyRepository emailVerifyRepository;
public void sendEmail(String uuid, String email){
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("[Welcome] Hello World!");
message.setText(
"http://localhost:8080/user/verify?uuid="+uuid
);
mailSender.send(message);
}
public UserDto.SignupResponse signup(UserDto.SignupRequest dto) {
String uuid = UUID.randomUUID().toString();
User user = userRepository.save(dto.toEntity(passwordEncoder.encode(dto.getPassword())));
//이메일인증 정보 저장
emailVerifyRepository.save(EmailVerify.builder().user(user).uuid(uuid).build());
//이메일 전송
sendEmail(uuid, dto.getEmail());
return UserDto.SignupResponse.from(user);
}
public void verify(String uuid) {
EmailVerify emailVerify = emailVerifyRepository.findByUuid(uuid).orElseThrow();
User user = emailVerify.getUser();
user.userVerify();
userRepository.save(user);
}
}
UserService에서 사용자의 메일로 인증을 위한 링크를 보내도록 수정하고, verify() 함수를 추가하여 서버가 생성한 토큰과 사용자가 가입할 때 메일로 받은 토큰값이 일치하는지 비교한다.


이메일로 온 링크를 클릭하면 EmailVerify에 등록된 uuid와 비교 후, 두 토큰 값이 일치하면 User의 enabled가 true로 바뀌면서 인증이 완료된다.
+)

만약 다음과 같은 화면이 뜬다면?

403 에러는 대개 권한 문제이므로 SecurityFilterChain에서 /verify()를 추가해주는 걸 잊지 말자
회원 정보 저장, 이메일 인증 정보 저장, 이메일 전송 각각의 기능에 앞서, 회원가입시 이메일 인증은 어떻게 처리하는 게 좋을까?
emailverify 기능
EmailVerifyController
EmailVerifyService (이메일 인증 정보 저장, 이메일 전송)
EmailVerifyRepository
EmailVerify
user기능
UserController
UserService (회원 정보 저장)
UserRepository
User
UserController -> EmailVerifyService
UserController -> UserService
장점 : 클라이언트와 서버 통신 줄어듦
단점 : 트랜잭션 처리가 애매해짐
UserController -> UserService -> EmailVerifyService
장점 : 클라이언트와 서버 통신 줄어듦, 트랜잭션 처리가 간단
단점 : 서비스에서 계층이 나눠짐 (의존성 문제가 생길 수 있음)
https://jangjjolkit.tistory.com/62 (서비스에서 서비스 호출)
UserController -> UserService
EmailVerifyController -> EmailVerifyService
장점 : 기능 별로 완전 분리, SRP 원칙
단점 : 클라이언트와 서버 통신이 많아진다 (성능이 떨어지므로 비추천)
emailverify 기능
EmailVerifyRepository
EmailVerify
user기능
UserController
UserService (회원 정보 저장, 이메일 인증 정보 저장, 이메일 전송)
UserRepository
User
UserController->UserService