PlayData 백엔드 부트캠프 정리

다시 시작하는 부트 캠프 하루 후기 2일차

효건 2024. 10. 16. 10:11

<이메일로 비밀번호 찾기 인증하는 로직>

1. Gradle에 2가지 라이브러리를 추가한다.

// spring-boot-starter-mail
implementation 'org.springframework.boot:spring-boot-starter-mail'
// 자바 언어를 통해 smtp 통신을 할 수 있게 하는 라이브러리
implementation 'jakarta.mail:jakarta.mail-api:2.1.2'

 

2.실습 아이디를 새로 만들어서 pop3/smtp 사용을 허용해야 한다.

-> 네이버

-> 다시 인텔리제이로 돌아와서

# email setting
spring.mail.host=smtp.naver.com
spring.mail.port=587
spring.mail.username=
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.starttls.enable=true

 

이렇게 초기설정을 마무리 여기서 비밀번호는 앱비밀번호를 사용해야 하며 username은 이메일을 기입해야 합니다.

혹시라도 누군가가 오남용 할수있으므로 깃허브 올릴때나 공개할때는 gitignore와 같은 방식을 이용해서 가릴수있도록 주의해야 합니다. 혹시라도 해킹당 할 수도 있다고 합니다!!!

 

3.컨트롤러 를 작성한다.

// 연습용 이메일 폼 화면
@GetMapping("/email")
public String emailForm() {
    return "email/email-form";
}

@PostMapping("/email")
@ResponseBody
public void mailCheck(@RequestBody String email) {
    log.info("이메일 인증 요청 들어옴!: {}", email);
}

 

4. 이메일 유틸을 작성한다.

여기서 컴포넌트 어노테이션을 이용하여 빈등록을 해서 백엔드에서 불러올수있도록 한다.

아래는 인증번호를 만들어서 전송하는 로직이라고 볼수있다.

@Component
@RequiredArgsConstructor
@Slf4j
public class MailSenderService {

    // EmailConfig에 선언한 객체가 주입됨.
    private final JavaMailSender mailSender;

    // 난수 발생 메서드
    private int makeRandomNumber() {
        // 난수의 범위: 111111 ~ 999999
        int checkNum = (int) ((Math.random()*999999) + 111111);
        log.info("인증번호: {}", checkNum);
        return checkNum;
    }

    // 가입할 회원에게 전송할 이메일 양식 준비
    // 이 메서드를 컨트롤러가 호출할 겁니다.
    public String joinMail(String email) { // 컨트롤러로부터 이메일을 전달받음.
        int authNum = makeRandomNumber();
        String setFrom = "stephen4951@gmail.com"; // 발신용 이메일 주소(properties랑 똑같아야 함!)
        String toMail = email; // 수신받을 이메일 (가입하고자 하는 사람의 이메일)
        String title = "Spring MVC 회원 가입 인증 이메일 입니다."; // 실제 이메일 제목
        String content = "홈페이지 가입을 신청해 주셔서 감사합니다." +
                "<br><br>" +
                "인증 번호는 <strong>" + authNum + "</strong> 입니다. <br>" +
                "해당 인증 번호를 인증번호 확인란에 기입해 주세요."; // 이메일에 삽입할 내용 (더 꾸며보세요)

        try {
            mailSend(setFrom, toMail, title, content);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }


        // 컨트롤러에게 인증번호 리턴(문자열)
        return Integer.toString(authNum);
    }

    // 여기서 실제 이메일이 전송
    private void mailSend(String setFrom, String toMail, String title, String content) throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        /*
            기타 설정들을 담당할 MimeMessageHelper 객체를 생성
            생성자의 매개값으로 MimeMessage 객체, bool, 문자 인코딩 설정
            true 매개값을 전달하면 MultiPart 형식의 메세지 전달이 가능 (첨부 파일)
        */
        MimeMessageHelper helper
                = new MimeMessageHelper(message, false, "utf-8");
        helper.setFrom(setFrom);
        helper.setTo(toMail);
        helper.setSubject(title);

        // 내용 채우기 (true를 전송하면 html이 포함되어 있다.)
        helper.setText(content, true);

        // 메일 전송
        mailSender.send(message);
    }


}

 

5. 예를 변경해서 혹시라도 이메일을 잘못기입했을때 사용자에게 알려주는 방법

