Skip to content

세션 정보 참조 가이드

dictionary-api는 spring-session-jdbc 기반 DB 세션 인증을 쓴다(JWT 아님). 로그인 사용자 정보는 SessionUser로 세션에 저장되고, 어디서든 ServletUtils.getSessionUser()로 조회한다.

common/model/SessionUser.java
@Getter
@Setter
@Builder
public class SessionUser implements Serializable {
private Long userId;
private String userEm;
private String userNm;
private String langCd;
private String adminYn;
private String guestYn;
@JsonIgnore private LocalDateTime lastLoginDt;
}

세션 조회 — ServletUtils.getSessionUser()

Section titled “세션 조회 — ServletUtils.getSessionUser()”
  1. 현재 요청의 세션에서 로그인 사용자 정보를 꺼낸다. 세션이 없거나 로그인하지 않았으면 null을 반환한다(예외를 던지지 않는다).
    common/utils/ServletUtils.java
    public static final String SESSION_USER_KEY = "S_USER";
    public static SessionUser getSessionUser() {
    final HttpSession session = getSession();
    if (Objects.nonNull(session)) {
    final Object user = session.getAttribute(SESSION_USER_KEY);
    return Objects.isNull(user) ? null : (SessionUser) user;
    }
    return null;
    }
  2. getSession()(인자 없음)은 세션이 없으면 새로 만들지 않고 null을 반환한다 — 조회만 할 때는 이 메소드를 쓴다. 세션을 새로 만들어야 하면 getSession(true)를 쓴다(로그인 시점에만 필요).
  3. 요청 컨텍스트가 없는 곳(배치, 테스트)에서 호출하면 항상 null이 반환된다 — 엔티티 감사 컬럼처럼 기본값으로 대체해야 하는 코드는 이 null을 직접 처리한다(아래 엔티티 감사 컬럼 참고).
  1. 로그인 성공 시 세션에 SessionUser를 저장하고, 만료 시간(8시간)을 설정한다.
    api/auth/service/AuthService.java
    private static final int SESSION_TIMEOUT_SECONDS = 8 * 60 * 60;
    private void createSession(final SessionUser sessionUser) {
    final HttpSession session = ServletUtils.getSession(true);
    if (Objects.nonNull(session)) {
    session.setAttribute(ServletUtils.SESSION_USER_KEY, sessionUser);
    session.setMaxInactiveInterval(SESSION_TIMEOUT_SECONDS);
    }
    }
  2. 로그아웃 시 세션 속성을 지우고 무효화한다.
    api/auth/service/AuthService.java
    private void clearSession() {
    final HttpSession session = ServletUtils.getSession();
    if (Objects.nonNull(session)) {
    session.removeAttribute(ServletUtils.SESSION_USER_KEY);
    session.invalidate();
    }
    }
  3. 이 8시간은 로그인 시점부터의 절대 시간이 아니라 마지막 요청으로부터의 유휴(idle) 타임아웃이다 — HttpSession.setMaxInactiveInterval은 “요청 사이의 간격”을 재는 스펙이라, 사용자가 계속 활동 중이면 매 요청이 만료 카운트다운을 다시 8시간으로 되돌린다.
  4. 같은 계정으로 다른 곳에 열려 있던 이전 세션이 있으면(UserEntity.lastSessionId) 새 세션을 만들기 전에 JdbcIndexedSessionRepository.deleteById()로 먼저 지운다(단일 세션 로그인, 동시 로그인 제한).

인가 검사 — AuthorizationInterceptor

