티스토리 뷰
스프링 시큐리티 완전 처음부터 4편 (Kotlin)
JWT 토큰 발급 + 인증 필터 만들기 (세션 없는 진짜 실무 구조)
여기까지 왔다면 솔직히 말해서
“시큐리티 하나도 몰라요” 라고 말하긴 좀 어렵다.
1편
→ 필터 체인, 기본 구조 감 잡고
2편
→ DB 기반 인증(UserDetailsService) 직접 붙이고
3편
→ formLogin 버리고 JSON 로그인 API까지 만들었다
이제 남은 마지막 퍼즐은 이거다.
“로그인 이후를 어떻게 유지할 것인가?”
- 세션? ❌ (서버 확장성, 모바일/SPA에서 불편)
- 쿠키? ❌ (CORS, CSRF, 도메인 이슈)
- 👉 JWT(Token) ⭕
이번 글에서는 실무에서 가장 많이 쓰는 구조로 간다.
✔ 이번 글에서 최종적으로 만들 것
로그인 시
POST /api/login
→ username / password 인증
→ JWT Access Token 발급
이후 요청
GET /private/hello
Authorization: Bearer {JWT}
서버 내부 흐름
요청
→ JwtAuthenticationFilter
→ Authorization 헤더에서 토큰 추출
→ 토큰 검증
→ UserDetails 조회
→ SecurityContextHolder 저장
→ 컨트롤러
📌 세션 사용 안 함 (Stateless)
📌 서버 여러 대여도 문제 없음
📌 모바일 / SPA / 외부 API 연동에 최적
0. JWT 개념, 진짜 필요한 만큼만
JWT(Json Web Token)는 딱 이렇게 이해하면 된다.
“서명된 사용자 정보 묶음”
구조는 3덩어리다.
Header.Payload.Signature
- Header → 알고리즘 정보
- Payload → 사용자 정보 (username, role 등)
- Signature → 위조 방지 서명
👉 서버는
- 토큰을 발급만 하고
- 이후 요청마다 검증만 한다
👉 세션 저장 ❌
1. JWT 라이브러리 추가
가장 많이 쓰는 jjwt 라이브러리 사용한다.
build.gradle.kts
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
2. JWT 유틸 클래스 만들기
토큰 생성 / 검증 / 정보 추출 전담 클래스다.
📄 JwtProvider.kt
package com.example.demo.security.jwt
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.util.*
@Component
class JwtProvider {
private val secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
private val accessTokenValidity = 1000 * 60 * 30 // 30분
fun generateToken(username: String): String {
val now = Date()
val expiryDate = Date(now.time + accessTokenValidity)
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey)
.compact()
}
fun getUsername(token: String): String {
return parseClaims(token).subject
}
fun validateToken(token: String): Boolean {
return try {
parseClaims(token)
true
} catch (e: Exception) {
false
}
}
private fun parseClaims(token: String): Claims {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.body
}
}
📌 중요 포인트
- secretKey → 실무에서는 환경변수로 관리
- 토큰 만료(exp) 필수
- Payload에는 최소 정보만 넣는다 (username 정도)
3. 로그인 성공 시 JWT 발급하도록 수정
3편에서 만든 CustomAuthenticationFilter 기억날 거다.
이제 로그인 성공 시 JWT를 내려주도록 수정한다.
📄 CustomAuthenticationFilter 수정
class CustomAuthenticationFilter(
private val authenticationManager: AuthenticationManager,
private val jwtProvider: JwtProvider
) : UsernamePasswordAuthenticationFilter() {
init {
setFilterProcessesUrl("/api/login")
}
override fun attemptAuthentication(
request: HttpServletRequest,
response: HttpServletResponse
): Authentication {
val loginRequest = ObjectMapper().readValue(
request.inputStream,
LoginRequest::class.java
)
val authToken = UsernamePasswordAuthenticationToken(
loginRequest.username,
loginRequest.password
)
return authenticationManager.authenticate(authToken)
}
override fun successfulAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
authResult: Authentication
) {
val token = jwtProvider.generateToken(authResult.name)
response.contentType = "application/json"
response.writer.write(
"""
{
"accessToken": "$token"
}
""".trimIndent()
)
}
}
이제 로그인 응답은 이렇게 바뀐다.
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9..."
}
4. JWT 인증 필터 만들기 (이게 핵심 중 핵심)
이 필터는 모든 요청마다 실행된다.
역할은 단 하나다.
“Authorization 헤더에 JWT 있으면 → 인증 처리”
📄 JwtAuthenticationFilter.kt
package com.example.demo.security.filter
import com.example.demo.security.CustomUserDetailsService
import com.example.demo.security.jwt.JwtProvider
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.web.filter.OncePerRequestFilter
class JwtAuthenticationFilter(
private val jwtProvider: JwtProvider,
private val userDetailsService: CustomUserDetailsService
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authHeader = request.getHeader("Authorization")
if (authHeader != null && authHeader.startsWith("Bearer ")) {
val token = authHeader.substring(7)
if (jwtProvider.validateToken(token)) {
val username = jwtProvider.getUsername(token)
val userDetails = userDetailsService.loadUserByUsername(username)
val authentication = UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.authorities
)
authentication.details =
WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authentication
}
}
filterChain.doFilter(request, response)
}
}
📌 이 필터의 존재로 인해:
- 세션 ❌
- 매 요청마다 JWT 검증
- 인증 정보는 요청 단위로 SecurityContext에 세팅
5. SecurityConfig – Stateless 구조 완성
이제 모든 걸 조립한다.
📄 SecurityConfig.kt (JWT 최종 버전)
@Bean
fun securityFilterChain(
http: HttpSecurity,
authenticationManager: AuthenticationManager,
jwtProvider: JwtProvider,
userDetailsService: CustomUserDetailsService
): SecurityFilterChain {
val loginFilter = CustomAuthenticationFilter(authenticationManager, jwtProvider)
val jwtFilter = JwtAuthenticationFilter(jwtProvider, userDetailsService)
http
.csrf { it.disable() }
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authorizeHttpRequests {
it.requestMatchers("/api/login", "/public/**").permitAll()
it.anyRequest().authenticated()
}
.addFilter(loginFilter)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
return http.build()
}
📌 진짜 중요한 설정
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
👉 이제 서버는 세션을 절대 만들지 않는다.
6. 실제 사용 흐름 정리
1️⃣ 로그인
POST /api/login
⬇
{
"accessToken": "jwt-token"
}
2️⃣ 인증 필요한 API 호출
GET /private/hello
Authorization: Bearer jwt-token
⬇
로그인한 사용자만 접근 가능
7. 이 구조를 이해했다면, 이제 이런 말이 가능해진다
- “JWT 기반 인증 구조 이해합니다”
- “세션 없는 인증 구현해봤습니다”
- “SecurityContext 흐름 알고 있습니다”
- “필터 기반 인증 구현 경험 있습니다”
👉 주니어 → 중급으로 넘어가는 지점이다.
다음 글 예고 (5편, 마지막)
권한(Role) 기반 접근 제어 + @PreAuthorize 실전 사용
- ROLE_USER / ROLE_ADMIN 분기
- URL 기반 권한 제어
- 메서드 레벨 권한 제어
- 실무에서 권한 설계할 때 주의점
- 관리자 API 보호 전략
여기까지 가면
스프링 시큐리티 풀코스 완주다.
출처
- Spring Security 공식 문서
- JWT RFC 7519
- 실무 Kotlin + Spring Security 운영 경험
스프링시큐리티, springsecurity, 코틀린, kotlin, jwt, stateless, authentication, authorization, 백엔드개발, 개발블로그
'study > kotlin' 카테고리의 다른 글
| 스프링 시큐리티 완전 처음부터 6편 (Kotlin) (0) | 2025.12.22 |
|---|---|
| 스프링 시큐리티 완전 처음부터 5편 (Kotlin) (0) | 2025.12.19 |
| 스프링 시큐리티 완전 처음부터 3편 (Kotlin) (0) | 2025.12.15 |
| 스프링 시큐리티 완전 처음부터 2편 (Kotlin) (0) | 2025.12.12 |
| 📘 Kotlin 기본 문법 완전 정리 (0) | 2025.12.11 |
- Total
- Today
- Yesterday
- NestJS
- 쿠버네티스
- Next.js
- Prisma
- Python
- kotlin
- ai철학
- REACT
- 백엔드개발
- 웹개발
- nextJS
- PostgreSQL
- seo 최적화 10개
- 딥러닝
- JAX
- 개발블로그
- 생성형AI
- Docker
- nodejs
- flax
- node.js
- rag
- llm
- fastapi
- LangChain
- SEO최적화
- JWT
- CI/CD
- Express
- DevOps
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
