
Spring Boot 3.2.0, Spring Security, JWT(Json Web Token), 그리고 Swagger(OpenAPI)를 사용하여 안전하고 문서화된 API를 구축하는 방법 기록.
최신 버전의 Spring Boot와 호환되는 설정 방법!!
먼저, Gradle을 사용하여 프로젝트를 설정합니다. build.gradle 파일에 필요한 의존성을 추가합니다. 특히, Spring Boot 3.2.0과 호환되는 라이브러리 버전을 명시하는 것이 중요합니다.
(3.4.1 사용하니까
Handler dispatch failed: java.lang.NoSuchMethodError: 'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object)
에러 발생
)
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'org.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot 스타터
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web-services'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Thymeleaf 보안 확장
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
// 로깅 (Logback)
implementation 'ch.qos.logback:logback-classic'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// Swagger(OpenAPI)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// 테스트
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
주의 사항:
org.springframework.boot 플러그인을 3.2.0으로 설정하여 안정화된 버전을 사용합니다.springdoc-openapi-starter-webmvc-ui는 2.1.0 버전을 사용하여 Spring Boot 3.2.x와 호환되도록 합니다.JWT는 클라이언트와 서버 간의 인증을 안전하게 관리하기 위해 사용됩니다. JWT를 구성하기 위해 JwtUtil, JwtAuthenticationFilter, JwtAuthorizationFilter 클래스를 구현합니다.
JWT 토큰 생성 및 검증을 위한 유틸리티 클래스입니다.
package org.example.infrastructure.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtUtil {
private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private final long jwtExpirationMs = 86400000; // 1일
// JWT 생성
public String createToken(String email, String role) {
return Jwts.builder()
.setSubject(email)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key)
.compact();
}
// JWT 파싱
public Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
// JWT 유효성 검사
public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// 로그 추가 가능
return false;
}
}
// 요청에서 JWT 추출
public String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
사용자가 로그인할 때 JWT를 생성하는 필터입니다.
package org.example.infrastructure.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.example.application.dto.LoginRequestDTO;
import org.example.domain.entity.UserRoleEnum;
import org.example.infrastructure.security.UserDetailsImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequestDTO requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDTO.class);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
requestDto.getEmail(),
requestDto.getPassword()
);
return getAuthenticationManager().authenticate(authToken);
} catch (IOException e) {
throw new RuntimeException("로그인 요청을 처리할 수 없습니다.", e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal();
String token = jwtUtil.createToken(userDetails.getUser().getEmail(), userDetails.getUser().getRole().name());
response.setHeader("Authorization", "Bearer " + token);
response.setStatus(HttpServletResponse.SC_OK);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
요청 시 JWT를 검증하여 인증을 수행하는 필터입니다.
package org.example.infrastructure.jwt;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.example.infrastructure.security.UserDetailsServiceImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
Claims claims = jwtUtil.parseClaims(token);
String email = claims.getSubject();
String role = claims.get("role", String.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetailsService.loadUserByUsername(email),
null,
userDetailsService.loadUserByUsername(email).getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return path.startsWith("/v3/api-docs") || path.startsWith("/swagger-ui");
}
}
Spring Security를 설정하여 JWT 필터를 적용하고, 특정 경로에 대한 접근 권한을 관리합니다.
package org.example.infrastructure.config;
import lombok.RequiredArgsConstructor;
import org.example.infrastructure.jwt.JwtAuthenticationFilter;
import org.example.infrastructure.jwt.JwtAuthorizationFilter;
import org.example.infrastructure.jwt.JwtUtil;
import org.example.infrastructure.security.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.disable())
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(auth -> auth
// Swagger/OpenAPI 문서 경로 전부 허용
.requestMatchers(
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/error"
).permitAll()
// 정적 리소스 (CSS, JS, 이미지 등)
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// 회원가입/로그인 등 공개 API
.requestMatchers("/api/user/**").permitAll()
// 나머지는 인증 필요
.anyRequest().authenticated()
);
// JWT 필터 등록
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 접근 불가 페이지 설정
http.exceptionHandling(exception -> exception.accessDeniedPage("/forbidden.html"));
return http.build();
}
}
주요 설정 포인트:
JwtAuthorizationFilter와 JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 추가하여 JWT 인증을 처리합니다.SessionCreationPolicy.STATELESS를 설정하여 세션을 사용하지 않도록 합니다.Swagger를 통해 API 문서를 자동으로 생성하고, JWT를 통한 인증을 지원하도록 설정합니다.
package org.example.infrastructure.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
private static final String SECURITY_SCHEME_NAME = "JWT";
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.openapi("3.0.3")
.info(apiInfo())
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(securityComponents());
}
private Info apiInfo() {
return new Info()
.title("")
.version("1.0.0")
.description("")
.contact(new Contact()
.name("")
.email("")
.url("")
);
}
private Components securityComponents() {
return new Components()
.addSecuritySchemes(SECURITY_SCHEME_NAME, new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization")
);
}
}
설명:
bearer 타입의 JWT 인증 방식을 추가하여 Swagger UI에서 인증 토큰을 입력할 수 있도록 합니다.API 엔드포인트를 작성하고 Swagger 어노테이션을 통해 문서화를 지원합니다.
package org.example.presentation.controller;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.domain.service.FinanceService;
import org.example.domain.service.SubscriptionService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/data")
public class DataController {
private final SubscriptionService subscriptionService;
private final FinanceService financeService;
@GetMapping("/sub")
@Operation(summary = "", description = "Swagger 문서화 예시")
public ResponseEntity<?> getSub(){
String result = subscriptionService.updateSub();
return ResponseEntity.ok(result);
}
}
설명:
SubscriptionService와 FinanceService를 이용하여 데이터를 처리하고 응답합니다.프로젝트를 빌드하고 실행하여 Swagger UI를 통해 API 문서를 확인합니다.
./gradlew clean build
./gradlew bootRun
브라우저에서 다음 URL로 접속하여 Swagger UI를 확인합니다:
http://localhost:8082/swagger-ui/index.html
확인 사항:
Authorize 버튼을 클릭하여 JWT 토큰을 입력하고, 인증이 필요한 엔드포인트에 접근할 수 있는지 테스트합니다.프로젝트 설정 과정에서 발생할 수 있는 문제와 그 해결 방법을 소개합니다.
NoSuchMethodError 발생 시문제 상황:
Swagger UI 접속 시 다음과 같은 오류가 발생할 수 있습니다:
Unable to render this definition
The provided definition does not specify a valid version field.
원인 및 해결 방법:
3.2.0으로 안정화하여 의존성 충돌을 방지합니다.WebSecurityConfig 클래스에서 /v3/api-docs와 Swagger UI 관련 모든 경로를 permitAll()로 설정합니다../gradlew clean build를 실행하고, 내장 톰캣을 사용하여 애플리케이션을 실행합니다.이번 포스팅에서는 Spring Boot 3.2.0, Spring Security, JWT, 그리고 Swagger(OpenAPI)를 사용하여 안전하고 문서화된 API를 구축하는 방법을 알아보았습니다. 특히, 버전 호환성 문제를 사전에 인지하고, 안정화된 버전을 사용함으로써 개발 과정에서 발생할 수 있는 오류를 예방하는 방법을 배웠습니다.
핵심 정리:
이 가이드가 여러분의 Spring Boot 프로젝트에 도움이 되었기를 바랍니다. 추가적인 질문이나 개선 사항이 있다면 댓글로 남겨주세요. 감사합니다!
참고 자료: