본문 바로가기
스프링 부트3

스프링 부트 3와 JWT 통합하기: 실전 가이드

by 굿센스굿 2024. 12. 4.
반응형

 

**JWT(Json Web Token)**는 안전하고 효율적인 인증 및 권한 부여를 제공하는 데 가장 널리 사용되는 기술 중 하나입니다. 스프링 부트 3는 JWT와의 통합을 손쉽게 처리할 수 있도록 다양한 도구와 유틸리티를 제공합니다. 이 글에서는 JWT의 기본 개념, 스프링 부트 3와의 통합 방법, 그리고 실제 애플리케이션에서의 구현 예제를 다룹니다.


1. JWT란 무엇인가?

JWT는 JSON 포맷을 기반으로 하여 정보를 안전하게 교환하기 위한 토큰입니다. 일반적으로 클라이언트와 서버 간 인증 및 데이터 전송에 사용됩니다.

JWT의 구조:

JWT는 점(.)으로 구분된 3개의 파트로 구성됩니다.

  1. Header (헤더)
  2. JWT의 타입과 서명 알고리즘 정보가 포함됩니다.
  3. { "alg": "HS256", "typ": "JWT" }
  4. Payload (페이로드)
  5. 사용자 정보와 클레임(Claim)이 담깁니다.
  6. { "sub": "user123", "name": "John Doe", "roles": ["USER"] }
  7. Signature (서명)
  8. 헤더와 페이로드를 기반으로 서버의 비밀키로 생성된 서명입니다. 이를 통해 데이터의 무결성을 보장합니다.

2. JWT를 사용하는 이유

  1. 상태를 유지하지 않는 인증
    서버가 세션을 저장하지 않고도 클라이언트를 인증할 수 있습니다.
  2. 빠른 인증 처리
    클라이언트가 매 요청 시 토큰을 보내면 서버는 토큰의 서명만 검증하여 사용자 인증을 처리합니다.
  3. 확장성
    JWT는 다양한 클라이언트(웹, 모바일 등)에서 사용될 수 있습니다.

3. 스프링 부트 3와 JWT 통합하기

(1) 프로젝트 설정

먼저 Maven 또는 Gradle에 필요한 의존성을 추가합니다.
Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

Gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

(2) JWT 생성 및 검증 유틸리티 클래스 작성

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {

    private static final String SECRET_KEY = "mySecretKey123456789012345678901234567890"; // 256-bit key
    private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
    }

    // JWT 생성
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // JWT 검증
    public Claims validateToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

(3) Security 설정

스프링 부트 3에서 JWT를 인증 필터로 사용하려면 SecurityFilterChain을 설정합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않음
                .and()
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

(4) JWT 필터 작성

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            try {
                String username = jwtUtil.validateToken(token).getSubject();
                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken(username, null, null));
            } catch (Exception e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

(5) 로그인 및 인증 API 작성

Controller

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final JwtUtil jwtUtil;
    private final PasswordEncoder passwordEncoder;

    public AuthController(JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
        this.jwtUtil = jwtUtil;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping("/login")
    public String login(@RequestBody AuthRequest request) {
        // 실제로는 데이터베이스에서 사용자 인증
        if ("user".equals(request.getUsername()) && "password".equals(request.getPassword())) {
            return jwtUtil.generateToken(request.getUsername());
        }
        throw new RuntimeException("Invalid credentials");
    }
}

class AuthRequest {
    private String username;
    private String password;

    // Getters and Setters
}

4. 실제 요청 흐름 예제

  1. 로그인 요청
    • 요청:
      POST /auth/login
      Content-Type: application/json
      {
          "username": "user",
          "password": "password"
      }
      
    • 응답:
      HTTP/1.1 200 OK
      {
          "token": "eyJhbGciOiJIUzI1NiIsInR..."
      }
      
  2. 인증된 API 호출
    • 요청:
      GET /api/resource
      Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
      
    • 응답:
      HTTP/1.1 200 OK
      {
          "data": "Secure Data"
      }
      

5. 마무리 및 추가 팁

스프링 부트 3와 JWT의 통합은 인증과 권한 부여를 효율적으로 처리할 수 있는 강력한 방법입니다. 위에서 소개한 예제와 함께 JWT를 활용하여 확장 가능한 보안 시스템을 구축해 보세요.
다음 단계로는 JWT와 Spring Security의 권한 관리 통합, Refresh Token 구현, 그리고 OAuth2와의 결합

반응형