|

Spring Security JWT 로그인은 어떻게 흘러갈까: 인증과 인가를 분리해서 이해하기

Spring Security JWT 로그인 흐름 대표 이미지
JWT 로그인은 토큰 발급보다 요청마다 인증 정보를 복원하는 흐름이 더 중요하다.

Spring Security JWT 로그인이 어려운 이유는 JWT 자체보다 Spring Security의 인증과 인가 흐름이 함께 등장하기 때문입니다. 토큰을 만들 수 있어도, 요청마다 그 토큰이 어디서 검증되고 어떤 객체로 바뀌는지 모르면 구조가 흔들립니다.

이 글에서는 인증은 사용자가 누구인지 확인하는 일, 인가는 그 사용자가 무엇을 할 수 있는지 판단하는 일이라는 기준으로 JWT 로그인 흐름을 나눠 보겠습니다.


Spring Security JWT 로그인 흐름 요약

Spring Security JWT 인증 인가 흐름 요약 카드
인증은 누구인지 확인하는 일이고, 인가는 어디까지 허용할지 판단하는 일이다.

로그인 요청에서 실제로 일어나는 일

사용자가 이메일과 비밀번호를 보내면 서버는 저장된 사용자 정보와 비교합니다. 이 단계는 인증입니다. 인증에 성공하면 서버는 세션을 만들 수도 있고, JWT를 만들어 클라이언트에게 줄 수도 있습니다. JWT 방식에서는 보통 서버가 로그인 응답으로 access token을 내려줍니다.

여기서 중요한 점은 JWT가 인증을 대신하는 마법 문자열이 아니라는 것입니다. JWT는 이미 인증된 사용자의 정보를 서명된 토큰 형태로 들고 다니는 수단입니다. 토큰이 유효한지, 만료되지 않았는지, 서명이 맞는지는 매 요청에서 다시 확인해야 합니다.

요청마다 JWT 필터가 해야 하는 일

로그인 이후 클라이언트는 보통 Authorization: Bearer ... 헤더에 토큰을 담아 요청합니다. Spring Security에서는 이 요청이 컨트롤러에 도착하기 전에 필터 체인을 통과합니다. 이때 JWT 검증 필터를 두어 토큰을 읽고 검증한 뒤 인증 객체를 만듭니다.

String token = resolveBearerToken(request);
Claims claims = jwtVerifier.verify(token);
Authentication authentication =
    new UsernamePasswordAuthenticationToken(
        claims.getSubject(),
        null,
        authoritiesFrom(claims)
    );
SecurityContextHolder.getContext().setAuthentication(authentication);

위 코드는 실제 프로젝트 코드가 아니라 흐름을 보여주는 예시입니다. Spring Security 공식 문서에 따르면 SecurityContextHolder는 현재 인증된 사용자의 세부 정보를 보관하는 곳입니다. JWT 필터의 핵심은 검증된 토큰을 바탕으로 이 컨텍스트를 채우는 것입니다.

Authentication은 무엇을 담을까

Authentication은 “현재 요청을 보낸 사용자가 누구인지”를 나타냅니다. principal에는 사용자 식별 정보가 들어가고, authorities에는 역할이나 권한이 들어갑니다. JWT 안에 role이나 scope를 넣었다면 이 값을 꺼내 GrantedAuthority로 바꾸는 과정이 필요합니다.

하지만 모든 정보를 JWT에 넣는 것은 좋지 않습니다. 토큰은 클라이언트가 들고 다니므로 민감한 정보는 피해야 하고, 너무 많은 정보를 넣으면 토큰이 커집니다. 보통은 사용자 식별자, 만료 시각, 필요한 최소 권한 정도만 담고, 세부 정보는 서버에서 다시 조회하는 식으로 균형을 잡습니다.

인가는 언제 일어날까

인가 단계는 인증 이후에 일어납니다. 예를 들어 /admin은 ADMIN 권한이 필요하고, /me는 로그인만 되어 있으면 된다는 규칙이 있을 수 있습니다. Spring Security는 현재 SecurityContext의 권한과 URL/메서드 규칙을 비교해 허용 여부를 판단합니다.

  • 토큰이 없거나 잘못되면 인증 실패다.
  • 토큰은 유효하지만 권한이 부족하면 인가 실패다.
  • 인증 실패는 보통 401, 인가 실패는 보통 403으로 구분한다.
  • JWT를 검증했더라도 권한 설계가 느슨하면 보호가 되지 않는다.

흔한 설계 실수

가장 흔한 실수는 로그인 성공 후 JWT만 발급하고, 이후 요청에서 SecurityContext를 제대로 채우지 않는 것입니다. 이러면 컨트롤러나 권한 검사에서 현재 사용자를 안정적으로 알 수 없습니다.

또 다른 실수는 인증과 인가를 한 코드에 섞는 것입니다. 토큰 파싱 코드에서 URL 권한까지 판단하기 시작하면 나중에 권한 정책이 바뀔 때 필터가 지나치게 복잡해집니다. JWT 필터는 토큰 검증과 인증 객체 생성에 집중하고, 권한 판단은 Spring Security의 인가 설정으로 분리하는 편이 관리하기 쉽습니다. 서비스 계층의 책임 경계를 함께 보고 싶다면 Spring Boot @Transactional 위치 글도 같이 읽으면 흐름이 이어집니다.

refresh token은 어디에 둘까

JWT 로그인 글에서 access token과 refresh token을 함께 이야기하는 경우가 많습니다. access token은 요청마다 인증 정보를 복원하는 데 쓰이고, refresh token은 access token이 만료되었을 때 새 access token을 받기 위한 장치로 쓰는 경우가 많습니다. 둘의 수명과 저장 위치를 같게 두면 보안상 이점이 줄어듭니다.

브라우저 기반 서비스에서는 토큰 저장 위치도 중요합니다. localStorage에 저장하면 구현은 쉽지만 XSS에 취약할 수 있고, httpOnly cookie를 쓰면 JavaScript에서 직접 읽을 수 없지만 CSRF 설계를 함께 봐야 합니다. 이 글의 범위를 넘는 부분이지만, JWT 로그인은 필터 코드만으로 끝나는 문제가 아니라 클라이언트 저장 방식까지 함께 설계해야 합니다.

구현 전에 확인할 체크리스트

  • 토큰 만료 시간을 access token과 refresh token에 다르게 둘 것인가
  • 토큰 서명 키를 코드에 하드코딩하지 않고 환경 변수나 secret store로 관리하는가
  • 탈퇴, 비밀번호 변경, 강제 로그아웃 때 기존 토큰을 어떻게 무효화할 것인가
  • 401과 403을 구분해 클라이언트가 재로그인과 권한 부족을 다르게 처리할 수 있는가
  • 권한 정보가 바뀌었을 때 이미 발급된 JWT와 서버 권한 정책이 어긋나지 않는가

이 체크리스트를 먼저 정리하면 JWT 필터 구현이 훨씬 명확해집니다. 단순히 토큰을 파싱하는 코드보다, 인증 실패와 인가 실패를 어떤 사용자 경험으로 돌려줄지까지 정해야 실제 서비스 흐름이 완성됩니다.

정리

Spring Security JWT 로그인은 “로그인 때 토큰을 만든다”에서 끝나지 않습니다. 이후 모든 요청에서 토큰을 검증하고, Authentication을 만들고, SecurityContext에 넣고, 마지막으로 권한 규칙과 비교합니다. 이 흐름을 나누어 보면 JWT 로그인 구현이 훨씬 덜 막연해집니다.

함께보면 좋은 글