[Spring Boot] JWT

handa·2025년 1월 21일
0
post-thumbnail

Spring Boot와 JWT (JSON Web Token)

Spring Boot는 Java 기반의 애플리케이션 개발을 간소화하는 프레임워크로, 인증과 권한 부여(Authentication and Authorization)에서도 자주 사용됩니다. JSON Web Token(JWT)은 인증 및 정보 교환 목적으로 사용되는 컴팩트하고 독립적인 토큰입니다. Spring Boot에서 JWT를 사용하는 것은 인증/인가를 처리하는데 매우 효과적입니다.


1. JWT란?

JWT는 JSON 포맷으로 데이터를 포함하며, 주로 다음 세가지 파트로 구성됩니다.

  • Header : 토큰 유형(JWT)과 서명 알고리즘 정보가 포함됩니다.
  • Payload : 클레임(Claims)이라고 불리는 사용자의 데이터가 포함됩니다. 이 데이터는 Base64로 인코딩됩니다.
  • Signature : 토큰의 무결성을 보장하기 위한 서명입니다. 비밀키를 사용하여 Header와 Payload를 해싱한 값입니다.

2. Spring Boot에서 JWT 인증 흐름

  1. 사용자 로그인
    • 사용자가 ID와 비밀번호를 서버로 전송합니다.
    • 서버는 이를 검증한 후 JWT를 생성하여 클라이언트에게 반환합니다.
  2. 클라이언트 요청
    • 클라이언트는 반환된 JWT를 Authorization 헤더에 포함하여 API 요청을 보냅니다.
        Authorization: Bearer <JWT>
  3. 서버에서 토큰 검증
    • 서버는 JWT의 유효성을 검증한 후 요청을 처리합니다.

3. Spring Boot에서 JWT구현

다음은 Spring Boot로 JWT 인증 시스템을 구현하는 기본단계입니다.

(1) 의존성 추가

pom.xml에 필요한 의존성을 추가합니다.

<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>

(2) JWT 생성 유틸리티

JWT 토큰을 생성하는 유틸리티 클래스를 작성합니다.

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtTokenUtil {

    private static final String SECRET_KEY = "your_secret_key";
    private static final long EXPIRATION_TIME = 86400000; // 1 day

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static String extractUsername(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public static boolean isTokenValid(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

(3) 필터 구현

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.core.userdetails.UserDetails;
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 {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String authorizationHeader = request.getHeader("Authorization");
        String username = null;
        String jwtToken = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwtToken = authorizationHeader.substring(7);
            username = JwtTokenUtil.extractUsername(jwtToken);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // UserDetailsService에서 사용자 정보를 로드 (필요시 구현)
            UserDetails userDetails = /* load user from DB */;
            if (JwtTokenUtil.isTokenValid(jwtToken)) {
                // SecurityContext에 인증 정보 설정
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

(4) Security 설정

Spring Security에 JWT 필터를 추가합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers("/authenticate", "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

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

4. 추가 고려사항

  1. 토큰 갱신 : 액세스 토큰과 리프레시 토큰을 사용하여 만료된 토큰을 갱신하는 메커니즘을 구현할 수 있습니다.
  2. 서버 사이드 로그아웃 : 로그아웃 시 JWT를 무효화하려면 블랙리스트를 사용하는 것이 일반적입니다.
  3. 보안 강화 : SECRET_KEY는 반드시 안전하게 보호하고, 필요시 키 로테이션을 고려하세요.
profile
진짜 해보자

0개의 댓글