TIL/[TIL]

[TIL]Spring Security와 JWT 기반 인증 흐름

namerong 2026. 6. 23. 18:00

1. 학습 주제

Spring Security를 사용해 회원가입, 로그인, 토큰 인증, 로그아웃까지 이어지는 인증 구조를 정리했다.
핵심은 세션 로그인 방식이 아니라 JWT 기반의 Stateless 인증 방식으로 동작하도록 구성한 점이다.

  • 회원가입 시 비밀번호 암호화
  • 로그인 시 사용자 검증 후 AccessToken, RefreshToken 발급
  • JWT 필터로 요청마다 사용자 인증 처리
  • RefreshToken 재발급 구조 이해
  • 인증 실패와 인가 실패를 분리해서 응답 처리
  • Spring Security가 요청을 어떤 흐름으로 처리하는지 정리

2. 전체 구조

이번 구성은 크게 아래 흐름으로 이해하면 된다.

  1. 회원가입 시 사용자 정보를 저장한다.
  2. 로그인 시 아이디와 비밀번호를 검증한다.
  3. 검증이 끝나면 AccessToken과 RefreshToken을 발급한다.
  4. 이후 요청에서는 AccessToken을 헤더에 담아 보낸다.
  5. 서버는 JWT 필터에서 토큰을 검사하고, 유효하면 인증 객체를 SecurityContext에 넣는다.
  6. 보호된 URL은 이 인증 정보를 기준으로 접근 가능 여부를 판단한다.
  7. AccessToken이 만료되면 RefreshToken으로 재발급한다.

즉, 로그인 한 번으로 끝나는 것이 아니라, 이후 모든 요청마다 토큰을 근거로 인증을 다시 세우는 구조다.


3. Spring Security가 하는 일

Spring Security는 요청이 들어오면 먼저 보안 필터 체인을 거치게 만든다.
여기서 인증이 필요한 요청인지, 토큰이 있는지, 권한이 맞는지를 검사한다.

이번 설정에서 중요했던 부분은 다음과 같다.

  • CSRF 비활성화
  • 세션을 사용하지 않도록 STATELESS 설정
  • 로그인, 회원가입, 토큰 재발급, 로그아웃은 허용
  • 특정 API는 USER 권한이 있어야 접근 가능
  • JWT 인증 필터를 기본 인증 필터보다 앞에 추가

이 설정은 “로그인 폼 기반 보안”이 아니라 “API 서버용 토큰 인증”에 맞는 형태다.


4. 회원가입 처리

회원가입은 단순히 사용자 정보를 저장하는 것이 아니라, 비밀번호를 반드시 암호화해서 저장해야 한다.

  • 사용자가 입력한 평문 비밀번호를 그대로 저장하면 안 된다.
  • PasswordEncoder로 암호화한 뒤 저장한다.
  • 여기서는 BCryptPasswordEncoder를 사용했다.

로그인 때는 평문끼리 비교하는 것이 아니라,

  • 사용자가 입력한 비밀번호
  • DB에 저장된 암호화 비밀번호

이 둘을 matches()로 비교한다.

즉, 비밀번호는 “복호화해서 비교”하는 것이 아니라, 입력값을 검증용으로 대조하는 방식이다.


5. 로그인과 토큰 발급

로그인 과정은 다음 순서로 이해하면 된다.

  1. 아이디로 사용자를 조회한다.
  2. 비밀번호가 맞는지 확인한다.
  3. 맞으면 AccessToken과 RefreshToken을 만든다.
  4. AccessToken은 응답 바디로 내려준다.
  5. RefreshToken도 함께 발급하지만, 브라우저에는 HttpOnly 쿠키로 저장한다.
  6. 서버는 RefreshToken을 DB에도 저장한다.

여기서 중요한 점은 역할 분리다.

AccessToken

  • 실제 API 요청 인증에 사용
  • 수명이 짧다
  • 헤더에 담아 보낸다

RefreshToken

  • AccessToken 재발급 용도
  • 수명이 길다
  • 탈취 위험을 줄이기 위해 HttpOnly 쿠키로 관리
  • 서버 DB에도 저장해 이중 검증한다

즉, RefreshToken은 단순히 발급만 하는 것이 아니라, 서버가 직접 저장하고 추적하는 값이다.


6. JWT 필터의 동작 방식

JWT 인증 필터는 요청이 올 때마다 실행된다.

동작 흐름은 다음과 같다.

  1. 요청 헤더의 Authorization 값을 확인한다.
  2. Bearer 토큰값 형식인지 검사한다.
  3. 토큰이 있으면 서명, 만료 여부를 검증한다.
  4. 토큰에서 username을 꺼낸다.
  5. UserDetailsService로 사용자 정보를 조회한다.
  6. 인증 객체를 만들어 SecurityContextHolder에 저장한다.
  7. 이후 Spring Security는 이 요청을 “인증된 사용자 요청”으로 본다.

여기서 핵심은 JWT만 검사한다고 끝나는 것이 아니라,
검사 후에 인증 객체를 SecurityContext에 넣어줘야 Spring Security가 인증된 요청으로 인정한다는 점이다.


7. UserDetailsService의 역할

