[Spring Boot] 인증 및 인가 (Spring Security, JWT)

Hood·2025년 4월 25일

Spring Boot

목록 보기
10/15
post-thumbnail

✍ Back-End 지식을 늘리자!

백엔드 개발자를 준비하면서 생긴 궁금증을 정리한 포스트입니다.


들어가기 전

A&I 동아리원들을 위한 서버를 만들면서 로그인 기능이 필요했습니다.
기초 CS 과정을 거친 뒤, 기준에 맞는 사용자만 서버에 접근할 수 있도록 해야 했기 때문입니다.

그래서 이번 글에서는 JWT를 이용해 Access Token을 발급하고,
이를 바탕으로 사용자를 확인하는 인증(Authentication)
인증된 사용자가 특정 자원에 접근할 수 있는지를 판단하는 인가(Authorization) 를 함께 정리해보려고 합니다.


용어 정리

인증 (Authentication)

인증은 사용자가 누구인지 확인하는 과정입니다.
가장 익숙한 예시는 아이디와 비밀번호를 이용한 로그인입니다.
즉, “이 사용자가 정말 본인이 맞는가?”를 확인하는 절차라고 볼 수 있습니다. (Home)

인가 (Authorization)

인가는 인증이 끝난 뒤,
해당 사용자가 어떤 자원에 접근할 수 있는지를 확인하는 과정입니다.
예를 들어 관리자만 접근 가능한 API가 있다면,
로그인에 성공했더라도 관리자 권한이 없는 사용자는 접근할 수 없습니다. (Home)

접근 주체 (Principal)

Principal은 보호된 자원에 접근하려는 주체, 즉 사용자 자신을 의미합니다.
Spring Security의 Authentication 인터페이스에서도 principal은 인증되는 주체의 신원으로 설명됩니다. (Home)

자격 증명 (Credentials)

Credentials는 그 사용자가 맞다는 것을 증명하는 정보입니다.
가장 대표적인 예시는 비밀번호입니다.
즉, Principal이 “누구인가”라면, Credentials는 “그 사람이 맞다는 증거”라고 이해하면 됩니다. (Home)

서블릿 (Servlet)

Servlet은 클라이언트의 요청을 받아 처리하고 응답을 만드는 자바 웹 컴포넌트입니다.
다만 이 글에서 사용하는 프로젝트는 Spring MVC가 아니라 WebFlux 기반이므로,
실제 보안 흐름을 설명할 때는 Servlet 기반 필터 체인보다 Reactive WebFlux 보안 흐름으로 이해하는 편이 더 잘 맞습니다.
Spring WebFlux는 Spring의 리액티브 웹 스택이며, Servlet API 중심의 Spring MVC와는 다른 실행 모델을 가집니다. (Home)


Spring Security

Spring Security는 스프링 기반 애플리케이션에서
인증(Authentication), 인가(Authorization), 그리고 여러 일반적인 보안 취약점 대응 기능을 제공하는 보안 프레임워크입니다.
또한 스프링 시큐리티는 전통적인 imperative 애플리케이션뿐 아니라 reactive 애플리케이션도 지원합니다. (Home)

즉, 개발자가 로그인 처리, 권한 검사, 보안 설정을 모두 직접 구현하지 않아도
체계적인 방식으로 보안 기능을 적용할 수 있도록 도와줍니다.


WebFlux 기반 Spring Security 흐름 이해하기

일반적으로 Spring Security를 설명할 때는 Filter Chain 중심으로 설명하는 경우가 많습니다.
하지만 현재 프로젝트는 WebFlux 기반이므로,
실제로는 SecurityWebFilterChainAuthenticationWebFilter 같은 리액티브 보안 컴포넌트를 통해 인증과 인가가 처리됩니다.
Spring Security의 AuthenticationWebFilter는 특정 요청에 대해 인증을 수행하는 WebFilter입니다. (Home)

즉, 요청이 들어오면 다음과 같은 흐름으로 생각할 수 있습니다.

  1. 클라이언트가 API 요청을 보냅니다.
  2. SecurityWebFilterChain이 해당 요청을 먼저 확인합니다.
  3. 인증이 필요한 요청이라면 AuthenticationWebFilter가 동작합니다.
  4. 헤더에서 Bearer Token을 추출하고, 이를 인증 객체로 변환합니다.
  5. ReactiveAuthenticationManager가 JWT를 검증합니다.
  6. 검증이 끝나면 인증된 사용자 정보와 권한이 보안 컨텍스트에 반영됩니다.
  7. 이후 인가 규칙에 따라 요청 허용 여부를 판단합니다. (Home)

JWT란?

JWT(Json Web Token)는 JSON 객체 형태의 정보를 담고,
그 내용이 위변조되지 않았는지를 서명(signature) 으로 검증할 수 있도록 만든 토큰 형식입니다.
JWT는 RFC 7519로 표준화되어 있으며, compact하고 self-contained한 방식으로 정보를 전달할 수 있도록 설계되었습니다. (JSON Web Tokens - jwt.io)

