Spring 인증/인가와 Servlet Filter 총정리

geoson·2025년 6월 5일

Spring & 백엔드

목록 보기
8/18
post-thumbnail

1. 쿠키(Cookie)

1.1 쿠키의 개념과 필요성

쿠키는 사용자의 웹 브라우저에 저장되는 작은 데이터 조각으로, HTTP의 Stateless 특성을 보완하여 사용자 상태를 유지하는 데 사용됩니다.

HTTP는 기본적으로:

  • Stateless: 각 요청은 독립적으로 처리됨
  • Connectionless: 요청-응답 후 연결 종료

이러한 특성으로 인해 서버는 이전 요청에 대한 정보를 기억하지 못하므로, 로그인 상태 유지와 같은 기능을 위해 쿠키가 필요합니다.

1.2 쿠키 헤더와 속성

쿠키 헤더:

  • Set-Cookie: 서버가 클라이언트에 쿠키 전달 (Response Header)
  • Cookie: 클라이언트가 서버에 쿠키 전달 (Request Header)

쿠키 주요 속성:

Set-Cookie: sessionId=abcd; expires=Sat, 11-Dec-2024 00:00:00 GMT; path=/; domain=example.com; Secure; HttpOnly; SameSite=Strict
  • 생명주기:

    • 세션 쿠키: expires, max-age 생략 시 브라우저 종료까지만 유지
    • 영속 쿠키: expires 또는 max-age로 유지 기간 설정
  • 도메인(domain):

    • 쿠키가 전송될 도메인 지정
    • domain=example.com 설정 시 서브도메인(sub.example.com)도 포함
  • 경로(path):

    • 쿠키가 전송될 URL 경로 지정
    • path=/ 설정 시 모든 경로에서 쿠키 전송
  • 보안 설정:

    • Secure: HTTPS 연결에서만 쿠키 전송
    • HttpOnly: JavaScript에서 쿠키 접근 방지 (XSS 방지)
    • SameSite: 교차 사이트 요청 시 쿠키 전송 제한 (CSRF 방지)

1.3 쿠키 사용 예시

로그인 상태 유지 과정:
1. 사용자 로그인 성공 시, 서버는 Set-Cookie 헤더로 쿠키 발급
2. 브라우저는 쿠키를 저장하고 이후 모든 요청에 Cookie 헤더로 포함
3. 서버는 쿠키 값으로 사용자 인증/인가 처리

// 로그인 성공 후 쿠키 설정
Cookie cookie = new Cookie("userId", String.valueOf(user.getId()));
cookie.setMaxAge(3600); // 1시간
cookie.setPath("/");
response.addCookie(cookie);

