프론트엔드와 백엔드에서의 JWT 토큰 관리

강성준·2024년 7월 9일

Header vs Body

JWT(JSON Web Token)를 사용하는 경우, 일반적인 권장 사항은 토큰을 응답의 HTTP 헤더에 포함시키는 것입니다. 이렇게 하면 클라이언트가 이후의 요청에서 해당 토큰을 쉽게 포함시켜 인증을 할 수 있습니다.

구체적으로, 보통 Authorization 헤더를 사용하며, 이 헤더의 값으로 Bearer 토큰을 전달합니다.

응답에 JWT 토큰을 헤더로 전달하기

다음은 Spring Boot에서 JWT 토큰을 헤더에 포함시켜 응답하는 예제입니다:

@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
    // 사용자 인증 로직 수행
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
    );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    // JWT 토큰 생성
    String jwt = tokenProvider.generateToken(authentication);

    // JWT 토큰을 헤더에 추가하여 응답
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);

    return new ResponseEntity<>(null, httpHeaders, HttpStatus.OK);
}

클라이언트 요청에 JWT 토큰을 헤더에 포함하기

프론트엔드에서는 JWT 토큰을 받아 이후의 요청에서 Authorization 헤더에 포함시켜 서버로 보내야 합니다. 예를 들어, JavaScript를 사용하는 경우:

fetch('https://api.example.com/protected-endpoint', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer ' + jwtToken
    }
})
.then(response => response.json())
.then(data => console.log(data));

JWT 토큰을 응답의 Body로 전달하기

JWT 토큰을 응답의 Body로 전달하는 것도 가능합니다. 하지만 이 경우 클라이언트에서 별도로 응답 Body에서 토큰을 추출하여 저장한 후 이후 요청에 헤더로 포함시켜야 하는 번거로움이 있습니다. 따라서 권장되지 않습니다.

종합적으로 고려할 사항

  1. 보안: JWT 토큰은 중요한 인증 정보이므로, 항상 HTTPS를 사용하여 전송해야 합니다.
  2. 표준화: Authorization 헤더에 Bearer 토큰을 사용하는 것은 표준화된 방법이므로, 대부분의 클라이언트와 라이브러리에서 쉽게 지원됩니다.
  3. 편의성: 헤더에 토큰을 포함시키면 클라이언트와 서버 간의 인터페이스가 명확해지고, 클라이언트 코드에서 토큰 관리가 단순해집니다.

따라서, 보안과 편의성, 표준화를 고려했을 때 JWT 토큰을 응답의 헤더에 포함시키는 것이 가장 좋은 방법입니다.


JWT 토큰 저장하기

프론트엔드에서 JWT 토큰을 저장하고 요청에 자동으로 포함시키는 방법으로는 쿠키와 로컬 스토리지가 주로 사용됩니다. 각각의 방법에는 장단점이 있으며, 보안 요구사항에 따라 적절한 방법을 선택해야 합니다.

쿠키를 사용하는 방법

장점:

  • 자동 전송: 쿠키는 동일한 도메인에 대한 모든 HTTP 요청에 자동으로 포함되므로, 요청마다 토큰을 수동으로 설정할 필요가 없습니다.
  • 보안: HttpOnlySecure 플래그를 사용하면 XSS(Cross-Site Scripting) 공격을 방지할 수 있습니다.

단점:

  • CSRF 취약점: 쿠키를 사용하면 CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있습니다. 이를 방지하기 위해서는 추가적인 CSRF 토큰을 사용하는 것이 좋습니다.

쿠키에 JWT 토큰을 저장하는 예제:

document.cookie = "token=" + jwtToken + "; HttpOnly; Secure";

로컬 스토리지를 사용하는 방법

장점:

  • 편리성: 토큰을 쉽게 저장하고 읽을 수 있습니다.
  • CSRF 방지: 로컬 스토리지는 CSRF 공격에 덜 취약합니다.

단점:

  • 보안 문제: 로컬 스토리지는 JavaScript에서 접근 가능하기 때문에 XSS 공격에 취약합니다.

로컬 스토리지에 JWT 토큰을 저장하는 예제:

localStorage.setItem('token', jwtToken);

권장 방법: 쿠키와 로컬 스토리지의 조합

보다 높은 보안을 위해 쿠키와 로컬 스토리지를 조합하여 사용하는 것도 좋은 방법입니다. 예를 들어, JWT 토큰을 쿠키에 저장하고, XSRF 토큰을 로컬 스토리지에 저장하여 사용하는 방법입니다.

종합적으로 고려할 사항

  1. 보안:

    • HTTPS를 사용하여 데이터를 전송해야 합니다.
    • 쿠키를 사용할 경우, HttpOnlySecure 플래그를 설정합니다.
    • CSRF 방지를 위해 CSRF 토큰을 추가로 사용합니다.
  2. 편의성:

    • 자동으로 토큰이 전송되는 쿠키를 사용하면 클라이언트 코드가 간단해집니다.
    • 클라이언트 측에서 명시적으로 토큰을 설정하는 로컬 스토리지는 관리가 다소 번거로울 수 있습니다.

