Article - 깊게 탐구하기/시간 기록, 관리 서비스 Pinit

[Pinit] JWT 기반 OIDC 로그인/인증 구현 - JWT 구현

조금씩 차근차근 2025. 12. 5. 23:38

코드는 다음 링크를 통해 확인 가능합니다.

 

GitHub - GoGradually/pinit-auth

Contribute to GoGradually/pinit-auth development by creating an account on GitHub.

github.com

 

 

 

첫 MSA 도입, 한번에 다 만들려고 하면 복잡하다.

각각 독립적인 기술인 만큼, JWT + OIDC 조합 두 단계로 나눠서 접근하자.

목차

  1. 내 JWT 인증 흐름 먼저 만들기
  2. 그다음 OIDC 로그인 붙여서 “로그인 수단”만 확장하기
  3. 회원가입 이벤트 발행 + be 서버에 공개 키 공유해서 인증 연동하기

 

이번 글에서는 0~1번 내용에 대해 다룬다.

 


0. 큰 그림 결정하기

  1. 아키텍처 결정
    • pinit-auth = 인증/토큰 발급 서버
    • pinit-be = 일정/통계 리소스 서버
    • FE(프론트)는 항상 내 서버 JWT를 들고 pinit-be를 호출
    • 외부(OIDC: 구글/네이버/카카오)는 “사용자 신원 확인 수단”으로만 쓰고,
      내 서버가 그걸 받아 자체 access/refresh JWT를 발급하는 구조로 사용한다.
  2. 토큰 정책
    • Access Token: 짧게 (5분~10분)
    • Refresh Token: 길게 (2주)
    • 서명: HS256(대칭 키) vs RS256(공개키/비밀키)
      • HS256은 간단. RS256은 키 관리가 번거로움.
      • 비밀 키를 auth 서버에만 두고, 공개키로 be 서버에서 검증하는 것이 안전함.
    • 로그아웃은 구현하지 않는다.
      • 필요에 따라 프론트에서 Access Token/Refresh Token 삭제 처리
      • 서버에서 Refresh Token 블랙리스트 관리도 가능하지만, 복잡도 증가 (글로벌 세션 스토리지 필요, 무상태성 훼손)
      • 따라서 당장은 구현하지 않음.
  3. 전달 방식
    • Access Token: Authorization: Bearer ...
    • Refresh Token: HttpOnly 쿠키
    • *.pinit.go-gradually.me 상위 도메인 전략 사용.

1. 순수 JWT 로그인/인증부터 완성하기

(1) Auth 도메인/테이블 정리

  • Member
    • id (PK)
    • 테스트용으로 email, password (BCrypt 해시) 필드 추가
    • 닉네임은 be api에 쿼리 파라미터 or body로 전달
  • OauthAccount
    • 나중 OIDC용
    • (iss, sub, memberId) 따로 두면 좋을듯

참고: OIDC는 식별자 발급자 (iss) + 피식별자(sub) 조합으로 유니크한 사용자를 구분한다.
이는 변경 가능성이 있는 이메일과 달리 안정적 식별자 역할을 한다.

ID 토큰이 sub을 의미하는 듯 하다.

아이디 토큰은 integer로 주어진다.

그런데 OIDC 공식 문서에 따르면 sub는 string이다.
https://openid.net/specs/openid-connect-core-1_0.html#IDToken

따라서 sub은 string으로 설계한다.

(이후 OIDC 로그인 붙일 때 제대로 설계할 것)

그리하여 Authentication 의 설계는 다음과 같다.

public class JwtAuthenticationToken extends AbstractAuthenticationToken {
    private final Long memberId;
    private final String token;

    public JwtAuthenticationToken(String token) {
        super(Collections.emptyList());
        this.memberId = null;
        this.token = token;
        super.setAuthenticated(false);
    }

    public JwtAuthenticationToken(Long principal, String token, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.memberId = principal;
        this.token = token;
        super.setAuthenticated(true);
    }

    @Override
    public String getCredentials() {
        return token;
    }