Section titled “인가 검사 — AuthorizationInterceptor”
flowchart TD
  req["요청"] --> h{"컨트롤러 메소드<br/>(HandlerMethod)인가?"}
  h -- "아니오" --> pass["통과"]
  h -- "예" --> pub{"@PublicApi가 있는가?"}
  pub -- "예" --> pass
  pub -- "아니오" --> login{"세션에 SessionUser가 있는가?"}
  login -- "아니오" --> e401["401 NOT_LOGIN"]
  login -- "예" --> guest{"게스트이고<br/>@GuestForbidden이 있는가?"}
  guest -- "예" --> e403["403 FORBIDDEN"]
  guest -- "아니오" --> pass
  1. 화이트리스트(fail-closed) 정책 — @PublicApi가 없는 모든 @RestController API는 세션이 없으면 401(NOT_LOGIN)로 거부한다.
    common/security/AuthorizationInterceptor.java
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
    final Object handler) {
    if (!(handler instanceof HandlerMethod handlerMethod)) {
    return true;
    }
    if (isPublic(handlerMethod)) {
    return true;
    }
    final SessionUser sessionUser = ServletUtils.getSessionUser();
    if (Objects.isNull(sessionUser)) {
    throw new BizException(CodeApiResult.NOT_LOGIN);
    }
    if ("Y".equals(sessionUser.getGuestYn()) && isGuestForbidden(handlerMethod)) {
    throw new BizException(CodeApiResult.FORBIDDEN);
    }
    return true;
    }
  2. 로그인 없이 열어야 하는 API는 클래스 또는 메소드에 @PublicApi를 붙인다.
    @PublicApi
    @PostMapping("/login")
    public ResponseEntity<ResponseDto<SessionUser>> login(...) { ... }
  3. 게스트(guestYn == "Y") 세션에게 막아야 하는 쓰기 API 등은 @GuestForbidden을 붙인다 — 로그인 자체는 허용하되 특정 액션만 403(FORBIDDEN)으로 거부한다.
    @GuestForbidden
    @PostMapping
    public ResponseEntity<ResponseDto<BoardResponseDto>> create(...) { ... }

컨트롤러에서 직접 참조 — findSession

Section titled “컨트롤러에서 직접 참조 — findSession”

프런트엔드가 로컬(localStorage)에 남은 로그인 상태를 신뢰하지 않고, 서버 세션이 실제로 살아있는지 확인할 때 쓰는 API다. @PublicApi가 없어 세션이 없으면 인터셉터가 먼저 401로 거부하므로, 서비스에서 별도로 null을 확인하지 않는다.

api/auth/controller/AuthController.java
@Operation(summary = "세션 조회", description = "현재 세션이 살아있는지 확인하는 API")
@GetMapping("/find-session")
public ResponseEntity<ResponseDto<SessionUser>> findSession() {
final SessionUser sessionUser = authService.findSession();
return ok(new ResponseDto<>(sessionUser));
}
api/auth/service/AuthService.java
public SessionUser findSession() {
return ServletUtils.getSessionUser();
}

세션 만료 시각 응답 — ResponseExpiresAtAdvice

Section titled “세션 만료 시각 응답 — ResponseExpiresAtAdvice”
  1. 슬라이딩 세션이라 로그인 시점 한 번만 만료 시각을 계산해 내려주면 실제 값과 어긋난다 — 컨트롤러마다 직접 채우는 대신, 모든 ResponseDto 응답이 나가기 직전 이 어드바이스가 “지금 이 순간” 기준 만료 시각을 일괄 계산해 채운다.
    common/api/advice/ResponseExpiresAtAdvice.java
    @ControllerAdvice
    public class ResponseExpiresAtAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public Object beforeBodyWrite(final Object body, ... ) {
    if (!(body instanceof ResponseDto<?> responseDto)) {
    return body;
    }
    final HttpSession session = ServletUtils.getSession();
    if (Objects.nonNull(session)) {
    responseDto.setExpiresAt(
    Instant.now().getEpochSecond() + session.getMaxInactiveInterval());
    }
    return responseDto;
    }
    }
  2. 로그인 세션이 없으면(@PublicApi 호출 등) expiresAt을 채우지 않는다 — 이때 세션을 새로 만들지 않도록 getSession()(생성하지 않는 조회)만 쓴다.

AbstractEntity가 등록/수정 시 ServletUtils.getSessionUser()로 사용자 ID를 채운다 — 세션이 없으면 기본값(1000)으로 대체한다. 자세한 코드는 MVC 코드 작성 가이드의 Entity 항목 참고.