ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 시리즈 9편 – 로그인 및 인증 처리 전략: JWT 기반 인증 흐름과 보안 설계
    개발/Spring Boot 2025. 4. 24. 14:00

    Spring Boot에서 JWT 기반 인증을 구현하는 방법을 설명합니다. 토큰 발급, 검증, Spring Security 연동, 보안 설계 전략까지 포함된 실전 인증 처리 가이드입니다.


    Spring Boot 시리즈 9편 – 로그인 및 인증 처리 전략: JWT 기반 인증 흐름과 보안 설계

    웹 애플리케이션이 성장하면서 로그인 처리 또한 단순 세션 기반에서 **토큰 기반 인증(JWT)**으로 빠르게 전환되고 있습니다.
    이번 글에서는 Spring Boot 환경에서 JWT를 이용한 인증 흐름을 어떻게 설계하고 구현할 수 있는지,
    그리고 Spring Security와 어떻게 통합하며 보안적으로 주의해야 할 점은 무엇인지 실무 중심으로 정리해보겠습니다.


    📌 1. 왜 JWT인가?

    ✅ JWT(Json Web Token)의 특징

    • 서버 상태를 저장하지 않는 Stateless 인증 구조
    • 클라이언트가 토큰만 있으면 어디서나 인증 가능
    • 로그인 이후 API 호출마다 Authorization 헤더만으로 인증 수행

    ✅ 전통적인 세션 방식과 비교

    항목 세션 기반 JWT 기반

    상태 저장 서버에 세션 저장 상태 없음 (Stateless)
    확장성 서버 수 늘릴수록 복잡 확장 용이 (Load Balancer 친화적)
    토큰 보관 위치 서버 클라이언트 (보통 LocalStorage or HttpOnly Cookie)

    🛠️ 2. JWT 인증 흐름 설계

    [1] 로그인 요청 (ID/PW)
           ↓
    [2] 인증 성공 → JWT 발급
           ↓
    [3] 클라이언트가 토큰 저장
           ↓
    [4] API 요청 시 Authorization: Bearer {토큰}
           ↓
    [5] JWT 유효성 검사 → 인증된 사용자로 요청 처리
    

    ✅ 3. JWT 발급 및 검증 구현

    1️⃣ JWT Utility 클래스

    @Component
    public class JwtTokenProvider {
    
        private final String secretKey = "my-secret-key"; // 환경 변수로 분리 권장
        private final long validityInMs = 3600000; // 1시간
    
        public String createToken(String userId, String role) {
            Claims claims = Jwts.claims().setSubject(userId);
            claims.put("role", role);
    
            Date now = new Date();
            Date expiry = new Date(now.getTime() + validityInMs);
    
            return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
                .compact();
        }
    
        public boolean validateToken(String token) {
            try {
                Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(secretKey.getBytes())
                    .parseClaimsJws(token);
                return !claims.getBody().getExpiration().before(new Date());
            } catch (Exception e) {
                return false;
            }
        }
    
        public String getUserId(String token) {
            return Jwts.parser()
                .setSigningKey(secretKey.getBytes())
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
        }
    }
    

    🔐 4. Spring Security와 통합 – 필터 구성

    1️⃣ SecurityConfig 설정

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.csrf().disable()
                .httpBasic().disable()
                .authorizeHttpRequests()
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
                .and()
                    .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    
            return http.build();
        }
    }
    

    2️⃣ JwtAuthenticationFilter

    public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
        private final JwtTokenProvider tokenProvider;
    
        public JwtAuthenticationFilter(JwtTokenProvider provider) {
            this.tokenProvider = provider;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain) throws ServletException, IOException {
    
            String token = resolveToken(request);
    
            if (token != null && tokenProvider.validateToken(token)) {
                String userId = tokenProvider.getUserId(token);
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userId, "", List.of());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
    
            chain.doFilter(request, response);
        }
    
        private String resolveToken(HttpServletRequest request) {
            String bearer = request.getHeader("Authorization");
            return (bearer != null && bearer.startsWith("Bearer ")) ? bearer.substring(7) : null;
        }
    }
    

    🧠 실무 적용 시 고려 사항

    항목 전략

    비밀키 관리 application.yml + 환경변수로 분리
    토큰 만료 시간 액세스 토큰 15~60분, 리프레시 토큰은 7일 이상
    사용자 권한 정보 role/permission claim으로 포함 가능
    리프레시 토큰 별도 DB에 저장, Access 토큰 갱신용으로 사용
    토큰 폐기 처리 블랙리스트 저장소(Redis 등) 사용

    📦 로그인 요청/응답 구조 예시

    🔹 로그인 요청

    {
      "email": "user@example.com",
      "password": "1234"
    }
    

    🔹 로그인 성공 응답

    {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...",
      "expiresIn": 3600
    }
    

    이후 API 호출 시 Authorization: Bearer {accessToken} 헤더를 사용


    ✅ 마무리 요약

    항목 핵심 요약

    인증 방식 Stateless 구조의 JWT 기반 인증
    핵심 구성 JwtTokenProvider, JwtAuthenticationFilter, Spring Security
    토큰 구조 Subject(사용자 ID), Role, Expiration 포함
    보안 전략 토큰 유효성 검사, 리프레시 토큰 분리, 예외 처리 통일
    테스트 로그인 → 토큰 발급 → 인증이 필요한 API 호출로 흐름 검증

    📌 다음 편 예고

    Spring Boot 시리즈 10편: Spring Security로 인가 처리 확장 – 권한 기반 API 보호 전략

     

Designed by Tistory.