///
Search
📧

비밀번호를 잊은 유저들을 위해 비밀번호 변경 메일 발송하기 with vue.js

태그
SMTP
vue.js
분류
Spring Boot

비밀번호를 잊어버린 유저는 어떻게 해야 하나요?

일반적으로 메일로 임시 비밀번호를 발급해줍니다.
하지만 임시 비밀번호를 발급받게 된다면, 저희팀 프로젝트 비밀번호 변경 프로세스상 로그인을 하고 프로필에 들어가서 임시비밀번호를 입력하고 새 비밀번호로 교체 해야합니다. 그러면 유저입장에서는 너무 귀찮겠죠.
그래서 비밀번호를 변경할 수 있는 url 을 이메일로 전송하고 그 url 을 클릭하면 비밀번호를 바꿀 수 있는 창을 띄우도록 서비스 로직을 세웠습니다.

비밀번호 변경 서비스 로직

처음에는 비밀번호 수정을 원할 경우, 이메일을 입력시 해당 이메일에 임시 비밀번호를 발송하고, 이를 DB에서 수정하는 것으로 생각했습니다. 하지만 저희 웹 특성상 개인정보를 받는 것이 이메일 밖에 없기 때문에 누군가 악의적으로 이메일을 입력 후 비밀번호 변경 시도할 경우 비밀번호가 계속 바꿀 수 있다는 생각이 들었습니다.
따라서 이메일을 입력하면 비밀번호를 변경할 수 있는 url을 이메일로 보내는 것으로 결정했습니다.
1.
비밀번호 변경 url을 입력한 메일로 전송합니다.
2.
입력한 메일은 sns 로그인이 아닌 저희 웹에서 회원가입한 계정이므로 이메일과 provider는 local 인 것으로 DB에서 찾습니다.

문제의 발생

준형 님이 아래와 같이 유저에게 임시토큰을 담은 url 을 메일로 보내는 데에 성공했습니다.
하지만 이 링크를 postman 으로 responseBody 에 새로운 비밀번호를 담아 보내면 아무런 반응이 나타나지 않는 것이였습니다.

문제의 해결

1. SSL 인증서 문제

처음에는 https:// 로 시작하도록 적었기 때문에 ssl 인증을 할 수 없다는 오류가 발생하였습니다. 따라서 https:// → http:// 로 변경해주었습니다. 하지만 이는 서버에 배포할 때 알맞은 엔드포인트로 변경해주어야 합니다.

2. 데이터 바인딩 문제

@Setter 와 @Builder 그리고 Jackson 에러 Feat.Editor 를 참고하면 알 수 있듯이 json → dto 로 변경을 하기 위해서는 기본 생성자가 필요합니다. 따라서 ChangePasswordDto 에 @NoArgsConstructor 를 추가해주었습니다.
또한 @RequestParam 로 이미 임시토큰을 받기 때문에 dto 에서 해당 필드를 제거해야 합니다.

3. 컨트롤러 생성

메일로 받은 url 을 클릭하면 비밀번호를 변경할 수 있는 주소를 반환하도록 컨트롤러를 생성하였습니다.
@GetMapping("/callback") public String getChangePasswordUrl(@RequestParam String tempToken) { return "http://localhost:5173/password/change?tempToken=" + tempToken; }
Java
복사
이를 통해 받은 url 을 이용하여 새로운 비밀번호를 설정하는 컨트롤러를 생성하였습니다.
*@PostMapping*("/change") public void **getUriMailToken(*@RequestParam String tempToken*, *@RequestBody ChangePasswordDto changePasswordDto*) { forgotPasswordService.changePassword(*changePasswordDto*, *tempToken*); }
Java
복사

4. 권한 문제

에러로그에서 계속 권한문제가 나타났습니다. 따라서 아래와 같이 시큐리티설정과 웹토큰 설정을 변경해주었습니다.
// JwtAuthenticationFilter requestURI.startsWith("/password") // SecurityConfig .requestMatchers("/password/**").permitAll()
Java
복사

5. 토큰에서 이메일 받아오기

tokenUtil.getEmailFromToken 메서드를 사용하여 유저의 이메일을 받아오고, 이를 이용하여 유저의 비밀번호를 변경해야 합니다. 하지만 존재하는 유저임에도 불구하고 계속 존재하지 않는 유저라는 에러가 발생했습니다.

tokenUtil

public String getEmailFromToken(String token) { String email = Jwts.parser().setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody().getSubject(); return email; }
Java
복사
문제가 발생한 메소드는 이부분이였습니다. 곰곰히 생각해보니 소셜로그인이 들어오면서 토큰 생성시 userId 로 토큰을 만들고 있었다는 것을 간과하였습니다. 따라서 아래와 같이 메서드를 변경하여 주었습니다.
public String getEmailFromToken(String token) { String userId = Jwts.parser().setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody().getSubject(); String email = memberRepository.findById(Long.parseLong(userId)).get().getEmail(); log.info("-------------------------유저 이메일: " + email); return email; }
Java
복사
이메일인줄 알았던 값은 userId 였기 때문에 계속 오류가 발생하는 것이였습니다. 따라서 이메일을 찾는 로직을 더 추가해주었습니다.

