Post

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

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

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

그래서 비밀번호를 변경할 수 있는 url 을 이메일로 전송하고 그 url 을 클릭하면 비밀번호를 바꿀 수 있는 창을 띄우도록 서비스 로직을 세웠습니다.



2. 비밀번호 변경 서비스 로직

처음에는 비밀번호 수정을 원할 경우, 이메일을 입력시 해당 이메일에 임시 비밀번호를 발송하고, 이를 DB에서 수정하는 것으로 생각했습니다. 하지만 저희 웹 특성상 개인정보를 받는 것이 이메일 밖에 없기 때문에 누군가 악의적으로 이메일을 입력 후 비밀번호 변경 시도할 경우 비밀번호가 계속 바꿀 수 있다는 생각이 들었습니다.

따라서 이메일을 입력하면 비밀번호를 변경할 수 있는 url을 이메일로 보내는 것으로 결정했습니다.

  1. 비밀번호 변경 url을 입력한 메일로 전송합니다.
  2. 입력한 메일은 sns 로그인이 아닌 저희 웹에서 회원가입한 계정이므로 이메일과 provider는 local 인 것으로 DB에서 찾습니다.



3. 문제의 발생

준형 님이 아래와 같이 유저에게 임시토큰을 담은 url 을 메일로 보내는 데에 성공했습니다.

https://velog.velcdn.com/images/sieunnnn/post/625882d7-00c8-4692-9537-a58023b2e031/image.png

하지만 이 링크를 postman 으로 responseBody 에 새로운 비밀번호를 담아 보내면 아무런 반응이 나타나지 않는 것이였습니다.



4. 문제의 해결

4.1. SSL 인증서 문제

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

4.2. 데이터 바인딩 문제

@Setter 와 @Builder 그리고 Jackson 에러 를 참고하면 알 수 있듯이 json → dto 로 변경을 하기 위해서는 기본 생성자가 필요합니다. 따라서 ChangePasswordDto@NoArgsConstructor 를 추가해주었습니다.

또한 @RequestParam 로 이미 임시토큰을 받기 때문에 dto 에서 해당 필드를 제거해야 합니다.

4.3. 컨트롤러 생성

메일로 받은 url 을 클릭하면 비밀번호를 변경할 수 있는 주소를 반환하도록 컨트롤러를 생성하였습니다.

1
2
3
4
@GetMapping("/callback")
public String getChangePasswordUrl(@RequestParam String tempToken) {
    return "http://localhost:5173/password/change?tempToken=" + tempToken;
}

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

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



5. 권한 문제

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

1
2
3
4
5
// JwtAuthenticationFilter
requestURI.startsWith("/password")

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



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

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

6.1. tokenUtil

1
2
3
4
5
6
public String getEmailFromToken(String token) {
        String email = Jwts.parser().setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody().getSubject();
        return email;
    }

문제가 발생한 메소드는 이부분이였습니다. 곰곰히 생각해보니 소셜로그인이 들어오면서 토큰 생성시 userId 로 토큰을 만들고 있었다는 것을 간과하였습니다. 따라서 아래와 같이 메서드를 변경하여 주었습니다.

1
2
3
4
5
6
7
8
9
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 였기 때문에 계속 오류가 발생하는 것이였습니다. 따라서 이메일을 찾는 로직을 더 추가해주었습니다.



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

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

ForgotPasswordService

1
2
3
4
5
6
7
// 이메일을 사용하여 멤버 찾기
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));



8. 테스트

8.1. 리퀘스트 보내기

https://velog.velcdn.com/images/sieunnnn/post/a56b2dfb-7c23-4e2e-9400-5f17ed467bf6/image.png

8.2. 메일 확인하기

https://velog.velcdn.com/images/sieunnnn/post/4be39375-c249-48ac-81a5-d03b4a0a8456/image.png

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

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

https://velog.velcdn.com/images/sieunnnn/post/3e633467-1007-44de-9998-d764f8d1ac30/image.png

https://velog.velcdn.com/images/sieunnnn/post/fec4fea3-4c28-4865-8725-d0c95ba3ae0a/image.png

https://velog.velcdn.com/images/sieunnnn/post/450e2737-b4d0-4bc0-b820-c4403f284077/image.png



9. localhost:8080 → dev.travel-planner.xyz

위와같이 localhost:8080 으로 요청을 보내면, front 에서는 어떻게 이를 잡아서 해결해야 할까요?

vue.js 를 이용하여 실제 어떻게 동작하는지 확인해보겠습니다.

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

유저가 비밀번호 분실 버튼을 클릭하고 나오는 페이지에 이메일을 입력하면 다음과 같은 메일이 옵니다.

https://velog.velcdn.com/images/sieunnnn/post/9a95f78f-efb6-4918-8e97-832cf915507d/image.png

유저의 정보가 담긴 임시토큰을 리퀘스트 @RequestParam 로 담아서 보내주게 됩니다.

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

링크를 클릭하면 화면에 아래와 같은 url 이 나타납니다.

1
https://dev.travel-planner.xyz/password/change?tempToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMCIsImlhdCI6MTY5NDk2MzM4NSwiZXhwIjoxNjk0OTY1MTg1fQ.zuMR5NzljWcuVG6x_U_FEknewP_uQJE-GoAYSpTkJfE

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

https://velog.velcdn.com/images/sieunnnn/post/c16724c2-8cb8-4f3b-9dbf-294b6af3de79/image.png

https://velog.velcdn.com/images/sieunnnn/post/edd26352-bc0b-45b3-87ec-f39b633908f6/image.png



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

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

10.1. axios 설정하기

1
2
3
4
5
6
7
8
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");

10.2. App.vue 작성하기

단순히 테스트용 이므로 App.vue 에 작성해보았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<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>

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

https://velog.velcdn.com/images/sieunnnn/post/9c14fe30-7ac7-4a49-9a1e-76743daab126/image.png

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

https://velog.velcdn.com/images/sieunnnn/post/98b56a38-5325-491f-9457-67eabc68f01d/image.png

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

/password/change

https://velog.velcdn.com/images/sieunnnn/post/edac8162-63e3-4f74-9f54-2cfd52335a94/image.png

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

https://velog.velcdn.com/images/sieunnnn/post/010f00a9-2af0-45fe-a649-1b4877dc3e4c/image.png

console.log

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

https://velog.velcdn.com/images/sieunnnn/post/40aad102-354d-4597-b251-f2631e48947a/image.png

This post is licensed under CC BY 4.0 by the author.