[TIL] 241111 HttpServletRequest와 HttpServletResponse, JWT

MONA·2024년 11월 11일

나혼공

목록 보기
24/92

로그인, 로그아웃 기능을 구현하면서 HttpServletResponse 객체를 주로 사용했는데, HttpServletRequest 객체가 있음을 알게 되었고 둘의 차이가 궁금해서 찾아보게 되었다.

HttpServletRequest와 HttpServletResponse

  • 클라이언트와 서버 간의 HTTP 통신을 담당하는 객체
  • 각각 요청과 응답 정보를 처리하는 데 사용됨

HttpServletRequest

  • 클라이언트가 서버로 보내는 Http 요청 정보를 담고 있는 객체
  • 클라이언트가 보낸 요청 헤더, URL, 요청 파라미터, HTTP 메서드 등 다양한 정보 제공
  • 일반적으로 컨트롤러나 서비스 레이어에서 요청에 포함된 데이터(Authorization 헤더의 토큰이나 요청 파라미터 등)을 가져올 때 사용됨

주요 메서드

  • getParameter(String name): 요청 파라미터를 가져옴
  • getHeader(String name): 요청 헤더를 가져옴
  • getMethod(): HTTP 메서드(GET, POST)를 반환
  • getRequestURI(): 요청 URI를 가져옴
String authToken = request.getHeader("Authorization");
String username = request.getParameter("username");

HttpServletResponse

  • 서버가 클라이언트에게 보내는 HTTP 응답 정보를 담고 있는 객체
  • 응답 상태 코드, 응답 헤더, 쿠키, 바디 등에 대한 정보 설정 가능
  • 클라이언트에게 특정 상태 코드(200, 400, 500 등)를 보내거나 헤더 추가, 쿠키 설정 등을 할 때 주로 이용됨

주요 메서드

  • setStatus(int sc): 응답 상태 코드 설정
  • addHeader(String name, String value): 응답 헤더 추가
  • addCookie(Cookie cookie): 쿠키 추가
  • getWriter(): 응답 본문에 내용을 작성할 수 있는 PrintWriter 객체 반환
response.setStatus(HttpServletResponse.SC_OK);  // 상태 코드 200 설정
response.addHeader("Content-Type", "application/json");  // 헤더 추가

로그인, 로그아웃에서의 HttpServletRequest와 HttpServletResponse

로그인 시 HttpServletResponse을 사용

  • 서버가 클라이언트로 응답하는 과정에서 JWT 토큰을 설정하거나 필요한 정보를 추가해 보내기 위함
// UserController
@PostMapping("/auth/login")
    public ResponseDto<?> login(@RequestBody LoginRequestDto request, HttpServletResponse res) {

        return userService.login(request, res);
    }
// UserService

public ResponseDto<?> login(LoginRequestDto request, HttpServletResponse res) {
        String email = request.getEmail();
        String password = request.getPassword();

        // 존재하는 유저인지 확인
        Optional<P_user> userOptional = userRepository.findByEmail(email);
        if (!userOptional.isPresent()) {
            return new ResponseDto<>(-1, "존재하지 않는 사용자입니다", null);
        }

        P_user user = userOptional.get();

        // 비밀번호 비교
        if (!passwordEncoder.matches(password, user.getPassword())) {
            return new ResponseDto<>(-1, "비밀번호가 일치하지 않습니다", null);
        }

        // JWT 생성, 쿠키 저장
        String token = jwtUtil.createToken(user.getNickname(), user.getRole());
        jwtUtil.addJwtToCookie(token, res);

        Cookie jwtCookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token);
        jwtCookie.setPath("/");
        jwtCookie.setHttpOnly(true); // HttpOnly 설정
        res.addCookie(jwtCookie);

        LoginResponseDto responseDto = LoginResponseDto.builder()
                .nickname(user.getNickname())
                .role(user.getRole().toString())
                .build();

        return new ResponseDto<>(1, "로그인이 성공적으로 완료되었습니다", responseDto);
    }

로그아웃 시 HttpServletRequest와 HttpServletResponse 사용

  • HttpServletRequest를 받아 클라이언트가 보낸 토큰을 추출해 무효화(블랙리스트 처리 등), 세션 종료 등의 처리 할 수 있음
  • HttpServletResponse를 통해 JWT가 담긴 쿠키를 삭제해 클라이언트 측에서도 토큰을 무효화 할 수 있음
    --> 블랙리스트 처리와 같이 빡센 금지가 필요 없다면 그냥 HttpServletResponse만 받아 처리 해도 됨
