[JWT] JWT 구현하기(Feat. Redis) (7) - Controller(AuthApiController), Postman & Redis에서 실행해보기, GitHub 공유

u-nij·2022년 10월 25일
0

JWT 구현하기

목록 보기
8/8
post-thumbnail

AuthApiController.class

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthApiController {

    private final AuthService authService;
    private final UserService userService;
    private final BCryptPasswordEncoder encoder;

    private final long COOKIE_EXPIRATION = 7776000; // 90일

    // 회원가입
    @PostMapping("/signup")
    public ResponseEntity<Void> signup(@RequestBody @Valid AuthDto.SignupDto signupDto) {
        String encodedPassword = encoder.encode(signupDto.getPassword());
        AuthDto.SignupDto newSignupDto = AuthDto.SignupDto.encodePassword(signupDto, encodedPassword);

        userService.registerUser(newSignupDto);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    // 로그인 -> 토큰 발급
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody @Valid AuthDto.LoginDto loginDto) {
        // User 등록 및 Refresh Token 저장
        AuthDto.TokenDto tokenDto = authService.login(loginDto);

        // RT 저장
        HttpCookie httpCookie = ResponseCookie.from("refresh-token", tokenDto.getRefreshToken())
                .maxAge(COOKIE_EXPIRATION)
                .httpOnly(true)
                .secure(true)
                .build();

        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, httpCookie.toString())
                // AT 저장
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenDto.getAccessToken())
                .build();
    }

    @PostMapping("/validate")
    public ResponseEntity<?> validate(@RequestHeader("Authorization") String requestAccessToken) {
        if (!authService.validate(requestAccessToken)) {
            return ResponseEntity.status(HttpStatus.OK).build(); // 재발급 필요X
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); // 재발급 필요
        }
    }
    // 토큰 재발급
    @PostMapping("/reissue")
    public ResponseEntity<?> reissue(@CookieValue(name = "refresh-token") String requestRefreshToken,
                                     @RequestHeader("Authorization") String requestAccessToken) {
        AuthDto.TokenDto reissuedTokenDto = authService.reissue(requestAccessToken, requestRefreshToken);

        if (reissuedTokenDto != null) { // 토큰 재발급 성공
            // RT 저장
            ResponseCookie responseCookie = ResponseCookie.from("refresh-token", reissuedTokenDto.getRefreshToken())
                    .maxAge(COOKIE_EXPIRATION)
                    .httpOnly(true)
                    .secure(true)
                    .build();
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .header(HttpHeaders.SET_COOKIE, responseCookie.toString())
                    // AT 저장
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + reissuedTokenDto.getAccessToken())
                    .build();

        } else { // Refresh Token 탈취 가능성
            // Cookie 삭제 후 재로그인 유도
            ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "")
                    .maxAge(0)
                    .path("/")
                    .build();
            return ResponseEntity
                    .status(HttpStatus.UNAUTHORIZED)
                    .header(HttpHeaders.SET_COOKIE, responseCookie.toString())
                    .build();
        }
    }

    // 로그아웃
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String requestAccessToken) {
        authService.logout(requestAccessToken);
        ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "")
                .maxAge(0)
                .path("/")
                .build();

        return ResponseEntity
                .status(HttpStatus.OK)
                .header(HttpHeaders.SET_COOKIE, responseCookie.toString())
                .build();
    }
}
  • signUp: 회원을 등록한다. 중복 이메일에 대한 검사가 추가적으로 필요하지만, 우선 단순하게 진행하겠다. 원래는 UserService에서 암호화하려고 했지만, 순환 참조 문제로 BCryptPasswordEncoder를 Controller단으로 가져왔다.
  • login: authService.login 메서드를 실행하고 토큰을 발급받는다. RT를 HTTP-ONLY Secure Cookie로, AT를 Authorization Header에 담아 보낸다.
  • validate: AT를 재발급받을 필요가 없다면 상태 코드 OK(200)을 반환하고, 재발급받아야 한다면 401을 반환한다.
  • reissue: validate 요청으로부터 UNAUTHORIZED(401)을 반환받았다면, 프론트에서 Cookie와 Header에 각각 RT와 AT를 요청으로 받아서 authService.reissue를 통해 토큰 재발급을 진행한다. 토큰 재발급이 성공한다면 login과 마찬가지로 응답 결과를 보내고, 토큰 재발급이 실패했을때(null을 반환받았을 때) Cookie에 담긴 RT를 삭제하고 재로그인을 유도한다.
  • logout: authService.logout을 진행한 후, Cookie에 담긴 RT를 삭제한다.

Postman & Redis

회원가입

로그인

  • Redis

재발급

로그아웃

  • Redis

이상 Spring Boot에서 Redis와 Spring Security를 이용해 JWT를 구현해보고, Postman에서 테스트해보는 시간을 가졌다. 글을 쓰기까지 JWT에 대해 공부하고, 직접 구현하고, 디버깅해보면서 Spring Security의 Filter Chain과 Authentication Architecture가 돌아가는 방식에 대해 더 자세하게 알 수 있었다. 더 좋은 코드와 서비스를 위해 리팩토링할 부분이 남아 있지만, 성능을 위해 어떤 것을 고려하고 선택해야 하는지 조금이나마 공부해보았다. 이어서, OAuth2.0 OPEN API를 사용해보겠다.

GitHub

https://github.com/u-nij/Authentication-Using-JWT
지금까지 제가 작성한 코드를 깃허브 저장소에 남겨두었습니다.(글로 작성된 Spring Boot 버전과 약간 다릅니다.) 도움이 되셨다면 Star를 눌러주세요!😁✨

4개의 댓글

comment-user-thumbnail
2023년 4월 5일

좋은 포스트 감사합니다! 흐름을 이해하는데 도움이 되었습니다 :)

1개의 답글
comment-user-thumbnail
2023년 4월 13일

안녕하세요!! 좋은 포스트 감사합니다!! 코드 참고해가면서 열심히 공부하고 있습니다!
혹시 공부를 하면서 궁금한점이 있는데
요청이 들어왔을 경우 JwtAuthenticationFilter에서 JwtTokenProvider의 AccessToken을 검증하는 메서드에서 토큰이 만료되었을 경우에도 True를 반환하시는 이유가 있는지 궁금합니다.
AccessToken이 만료가되면 False를 반환해야하는게 흐름상 맞는게 아닌가 싶은데 혹시 제가 생각하지못하는 다른 이유가 있으신건 아닌지 궁금합니다!!

1개의 답글