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

시은·2024년 2월 18일
0
post-thumbnail

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

일반적으로 메일로 임시 비밀번호를 발급해줍니다.

하지만 임시 비밀번호를 발급받게 된다면, 저희팀 프로젝트 비밀번호 변경 프로세스상 로그인을 하고 프로필에 들어가서 임시비밀번호를 입력하고 새 비밀번호로 교체 해야합니다. 그러면 유저입장에서는 너무 귀찮겠죠.

그래서 비밀번호를 변경할 수 있는 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;
}

이를 통해 받은 url 을 이용하여 새로운 비밀번호를 설정하는 컨트롤러를 생성하였습니다.

*@PostMapping*("/change")
public void **getUriMailToken(*@RequestParam String tempToken*, *@RequestBody ChangePasswordDto changePasswordDto*) {
    forgotPasswordService.changePassword(*changePasswordDto*, *tempToken*);
}

4. 권한 문제

에러로그에서 계속 권한문제가 나타났습니다. 따라서 아래와 같이 시큐리티설정과 웹토큰 설정을 변경해주었습니다.

// JwtAuthenticationFilter
requestURI.startsWith("/password")

// SecurityConfig
.requestMatchers("/password/**").permitAll()

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

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

tokenUtil

public String getEmailFromToken(String token) {
        String email = Jwts.parser().setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody().getSubject();
        return email;
    }

문제가 발생한 메소드는 이부분이였습니다. 곰곰히 생각해보니 소셜로그인이 들어오면서 토큰 생성시 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;
    }

이메일인줄 알았던 값은 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));

테스트

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

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");

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>

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

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

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

/password/change

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

console.log

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

profile
창의력 대장이 되기 위한 여정

0개의 댓글