Development/Spring

[Spring] Spring 메일 발송 - 회원가입 인증 메일/ID 찾고 메일로 전송/비밀번호 변경 및 임시 비밀번호 메일로 전송

hyelie 2022. 10. 6. 09:38

https://offbyone.tistory.com/167

https://velog.io/@ehdrms2034/Spring-Security-JWT-Redis%EB%A5%BC-%ED%86%B5%ED%95%9C-%ED%9A%8C%EC%9B%90%EC%9D%B8%EC%A6%9D%ED%97%88%EA%B0%80-%EA%B5%AC%ED%98%84-4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%9D%B8%EC%A6%9D-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%95%84%EC%9D%B4%EB%94%94-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%B0%BE%EA%B8%B0

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는 로그인 되어 있어야 한다. 이외의 것들은 없어도 되므로 이렇게 설정, 관리자 페이지는 관리자만 접속할 수 있으니 이렇게 하고 마지막 로직은 모든 사용자가 이용할 수 있으므로 위와 같이 설정했다.

 

그러면 완성!