[Spring] Spring 메일 발송 - 회원가입 인증 메일/ID 찾고 메일로 전송/비밀번호 변경 및 임시 비밀번호 메일로 전송
https://offbyone.tistory.com/167
https://m.blog.naver.com/jny9708/221773002779
항상 그렇듯 참고한 글들이다.
먼저 회원가입 관련해서 메일 인증을 하기 위해 총 3가지 정도를 구현하고자 한다.
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
2) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
3) 비밀번호 변경
4) 회원가입 시 메일 인증
1. 기본적인 설정
일단 나는 gmail로 메일을 보내려 한다. 그러려면 2가지 설정을 해야 한다.
- https://myaccount.google.com/
- https://myaccount.google.com/u/1/signinoptions/two-step-verification/enroll-welcome
접속 및 시작하기, 2단계 인증 설정(문자나 메일 인증)
- https://security.google.com/settings/security/apppasswords?pli=1
접속 및 앱(메일), 기기 선택 클릭
그러면 이런 것이 나온다. 비밀번호를 메모하자.
- https://support.google.com/accounts/answer/6010255?hl=ko
위 링크에서 보안 수준이 낮음으로 설정을 변경하자. 그러면 설정은 끝났다.
2. 코드
항상 그렇듯 build.gradle 설정, application.properties 설정을 하자.
build.gradle
dependencies{
implementation 'commons-io:commons-io:2.6'
implementation group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2'
implementation group: 'org.springframework', name: 'spring-context-support', version: '5.3.10'
}
이렇게 3줄을 추가하자.
application.properties
spring.mail.host=smtp.gmail.com
spring.mail.port=587 #gmail이기 때문에 587 사용, 다른 메일인 경우에는 다른 것을 사용.
spring.mail.username={메일을 전송할 메일. google에서 사용 설정한 메일을 작성}
spring.mail.password=rzhtznbalzgvghrw
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.auth=true
smtp를 이용해 메일을 전송하기 때문에 위와 같은 설정을 해 주어야 한다. 추가적으로, local에서는 localhost로 인증 메일을 보내면 되지만 서버 인증 메일은 서버 ip를 작성해야 하기 때문에 profile에 맞춰서 잘 적자.
spring.config.activate.on-profile=common
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=swmteam12@gmail.com
spring.mail.password=rzhtznbalzgvghrw
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.auth=true
#---
spring.config.activate.on-profile=localdb
...
mail.verification.link=http://localhost:8080/member/verify/
#---
spring.config.activate.on-profile=cidb
...
mail.verification.link=http://34.132.212.36:8080/member/verify/
#---
spring.profiles.group.local=localdb,common
#---
spring.profiles.group.ci=cidb,common
#---
spring.profiles.active=ci
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
flow는 아래와 같다.
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
- 해당 mail에 해당하는 사용자를 찾아서 member id를 가져옴
- 찾은 member id를 메일로 전송
2) 비밀번호 변경
flow는 아래와 같다.
- 프론트에서 로그인 된 상태에서 기존 비밀번호와 새 비밀번호를 post로 전송
- 기존 비밀번호가 맞는 경우 새 비밀번호를 재설정
3) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
flow는 아래와 같다.
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
- 임의로 비밀번호 발급 및 해당 사용자의 비밀번호를 임시 비밀번호로 변경
- 임시 비밀번호를 이메일로 전송
4) 회원가입 시 메일 인증
- 회원가입 시 사용자 역할을 ROLE_NOT_PERMITTED로 두고
- 추가적으로 인증 링크가 담긴 인증 메일 전송
- 인증 링크가 클릭되었을 경우 해당 사용자의 role을 ROLE_USER로 변경
일단 제일 먼저 메일을 보내는 함수를 만들어 보자.
EmailService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
@Service
@AllArgsConstructor
public class EmailService{
@Autowired
private JavaMailSenderImpl mailSender;
public void sendMail(String to, String title, String text){
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setTo(to);
simpleMailMessage.setSubject(title);
simpleMailMessage.setText(text);
mailSender.send(simpleMailMessage);
}
public String idTitle(){
return "비즈킥스 아이디 찾기";
}
public String idText(String memberId, String name){
String msg = "";
msg += name + "님의 아아디는 다음과 같습니다.\n";
msg += "아이디 : " + memberId;
return msg;
}
public String tempPasswordTItle(){
return "비즈킥스 임시 비밀번호";
}
public String tempPasswordText(String name, String tempPassword){
String msg = "";
msg += name + "님의 임시 비밀번호입니다. 비밀번호를 변경하여 사용하세요.\n";
msg += "임시 비밀번호 : " + tempPassword;
return msg;
}
public String verifyEmailTitle(){
return "비즈킥스 인증 메일";
}
public String verifyEmailText(String link){
String msg = "";
msg += "접속 링크 : " + link;
return msg;
}
}
나는 텍스트를 한 줄에 적는 게 싫어서 이렇게 뺐다. 그리고 디자인은 할 줄 모르기 대문에 간단하게 적었다.
sendMail 함수에서 누구에게 보낼지, 제목은 뭔지, 내용은 뭔지 담아서 보내줄 것이다.
1) 아이디를 잊어버렸을 경우 아이디를 가입한 이메일로 전송
- 프론트에서 회원가입 시 등록한 이메일을 post로 전송
EmailDto.java
@Data
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmailDto {
private String email;
}
AuthController.java
@PostMapping(value="/member/find/id")
public ResponseEntity<Object> findMemberId(@RequestBody EmailDto emailDto) {
String email = emailDto.getEmail();
authService.sendIdEmail(email);
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "Success");
log.info("아이디 찾기 요청 : " + email);
return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK);
}
AuthService.java
public void sendIdEmail(String email){
Member member = memberRepository.getMemberByEmail(email);
String text = emailService.idText(member.getMemberId(), member.getName());
String title = emailService.idTitle();
emailService.sendMail(member.getEmail(), title, text);
}
- 해당 mail에 해당하는 사용자를 찾아서 member id를 가져옴
- 찾은 member id를 메일로 전송
2) 비밀번호 변경
flow는 아래와 같다.
- 프론트에서 로그인 된 상태에서 기존 비밀번호와 새 비밀번호를 post로 전송
AuthController.java
@PostMapping(value="/member/modify/password")
public ResponseEntity<Object> changeMemberPassword(@RequestBody PasswordDto passwordDto) {
Member member = memberService.getCurrentMemberInfo();
authService.changeMemberPassword(member, passwordDto.getOld_password(), passwordDto.getNew_password());
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "Success");
log.info("사용자 {} 비밀번호 수정 요청", member.getMemberId());
return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK);
}
memberService.getCurrentMemberInfo()는 현재 로그인 된 사용자의 정보를 가져오는 함수이다.
AuthService.java
public void changeMemberPassword(Member member, String oldPassword, String newPassword){
if(!this.passwordEncoder.matches(oldPassword, member.getPassword())){
throw new CustomException(ErrorCode.PASSWORD_NOT_VALID);
}
member.setPassword(passwordEncoder.encode(newPassword));
memberRepository.save(member);
}
AuthService 내에 passwordEncoder를 미리 선언해 두었다. 이후 해당 encoder에 oldPassword를 넣었는데 encoding된 값과 동일하지 않다 -> 그러면 old password가 틀린 비밀번호다. 이 경우에는 password가 틀렸다는 오류를 발생시키고, 아니라면 새 비밀번호를 설정하고 memberRepository에 update해 준다.
3) 비밀번호를 잊어버렸을 경우 임시 비밀번호 발급 및 임시 비밀번호를 이메일로 전송
AuthController.java
@PostMapping(value="/member/find/password")
public ResponseEntity<Object> findMemberPassword(@RequestBody EmailDto emailDto) {
authService.reissuePassword(emailDto.getEmail());
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "Success");
log.info("비밀번호 찾기 요청 : " + emailDto.getEmail());
return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK);
}
프론트에서 회원가입 시 등록한 이메일을 post로 전송
AuthService.java
public void reissuePassword(String email){
Member member = memberRepository.getMemberByEmail(email);
String tempPassword = "";
for (int i = 0; i < 12; i++) {
tempPassword += (char) ((Math.random() * 26) + 97);
}
member.setPassword(passwordEncoder.encode(tempPassword));
memberRepository.save(member);
String text = emailService.tempPasswordText(member.getName(), tempPassword);
String title = emailService.tempPasswordTItle();
emailService.sendMail(member.getEmail(), title, text);
}
email로 멤버를 검색하고, 랜덤을 이용해서 12개 길이를 가진 알파벳 문자열을 만든다. 그리고 비밀번호를 임시 비밀번호로 바꾸고, memberRepository에 update한다. 이후 메일을 보낸다.
4) 회원가입 시 메일 인증
AuthService.java
@PostMapping("/member/signup")
public ResponseEntity<Object> signup(@RequestBody MemberDto memberDto){
Member member = authService.signup(memberDto);
authService.sendVerificationEmail(member);
JSONObject returnObject = new JSONObject();
returnObject.put("msg", "Success");
log.info("새로운 회원가입");
return new ResponseEntity<Object>(returnObject.toString(), HttpStatus.CREATED);
}
회원가입 시 사용자 역할을 ROLE_NOT_PERMITTED로 두어야 한다. authService.signup() 함수 내부에 이 내용을 추가한다. 추가적으로 authService.sendVerificationEmail() 함수를 call해서 인증 링크가 담긴 인증 메일 전송한다.
AuthService.java
@Value("${mail.verification.link}")
private String VERIFICATION_LINK;
public static final Long EmailExpireTime = 1000 * 60 * 30L; // 30분
...
public void sendVerificationEmail(Member member){
UUID uuid = UUID.randomUUID();
redisUtil.set(uuid.toString(), member.getMemberId(), EmailExpireTime);
String link = this.VERIFICATION_LINK + uuid.toString();
String title = emailService.verifyEmailTitle();
String text = emailService.verifyEmailText(link);
emailService.sendMail(member.getEmail(), title, text);
}
public void verifyEmail(String key){
if(!redisUtil.hasKey(key)){
throw new CustomException(ErrorCode.LINK_NOT_EXIST);
}
String memberId = redisUtil.get(key);
Member member = getWithNullCheck.getMemberByMemberId(memberRepository, memberId);
modifyUserRole(member, UserRole.ROLE_USER);
}
public void modifyUserRole(Member member, UserRole userRole){
member.setUserROle(userRole);
memberRepository.save(member);
return;
}
UUID는 unique한 값을 생성해 주는 함수이다. 이걸 만들고, uuid를 key로, member id를 value로 redis에 30분간 저장되게 저장한다. 이후 uuid가 담긴 링크를 메일로 보낸다.
AuthController.java
@GetMapping(value = "/member/verify/{key}")
public ResponseEntity<Object> verifyUserWIthKey(@PathVariable(value = "key", required = true) String key){
authService.verifyEmail(key);
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "Success");
log.info("이메일 인증 완료");
return new ResponseEntity<Object>(jsonObject.toString(), HttpStatus.OK);
}
이후 인증 링크가 클릭되었을 경우 method를 하나 만든다.
AuthService.java
public void verifyEmail(String key){
if(!redisUtil.hasKey(key)){
throw new CustomException(ErrorCode.LINK_NOT_EXIST);
}
String memberId = redisUtil.get(key);
redisUtil.delete(key);
Member member = memberRepository.getMemberByMemberId(memberId);
modifyUserRole(member, UserRole.ROLE_USER);
}
public void modifyUserRole(Member member, UserRole userRole){
member.setUserROle(userRole);
memberRepository.save(member);
return;
}
인증 링크가 클릭되었을 경우 key에 해당하는 value가 있는지, 있는 경우 redis에서 key에 대한 값을 삭제하고 member id로 값을 찾고, 해당 사용자의 role을 ROLE_USER로 승격시킨다.
진짜 마지막으로 security config만 바꾸면 된다.
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter{
private final JwtUtil jwtUtil;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/member/modify/**").hasAnyRole("USER", "MANAGER")
.antMatchers("/member/**","/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**", "/admin/upload-csv").permitAll()
.antMatchers("/manage/**", "/dashboard/**").hasRole("MANAGER")
.antMatchers("/myapp/**").hasAnyRole("USER", "MANAGER")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.apply(new JwtSecurityConfig(jwtUtil));
}
}
member/modify는 로그인 되어 있어야 한다. 이외의 것들은 없어도 되므로 이렇게 설정, 관리자 페이지는 관리자만 접속할 수 있으니 이렇게 하고 마지막 로직은 모든 사용자가 이용할 수 있으므로 위와 같이 설정했다.
그러면 완성!