예제: 쿠키를 사용한 JWT 토큰 설정

백엔드 (Spring Boot)

@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
    );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    String jwt = tokenProvider.generateToken(authentication);

    Cookie cookie = new Cookie("token", jwt);
    cookie.setHttpOnly(true);
    cookie.setSecure(true);
    cookie.setPath("/");
    cookie.setMaxAge(7 * 24 * 60 * 60); // 1 week
    response.addCookie(cookie);

    return ResponseEntity.ok().build();
}

프론트엔드

쿠키에 저장된 JWT 토큰은 자동으로 요청에 포함되므로, 추가적인 작업이 필요 없습니다. 다만, CSRF 토큰을 사용하는 경우 다음과 같이 설정합니다.

const csrfToken = localStorage.getItem('csrfToken');

fetch('https://api.example.com/protected-endpoint', {
    method: 'GET',
    headers: {
        'X-CSRF-TOKEN': csrfToken
    }
})
.then(response => response.json())
.then(data => console.log(data));

이와 같은 방법으로 JWT 토큰을 쿠키에 저장하고 필요할 때 자동으로 꺼내 사용하면, 보안성과 편리성을 모두 확보할 수 있습니다.

세션을 사용하는 방법

세션을 이용하여 JWT 토큰을 저장하고 사용하는 방법도 가능합니다. 세션을 사용하면 서버 측에서 클라이언트의 상태를 관리할 수 있으며, 이는 보안적인 장점이 있습니다. 그러나 JWT의 장점 중 하나는 서버가 상태를 유지하지 않는다는 점이므로, JWT를 세션과 함께 사용하는 것은 JWT의 무상태성을 어느 정도 포기하는 것이 됩니다.

세션을 이용한 JWT 관리 방법

  1. 서버 측 세션 관리: JWT 토큰을 생성하여 클라이언트에 전달하는 대신, 서버 측에서 세션 ID를 생성하고 클라이언트에 전달합니다. 클라이언트는 이 세션 ID를 쿠키에 저장하고, 서버는 해당 세션 ID를 사용하여 JWT 토큰을 포함한 세션 데이터를 관리합니다.

  2. 클라이언트 측 세션 관리: 서버에서 JWT 토큰을 클라이언트에 전달하고, 클라이언트는 이를 세션 스토리지나 쿠키에 저장하여 사용하는 방법입니다. 클라이언트 측에서 세션을 관리하면서 서버에 요청할 때마다 JWT 토큰을 포함합니다.

예제: 서버 측 세션 관리를 이용한 JWT 관리

1. 서버 측 세션 관리

백엔드 (Spring Boot)

@RestController
public class AuthController {

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest, HttpSession session) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        // JWT 토큰 생성
        String jwt = tokenProvider.generateToken(authentication);

        // 세션에 JWT 토큰 저장
        session.setAttribute("jwt", jwt);

        return ResponseEntity.ok().build();
    }

    @GetMapping("/protected-endpoint")
    public ResponseEntity<?> protectedEndpoint(HttpSession session) {
        // 세션에서 JWT 토큰 가져오기
        String jwt = (String) session.getAttribute("jwt");

        if (jwt == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // JWT 토큰을 이용한 인증 처리
        // ...

        return ResponseEntity.ok("Protected content");
    }
}

프론트엔드

클라이언트 측에서는 세션 쿠키가 자동으로 포함되므로 추가적인 JWT 토큰 설정이 필요하지 않습니다.

fetch('https://api.example.com/protected-endpoint', {
    method: 'GET',
    credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data));

종합적으로 고려할 사항

  1. 무상태성의 포기: 서버 측 세션 관리는 JWT의 무상태성(stateless)의 이점을 포기하게 됩니다. 서버는 클라이언트의 세션 상태를 관리해야 하므로, 서버의 부하가 증가할 수 있습니다.

  2. 보안: 세션을 이용하면 클라이언트 측에서 토큰을 저장하는 보안 문제(XSS 등)를 완화할 수 있습니다. 하지만 CSRF 공격에 대비하여 적절한 방어 기법(CSRF 토큰 등)을 적용해야 합니다.

  3. 편리성: 클라이언트 측 코드가 간단해질 수 있습니다. 서버에서 세션을 관리하므로 클라이언트는 세션 쿠키를 자동으로 포함하여 요청을 보낼 수 있습니다.

따라서, JWT 토큰을 세션을 이용하여 관리하는 방법은 보안 요구사항과 시스템 설계에 따라 적절히 선택할 수 있는 방법입니다. 그러나, JWT의 무상태성(stateless)이라는 주요 이점을 활용하고 싶다면, 세션보다는 앞서 설명한 쿠키나 로컬 스토리지와 같은 방법을 사용하는 것이 더 적합할 수 있습니다.

profile
백엔드 개발자 지망생

0개의 댓글