// UserController
    @PostMapping("/auth/logout")
    public ResponseDto<?> logout(HttpServletResponse res) {
        return userService.logout(res);
    }
// UserService
public ResponseDto<?> logout(HttpServletResponse res) {

        Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, null);
        cookie.setPath("/");
        cookie.setMaxAge(0);
        res.addCookie(cookie);

        return new ResponseDto<>(1, "로그아웃이 완료되었습니다", null);
    }

정리해 본 결과

  • HttpServletResponse: 응답에 추가 정보(쿠키, 헤더 등)를 설정할 때 사용됨
  • HttpServletRequest: 응답에서 추가 정보(쿠키, 헤더)를 추출하여 사용할 때 사용됨

HttpServletRequest를 이용해 마이페이지 조회 기능 구현하기

  • HttpServletRequest를에서 사용자 정보를 추출해 조회, 반환한다.
// UserController

@GetMapping("/mypage")
    public ResponseDto getMypage(HttpServletRequest req) {
        return userService.getMypage(req);
    }
// UserService

public ResponseDto getMypage(HttpServletRequest req) {

        P_user user = validateTokenAndGetUser(req).orElse(null);
        if (user == null) {
            return new ResponseDto<>(-1, "유효하지 않은 토큰이거나 존재하지 않는 사용자입니다", null);
        }

        MypageResponseDto response = MypageResponseDto.builder()
                .email(user.getEmail())
                .nickname(user.getNickname())
                .phone(user.getPhone())
                .birth(user.getBirth())
                .imageProfile(user.getImageProfile())
                .address(user.getAddress())
                .lat(user.getLatLng().getY())
                .lng(user.getLatLng().getX())
                .build();

        return new ResponseDto<>(1, null, response);
    }

JWT

오늘은 거의 절반을 인증 로직이랑 싸웠다.
현재 JWT 토큰 값을 쿠키에 담아 전달하는 방식을 사용하고 있는데, 쿠키에는 공백값이 허용되지 않는다.
망충한 나는 그걸 모르고 테스트를 하면서 쿠키 값에 Bearer 접두사와 공백을 포함했고, 그 결과 유효하지 않은 쿠키 에러가 계속 발생했다.
심지어 쿠키에는 접두사도 필요없다.. 그래서 좀 더 자세히 알아보고 정리할 필요가 있었다.

JWT 저장 및 전송 방식

1. Authorization 헤더에 JWT 토큰을 포함하여 전송

  • 가장 일반적인 방식

  • 클라이언트가 Authorization 헤더에 Bearer 토큰을 포함해 서버로 전송

  • 쿠키 대신 요청 헤더를 사용해 전송

  • 클라이언트가 요청마다 JWT를 Authorization 헤더에 추가해 서버에 전송

  • 장점

    • XSS 공격으로부터 상대적으로 안전
    • CSRF 공격을 예방하기 위해 별도 조치가 필요하지 않음
  • 단점

    • 클라이언트 측에서 Authorization 헤더에 토큰을 추가하는 코드가 필요

2. 세션 저장소에 JWT 저장 (주로 서버 측 세션과 조합)

  • JWT 토큰을 서버 측 세션 저장소에 저장하여 관리하는 방식

  • JWT가 클라이언트가 아닌 서버에서 관리되므로 보안이 강화됨

  • JWT의 장점 중 하나인 무상태성을 활용하지 못하고, 세션 관리를 필요로 하게 됨

  • 장점

    • 서버에서 JWT를 관리, 클라이언트 측 보안 위협이 줄어듦
  • 단점

    • JWT의 무상태성을 포기하고 서버에 세션 관리를 도입해야 함

3. Local Storage 또는 Session Storage에 JWT 저장

  • 클라이언트에서 Local StorageSession Storage에 JWT를 저장

  • 요청 시마다 Authorization 헤더에 토큰을 포함하는 방식

  • JWT를 클라이언트 측에서 직접 관리하므로 쿠키보다 XSS 공격에 취약할 수 있음

  • 장점

    • 구현이 쉽고, 쿠키 설정 없이 Authorization 헤더에 토큰을 직접 추가하여 전송할 수 있음
  • 단점

    • XSS 공격에 취약할 수 있어서 추가적인 보안 조치가 필요