즉, 서버는 사용자 정보와 권한 같은 내용을 토큰에 담아 서명하고,
클라이언트는 이후 요청마다 이 토큰을 함께 보내 인증에 활용할 수 있습니다.


JWT 구조

JWT는 크게 Header, Payload, Signature 세 부분으로 구성됩니다. (JSON Web Tokens - jwt.io)

Header에는 토큰 타입과 서명 알고리즘 정보가 들어갑니다.

  • typ : 토큰 타입
  • alg : 서명 알고리즘

여기서 주의할 점은 다음과 같습니다.

Payload

Payload에는 사용자 정보나 토큰 정보가 Claim 형태로 들어갑니다.
대표적인 등록 클레임은 다음과 같습니다.

  • iss : 발급자
  • sub : 주제
  • aud : 대상자
  • iat : 발급 시각
  • exp : 만료 시각 (IETF Datatracker)

Signature

Signature는 Header와 Payload를 바탕으로
정해진 알고리즘과 키를 사용해 생성한 서명 값입니다.

여기서 중요한 점은
Header와 Payload는 단순히 Base64URL로 인코딩된 것이지 암호화된 것이 아니라는 점입니다.
즉, 누구나 내용을 열어볼 수는 있지만,
서명이 올바르지 않으면 토큰이 위조되었는지 검증할 수 있습니다.
따라서 JWT는 “내용을 숨기는 토큰”이라기보다
“내용이 위변조되지 않았는지 확인할 수 있는 토큰”으로 이해하는 편이 더 정확합니다. (JSON Web Tokens - jwt.io)


JWT 인증 과정

JWT를 활용한 인증 흐름은 보통 다음과 같습니다.

  1. 사용자가 로그인 요청을 보냅니다.
  2. 서버는 사용자 정보를 확인한 뒤 JWT를 발급합니다.
  3. 클라이언트는 발급받은 토큰을 저장합니다.
  4. 이후 API 요청 시 Authorization: Bearer <token> 헤더에 담아 전송합니다.
  5. 서버는 전달받은 JWT를 검증합니다.
  6. 토큰이 유효하면 인증된 사용자로 처리하고 요청을 수행합니다. (JSON Web Tokens - jwt.io)

다만 토큰 저장 위치는 보안 정책에 따라 달라질 수 있으므로,
단순히 “무조건 로컬 스토리지에 저장한다”라고 단정하기보다는
프로젝트 요구사항에 따라 안전한 저장 방식을 선택한다고 적는 편이 좋습니다.


JWT 기반 인증 시스템에서 자주 사용하는 토큰

Access Token

Access Token은 실제 API 요청에 사용되는 짧은 수명의 토큰입니다.
클라이언트는 보통 이 토큰을 Authorization 헤더에 담아 서버로 보냅니다.

즉, 서버 입장에서는 이 토큰을 통해
“이 요청을 보낸 사용자가 인증된 사용자인가?”를 확인하게 됩니다.

Refresh Token

Refresh TokenAccess Token이 만료되었을 때
새로운 Access Token을 발급받기 위해 사용하는 긴 수명의 토큰입니다.

보통 Refresh Token은 Access Token보다 더 민감하게 다뤄야 하며,
탈취되지 않도록 더욱 주의해서 관리해야 합니다.

토큰이 만료되면?

JWT에는 exp 클레임으로 만료 시각을 담을 수 있습니다.
만료된 토큰으로 요청하면 서버는 보통 인증 실패로 처리하며,
이때 Refresh Token을 사용해 Access Token을 다시 발급받는 구조를 많이 사용합니다. (IETF Datatracker)

또한 JWT 기반 인증은 보통 stateless하게 운용되기 때문에,
발급된 Access Token 자체를 서버가 매번 저장하지 않는 구조를 많이 사용합니다.
그래서 Access Token은 짧게, Refresh Token은 별도 저장소와 함께 관리하는 방식이 자주 사용됩니다.


In Kotlin

현재 A&I 과제 서버는 Coroutine + WebFlux + MongoDB + Kotlin으로 구성되어 있습니다.
또한 JWT를 이용한 간단한 인증 구조를 사용하고 있기 때문에,
Spring Security를 프로젝트 구조에 맞게 커스터마이징해 적용하였습니다.

의존성 추가

Spring Security는 인증과 인가를 처리하기 위해 사용하고,
JJWT는 JWT 생성, 파싱, 검증을 돕는 라이브러리입니다.
JJWT는 JWT를 만들고 검증하는 JVM용 라이브러리로 소개되고 있습니다. (Home)

JWT 토큰 생성 및 추출

JwtTokenProvider에서는 비밀 키를 이용해 토큰을 생성하고,
토큰을 다시 파싱해 사용자 ID를 추출합니다.

이때 subject, roles, issuedAt, expiration 같은 값을 넣어
사용자 식별과 권한 확인, 만료 시간 검사를 함께 처리할 수 있습니다.
JJWT는 signWith(...), verifyWith(...), parseSignedClaims(...) 같은 방식으로
서명된 JWT를 생성하고 검증할 수 있습니다. (GitHub)

커스텀 토큰