// 로그아웃 시 쿠키 삭제
Cookie cookie = new Cookie("userId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);

1.4 쿠키의 문제점

  1. 보안 취약성:

    • 쿠키 값은 클라이언트에서 임의로 변경 가능
    • 네트워크 전송 과정에서 탈취 위험
    • 민감한 정보 저장 시 보안 위험
  2. 해결 방법:

    • 쿠키에 중요한 정보를 직접 저장하지 않음
    • 암호화된 토큰이나 임의의 식별자만 저장
    • HTTPS 사용
    • 토큰 만료시간 설정

2. 세션(Session)

2.1 세션의 개념

세션은 서버에서 사용자 상태 정보를 유지하는 방법으로, 쿠키의 보안 문제를 해결합니다.

  • 쿠키는 중요 정보를 클라이언트에 저장
  • 세션은 중요 정보를 서버에 저장하고, 클라이언트에는 세션 식별자만 쿠키로 전달

2.2 세션 동작 원리

  1. 세션 생성:

    • 로그인 성공 시 서버는 임의의 SessionID 생성
    • 서버의 세션 저장소에 SessionID와 사용자 정보 저장
    • 클라이언트에 SessionID를 쿠키로 전달 (Set-Cookie: JSESSIONID=랜덤값)
  2. 세션 사용:

    • 클라이언트는 모든 요청에 SessionID 쿠키 포함
    • 서버는 SessionID로 세션 저장소를 조회해 사용자 정보 확인
    • 인증/인가 처리 수행

2.3 서블릿의 HttpSession

Spring에서는 HttpSession을 사용해 세션을 쉽게 관리할 수 있습니다.

// 세션 생성 또는 조회
HttpSession session = request.getSession(); // 세션 없으면 생성
// 또는
HttpSession session = request.getSession(false); // 세션 없으면 null 반환

// 세션에 데이터 저장
session.setAttribute("loginUser", userDto);

// 세션에서 데이터 조회
UserDto loginUser = (UserDto) session.getAttribute("loginUser");

// 세션 무효화(로그아웃)
session.invalidate();

Spring MVC에서는 @SessionAttribute 애노테이션으로 더 간편하게 사용 가능:

@GetMapping("/home")
public String home(
        @SessionAttribute(name = "loginUser", required = false) UserDto loginUser,
        Model model
) {
    // 로직 구현
}

2.4 세션 타임아웃

  • 세션은 서버 메모리를 사용하므로 적절한 관리 필요
  • 기본적으로 30분 타임아웃 설정
  • HttpSession은 최근 요청 시간(LastAccessedTime) 기준으로 타임아웃 계산
# application.properties에서 세션 타임아웃 설정
server.servlet.session.timeout=1800 # 30분(초 단위)

2.5 세션의 한계

  • 확장성 문제: 서버 확장 시 세션 공유 문제 발생
  • 성능 오버헤드: 모든 요청마다 세션 저장소 조회 필요
  • 모바일 환경: 쿠키 기반이라 모바일 앱 등에서 제한적

3. 토큰(Token)

3.1 토큰의 개념과 필요성

토큰은 인증/인가 과정에서 사용되는 디지털 문자열로, 세션의 한계를 보완합니다.

토큰 사용 이유:

  • 서버 부담 감소 (클라이언트 저장)
  • 다양한 클라이언트 환경 지원 (쿠키 의존성 제거)
  • 서버의 무상태(Stateless) 유지로 확장성 향상
  • 위변조 방지를 위한 서명 포함

3.2 토큰 인증 과정

  1. 토큰 발급:

    • 사용자 로그인 성공 시 서버는 고유한 토큰 생성
    • 토큰에 사용자 식별 정보와 서명 포함
    • 클라이언트에 토큰 전달
  2. 토큰 사용:

    • 클라이언트는 모든 요청의 Authorization 헤더에 토큰 포함
    • 서버는 토큰 유효성 검증 (서명 확인)
    • 토큰에서 사용자 정보 추출하여 인증/인가 처리

3.3 JWT(JSON Web Token)

JWT는 가장 널리 사용되는 토큰 형식으로, 세 부분으로 구성됩니다:

xxxxx.yyyyy.zzzzz
Header.Payload.Signature

1. Header (헤더):

{
  "alg": "HS256",
  "typ": "JWT"
}
  • 토큰 타입과 사용된 해싱 알고리즘 정보

2. Payload (페이로드):

{
  "sub": "1234567890",
  "name": "John Doe",
  "exp": 1516239022
}
  • 실제 토큰에 담길 데이터(Claims)
  • Registered Claims: 표준 클레임 (iss, exp, sub, iat 등)
  • Public/Private Claims: 사용자 정의 클레임

3. Signature (서명):

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
  • Header와 Payload를 비밀키로 서명한 값
  • 토큰 위변조 여부 확인에 사용

3.4 Access Token과 Refresh Token

토큰 보안 강화를 위한 이중 토큰 구조:

1. Access Token:

  • 실제 API 접근에 사용되는 토큰
  • 짧은 유효기간 (일반적으로 30분~2시간)
  • 탈취 시 피해 최소화

2. Refresh Token:

  • Access Token 재발급용 토큰
  • 긴 유효기간 (일반적으로 2주~1개월)
  • 서버 DB에 저장하여 관리
  • Access Token 만료 시 사용자 재로그인 없이 갱신 가능

인증 흐름:
1. 로그인 성공 → Access Token + Refresh Token 발급
2. API 호출 시 Access Token 사용
3. Access Token 만료 → Refresh Token으로 재발급 요청
4. 서버는 Refresh Token 유효성 검증 후 새 Access Token 발급

3.5 토큰의 장단점

장점:

  • 서버 부담 감소 (Stateless)
  • 확장성 향상 (서버 간 세션 공유 불필요)
  • 다양한 클라이언트 지원
  • 보안성 향상 (서명으로 위변조 방지)

단점:

  • 토큰 사이즈로 인한 네트워크 부하
  • Payload 암호화 없음 (민감 정보 저장 불가)
  • 토큰 탈취 시 즉각 무효화 어려움

4. Servlet Filter

4.1 공통 관심사(Cross-cutting Concerns)

공통 관심사란 여러 위치에서 공통적으로 사용되는 부가 기능을 의미합니다.

  • 핵심 기능: 비즈니스 로직
  • 부가 기능: 로깅, 인증/인가, 보안, 트랜잭션 등

인증/인가 처리와 같은 공통 관심사를 각 컨트롤러마다 구현하면:

  • 코드 중복
  • 유지보수 어려움
  • 책임 분산

이러한 문제를 해결하기 위해 Servlet Filter를 사용합니다.

4.2 Servlet Filter 개념

Servlet Filter는 HTTP 요청과 응답을 가로채서 전처리/후처리하는 컴포넌트입니다.

  • 모든 요청은 필터를 거쳐 서블릿(DispatcherServlet)에 도달
  • 여러 필터가 체인 형태로 연결 가능
  • 특정 URL 패턴에만 적용 가능

4.3 Filter Interface

Filter 인터페이스를 구현하여 필터를 만들 수 있습니다:

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {}
    
    void doFilter(ServletRequest request, ServletResponse response,
                  FilterChain chain) throws IOException, ServletException;
    
    default void destroy() {}
}