UserDetailsService는 Spring Security가 사용자 정보를 읽을 때 사용하는 표준 방식이다.

이번 구조에서는 username으로 사용자를 조회한 뒤,

  • username
  • password
  • 권한 정보

를 담은 UserDetails 객체로 변환해서 넘긴다.

즉, DB의 사용자 엔티티를 그대로 쓰는 것이 아니라,
Spring Security가 이해할 수 있는 인증용 사용자 객체로 바꿔주는 역할을 한다.


8. 인증과 인가의 차이

헷갈리기 쉬운 부분이라 같이 정리해두면 좋다.

인증 Authentication

  • “누구인지 확인하는 것”
  • 로그인했는지, 토큰이 유효한지 확인
  • 예: 이 사용자가 정말 해당 계정 사용자인가?

인가 Authorization

  • “무엇을 할 수 있는지 확인하는 것”
  • 인증된 사용자가 특정 기능에 접근 가능한지 검사
  • 예: USER 권한으로 이 API를 호출할 수 있는가?

즉,

  • 인증 실패는 “너 누구야?”
  • 인가 실패는 “누군지는 알겠는데 이건 못 해”

라는 차이로 보면 이해가 쉽다.


9. 401과 403의 차이

이번 구성에서는 인증 실패와 인가 실패를 핸들러로 따로 처리했다.

401 Unauthorized

  • 로그인 안 했거나
  • 토큰이 없거나
  • 토큰이 유효하지 않은 경우

403 Forbidden

  • 로그인은 됐지만
  • 해당 권한이 없어서 접근이 막힌 경우

이 둘을 분리하면 프론트엔드도 상황에 맞게 대응하기 쉬워진다.

  • 401이면 로그인 페이지 이동
  • 403이면 권한 없음 안내

처럼 나눌 수 있다.


10. RefreshToken 재발급 흐름

토큰 재발급은 단순히 RefreshToken만 있으면 바로 발급하는 구조가 아니다.

  1. 쿠키에서 RefreshToken을 읽는다.
  2. 토큰 자체가 유효한지 검사한다.
  3. 토큰에서 username을 꺼낸다.
  4. DB에 저장된 RefreshToken을 조회한다.
  5. 요청으로 들어온 토큰과 DB 저장 토큰이 같은지 비교한다.
  6. DB에 저장된 만료 시간도 다시 확인한다.
  7. 모든 검증이 통과하면 새로운 AccessToken, RefreshToken을 발급한다.
  8. 새 RefreshToken으로 DB 값을 갱신한다.

즉, 재발급은 “토큰 문자열만 보고 믿는 것”이 아니라
JWT 검증 + DB 검증을 함께 거치는 구조다.


11. 로그아웃 처리

로그아웃은 단순히 브라우저에서 토큰을 지우는 것으로 끝나지 않는다.

  • 서버 DB에 저장된 RefreshToken을 삭제하고
  • 브라우저의 쿠키도 만료시켜 제거한다

이렇게 해야 이후 재발급 요청도 막을 수 있다.
즉, 서버 쪽 상태와 브라우저 쪽 상태를 함께 정리해야 로그아웃이 제대로 된다.


12. HttpOnly 쿠키를 사용하는 이유

RefreshToken을 쿠키로 저장할 때 HttpOnly를 주는 이유도 중요하다.

  • JavaScript로 접근할 수 없게 만든다.
  • XSS 공격으로 토큰이 탈취되는 위험을 줄인다.
  • SameSite 설정으로 CSRF 위험도 일부 완화할 수 있다.

즉, 민감한 토큰을 브라우저에 둘 때는
“그냥 저장”이 아니라 어떻게 저장하느냐가 중요하다.


13. hasRole과 hasAuthority 차이

권한 검사에서 자주 헷갈리는 부분이다.

  • hasRole("USER") 는 내부적으로 ROLE_USER를 찾는다.
  • hasAuthority("USER") 는 문자열 USER 자체를 찾는다.

DB에 권한이 USER로 저장되어 있다면 hasAuthority("USER")가 자연스럽다.
권한 값을 어떻게 저장했는지에 따라 선택이 달라진다.


14. 핵심 정리

  1. Spring Security는 요청마다 필터 체인에서 인증과 인가를 검사한다.
  2. 세션을 쓰지 않는 JWT 방식에서는 STATELESS 설정이 중요하다.
  3. 회원가입 시 비밀번호는 반드시 암호화해서 저장해야 한다.
  4. 로그인 성공 후 AccessToken과 RefreshToken을 발급한다.
  5. AccessToken은 API 호출용, RefreshToken은 재발급용이다.
  6. JWT 필터는 토큰을 검증하고 인증 객체를 SecurityContext에 저장한다.
  7. UserDetailsService는 사용자 정보를 Spring Security 형식으로 바꿔준다.
  8. 인증과 인가는 다르며, 401과 403도 구분해서 처리해야 한다.
  9. RefreshToken은 쿠키에만 두는 것이 아니라 서버 DB에서도 함께 관리할 수 있다.
  10. 로그아웃은 서버 토큰 삭제와 쿠키 만료를 같이 처리해야 한다.