    @Override
    public Long getPrincipal() {
        return memberId;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) {
        if (isAuthenticated) {
            throw new IllegalArgumentException("인증 상태 설정은 생성자에서만 할 수 있습니다.");
        }
        super.setAuthenticated(false);
    }
}

(2) JWT 발급/검증 유틸 만들기

스프링 시큐리티 아키텍처의 형식은?

출처: 내 블로그

 

스프링 시큐리티 인증 아키텍처

전체 구조AuthenticationFilter : AuthenticationManager = N : 1AuthenticationManager : AuthenticationProvider = 1 : NSecurityContextHolder - Spring Security에서 인증된 사용자의 세부 정보를 저장하는 곳입니다. 이는 현재 애플리

dev.go-gradually.me

 

  • Filter: 인증/인가 처리 진입점
  • AuthenticationManager
    • Spring Security의 필터가 인증을 수행하는 방법을 정의하는 API
    • 이는 인증 프로세스의 핵심 인터페이스로, Authentication 객체를 입력으로 받아 인증을 수행합
  • Authentication - 두 가지 주요 목적으로 사용됨
    1. AuthenticationManager에 입력으로 제공되어 사용자가 인증을 위해 제공한 자격 증명의 목적
    2. SecurityContext에서 현재 인증된 사용자를 나타내는 목적
    • 이 객체는 주체, 자격 증명, 권한 등의 정보를 포함한다.
  • AuthenticationProvider
    • ProviderManager에 의해 사용되어 특정 유형의 인증을 수행한다.
    • 예를 들어, 사용자 이름/비밀번호 기반 인증, LDAP 인증, OAuth 인증 등 다양한 인증 방식을 구현할 수 있다.

즉, provider에서 UserDetailsService로 사용자 조회하고, PasswordEncoder로 비밀번호 검증한다.

  • JwtTokenProvider 같은 컴포넌트
    • String createAccessToken(memberId, roles, ...)
    • String createRefreshToken(memberId, ...)
    • Claims parse(String token)
    • 유효성 검증/만료 체크 등

그렇게 구현하려는데, signWith 메서드에 Deprecated 된 알고리즘들이 보였다.

Jwts.SIG를 구현한 알고리즘을 사용하라고 한다.

 

암호화 알고리즘의 확장성(직접 암호화 알고리즘 구현)을 지원하기 위해, jjwt 0.12.0부터는 SecureDigestAlgorithm인터페이스를 도입했다.


이와 같이 쓰면 된다고 하니, 잘 써보자.

(3) JWT 필터 구현

  • JwtAuthenticationFilter (OncePerRequestFilter 구현)
    • doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
    • Authorization 헤더에서 Bearer xxx 토큰 추출
    • JwtAuthenticationManager로 인증 시도
    • JwtAuthenticationToken 만들어서 SecurityContext에 set

(4) 테스트용 로그인 API (ID/PW) + 토큰 발급

  1. /auth/login (POST)
    • email, password 받기
    • UserDetailsService 또는 직접 레포지토리로 사용자 조회
    • 비밀번호 매칭 (BCryptPasswordEncoder)
    • 성공 시 access/refresh JWT 발급
    • 응답:
      • JSON body에 accessToken 넣고,
      • refreshToken은 HttpOnly 쿠키로 내려주는 패턴으로
  2. /auth/refresh
    • refreshToken 쿠키(or 헤더) 검증
    • 유효하면 새 accessToken 발급
  3. /auth/logout
    • 클라이언트 쿠키 삭제 + (옵션) 서버에서 refresh blacklist 처리

(4) 스프링 시큐리티 필터/설정

be 로직의 필터에 들어가야 하는 로직
JWT를 검증해서 SecurityContext에 인증 정보 넣는 필터 구현

  • OncePerRequestFilter 구현해서:
    • Authorization: Bearer xxx 추출
    • 유효한 토큰이면 UsernamePasswordAuthenticationToken 만들어 SecurityContext에 set
  • SecurityFilterChain 설정:
    • /auth/**, /oauth2/** 등은 permitAll
    • 나머지는 authenticated()
    • Session은 STATELESS