주요 메서드:

  • init(): 필터 초기화
  • doFilter(): 실제 필터링 로직 구현
  • destroy(): 필터 종료 시 호출

4.4 Filter 구현 예시

로그인 체크 필터 구현:

@Slf4j
public class LoginCheckFilter implements Filter {
    private static final String[] WHITE_LIST = {"/", "/login", "/logout", "/signup"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        
        log.info("인증 체크 필터 시작: {}", requestURI);
        
        // 인증이 필요없는 URI인지 확인
        if (!isLoginCheckPath(requestURI)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 세션 확인
        HttpSession session = httpRequest.getSession(false);
        if (session == null || session.getAttribute("loginUser") == null) {
            log.info("미인증 사용자 요청: {}", requestURI);
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.sendRedirect("/login");
            return;
        }
        
        // 인증 성공, 다음 필터 또는 서블릿 호출
        chain.doFilter(request, response);
    }
    
    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}

4.5 Filter 등록

Configuration 클래스에서 필터 등록:

@Configuration
public class WebConfig {
    
    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        
        // 필터 설정
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        
        // 필터 순서 설정
        filterRegistrationBean.setOrder(1);
        
        // URL 패턴 설정
        filterRegistrationBean.addUrlPatterns("/*");
        
        return filterRegistrationBean;
    }
}

4.6 Filter Chain

여러 필터를 순서대로 적용할 수 있습니다:

@Configuration
public class WebConfig {
    
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
    
    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}
  • 낮은 순서(order)의 필터가 먼저 실행됨
  • chain.doFilter()를 호출해야 다음 필터 또는 서블릿으로 제어가 넘어감
  • 호출하지 않으면 요청 처리 중단

5. 인증과 인가 정리

5.1 인증(Authentication)

  • 개념: 사용자가 누구인지 확인하는 과정
  • 예시: 로그인
  • 방법:
    • 쿠키-세션 방식
    • 토큰 기반 방식 (JWT)

5.2 인가(Authorization)

  • 개념: 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정
  • 예시: 관리자만 접근 가능한 페이지, 본인만 수정 가능한 게시글
  • 방법:
    • 세션에 저장된 사용자 권한 확인
    • JWT 토큰의 권한 정보 확인

5.3 실무 적용 가이드

1. 쿠키-세션 방식 선택 시:

  • 서버 확장성이 크게 중요하지 않은 경우
  • 웹 브라우저 환경만 지원하는 경우
  • 서비스 보안이 중요한 경우

2. 토큰 방식 선택 시:

  • 서버 확장성이 중요한 경우
  • 다양한 클라이언트(웹, 모바일 앱 등) 지원 필요
  • API 서버를 구축하는 경우

3. 참고 사항:

  • Spring Security는 이러한 인증/인가 메커니즘을 표준화된 방식으로 제공
  • 직접 구현보다는 검증된 라이브러리 활용 권장

6. 핵심 요약

  1. 쿠키(Cookie)

    • 클라이언트 측에 저장되는 데이터
    • HTTP의 Stateless 특성 보완
    • 보안에 취약하므로 민감 정보 저장 지양
  2. 세션(Session)

    • 서버 측에 사용자 정보 저장
    • 클라이언트에는 SessionID만 쿠키로 전달
    • 서버 확장성 제한적
  3. 토큰(Token)

    • 클라이언트 측에 저장, 서버는 Stateless 유지
    • JWT가 대표적인 토큰 형식
    • 서명으로 위변조 방지, 서버 부담 감소
  4. Servlet Filter

    • 공통 관심사(인증/인가 등) 처리를 위한 컴포넌트
    • 요청 전처리/후처리 담당
    • 체인 형태로 여러 필터 적용 가능
  5. 인증과 인가

    • 인증: 사용자 신원 확인
    • 인가: 리소스 접근 권한 확인
    • 상황에 맞는 적절한 방식 선택 필요

0개의 댓글