프로젝트에서는 JWT에서 읽어온 사용자 ID와 권한을
Spring Security가 이해할 수 있는 인증 객체로 감싸기 위해
커스텀 Authentication 구현체를 사용하고 있습니다.

이 구조는 Spring Security의 Authentication 개념과 잘 맞습니다.
인증 객체에는 principal, credentials, authorities 같은 정보가 담길 수 있습니다. (Home)

커스터마이징 Security 설정

현재 설정에서는 SecurityWebFilterChain을 사용해
경로별 인가 정책을 정의하고,
AuthenticationWebFilter를 통해 JWT 인증을 처리하고 있습니다.
이는 WebFlux 환경에서 사용하는 전형적인 reactive security 구성 방식과 맞닿아 있습니다. (Home)

다만 비밀번호 처리 부분은 한 가지 꼭 짚고 넘어가는 것이 좋습니다.
현재 예제의 PasswordEncoder는 사실상 평문 비교에 가까운 형태인데,
실제 서비스에서는 이런 방식보다 BCryptPasswordEncoder처럼
단방향 해시 기반의 안전한 인코더를 사용하는 것이 더 적절합니다.
Spring Security도 PasswordEncoder를 안전한 비밀번호 저장을 위한 one-way transformation으로 설명합니다. (Home)


A&I 과제 서버의 인증/인가 동작 과정

1. 로그인 요청

사용자가 /api/member/login으로 아이디와 비밀번호를 전송하면,
서버는 해당 사용자를 조회하고 비밀번호를 검증합니다.
검증에 성공하면 JWT를 발급해 클라이언트에 반환합니다.

2. JWT 토큰 발급

로그인 성공 시 서버는 사용자 ID와 역할 정보를 담은 JWT를 생성합니다.
이 토큰은 서명되어 이후 요청에서 사용자 식별과 권한 판별에 활용됩니다.
JWT의 등록 클레임과 서명 구조를 기준으로 보면, 이 과정은 토큰 기반 인증의 전형적인 형태입니다. (IETF Datatracker)

3. Authorization 헤더에서 토큰 추출

이후 사용자가 보호된 API를 호출할 때는
Authorization: Bearer <token> 형식으로 JWT를 전송합니다.

서버는 bearerConverter()에서 이 헤더를 읽고,
토큰 문자열을 JwtAuthenticationToken 객체로 감싸 인증 단계로 넘깁니다.
Reactive 환경에서 AuthenticationWebFilter는 요청을 Authentication으로 변환해 인증을 시도할 수 있습니다. (Home)

4. JWT 검증

ReactiveAuthenticationManager는 전달받은 토큰을 파싱하고,
서명을 검증한 뒤 claims에서 userIdroles를 추출합니다.
이 과정은 JWT가 위조되지 않았는지, 그리고 어떤 사용자인지를 확인하는 핵심 단계입니다. (GitHub)

5. 권한 부여

토큰에서 추출한 역할 정보는 GrantedAuthority로 변환됩니다.
Spring Security는 이렇게 변환된 권한 정보를 바탕으로
hasRole(...), authenticated() 같은 인가 규칙을 적용합니다.
Authentication은 권한 컬렉션을 포함할 수 있도록 설계되어 있습니다. (Home)

6. 인증 객체 생성

검증이 끝나면 JwtAuthenticatedToken을 생성해
“인증된 사용자” 상태로 처리합니다.
이 객체에는 사용자 식별 정보와 권한 정보가 함께 담깁니다. (Home)

7. 인증 정보 반영

인증이 완료되면 Spring Security는
이 인증 정보를 기반으로 이후 요청 처리에서 인가를 수행합니다.
즉, 사용자가 어떤 권한을 가졌는지에 따라 접근 허용 여부가 달라집니다. (Home)

8. Swagger에서 테스트

Swagger UI에 Bearer Token 인증 설정을 해두면,
발급받은 JWT를 입력해 보호된 API를 직접 테스트할 수 있습니다.
이렇게 하면 로그인 이후의 인증/인가 흐름을 훨씬 쉽게 확인할 수 있습니다.


📌 결론

이번 글에서는 로그인 기능을 구현하면서 인증과 인가가 어떤 흐름으로 동작하는지 정리해보았습니다.

정리해보면,
인증(Authentication)은 사용자가 누구인지 확인하는 과정이고,
인가(Authorization)는 그 사용자가 어떤 자원에 접근할 수 있는지 판단하는 과정입니다.
그리고 JWT는 이 인증 정보를 토큰 형태로 담아 전달하기 위한 수단으로 사용할 수 있습니다. (Home)

또한 현재 프로젝트처럼 WebFlux 기반 환경에서는
기존 Servlet 중심 설명과는 조금 다르게
SecurityWebFilterChain, AuthenticationWebFilter, ReactiveAuthenticationManager 같은
reactive 보안 흐름으로 이해하는 것이 더 자연스럽습니다. (Home)

처음에는 구현에만 집중해도,
이 과정을 한 번 개념적으로 정리해두면 이후 보안 관련 기능을 확장하거나 유지보수할 때 훨씬 도움이 됩니다.

profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글