4. 쿠키(Cookie) 방식

  • JWT를 클라이언트의 쿠키에 저장하여 사용

  • 서버로 요청을 보낼 때 토큰을 쿠키에 담아 전송

  • 장점

    • 자동 전송: 브라우저가 쿠키를 자동으로 서버에 전송하므로 인증 헤더를 수동으로 설정할 필요가 없음
    • 보안 설정 기능: HttpOnlySecure 설정을 통해 쿠키 접근을 제한하고 HTTPS에서만 전송하도록 할 수 있음
    • CSRF 방지 기능: 쿠키에 SameSite 옵션을 설정하면 CSRF 공격을 방지할 수 있음
  • 단점

    • 쿠키 사이즈 제한: 쿠키는 크기 제한이 있어 JWT가 길어질 경우 문제가 될 수 있음
    • CORS 설정 필요: 다른 도메인에서 쿠키를 전송하려면 CORS 정책을 맞추고 쿠키 설정을 해야 함

어떤 방법이 좋을까?

  • 보안 요구가 높고 세션을 사용할 수 있다
    -> 서버 측 세션 관리 방식
  • 브라우저 기반 애플리케이션
    -> 쿠키에 JWT를 저장하고 HttpOnly, Secure 옵션을 설정해 CSRF 및 XSS 방지 조치를 하는 것이 안전
  • 간단한 SPA(Single Page Application)
    -> Local Storage나 Session Storage를 이용하고, Authorization 헤더에 토큰을 포함하여 전송하는 방식이 적합

Bearer 접두사

  • 토큰 기반 인증에서 토큰의 유형을 명확히 하기 위해 사용
  • Authorization 헤더에서 'Bearer ' 접두사를 사용하여 서버가 헤더에 포함된 값이 Bearer 토큰(JWT)임을 인식하게 함

Bearer 접두사의 주요 목적
1. 토큰 유형 구분

  • Authorization 헤더는 다양한 인증 방식에서 사용됨
  • Bearer는 서버가 Authorization 헤더에 담긴 값이 JWT와 같은 Bearer 토큰임을 알 수 있게 해주는 접두사임
  • Basic, Digest, Bearer 등 다양한 인증 방식 중 Bearer 방식을 사용하고 있음을 알 수 있음
  1. 간결하고 표준화된 형식 제공
  • Bearer 방식은 IETF(Internet Engineering Task Force)에서 정의한 표준 인증 방식으로 OAuth 2.0 인증 프로토콜에서도 사용됨
  • Authorization: Bearer <token> 이라는 간결한 형식을 제공하여 인증에 일관성을 유지할 수 있게 함

쿠키에 접두사가 필요없는 이유

  1. 쿠키는 이름-값 쌍으로 구분되기 때문에
  • 그냥 쿠키에 token 이라는 이름으로 토큰값을 저장하면 서버는 token이라는 이름을 가진 값이 JWT임을 알 수 있다
  1. 쿠키의 용도가 인증 방식과 무관하기 때문에
  • 쿠키는 인증 뿐 아니라 다른 여러 목적으로도 사용된다
  • 쿠키의 값이 토큰인지 다른 데이터인지는 이름으로 구분 가능하기 때문에 굳이 JWT 토큰이 담긴 쿠키에 Bearer 접두사를 붙일 필요가 없다
  • Bearer 접두사는 오직 Authorization 헤더에서 토큰 인증 방식을 명시하기 위한 접두사이기 때문에 쿠키에서는 사용되지 않는다
  1. 쿠키는 Authorization 헤더와 달리 특정한 인증 프로토콜을 나타내지 않기 때문에
  • Authorization 헤더는 본 인증(Basic Authentication), 다이제스트 인증(Digest Authentication), OAuth 등의 여러 인증 방식에서 사용됨 -> 그래서 Bearer 접두사로 헤더의 값이 Bearer 토큰임을 명확히 해야 함
  • 쿠키는 인증과 무관하게 데이터를 주고받는 목적으로 설계되었기에 JWT를 담을 때에도 별도의 접두사가 필요하지 않음
  1. 서버가 쿠키를 별도로 검증하기 때문에
  • 서버는 쿠키에서 token이나 authToken과 같은 특정 이름의 쿠키 값을 가져와 이를 JWT로 처리하도록 미리 설정
  • 쿠키 값을 가져오는 시점에서 이미 그 값이 JWT임을 알고 있으므로 Bearer 접두사가 필요하지 않음

이로서 오늘 반나절동안 바보짓을 한 댓가를 치루었다..
나머지 기능 구현하고 찬찬히 로직 수정해야겠다.

profile
고민고민고민

0개의 댓글