Java

[보안, Java] Spring Boot에서 JWT 구현하기

제익 2026. 4. 28. 17:15
반응형

 

이전 글 : 2026.04.28 - [보안 시스템] - [보안 시스템] JWT(JSON Web Token) 정리

 

이전 글에서는 JWT의 구조와 인증 흐름, 보안 주의사항에 대해 정리했다.
이번 글에서는 Spring Boot 프로젝트에 JWT를 직접 구현하는 방법을 정리해보겠다.


1. 의존성 추가

JWT를 다루기 위해 jjwt(Java JWT) 라이브러리를 사용한다. build.gradle에 아래 의존성을 추가한다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // jjwt
    implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

2. application.yml 설정

Secret Key와 토큰 만료 시간을 설정 파일에서 관리한다.

jwt:
  secret: mySecretKey1234567890mySecretKey1234567890  # 256bit 이상
  access-expiration: 1800000    # 30분 (ms)
  refresh-expiration: 604800000 # 7일 (ms)

3. JwtProvider 구현

토큰의 생성, 파싱, 검증을 담당하는 핵심 클래스다.

@Component
public class JwtProvider {

    private final SecretKey secretKey;
    private final long accessExpiration;
    private final long refreshExpiration;

    public JwtProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.access-expiration}") long accessExpiration,
            @Value("${jwt.refresh-expiration}") long refreshExpiration) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.accessExpiration = accessExpiration;
        this.refreshExpiration = refreshExpiration;
    }

    // Access Token 생성
    public String generateAccessToken(String username, String role) {
        return Jwts.builder()
                .subject(username)
                .claim("role", role)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                .signWith(secretKey)
                .compact();
    }

    // Refresh Token 생성
    public String generateRefreshToken(String username) {
        return Jwts.builder()
                .subject(username)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + refreshExpiration))
                .signWith(secretKey)
                .compact();
    }

    // 토큰에서 username 추출
    public String getUsername(String token) {
        return getClaims(token).getSubject();
    }

    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            getClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    private Claims getClaims(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}

4. JwtFilter 구현

모든 요청에서 JWT를 꺼내 검증하고, 인증 정보를 SecurityContext에 등록하는 필터다.
OncePerRequestFilter를 상속받아 요청당 한 번만 실행되도록 한다.

@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

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

        String token = resolveToken(request);

        if (token != null && jwtProvider.validateToken(token)) {
            String username = jwtProvider.getUsername(token);

            // 인증 객체 생성 후 SecurityContext에 등록
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(username, null, List.of());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    // Authorization 헤더에서 Bearer 토큰 추출
    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

5. SecurityConfig 설정

Spring Security에 JWT 필터를 등록하고, 각 엔드포인트별 접근 권한을 설정한다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            // 세션을 사용하지 않음 (JWT는 Stateless)
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()  // 로그인, 회원가입은 허용
                .anyRequest().authenticated()                 // 나머지는 인증 필요
            )
            // UsernamePasswordAuthenticationFilter 앞에 JwtFilter 추가
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

6. 로그인 API 구현

사용자 인증에 성공하면 Access Token과 Refresh Token을 함께 응답으로 반환한다.

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

    private final JwtProvider jwtProvider;
    private final PasswordEncoder passwordEncoder;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {

        // 실제 서비스에서는 DB에서 사용자 조회 후 비밀번호 검증
        // 예시로 하드코딩
        if (!"user".equals(request.getUsername()) ||
            !passwordEncoder.matches(request.getPassword(), encodedPassword)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패");
        }

        String accessToken = jwtProvider.generateAccessToken(request.getUsername(), "ROLE_USER");
        String refreshToken = jwtProvider.generateRefreshToken(request.getUsername());

        return ResponseEntity.ok(Map.of(
                "accessToken", accessToken,
                "refreshToken", refreshToken
        ));
    }
}

// 요청 DTO
public record LoginRequest(String username, String password) {}

7. 동작 흐름 정리

전체적인 요청 처리 흐름은 아래와 같다.

단계 처리 주체 설명
① 로그인 요청 AuthController ID/PW 검증 후 Access/Refresh Token 발급
② API 요청 Client Authorization: Bearer {token} 헤더에 포함
③ 토큰 추출·검증 JwtFilter 헤더에서 토큰 꺼내 서명 및 만료 여부 확인
④ 인증 등록 JwtFilter SecurityContext에 Authentication 객체 저장
⑤ 권한 확인 SecurityConfig 요청 URL에 따라 접근 허용/거부
⑥ 비즈니스 로직 Controller 인증 통과 후 실제 처리 수행

 


📌 이번 글 정리

  • jjwt 라이브러리로 토큰 생성, 파싱, 검증을 처리하는 JwtProvider를 구현한다.
  • JwtFilter는 모든 요청에서 토큰을 꺼내 검증하고 SecurityContext에 인증 정보를 등록한다.
  • SecurityConfig에서 세션을 STATELESS로 설정하고 JwtFilter를 등록한다.
  • 로그인 성공 시 Access TokenRefresh Token을 함께 발급한다.
  • Secret Key는 반드시 256bit 이상으로 설정하고, 외부에 노출되지 않도록 관리한다.

 

다음 글에서는 Refresh Token을 활용한 Access Token 재발급 로직을 구현해보겠다.

 

반응형

'Java' 카테고리의 다른 글

[Java] 자바의 작동원리(JVM, JRE, JDK)  (1) 2025.03.26
[Java] 4. HashMap  (1) 2025.03.24
[Java] 3. ArrayList  (1) 2025.03.15
[Java] String to int, int to String  (1) 2025.03.06
[Java] 내장 함수 2. String  (1) 2025.03.02