public String joinMail(String email) throws MessagingException { // 컨트롤러로부터 이메일을 전달받음.
    int authNum = makeRandomNumber();
    String setFrom = "stephen4951@gmail.com"; // 발신용 이메일 주소(properties랑 똑같아야 함!)
    String toMail = email; // 수신받을 이메일 (가입하고자 하는 사람의 이메일)
    String title = "Spring MVC 회원 가입 인증 이메일 입니다."; // 실제 이메일 제목
    String content = "홈페이지 가입을 신청해 주셔서 감사합니다." +
            "<br><br>" +
            "인증 번호는 <strong>" + authNum + "</strong> 입니다. <br>" +
            "해당 인증 번호를 인증번호 확인란에 기입해 주세요."; // 이메일에 삽입할 내용 (더 꾸며보세요)

        mailSend(setFrom, toMail, title, content);


 

이렇게 예외를 try catch문을 위처럼 바꿔서 기입한다 -> 컨트롤러로 예외처리를 넘기는 것이다.

6. 예외처리 하기 

@PostMapping("/email")
@ResponseBody
public ResponseEntity<?> mailCheck(@RequestBody String email) {
    log.info("이메일 인증 요청 들어옴!: {}", email);
    try {
        String authNum = mailSenderService.joinMail(email);
        return ResponseEntity.ok().body(authNum);
    } catch (MessagingException e) {
        e.printStackTrace();
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

 

7. 화면단에 표기하는 방법등의 로직을 작성ㅎ나다.

 const email = document.getElementById('userEmail').value.trim();
    console.log('완성된 email: ', email);

    fetch('/members/email', {
            method: 'post',
            headers: {
                'Content-type': 'text/plain',
            },
            body: email
        })
        .then((res)=> {
            if(res.status === 200){
                return res.text();
            }else{
                alert('존재하지 않는 이메일')
            }
        })
        .then((data)=>{
            console.log('인증번호: ',data);
            alert('인증번호가 전송되었습니다. 입력란에 정확히 입력해주세요')
            document.getElementById('mail-check-input').disabled=false;
        })

};

 

8. 화면에서 인증번호 확인 로직 

// 인증번호 검증
// blur -> focus가 빠지는 경우 발생.
document.getElementById('mail-check-input').onblur = e => {
    console.log('blur 이벤트 발생!');
    const inputCode = e.target.value;
    if (inputCode === code) {
        document.getElementById('mailCheckMsg').textContent = '인증번호가 일치합니다!';
        document.getElementById('mailCheckMsg').style.color = 'skyblue';
        e.target.style.display = 'none';
    } else {
        document.getElementById('mailCheckMsg').textContent = '인증번호를 다시 확인하세요!';
        document.getElementById('mailCheckMsg').style.color = 'red';
        e.target.focus();
    }

위와 같이 인증번호를 확인하여 할수있습니다!!!

 

<KaKao 로그인 API>

1. 컨트롤러에서 인가요청을 보냅니다, 

@GetMapping("/kakao/login")
public String kakaoLogin() {
    // 카카오 인가코드 신청 -> 카카오 인증 서버에서 클라이언트와 로그인 과정을 거친 후
    // redirect uri로 요청을 보내게 됨.
    String uri = "https://kauth.kakao.com/oauth/authorize";
    uri += "?client_id=" + kakaoAppKey;
    uri += "&redirect_uri=" + kakaoRedirectUri;
    uri += "&response_type=code";
    return "redirect:" + uri;
}

 

2. 이후 컨트롤러에서 인가 코드를 받습니다

// 약속된 redirect uri로 인가 코드가 옵니다.
@GetMapping("/auth/kakao")
public void authCodeKakao(@RequestParam String code) {
    log.info("인가 코드: {}", code);
    // 인가 코드를 가지고 카카오 인증 서버에게 토큰 발급을 요청하자
    // (server to server 통신)
    Map<String, String> params = new HashMap<>();
    params.put("appKey", kakaoAppKey);
    params.put("redirect", kakaoRedirectUri);
    params.put("code", code);
    kakaoService.login(params);
}

 

3. 이후 서비스에서 로그인 처리를 요청합니다.

// 로그인 처리
public void login(Map<String, String> params) {

    String accessToken = getKakaoAccessToken(params);
    // 발급받은 엑세스 토크으로 사용자 정보 가져오기
    getKakoUserInfo(accessToken);
}

private void getKakoUserInfo(String accessToken) {
    String requestUri = "https://kapi.kakao.com/v2/user/me";

    //요청 헤더
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + accessToken);
    headers.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

    // 요청 보내기
    RestTemplate Template = new RestTemplate();
    ResponseEntity<Map> responseEntity = Template.exchange(
            requestUri,
            HttpMethod.POST,
            new HttpEntity<>(headers),
            Map.class
    );
}

 

4. 이후 토급을 요청하고 이에 따라 응답데이터를 JSON으로 추출한후에 토큰을 추출합니다.

// 토큰 발급 요청
private String getKakaoAccessToken(Map<String, String> requestParam) {

    // 요청 URI
    String requestUri = "https://kauth.kakao.com/oauth/token";

    // 요청 헤더 설정
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    // 요청 바디에 파라미터 세팅
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("client_id", requestParam.get("appKey"));
    params.add("redirect_uri", requestParam.get("redirect"));
    params.add("code", requestParam.get("code"));

    // 카카오 인증 서버로 Post 요청 날리기
    RestTemplate template = new RestTemplate();

    // 헤더 정보와 파라미터를 하나로 묶기
    HttpEntity<Object> requestEntity = new HttpEntity<>(params, headers);

    /*
    - RestTemplate 객체가 REST API 통신을 위한 API인데 (자바스크립트 fetch역할)
            - 서버에 통신을 보내면서 응답을 받을 수 있는 메서드가 exchange
    param1: 요청 URL
    param2: 요청 방식 (get, post, put, patch, delete...)
    param3: 요청 헤더와 요청 바디 정보 - HttpEntity로 포장해서 줘야 함
    param4: 응답결과(JSON)를 어떤 타입으로 받아낼 것인지 (ex: DTO로 받을건지 Map으로 받을건지)
     */
    ResponseEntity<Map> responseEntity
            = template.exchange(requestUri, HttpMethod.POST, requestEntity, Map.class);

    // 응답 데이터에서 JSON 추출
    Map<String, Object> responseJSON = responseEntity.getBody();
    log.info("응답 JSON 데이터: {}", responseJSON);

    // access token 추출 (카카오 로그인 중인 사용자의 정보를 요청할 때 써야합니다.)
    String accessToken = (String) responseJSON.get("access_token");
    return accessToken;
}

 

5. DTO를 이용해서 우리 서버에 있는 것이 맞는 지를확인하기 위해 필요한 정보를 json에서 추출합니다.

여기서 JsonProperty 어노테이션을 이용하여 원래의 (JSON)에서 이름을 명시해야 합니다.

또한 Properties는 어차피 카카오에서 보낸 JSON에 들어있는 정보중에 필요한것만 추출한것이 enumclass를 만들어 사용합니다.

@Setter@Getter@ToString
@NoArgsConstructor
@AllArgsConstructor
public class KakaoUserResponseDTO {

    private long id;

    @JsonProperty("connected_at")
    private LocalDateTime connectedAt;

    private Properties properties;

    @JsonProperty("kakao_account")
    private KakaoAccount account;

    @Setter@Getter@ToString
    public static class Properties {
        private String nickname;

        @JsonProperty("profile_image")
        private String progileImage;

        @JsonProperty("thumbnail_image")
        private String thumbnail;
    }

    @Setter@Getter@ToString
    public static class KakaoAccount {
        private String email;
    }

}

 

6. 카카오 서비스도 DTO와 관련된 것으로 바꿔 가독성을 높입니다

// 요청 보내기
RestTemplate Template = new RestTemplate();
ResponseEntity<KakaoUserResponseDTO> responseEntity = Template.exchange(
        requestUri,
        HttpMethod.POST,
        new HttpEntity<>(headers),
        KakaoUserResponseDTO.class
);

 

7. 만든사이트에 회원가입 시키는 로직

// 우리사이트 회원가입 시키기
if (!memberService.checkIdentifier("email","kakaoUser.getAccount().getEmail()"));{

    //한번도 카카오 로그인을 한적이 없다면 회원가입이 들어간다.
    memberService.join(SignUpRequestDto.builder()
                    .account(String.valueOf(kakaoUser.getId()))
                    .password(UUID.randomUUID().toString())
                    .name(kakaoUser.getProperties().getNickname())
                    .email(kakaoUser.getAccount().getEmail())
            .build(),
            kakaoUser.getProperties().getProgileImage() );

    //우리사이트 로그인 처리
    memberService.maintainLoginState(session, String.valueOf(kakaoUser.getId()));
}

 

8. 다시 화면으로 돌아오게 하여 사용자의 편의성을 확보한다.

// 약속된 redirect uri로 인가 코드가 옵니다.
@GetMapping("/auth/kakao")
public String  authCodeKakao(@RequestParam String code,
                          HttpSession session) {
    log.info("인가 코드: {}", code);
    // 인가 코드를 가지고 카카오 인증 서버에게 토큰 발급을 요청하자
    // (server to server 통신)
    Map<String, String> params = new HashMap<>();
    params.put("appKey", kakaoAppKey);
    params.put("redirect", kakaoRedirectUri);
    params.put("code", code);
    kakaoService.login(params, session);
    
    //로그인 처리 완료되면 홈화면으로 보내자
    return "redirect:/";
}