세션 정보 참조 가이드
dictionary-api는 spring-session-jdbc 기반 DB 세션 인증을 쓴다(JWT 아님). 로그인 사용자 정보는
SessionUser로 세션에 저장되고, 어디서든 ServletUtils.getSessionUser()로 조회한다.
SessionUser 모델
Section titled “SessionUser 모델”@Getter@Setter@Builderpublic 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()”- 현재 요청의 세션에서 로그인 사용자 정보를 꺼낸다. 세션이 없거나 로그인하지 않았으면
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;} getSession()(인자 없음)은 세션이 없으면 새로 만들지 않고null을 반환한다 — 조회만 할 때는 이 메소드를 쓴다. 세션을 새로 만들어야 하면getSession(true)를 쓴다(로그인 시점에만 필요).- 요청 컨텍스트가 없는 곳(배치, 테스트)에서 호출하면 항상
null이 반환된다 — 엔티티 감사 컬럼처럼 기본값으로 대체해야 하는 코드는 이null을 직접 처리한다(아래 엔티티 감사 컬럼 참고).
세션 생성/해제 — AuthService
Section titled “세션 생성/해제 — AuthService”- 로그인 성공 시 세션에
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);}} - 로그아웃 시 세션 속성을 지우고 무효화한다.
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();}} - 이 8시간은 로그인 시점부터의 절대 시간이 아니라 마지막 요청으로부터의 유휴(idle) 타임아웃이다 —
HttpSession.setMaxInactiveInterval은 “요청 사이의 간격”을 재는 스펙이라, 사용자가 계속 활동 중이면 매 요청이 만료 카운트다운을 다시 8시간으로 되돌린다. - 같은 계정으로 다른 곳에 열려 있던 이전 세션이 있으면(
UserEntity.lastSessionId) 새 세션을 만들기 전에JdbcIndexedSessionRepository.deleteById()로 먼저 지운다(단일 세션 로그인, 동시 로그인 제한).
인가 검사 — AuthorizationInterceptor
Section titled “인가 검사 — AuthorizationInterceptor”- 화이트리스트(fail-closed) 정책 —
@PublicApi가 없는 모든@RestControllerAPI는 세션이 없으면 401(NOT_LOGIN)로 거부한다.common/security/AuthorizationInterceptor.java @Overridepublic 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;} - 로그인 없이 열어야 하는 API는 클래스 또는 메소드에
@PublicApi를 붙인다.@PublicApi@PostMapping("/login")public ResponseEntity<ResponseDto<SessionUser>> login(...) { ... } - 게스트(
guestYn == "Y") 세션에게 막아야 하는 쓰기 API 등은@GuestForbidden을 붙인다 — 로그인 자체는 허용하되 특정 액션만 403(FORBIDDEN)으로 거부한다.@GuestForbidden@PostMappingpublic ResponseEntity<ResponseDto<BoardResponseDto>> create(...) { ... }
컨트롤러에서 직접 참조 — findSession
Section titled “컨트롤러에서 직접 참조 — findSession”프런트엔드가 로컬(localStorage)에 남은 로그인 상태를 신뢰하지 않고, 서버 세션이 실제로 살아있는지 확인할
때 쓰는 API다. @PublicApi가 없어 세션이 없으면 인터셉터가 먼저 401로 거부하므로, 서비스에서 별도로
null을 확인하지 않는다.
@Operation(summary = "세션 조회", description = "현재 세션이 살아있는지 확인하는 API")@GetMapping("/find-session")public ResponseEntity<ResponseDto<SessionUser>> findSession() { final SessionUser sessionUser = authService.findSession(); return ok(new ResponseDto<>(sessionUser));}public SessionUser findSession() { return ServletUtils.getSessionUser();}세션 만료 시각 응답 — ResponseExpiresAtAdvice
Section titled “세션 만료 시각 응답 — ResponseExpiresAtAdvice”- 슬라이딩 세션이라 로그인 시점 한 번만 만료 시각을 계산해 내려주면 실제 값과 어긋난다 — 컨트롤러마다
직접 채우는 대신, 모든
ResponseDto응답이 나가기 직전 이 어드바이스가 “지금 이 순간” 기준 만료 시각을 일괄 계산해 채운다.common/api/advice/ResponseExpiresAtAdvice.java @ControllerAdvicepublic class ResponseExpiresAtAdvice implements ResponseBodyAdvice<Object> {@Overridepublic 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;}} - 로그인 세션이 없으면(
@PublicApi호출 등)expiresAt을 채우지 않는다 — 이때 세션을 새로 만들지 않도록getSession()(생성하지 않는 조회)만 쓴다.
엔티티 감사 컬럼에서 참조
Section titled “엔티티 감사 컬럼에서 참조”AbstractEntity가 등록/수정 시 ServletUtils.getSessionUser()로 사용자 ID를 채운다 — 세션이 없으면
기본값(1000)으로 대체한다. 자세한 코드는 MVC 코드 작성
가이드의 Entity 항목 참고.