📚 Spring Boot(Kotlin) 서버 기본 셋팅 — 시리즈 안내
- 왜 멀티 모듈 구조인가? (아키텍처 철학 & 전체 설계 편)
- API Response 포맷 설계
- 글로벌 예외 처리(GlobalExceptionHandler)
- Swagger(OpenAPI) 설정
- Security(JWT) 기본 골격 ← 현재 글
- JWT TokenProvider
- Redis 설정
- Validation 설정
- Logging + MDC(traceId)
- application.yml 프로필 분리 (local/dev/prod)
- 멀티모듈 + JPA 기본 구조 정리
- 완성된 프로젝트 템플릿 git 공유
📌 Security(JWT) 설정은 왜 필요할까?
Security 설정은 API 서버 전체의 보안 정책을 총괄하는 핵심 설정입니다.
✔ 어떤 URL을 허용할지
✔ 401/403 같은 보안 예외를 어떻게 응답할지
✔ 어떤 필터를 먼저 실행할지
API 서버에서 인증을 구현하기 위해 세션 기반 인증과 JWT 인증 방식을 많이 사용합니다.
정답은 없지만 저는 이번 시리즈에서 토큰방식을 선택했습니다.
JWT는 서버가 상태(Session)를 저장하지 않아도 되기 때문에, 확장성이 좋다고 생각합니다.
✔ 서버 확장(Scale-out)에 유리
✔ 토큰만 있으면 인증이 가능 (Stateless 구조)
✔ SecurityFilter를 커스터마이징하여 인증 흐름을 유연하게 설계 가능
이번 글에서는 JWT 인증의 기본 뼈대(SecurityConfig)를 구성하고, JWT를 검증하는 필터, 인증 실패/인가 실패 처리 방식을 만듭니다.
📌 필요한 의존성
JWT 기반 Spring Security를 구성하기 위해서는 다음 의존성이 필요합니다.
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
✔ JSON 응답 생성을 위한 Jackson Kotlin module
✔ JWT 파싱/생성은 다음 편(JWT TokenProvider)에서 설명
📌 SecurityConfig는 왜 API 모듈에 둘까?
Security는 HTTP 요청을 필터링하는 계층입니다.
즉, SecurityConfig는 외부 요청을 가장 먼저 처리하는 레이어이므로 API 모듈에 있는게 자연스럽다고 생각했습니다.
📌 SecurityConfig 전체 코드
@Configuration
@EnableWebSecurity
class SecurityConfig(
private val accessDeniedHandler: CustomAccessDeniedHandler,
private val authenticationEntryPoint: CustomAuthenticationEntryPoint,
private val jwtAuthenticationFilter: JwtAuthenticationFilter
) {
@Bean
fun userDetailsService(): UserDetailsService {
return UserDetailsService { _ ->
throw UsernameNotFoundException("Not using UserDetailsService in JWT authentication")
}
}
@Bean
fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager {
return authConfig.authenticationManager
}
companion object {
private val PUBLIC_ENDPOINTS = listOf(
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/v1/auth/**",
"/error"
)
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.formLogin { it.disable() }
.httpBasic { it.disable() }
.cors {
it.configurationSource {
CorsConfiguration().apply {
allowedOriginPatterns = listOf("*")
allowedMethods = listOf("*")
allowedHeaders = listOf("*")
allowCredentials = true
}
}
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.exceptionHandling {
it.authenticationEntryPoint(authenticationEntryPoint)
it.accessDeniedHandler(accessDeniedHandler)
}
.authorizeHttpRequests {
it
.requestMatchers(*PUBLIC_ENDPOINTS.toTypedArray())
.permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter::class.java
)
return http.build()
}
}
📌 SecurityConfig 코드 설명
1️⃣ 기본 인증/보안 방식 비활성화
2️⃣ CORS 설정
모바일 앱 & 프론트엔드를 연결하기 위한 CORS 설정을 global level에서 정의합니다.
3️⃣ Session을 Stateless로 설정
4️⃣ 인증 실패 / 인가 실패 처리
override fun commence(request, response, authException)
⇒ 인증 실패 (401)
override fun handle(request, response, accessDeniedException)
⇒ 권한 부족 (403)
5️⃣ Public Endpoints 지정
로그인/회원가입/토큰재발급, Swagger, 에러 엔드포인트 등 인증 없이 호출할 URL 목록
6️⃣ JWT 필터 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
📌 JWTAuthenticationFilter 코드
@Component
class JwtAuthenticationFilter(
private val tokenProvider: TokenProvider
) : OncePerRequestFilter() {
override fun doFilterInternal(request, response, filterChain) {
val token = resolveToken(request)
if (token != null && tokenProvider.validateToken(token)) {
val auth = tokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = auth
val userId = tokenProvider.getUserId(token)
MDC.put("userId", userId.toString())
}
filterChain.doFilter(request, response)
}
private fun resolveToken(request: HttpServletRequest): String? {
val bearer = request.getHeader("Authorization")
return if (bearer != null && bearer.startsWith("Bearer ")) {
bearer.substring(7)
} else null
}
}
✔ 토큰 검증
✔ 인증 객체 생성 후 SecurityContext에 저장
✔ 로그 트래킹을 위한 userId를 MDC(현재 요청과 관련된 부가 정보를 자동으로 붙여주는 기능)에 등록
📌 CustomAuthenticationEntryPoint (401)
@Component
class CustomAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(request, response, authException) {
val body = ApiResponse.fail(
code = ErrorCode.UNAUTHORIZED.code,
messageKey = ErrorCode.UNAUTHORIZED.messageKey,
detail = "Authorization header missing or invalid"
)
response.status = ErrorCode.UNAUTHORIZED.status
response.contentType = "application/json;charset=UTF-8"
response.writer.write(body.toJson())
}
}
✔ CustomAuthenticationEntryPoint는 “인증 자체가 존재하지 않거나 실패한 경우”에 호출됩니다.
즉, 로그인을 하지 않았거나, 유효하지 않은 JWT 토큰을 보냈을 때 발생하는 오류입니다. 이후 우리가 설계한 통일된 API Response구조로 클라이언트에게 결과를 전송합니다.
📌 CustomAccessDeniedHandler (403)
@Component
class CustomAccessDeniedHandler : AccessDeniedHandler {
override fun handle(request, response, accessDeniedException) {
val body = ApiResponse.fail(
code = ErrorCode.FORBIDDEN.code,
messageKey = ErrorCode.FORBIDDEN.messageKey,
detail = "You do not have permission to access this resource"
)
response.status = ErrorCode.FORBIDDEN.status
response.contentType = "application/json;charset=UTF-8"
response.writer.write(body.toJson())
}
}
즉, 유효한 JWT로 로그인했지만 해당 리소스에 접근할 권한이 없는 상황에서 발생한다.
📌 결론
📌 SecurityConfig는 JWT 기반 인증 요청을 처리하는 첫 관문이며, 실제로 클라이언트 요청이 API 계층에 들어오는 순간 가장 먼저 실행되는 필터 체인 구성요소이기 때문에 API 모듈에 존재해야 합니다.
📌 인증 실패(401)와 인가 실패(403)를 모두 JSON 형태의 공통 응답 포맷으로 통일하여, 전체 API에서 예외 처리 일관성을 유지합니다.
📌 Stateless 구조 + JWT 인증 + Custom Filter 기반으로, 세션을 사용하지 않는 현대적인 인증 아키텍처를 완성합니다.
📌 다음 편 예고
6편 — JWT TokenProvider
JWT 생성/검증/파싱 로직을 애플리케이션 모듈에서 어떻게 구현하는지 다룬다.
https://jaemoi8.tistory.com/38
Spring Boot(Kotlin) — 6편. JWT TokenProvider 이해하기 (토큰 생성·검증·구조 설계까지)
📚 Spring Boot(Kotlin) 서버 기본 셋팅 — 시리즈 안내왜 멀티 모듈 구조인가? (아키텍처 철학 & 전체 설계 편)API Response 포맷 설계글로벌 예외 처리(GlobalExceptionHandler)Swagger(OpenAPI) 설정Security(JWT) 기본
jaemoi8.tistory.com
'개발' 카테고리의 다른 글
| Spring Boot(Kotlin) — 7편. 왜 Redis인가? Spring Boot 애플리케이션에서 Redis를 사용하는 이유와 설정 (0) | 2025.11.25 |
|---|---|
| Spring Boot(Kotlin) — 6편. JWT TokenProvider 이해하기 (토큰 생성·검증·구조 설계까지) (0) | 2025.11.25 |
| Spring Boot(Kotlin) — 4편. Swagger(OpenAPI) 설정 가이드 (0) | 2025.11.25 |
| ⚠️ Spring Boot(Kotlin) — 3편. 글로벌 예외 처리(GlobalExceptionHandler) (0) | 2025.11.24 |
| Spring Boot(Kotlin) — 2편. API Response 포맷 설계 (0) | 2025.11.24 |