JWT 수업이 개인 프로젝트와 함께 며칠에 걸쳐서 띄어띄엄 이루어지는 바람에 중간 날짜쯤 실습때 오류가 나면서 가뜩이나 머리에 한 번에 안들어왔던 내용이 다시 머릿속에서 어지럽게 흩어지면서 답답함을 많이 느꼈다. 그래서 이렇게 지난 주 velog로 총체적인 그림을 그려본 것에 이어서 실습 코드를 바탕으로 다시 한 번 더 복습하고 공부하면서 머릿속에 완전히 넣어보려고 한다.


JWT의 개념과 특장점

개념

JWT는 서버가 사용자를 인증한 후, 그 정보를 안전하게 클라이언트에 전달하기 위한 토큰 기반 인증 방식

JWT 구성

HEADER.PAYLOAD.SIGNATURE

  • HEADER : 토큰 타입(JWT)과 해싱 알고리즘(HS256 등) 명시
  • PAYLOAD : 사용자 정보(sub, role, exp 등)을 포함하는 부분
  • SIGNATURE : 서버의 비밀키로 서명한 부분 (변조 방지 역할)

작동 개요

  1. 클라이언트가 아이디/비밀번호로 로그인 요청을 보냄
  2. 서버가 이를 검증하고 JWT를 생성하여 클라이언트에게 반환
  3. 클라이언트는 이후 요청 시 이 토큰을 Authorization: Bearer <token> 헤더에 실어 보냄
  4. 서버는 요청 헤더의 토큰을 검증하여 사용자를 식별하고, 세션 없이 인증 처리

JWT의 주요 특장점

항목설명
무상태 인증 (Stateless)서버에 세션 저장 X → 확장성 높음
빠른 인증 처리DB 조회 없이 토큰 서명만 검증
Cross-domain 지원 용이REST API, 모바일 등 다양한 환경에서 사용 가능
서명 기반 보안서버의 비밀키로 서명되어 토큰 변조 불가

전체 로그인 플로우 요약

🔸처음 요청
[1] 클라이언트 → /login (POST, ID/PW)

[2] LoginController
  → AuthenticationManager.authenticate()

[3] UserDetailsServiceImpl.loadUserByUsername()
  → DB에서 username으로 사용자 조회

[4] 비밀번호 검증(BCryptPasswordEncoder)

[5] 인증 성공 시 JwtService.getToken() 호출
  → JWT 생성

[6] JWT를 Authorization 헤더에 담아 ResponseEntity로 반환


🔸JWT Token 받은 후 요청
[7] (이후 요청 시) 클라이언트가 Authorization 헤더에 JWT 첨부

[8] AuthenticationFilter.doFilterInternal()
  → JwtService.getAuthUser()토큰 검증

[9] 유효하면 SecurityContextHolder에 인증 정보 저장

[10] 컨트롤러 접근 허용 (Spring Security 인증 완료)

[11] 유효하지 않으면 AuthEntryPoint가 401 응답 반환


각 구성 요소별 상세 설명

✅ (1) LoginController

역할: 로그인 요청을 처리하고 JWT를 발급함

recordcontroller <코드 설명>

  • new UsernamePasswordAuthenticationToken
    : 인증용 토큰 객체 생성
  • authenticationManager.authenticate(creds);
    : AuthenticationManager로 인증 요청
  • jwtService.getToken(auth.getName());
    : 인증 성공 시 JWT 생성
  • return ResponseEntity~
    : Authorization 헤더에 JWT 추가해 응답

    <내부 동작>
  • Spring Security의 AuthenticationManager가 내부적으로 UserDetailsServiceImpl을 호출하여 DB 검증
  • 인증 성공 시 JwtService로 JWT를 생성 후 클라이언트로 전송

✅ (2) UserDetailsServiceImpl

역할: AuthenticationManager가 호출하여 DB에서 사용자 정보를 로드함.
사용 이유: 내부적으로 기본값으로 동작하는 코드에서는 DB가 전혀 필요하지 않음. 모든 사용자 정보가 메모리 상에 하드코딩된 사용자 목록에서 검증됨. 개발자가 DB에 있는 사용자 정보를 이용해서 검증을 하기위해 커스터마이징 하는 것임 (Spring Security가 InMemoryUserDetailsManager에서 사용자를 찾음)

AppUser

  • DB에 구성되는 Entity

UserDetailsServiceImpl

  • Spring Security는 내 엔티티 AppUser를 모르니까 Security가 이해할 수 있는 포맷인 UserDetails로 변환해줘야 함 (커스텀 UserDetails 구현도 가능)
  • 인증 시 비밀번호는 SecurityConfig에 설정된 BCryptPasswordEncoder로 비교
    • Authentication auth = authenticationManager.authenticate(creds);
      이 코드가 실행되면 내부적으로 비밀번호 비교가 DaoAuthenticationProvider 클래스에서 일어남
    • Spring Security가 사용할 구성파일 SecurityConfig
    • SecurityConfig에 작성한 PasswordEncoder 객체를 이용해서
    • 내부적으로 auth.userDetailsService(userDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); 이러한 코드 사용
    • 위 코드때문에 로그인 시 입력받은 비밀번호를 자동으로 BCrypt 해시 검증

=> 이렇게 되면 전체적인 플로우에서 사용자 입력 정보 두 개가 검증된 4번까지 실행된 것!

이제 이 부분이 실행될 차례


✅ (3) JwtService

