Spring Security와 JWT 통합: 핵심 유틸리티 및 설정 구성

이전 단계에서 데이터베이스 스키마와 기본 도메인 모델을 준비했다면, 이제 인증 및 인가 처리를 위한 핵심 유틸리티 클래스와 시스템 전반에 사용될 설정을 구성해야 합니다. 본 장에서는 API 표준 응답 객체, JWT 처리 로직, CORS 설정, 그리고 API 문서화 도구인 Swagger 구성에 대해 다룹니다.

공통 상수 및 API 응답 객체 정의

코드의 유지보수성을 높이기 위해 반복적으로 사용되는 문자열 값과 API 응답 포맷을 별도의 클래스로 관리합니다.

package com.example.demo.common.constants;

public class SecurityConstants {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String REDIS_USER_PREFIX = "LOGIN_USER:";
    public static final int TOKEN_START_INDEX = 7;
}

클라이언트에게 일관된 데이터 형식을 반환하기 위한 제네릭 응답 클래스를 작성합니다. 성공과 실패 경우를 명확히 구분하여 처리합니다.

package com.example.demo.common.dto;

import lombok.Data;

@Data
public class ApiResponse<T> {
    private int status;
    private String message;
    private T data;

    public ApiResponse() {}

    public ApiResponse(T data) {
        this.data = data;
    }

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>(data);
        response.setStatus(200);
        response.setMessage("Success");
        return response;
    }

    public static ApiResponse<Void> success() {
        return success(null);
    }

    public static ApiResponse<Void> fail(int code, String msg) {
        ApiResponse<Void> response = new ApiResponse<>();
        response.setStatus(code);
        response.setMessage(msg);
        return response;
    }
}

CORS(Cross-Origin Resource Sharing) 설정

프론트엔드와 백엔드가 분리된 환경에서 브라우저 보안 정책으로 인한 리소스 공유 문제를 해결하기 위해 CORS를 허용하는 설정을 추가합니다.

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

Swagger API 문서화 설정

REST API 테스트 및 명세서 작성을 위해 Swagger를 활성화하고 Docket 빈을 등록합니다.

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Spring Security JWT API")
                .description("인증 및 인가 관련 API 테스트 문서")
                .version("1.0")
                .contact(new Contact("Dev Team", "", ""))
                .build();
    }
}

JWT 생성 및 검증 유틸리티

JWT 토큰의 생성, 파싱, 검증 로직을 담당하는 핵심 유틸리티 클래스입니다. Auth0 라이브러리를 사용하며, 사용자 정보는 Redis에 캐싱하여 관리합니다.

package com.example.demo.security;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.demo.common.constants.SecurityConstants;
import com.example.demo.domain.SysUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Calendar;
import java.util.Date;
import java.util.Objects;

@Component
@RequiredArgsConstructor
public class TokenManager {

    private static final String SECRET_KEY = "secure_secret_key_2024";
    private final RedisTemplate<String, Object> redisTemplate;

    public String generateToken(SysUser user) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 30);
        Date expireDate = calendar.getTime();

        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        return JWT.create()
                .withSubject(user.getId().toString())
                .withExpiresAt(expireDate)
                .withIssuedAt(new Date())
                .sign(algorithm);
    }

    public DecodedJWT parseAndVerifyToken(String headerToken) {
        String rawToken = extractBearerToken(headerToken);
        if (!StringUtils.hasText(rawToken)) {
            return null;
        }

        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            return JWT.require(algorithm).build().verify(rawToken);
        } catch (Exception e) {
            return null;
        }
    }

    public SysUser retrieveUserFromCache(DecodedJWT jwt) {
        String userIdStr = jwt.getSubject();
        if (!StringUtils.hasText(userIdStr)) {
            throw new RuntimeException("유효하지 않은 토큰입니다.");
        }

        String cacheKey = SecurityConstants.REDIS_USER_PREFIX + userIdStr;
        SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get(cacheKey);
        
        if (Objects.isNull(cachedUser)) {
            throw new RuntimeException("로그인 세션이 만료되었습니다.");
        }
        return cachedUser;
    }

    private String extractBearerToken(String authHeader) {
        if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(SecurityConstants.TOKEN_START_INDEX);
        }
        return null;
    }
}

이제 모든 필수 유틸리티와 기본 설정이 완료되었습니다. Swagger 접속 주소(http://localhost:8080/swagger-ui.html)를 통해 설정이 정상적으로 로드되었는지 확인할 수 있습니다. 다음 단계에서는 Spring Security의 필터 체인을 구성하여 이 유틸리티들을 실제 인증 로직에 연결할 것입니다.

9월 21일 04:16에 게시됨