세션 인증은 서버가 사용자의 로그인 상태를 기억하고 관리하는 방식.
마치 단골 가게 주인이 우리를 기억하고 "늘 먹던 걸로 드릴까요?"라고 묻는 것과 비슷합니다.
핵심 특징: Stateful (상태 유지)
세션 방식의 가장 큰 특징은 Stateful,
즉 서버가 사용자의 상태(로그인 정보)를 계속해서 저장하고 유지한다는 점입니다.
로그인을 하면 서버는 무작위의 유니크한 Session ID(=Session Key)를 생성하여 이를 키 값으로 하여 로그인 정보를 저장(Key: Value 형태)하고, Session ID를 클라이언트에게 반환합니다.
이후, 클라이언트는 요청 시 Session ID를 헤더(or 쿠키)에 넣어 서버에 요청하면, 서버는 Session ID를 키로 하여 로그인 정보를 가져와 유저를 식별합니다.
인증에 필요한 정보들을 암호화시킨 JSON 객체를 사용하여 인증을 처리하는 방식.
서버가 사용자를 기억하는 것이 아니라, 사용자가 자신의 신분증을 들고 다니는 것과 같습니다. 모든 정보가 JWT에 들어있기 때문에, 서버는 로그인 상태를 기억하지 않아도 됩니다!
핵심 특징: Stateless (무상태)
aaaa.bbbb.cccc와 같이 점(.)으로 구분된 세 부분으로 구성됩니다.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c헤더 (Header):JSON
{
"alg": "HS256",
"typ": "JWT"
}페이로드 (Payload):JSON
{
"sub": "user123", // 토큰 제목 (사용자 ID)
"name": "Spartan Kim", // 사용자 이름
"iat": 1516239022, // 토큰 발급 시간
"exp": 1516242622 // 토큰 만료 시간
}서명 (Signature):
JWT를 사용하다 보면 한 가지 딜레마에 빠집니다.
이 문제를 해결하기 위해 Access Token과 Refresh Token이라는 두 가지 종류의 토큰을 함께 사용하는 전략을 사용합니다.
HttpServletRequest 객체는 HTTP 요청 메시지 전체를 담고 있는 그릇이라고 생각하면 쉽습니다. 클라이언트(브라우저)가 서버로 보내는 모든 정보가 이 안에 담겨있습니다. HTTP 요청을 자바에서 활용하기 위해, HTTP 요청이 자바 객체로 변환된 것입니다.
즉, 그렇기 때문에 HTTP 요청에 있는 Start Line, Headers, Message Body 구조의 데이터가 그대로 HttpServletRequest 객체 안에 모두 포함되어있습니다.
@RestController
public class RequestController {
@RequestMapping("/request-info")
public String getRequestInfo(HttpServletRequest request) {
String method = request.getMethod(); // HTTP 메소드
String uri = request.getRequestURI(); // 요청 URI
// ...
}
}
위 코드 예시와 같이 스프링 컨트롤러에서 이 HttpServletRequest 객체를 직접 파라미터로 받아 모든 정보에 접근할 수 있지만, 스프링은 더 편리한 방법들을 제공합니다.
그중 하나가 바로 @RequestHeader입니다.
@RequestHeader 애노테이션을 사용하면 HttpServletRequest 객체를 통째로 가져오지 않고도 특정 헤더 값을 파라미터로 직접 받아올 수 있습니다. 코드가 훨씬 간결하고 의도가 명확해집니다.
@RequestHeader("헤더이름") String 변수명 형태로 사용합니다.
@RestController
public class HeaderController {
@GetMapping("/headers")
public String getHeaders(
@RequestHeader("session-id") String sessionId,
@RequestHeader(value = "User-Agent") String userAgent
) {
// ...
}
}
value 또는 name:
가져올 헤더의 이름을 지정합니다. (필수값, 단순 String만 입력하면 이 String 값이 value가 됩니다)
required:
해당 헤더가 필수인지 여부를 결정합니다.
기본값은 true이며, true일 때 헤더가 없으면 400 Bad Request 오류가 발생합니다. false로 설정하면 헤더가 없을 경우 null이 주입됩니다.
defaultValue:
required가 false일 때, 헤더 값이 없을 경우 대신 주입될 기본값을 설정할 수 있습니다.
“Session ID만 편하게 가져올 수 있는 ArgumentResolver가 있으면 좋겠다!”
그 기능을 하는 도구가 바로@SessionAttribute입니다.
1. 유저 등록
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
User user = userService.register(request);
RegisterResponse response = new RegisterResponse(user.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
2. 로그인
// SessionUser DTO
@Getter
public class SessionUser {
private final Long id;
private final String email;
public SessionUser(Long id, String email) {
this.id = id;
this.email = email;
}
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request, HttpSession session) {
User user = userService.login(request);
SessionUser sessionUser = new SessionUser(user);
session.setAttribute("loginUser", sessionUser);
LoginResponse response = new LoginResponse(user.getId(), user.getEmail());
return ResponseEntity.status(HttpStatus.OK).body(response);
}
HttpSession session:HttpSession을 선언하면, 스프링이 현재 요청에 해당하는 세션 객체를 자동으로 주입해 줍니다.session.setAttribute("loginUser", sessionUser):"loginUser" 라는 이름(Key)으로 sessionUser 객체(Value)를 세션 저장소에 저장합니다.loginUser 정보가 계속 남아있게 됩니다.💡 Tip!: User 엔티티 전체가 아닌, 세션에 필요한 정보만 담은
가벼운 SessionUser DTO를 만들어 저장하는 것이 성능과 보안 면에서 좋은 습관입니다.Set-Cookie 헤더를 통해 JSESSIONID 를 전달합니다. 브라우저는 이 쿠키를 저장하고, 앞으로 모든 요청에 이 쿠키를 담아 보내 서버가 "아, 이 사용자는 로그인된 사용자구나!"라고 식별할 수 있게 해줍니다.JSESSIONID : 스프링에서 HttpSession을 쓸 때, 쿠키의 Session ID Key의 디폴트 이름
(SESSIONID, session-id 같은 일반적인 이름을 쓰면 겹칠 수도 있기에 버그 발생 위험!)3. 로그아웃
세션을 무효화합니다.
<@PostMapping("/logout")
public ResponseEntity<Void> logout(@SessionAttribute(name = "loginUser", required = false) SessionUser sessionUser, HttpSession session) {
if (sessionUser == null) {
return ResponseEntity.badRequest().build();
}
session.invalidate();
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@SessionAttribute"loginUser"라는 이름으로 저장된 세션 속성을 찾아 sessionUser 파라미터에 주입해줍니다.session.invalidate(): 세션을 무효화하는 메서드입니다.4. 테스트 진행
// 로그인 상태(세션이 유효한 상태)에서만 동작하는지 테스트해볼 수 있는 API
@GetMapping("/test")
public ResponseEntity<String> test(@SessionAttribute(name = "loginUser", required = false) SessionUser sessionUser) {
if (sessionUser == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return ResponseEntity.status(HttpStatus.OK).build();
}