역할: JWT 생성 및 검증 담당

  • 로그인 시엔 getToken()이 자동으로 쓰임
  • JWT 토큰 생성 → 응답 헤더로 클라이언트에게 전달
  • 클라이언트는 localStorage 또는 HTTP Header에 저장
  • 로그인 이후 요청에서는 getAuthUser()가 자동으로 쓰임
  • 사용자가 로그인 이후 API 요청을 보낼 때는
    HTTP 헤더에 JWT를 포함시켜 보냄
  • 이 요청을 받으면 Spring Security의 FilterChain 중에서
    개발자가 만든 AuthenticationFilter가 실행됨 (바로 아래 설명)
  • 핵심은 getAuthUser가 토큰 유효성을 검사하고 토큰에 들어 있던 username(=subject)을 꺼내주는 것
  • 토큰이 유효하다면 인증된 사용자라고 Security Context에 저장하고 Controller에 접근 허용
  • 이 둘은 직접 선택해서 호출하지 않아도 Spring Security 흐름에 따라 자연스럽게 실행

✅ (4) AuthenticationFilter

역할: 매 요청마다 JWT를 확인하여 인증 객체 생성

  • 요청 헤더에 JWT 존재 시 유효성 검증 (jws != null)
  • 검증 성공하면 Spring Security Context에 인증 객체 저장 setAuthentication
  • 이후 컨트롤러에서 @AuthenticationPrincipal 등을 통해 사용자 정보 접근 가능

✅ (5) AuthEntryPoint

역할: 인증 실패 시(토큰 누락, 만료 등) 401 응답 반환


✅ (6) SecurityConfig

역할: 전체 시큐리티 구성 제어 (JWT 기반 무상태 인증 구조)

핵심 포인트

  • 세션 완전 비활성화 (STATELESS)
  • /login 요청만 인증 없이 허용
  • 나머지 요청은 반드시 JWT 인증 필요
  • AuthenticationFilter가 시큐리티 필터 체인에서 UsernamePasswordAuthenticationFilter 앞에 배치됨
  • AuthEntryPoint로 인증 실패 처리

실제 요청 흐름

✅ (1) 로그인 요청

✅ (2) 서버 응답

✅ (3) 인증 요청

✅ (4-1) 인증 요청 성공

✅ (4-2) 인증 요청 실패

  • 토큰을 일부러 손상시켜서 요청하면 401에러가 남

✅ 정리 요약

구분클래스역할
로그인 요청 처리LoginController사용자 인증 및 JWT 발급
사용자 정보 로드UserDetailsServiceImplDB에서 UserDetails 반환
토큰 생성/검증JwtServiceJWT 생성 및 파싱
요청 필터AuthenticationFilter요청마다 JWT 검증 및 인증 처리
예외 처리AuthEntryPoint인증 실패 시 401 반환
보안 설정SecurityConfig무상태 구성, 필터/엔드포인트 설정

🔹오늘의 나는 무엇을 잘했는지
오늘은 JWT의 흐름과 내부 숨겨진 동작까지 전부 파헤쳐서 중간중간 생략된 내부 알고리즘으로 인해 이해가 모호했던 인증 플로우를 깔끔하게 이해했다. 너무 뿌듯하다😊

🔹어떤 문제를 겪었고, 어떻게 해결할지
스프링 내부에서 자체적으로 구현하는 복잡한 코드들이 많다보니까 그냥 이런 이런 순서대로 코드를 작성하면 토큰 인증이 구현된다~는 식으로 굉장히 모호하게 이해되고 개념이 자리잡지 못하는 문제가 있었는데 오늘처럼 실습코드를 전부 분석해서 하나하나 따라가다보니까 이 문제가 해결되었다.

🔹오늘 배운 것
JWT의 인증의 흐름과 내부 동작 과정을 전부 세밀하게 이해할 수 있었다! 처음에는 그냥 인증이구나 정도로 자리잡았다면 복습과 반복적인 이해를 통해서 스프링의 인증 철학(?)을 어느정도 이해할 수 있었다.

🔹좋았던 점 & 아쉬웠던 점
다른 수업은 그렇지 않았는데 JWT수업은 개인 프로젝트 하는 시간과 수업시간이 혼재해서 흐름이 끊기고 앞 부분의 내용이 기억이 안나서 뒷 부분을 다시 이해하는데 시간이 많이 소요됐다. 아무래도 시간이 촉박하다보니까 내부 알고리즘에 대해서는 깊이있게 설명해주시기 어려웠던 것 같다. 이 부분은 개인 공부로 커버가 되지만 수업시간과 개인 프로젝트 시간이 명확히 나누어 졌으면 훨씬 좋았을 것 같긴 하다

🔹나만의 팁 or 복습 방법
딱 오늘처럼 잘 돌아가는 실습코드를 시작점부터 끝 지점까지 플로우를 명확하게 한 번 정리하고 이를 차근히 따라가면서 공부하는게 효과적이다. 내부 코드의 경우는 어떻게 해서든 찾아서 대략적으로라도 어떤 작업을 하는지 알고 넘어가고 이때 중요한건 큰 플로우를 놓치지 않고 가져가면서 내가 지금 공부하는 부분이 어디인지를 놓치지 않는 것 같다. 세부적인 것들을 이해하다보면 흐름을 놓치기 쉬우니까! 오늘은 아주 만족스러운 복습 시간이었다. 머리에 너무 모호하게 자리잡아서 괴로웠는데..ㅎㅎ

0개의 댓글