개발이군고구마

[Spring Security] redis/DB로 자동로그인 구현 본문

SERVER/Spring

[Spring Security] redis/DB로 자동로그인 구현

김구황 2025. 11. 28. 00:09
728x90
Problem

 

  • 자동로그인 시도시 로그인 ""지연"" 현상 
  • 자동로그인을 해놓을 경우 다른 기기에서 로그인을 하더라도 해당 정보 계속해서 유지됨 
    • 예를 들어 타 기기에서 의도치 않게 자동로그인이 되면 계속해서 그 정보가 남아 타인이 계정에 계속 접속하는 것이 가능하여 ""보안"" 우려

 

Cause

 

  • 자동로그인 토큰 검색을 위해 매번 DB 조회
    • 특히, DB 버퍼에 쿼리 캐시가 없다면 디스크 I/O 필요하므로 시간, CPU 비용이 상승하게 되는 것
  • 자동로그인 여부 확인 시 따로 ""기기 식별 번호"" 를 검증하는 로직이 없음 
    • REDIS 조회 {완전로그인(KEY) : UTID}
    • DB 조회 : WHERE -> 완전로그인

 

 

Solution 

 

1️⃣ REDIS 연동 

  • 완전 로그인 토큰을 key 값으로 데이터 insert 
  • 만료시간은 15일
  • 참고) remember-me 쿠키 값 만료날짜는 30일 

 

1) 최초로그인

  • 첫 로그인 시에는 로그인 url 로 들어오기 때문에 password filter를 타게 됨 
  • password filter 이후 remeber me 서비스 실행 
if (authenticationIsRequired(username)) {
 Authentication authResult = this.authenticationManager
   .authenticate(authRequest); ** 인증 시작 (PROVIDER, SERVICE- loadByUsername, UserDetail)

 if (debug) {
  this.logger.debug("Authentication success: " + authResult);
 }

 SecurityContextHolder.getContext().setAuthentication(authResult); ** 세션 생성

 this.rememberMeServices.loginSuccess(request, response, authResult); ** remember me 쿠키값

 onSuccessfulAuthentication(request, response, authResult);
}

 

 

 

2) 최초 로그인 이후 세션 유지 상태

  • security context 있는 경우 remember me filter를 생략함 
if (SecurityContextHolder.getContext().getAuthentication() == null) { ** ✨세션이 없는 경우만 
 Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
   response);

 ...

 chain.doFilter(request, response);
}
else {
 if (logger.isDebugEnabled()) {
  logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
    + SecurityContextHolder.getContext().getAuthentication() + "'");
 }

 chain.doFilter(request, response);
}

 

 

 

3) 세션 만료 이후 자동로그인 

 

 

2️⃣ COOKIE 에 [완전로그인+자동로그인]을 조합한 값을 인코딩하여 DB 조회시 인덱싱 효과 

  • DB 조회시에 조건이 2개이 걸려 한 행만 조회하여 검색 시간이 단축됨 

 

3️⃣ 다른 기기로 로그인할 경우 완전로그인 KEY 값에 대한 데이터 삭제 

  • REDIS 가져온 기기 식별자와 다른 모든 식별번호(행)를 삭제 (완전로그인 - 현재 로그인한 기기)
  • DB 가져온 자동로그인 토큰외에 모든 자동로그인 토큰 삭제 (완전로그인 - 당시 로그인한 자동로그인 토큰)
  • REDIS / DB 완전로그인 인자에 대해 기기 식별 번호에 대한 정보만 가지고 있기 때문에 다른 디바이스로 로그인 하는 경우 값이 없는 걸로 인지하여 로그인 불가

 

 

[why & what if] 

? 왜 REDIS를 사용했는가, 다른 옵션을 없었을까

 

? 왜 자동로그인 토큰 Persistent Remember-Me 토큰 은 매번 유저가 접속할 때마다 갱신되는 것일까? 

  • 자동로그인은 쿠키 기반이라 탈취 위험이 높기 때문에,
    “한 번 사용된 토큰은 즉시 폐기”하는 보안 정책을 적용하기 위해 매번 토큰을 바꾼다고 함 ( Replay 공격 방지)

 

? 자동로그인 토큰 쿠키 값이 만료되더라도 DB 값들은 삭제되지 않는 것일까

 

? JWT스럽게 개선하려면 어떤 아키텍처가 될 수 있을까

 

 

 

Impact

 

? HTTP SESSION의 값은 어디에 저장되는 것일까? SERVLET의 가장 앞단에 위치하는 것일까

  • (시점) 세션 객체가 처음 만들어지는 순간 = request.getSession(true) 가 호출되는 순간 (filter들의 success handler)
    • HTTP SESSION 객체 (MAP형태)를 생성함 
    • 이후 HTTP SESSION 객체에 SET 을 할 때마다 쿠키에 저장되는 ""공통 메서드"" 정의함 
