프로젝트 2일차 실제로 작성한 명세서와 와이어프레임, ERD를 참고하여 각자 역할을 나누어서 기능을 구현하기 시작했다.
내가 자진해서 맡은 역할은 JWT를 이용해서 사용자 인증기능을 구현하는 기능을 맡았다 이유는 JWT가 익숙하지 않아서 익숙해지고 싶어서 자진해서 역할을 맡았다.
위의 이미지가 내가 맡은 기능의 전체적인 흐름이다.
먼저 로그인 기능을 구현했습니다. 사용자가 로그인 요청을 보내면 서버는 사용자의 자격 증명(이메일과 비밀번호)을 확인하고, 올바른 경우 JWT를 생성하여 클라이언트에 반환합니다.
JWT는 Authorization 헤더에 "Bearer 토큰" 형식으로 담겨 응답됩니다. 이후 클라이언트는 이 토큰을 사용하여 인증이 필요한 API 요청을 할 수 있습니다.
로그인 요청 처리 흐름:
@PostMapping("/auth/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) {
String bearerToken = authService.login(loginRequest);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, bearerToken)
.build();
}
JWT 검증 필터(JwtFilter)는 클라이언트가 보낸 요청의 헤더에서 JWT를 추출해 검증하는 역할을 합니다. 이 필터는 모든 요청에 대해 동작하지만, /auth로 시작하는 URL은 필터링 대상에서 제외됩니다.
필터 처리 흐름:
public class JwtFilter extends OncePerRequestFilter {
// JWT 검증 로직 구현
}
로그아웃 구현은 JWT를 만료시키는 방식으로 이루어집니다. 클라이언트가 로그아웃 요청을 보내면 서버는 JWT가 담긴 쿠키 또는 헤더를 만료시키거나 삭제 처리합니다. 여기서는 헤더에 담긴 JWT를 만료 처리하는 방법을 사용했습니다.
로그아웃 처리 흐름:
@PostMapping("/auth/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
// JWT 무효화 로직
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, "")
.build();
}
참고
서버가 헤더에서 JWT를 제거하면, 클라이언트는 이후 요청에서 더 이상 유효한 토큰을 보유하지 않게 되므로, 재인증이 필요합니다.
비밀번호 찾기 기능은 사용자가 이메일을 통해 비밀번호 재설정 요청을 하면 JWT 기반의 토큰을 발급하여 이메일로 전달하는 방식으로 구현. 이 토큰을 사용해 비밀번호를 재설정할 수 있습니다.
비밀번호 재설정 흐름:
사용자가 비밀번호 재설정 요청을 /auth/reset로 보냅니다.
서버는 JWT 토큰을 생성해 이메일로 발송합니다.
사용자가 토큰과 새로운 비밀번호를 함께 보내면 서버는 해당 토큰을 검증한 후 비밀번호를 변경합니다.
@PostMapping("/auth/reset-password")
public ResponseEntity<ApiResponse> resetPassword(@RequestBody ResetPasswordRequest request) {
// 비밀번호 재설정 로직
return ResponseEntity.ok(new ApiResponse("비밀번호가 성공적으로 변경되었습니다.", HttpStatus.OK.value()));
}
이러한 흐름으로 만들려고 하였으나 벡엔드 기능만으로는 구현하기에는 링크를 발급하기 힘들어서 실패
레디스를 이용하여 추후에 작업해볼 것
클라이언트 → 서버: 로그인 요청 (POST /v2/auth/signin)
서버 → 클라이언트: JWT 생성 및 응답 (헤더에 JWT 포함)
설명:
클라이언트가 로그인 정보를 포함한 POST 요청을 /v2/auth/signin으로 보냄.
서버가 JWT를 생성하여 응답.
클라이언트가 응답 헤더에서 JWT를 저장하고 인증에 사용.
설명:
클라이언트가 요청을 서버로 전송.
서버가 Authorization 헤더에서 JWT를 추출.
서버가 JWT의 유효성을 검증.
유효성 검증 결과에 따라 요청을 처리하거나 오류 응답을 반환.
클라이언트가 로그아웃 요청
서버에서 쿠키 만료 처리
서버가 로그아웃 성공 응답
클라이언트에서 쿠키 삭제
설명:
클라이언트가 로그아웃 요청을 서버로 전송.
서버가 쿠키를 만료 처리.
서버가 로그아웃 성공 메시지를 응답.
클라이언트가 쿠키를 삭제.
문제 설명: 로그인 처리 중 LoginRequest라는 객체를 java.lang.String으로 변환하려고 시도했으나, 변환이 불가능해 오류가 발생했습니다. 이 문제는 Hibernate나 데이터베이스 처리 과정에서 객체를 문자열로 변환할 수 없기 때문에 발생한 것입니다.
해결 방법: LoginRequest 객체를 직접 문자열로 변환하는 대신, 이메일 속성을 추출하여 사용하도록 수정했습니다. Optional findByEmail(LoginRequest loginRequest)를 Optional findByEmail(String email)로 변경하여 이메일을 직접 문자열로 처리하도록 했습니다.
변경 사항:
// 변경 전
Optional<User> findByEmail(LoginRequest loginRequest);
// 변경 후
Optional<User> findByEmail(String email);
설명: 이렇게 하면 LoginRequest 객체 대신 이메일 문자열만을 데이터베이스 조회에 사용할 수 있습니다. 이는 객체 변환 문제를 피할 수 있는 방법입니다.
문제 설명: 로그아웃 기능 구현 시 쿠키를 헤더에 담지 않아, 유저 정보를 조회할 때 문제가 발생했습니다. 이로 인해 400 Bad Request 오류가 발생했습니다.
해결 방법: addJwtToCookie 메서드를 구현하여 JWT를 쿠키에 담아 전송하도록 수정했습니다. 쿠키가 제대로 설정되면, 클라이언트는 이후 요청에서 해당 쿠키를 포함하여 서버와 상호작용할 수 있습니다.
설명:
쿠키를 설정하고 JWT를 쿠키에 저장하여 클라이언트가 이를 포함하여 서버로 요청하도록 합니다.
쿠키 설정 예시:
Cookie cookie = new Cookie("Authorization", token);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
문제 설명: JWT를 쿠키에서 추출할 때, JWT가 URL에서 인코딩된 상태여서 디코딩이 제대로 이루어지지 않았습니다. 이로 인해 인증에 실패하거나 오류가 발생했습니다.
해결 방법: URLDecoder.decode() 메서드를 사용하여 쿠키 값을 디코딩했습니다. 이를 통해 인코딩된 JWT를 원래의 형태로 복원할 수 있었습니다.
변경 사항:
String token = URLDecoder.decode(cookie.getValue(), "UTF-8");
설명:
URL 인코딩된 쿠키 값을 디코딩하여 원래의 JWT를 복원합니다.
디코딩된 JWT를 사용하여 인증을 처리하면 문제를 해결할 수 있습니다.
문제 설명: 헤더에서 Authorization 값을 선택적으로 받아야 하는 경우에, 해당 값이 없을 때의 처리가 부족하여 오류가 발생했습니다.
해결 방법: @RequestHeader(value = "Authorization", required = false)를 사용하여 헤더의 Authorization 값이 없어도 오류가 발생하지 않도록 설정했습니다. 이 설정으로 헤더가 없는 경우에도 요청을 정상적으로 처리할 수 있습니다.
변경 사항:
@RequestHeader(value = "Authorization", required = false) String authHeader
설명:
required = false로 설정하면 헤더가 없어도 요청이 처리됩니다. 값이 없는 경우에는 null이 주입되며, 이 값을 통해 선택적인 처리를 할 수 있습니다.