JWT(JSON Web Token)를 사용하는 경우, 일반적인 권장 사항은 토큰을 응답의 HTTP 헤더에 포함시키는 것입니다. 이렇게 하면 클라이언트가 이후의 요청에서 해당 토큰을 쉽게 포함시켜 인증을 할 수 있습니다.
구체적으로, 보통 Authorization 헤더를 사용하며, 이 헤더의 값으로 Bearer 토큰을 전달합니다.
다음은 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 토큰을 받아 이후의 요청에서 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로 전달하는 것도 가능합니다. 하지만 이 경우 클라이언트에서 별도로 응답 Body에서 토큰을 추출하여 저장한 후 이후 요청에 헤더로 포함시켜야 하는 번거로움이 있습니다. 따라서 권장되지 않습니다.
Authorization 헤더에 Bearer 토큰을 사용하는 것은 표준화된 방법이므로, 대부분의 클라이언트와 라이브러리에서 쉽게 지원됩니다.따라서, 보안과 편의성, 표준화를 고려했을 때 JWT 토큰을 응답의 헤더에 포함시키는 것이 가장 좋은 방법입니다.
프론트엔드에서 JWT 토큰을 저장하고 요청에 자동으로 포함시키는 방법으로는 쿠키와 로컬 스토리지가 주로 사용됩니다. 각각의 방법에는 장단점이 있으며, 보안 요구사항에 따라 적절한 방법을 선택해야 합니다.
장점:
HttpOnly와 Secure 플래그를 사용하면 XSS(Cross-Site Scripting) 공격을 방지할 수 있습니다.단점:
쿠키에 JWT 토큰을 저장하는 예제:
document.cookie = "token=" + jwtToken + "; HttpOnly; Secure";
장점:
단점:
로컬 스토리지에 JWT 토큰을 저장하는 예제:
localStorage.setItem('token', jwtToken);
보다 높은 보안을 위해 쿠키와 로컬 스토리지를 조합하여 사용하는 것도 좋은 방법입니다. 예를 들어, JWT 토큰을 쿠키에 저장하고, XSRF 토큰을 로컬 스토리지에 저장하여 사용하는 방법입니다.
보안:
HttpOnly와 Secure 플래그를 설정합니다.편의성:
@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 토큰을 생성하여 클라이언트에 전달하는 대신, 서버 측에서 세션 ID를 생성하고 클라이언트에 전달합니다. 클라이언트는 이 세션 ID를 쿠키에 저장하고, 서버는 해당 세션 ID를 사용하여 JWT 토큰을 포함한 세션 데이터를 관리합니다.
클라이언트 측 세션 관리: 서버에서 JWT 토큰을 클라이언트에 전달하고, 클라이언트는 이를 세션 스토리지나 쿠키에 저장하여 사용하는 방법입니다. 클라이언트 측에서 세션을 관리하면서 서버에 요청할 때마다 JWT 토큰을 포함합니다.
백엔드 (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));
무상태성의 포기: 서버 측 세션 관리는 JWT의 무상태성(stateless)의 이점을 포기하게 됩니다. 서버는 클라이언트의 세션 상태를 관리해야 하므로, 서버의 부하가 증가할 수 있습니다.
보안: 세션을 이용하면 클라이언트 측에서 토큰을 저장하는 보안 문제(XSS 등)를 완화할 수 있습니다. 하지만 CSRF 공격에 대비하여 적절한 방어 기법(CSRF 토큰 등)을 적용해야 합니다.
편리성: 클라이언트 측 코드가 간단해질 수 있습니다. 서버에서 세션을 관리하므로 클라이언트는 세션 쿠키를 자동으로 포함하여 요청을 보낼 수 있습니다.
따라서, JWT 토큰을 세션을 이용하여 관리하는 방법은 보안 요구사항과 시스템 설계에 따라 적절히 선택할 수 있는 방법입니다. 그러나, JWT의 무상태성(stateless)이라는 주요 이점을 활용하고 싶다면, 세션보다는 앞서 설명한 쿠키나 로컬 스토리지와 같은 방법을 사용하는 것이 더 적합할 수 있습니다.