public void onAuthenticationSuccess(NcpAuthenticaitonRequest ncpAuthenticaitonRequest)
        throws IOException, ServletException {
 
    HttpServletRequest request = ncpAuthenticaitonRequest.getRequest();
    HttpServletResponse response = ncpAuthenticaitonRequest.getResponse();
    Authentication authentication = ncpAuthenticaitonRequest.getAuthentication();
.....
    Map<String, Object> rsltData = null;
    try {
 
        HttpSession session = request.getSession();

 

session.setAttribute("SPRING_SECURITY_CONTEXT", context); ** 쿠키 JSESSION ID로 저장
  • (영역) Servlet Container 내부의 Session Manager 메모리 영역에 HTTP SESSION 객체 저장
    • 그런데 어차피 쿠키에 http session 객체를 인코딩 해서 집어넣고, 가져와서 디코딩으로 푼 후에 인증을 하기 때문에 메모리에 큰 의미는 없지 않을까 싶음.

 

? AUTHENTICATE와 같은 유저 정보 또는 SESSION ID 는 매번 유저가 로그인 할 때마다 갱신되는 것인지 

  • 아니다. 모두 재인증일 때만 갱신된다 
    1. Authentication(유저 정보)
      • ✅ “로그인(재인증)에 성공할 때마다” 새로 세팅된다. 
        • UsernamePasswordAuthenticationFilter 나 RememberMeAuthenticationFilter 가 인증에 성공
        • SecurityContextHolder.getContext().setAuthentication(newAuth);
      • ❌ 단순 요청/페이지 이동에서는 갱신 안 하고 기존 Authentication  재사용.
        • 이미 세션이 있고, 거기에 SPRING_SECURITY_CONTEXT 가 들어있으면 SecurityContextPersistenceFilter 가 그 값을 그냥 꺼내서 ThreadLocal에 올려 준다.
    2. SESSION ID(JSESSIONID)
      • ✅ 인증 성공 시마다 세션 ID를 새로 발급해서 바꾸는 게 기본이다.
      • ❌ 이미 로그인된 상태에서 그냥 요청만 하는 동안에는 그대로 유지된다.

 

? 세션 정보도 있고 REMEMBER ME 정보도 쿠키에 있는 경우 RememberMeServices.loginSuccess()를 통해 REDIS 및 DB 에 자동로그인 토큰 값이 갱신이 되나? 

  • 아니다. ID/PW로 명시적으로 로그인(로그인 URL)하면서 remember-me를 요청(true)했거나, 세션 정보가 없는 경우만 갱신 
    • UsernamePasswordAuthenticationFilter 같은 “로그인 처리 필터”가 인증에 성공했을 때 (로그인 URL이 있을 때만 타는 필터임) successfulAuthentication() 안에서 성공 핸들러 → RememberMeServices.loginSuccess() 를 호출하는 구조
    • 세션이 있다면 rememberfilter를 건너뜀 
세션 O + Remember-Me 쿠키 O ❌ 실행 안함 ❌ 갱신 안됨
세션 X + Remember-Me 쿠키 O ✔ 실행 ✔ 갱신
Remember-Me 쿠키 없음 ❌ 실행 안함 ❌ 없음

 

 

? 완전로그인토큰/자동로그인토큰 생성 방식과 JWT 토큰 생성방식과의 유사점과 사용 맥락 

  • 완전로그인토큰/자동로그인토큰 = 서버 저장소 기반의 랜덤 토큰(key-value 기반)
    JWT = 자체 정보·서명 포함한 self-contained 토큰

- 유사점

  완전로그인/자동로그인 토큰 JWT
클라이언트가 토큰을 보관 쿠키(Remember-me) 사용 쿠키/Authorization 헤더
토큰을 서버에 다시 보내 인증을 복구 쿠키에 있는 Series/Token 조합으로 DB 조회 JWT 자체에 포함된 Signature 검증
세션 없이도 인증 가능 세션 끊겨도 자동 로그인 원래 세션不要 방식
유출되면 위험 쿠키 탈취 → 계정 탈취 토큰 탈취 → 계정 탈취
토큰 롤링(재발급) 자동로그인 시마다 토큰 갱신 Refresh Token 존재 시 롤링

 

- 차이점

  • 토큰 검증 방식
  • 보안 전략
    • 매번 자동로그인 성공 시 토큰 롤링(Token Rotation) 필요
    • Access Token은 짧은 TTL
  • 토큰에 담는 내용
    • 의미 없는 랜덤 문자열
    • 클레임(Claims)에 사용자 정보, 권한, 만료 시간 등 내장
  • 사용맥락
    • 세션이 없으면 → remember-me 쿠키로 세션을 복구해주는 보조 인증, 세션 기반(Spring Security 세션)과 함께 사용
    • JWT Access Token을 헤더에 넣어 API 요청 서버는 DB 조회 없이 바로 인증, 모바일 앱/SPA(React, Vue) 환경