6. 비밀번호 변경하려는 멤버 찾기

현재 소셜 로그인과 로컬로그인에서 이메일이 겹치는 경우가 생길 수 있습니다. 때문에 provider 를 이용해서 유일한 멤버를 도출해야 합니다. 따라서 아래와 같이 코드를 수정하였습니다.

ForgotPasswordService

// 이메일을 사용하여 멤버 찾기 Member memberToUpdate = memberRepository.findByEmail(email) .orElseThrow(() -> new ApiException(ErrorType.USER_NOT_FOUND)); // 이메일을 사용하여 멤버 찾기 Member memberToUpdate = memberRepository.findByEmailAndProvider(email, "local") .orElseThrow(() -> new ApiException(ErrorType.USER_NOT_FOUND));
Java
복사

테스트

1. 리퀘스트 보내기

2. 메일 확인하기

3. 해당 링크로 적절한 값 보내기

링크를 클릭하면 나오는 이 주소를 포스트맨에 입력하고, 적절한 값을 넣어줍니다.

localhost:8080 → dev.travel-planner.xyz

위와같이 localhost:8080 으로 요청을 보내면, front 에서는 어떻게 이를 잡아서 해결해야 할까요?
vue.js 를 이용하여 실제 어떻게 동작하는지 확인해보겠습니다.

비밀번호 변경 서비스 로직

1. 유저에게 비밀번호 변경 url 이 담긴 메일 보내기

유저가 비밀번호 분실 버튼을 클릭하고 나오는 페이지에 이메일을 입력하면 다음과 같은 메일이 옵니다.
유저의 정보가 담긴 임시토큰을 리퀘스트 @RequestParam 로 담아서 보내주게 됩니다.

2. 링크를 클릭하여 비밀번호 변경 url 얻기

링크를 클릭하면 화면에 아래와 같은 url 이 나타납니다.
https://dev.travel-planner.xyz/password/change?tempToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMCIsImlhdCI6MTY5NDk2MzM4NSwiZXhwIjoxNjk0OTY1MTg1fQ.zuMR5NzljWcuVG6x_U_FEknewP_uQJE-GoAYSpTkJfE
Java
복사

3. 새로운 비밀번호를 요청바디에 넣어 비밀번호 변경하기

프론트에서는 이를 어떻게 처리해야할까?

이론상으로는 맞는 것 같으나 뭔가 프팀에게 건내주기 찝찝한 건은 로직을 잘 알고있는 백팀이 테스트를 해 볼 필요가 있습니다. 그래서 저는 vue.js 를 이용하여 간단하게 코드를 작성해봤습니다. (기본적인 설명은 생략합니다.)

1. axios 설정하기

import { createApp } from "vue"; import App from "./App.vue"; import axios from "axios"; axios.defaults.baseURL = "https://dev.travel-planner.xyz"; const app = createApp(App); app.config.globalProperties.axios = axios; app.mount("#app");
Java
복사

2. App.vue 작성하기

단순히 테스트용 이므로 App.vue 에 작성해보았습니다.
<template> <div> <h1>비밀번호 변경하기</h1> <input type="text" v-model="newPass"> <button @click="changePasswordTest()">button</button> </div> </template> <script> export default { name: "About", components: [], data() { return { redirectUrl: "", password: "", newPass: "" }; }, methods: { get() { this.axios.get("/password/callback"+ window.location.search).then((response) => { console.log('현재 주소: ', window.location.href) console.log('redirectUrl: ', response.data) this.redirectUrl = response.data; }) }, changePasswordTest() { console.log('새로 변경한 비밀번호: ', this.newPass) this.axios.post(this.redirectUrl, { newPassword: this.newPass }).then((response) => { this.password = response.data; }); } }, mounted() { this.get(); }, }; </script>
JavaScript
복사

3. 결과 확인하기 Feat.개발자도구

/password/callbackResponse 탭에서 스트링으로 반환한 비밀번호 변경 url 도 확인할 수 있습니다.

https: //dev.travel-planner.xyz/password/change?tempToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMCIsImlhdCI6MTY5NDk2NDIwMSwiZXhwIjoxNjk0OTY2MDAxfQ.1GB6n64Yoq7OeVhNaNP3y3ocxkGc9Qj92HnG_PWb-qA
Plain Text
복사

/password/change

응답은 따로 없으므로 서버에서 결과를 마저 확인합니다.

console.log

콘솔로 직접 값을 확인 해보는것도 절 대 놓치면 안됩니다.