본 문서는 Spring Boot 기반 애플리케이션에 JWT(JSON Web Token) 인증 시스템을 구축하기 위한 기술 구현 계획을 담고 있다. 현재 애플리케이션은 인증 기능이 부재하여 보안에 취약하며, 사용자별 권한 관리가 어렵다. JWT 인증 시스템 도입을 통해 다음과 같은 목표를 달성하고자 한다.
본 계획은 개발팀 내부 공유를 목적으로 하며, 실제 구현 과정에서 변경될 수 있다.
다음은 JWT 인증 시스템의 예상 구조를 ASCII 다이어그램으로 표현한 것이다.
[클라이언트] ──→ [API Gateway] ──→ [인증 서버] ──→ [사용자 DB] │ │ │ │ │ └──→ JWT 발급 │ │ │ └──→ JWT 저장 (Local Storage/Cookie) │ └──→ API 요청 (Authorization 헤더에 JWT 포함)
[API Gateway] ──→ [리소스 서버] ──→ [DB/Cache] │ └──→ JWT 검증
사용자 인증 흐름은 다음과 같다.
다음은 JWT 인증 시스템의 데이터 흐름을 나타내는 ASCII 흐름도이다.
[클라이언트] --> (1. 로그인 요청) --> [인증 서버] [인증 서버] --> (2. 사용자 인증) --> [사용자 DB] [사용자 DB] --> (3. 인증 결과) --> [인증 서버] [인증 서버] --> (4. JWT 발급) --> [클라이언트] [클라이언트] --> (5. JWT 저장) --> [로컬 저장소] [클라이언트] --> (6. API 요청 (JWT 포함)) --> [API Gateway/리소스 서버] [API Gateway/리소스 서버] --> (7. JWT 검증) --> [인증 서버 (선택적)] [API Gateway/리소스 서버] --> (8. API 처리) --> [DB/Cache]
JWT 생성 및 검증에 사용될 알고리즘은 다음과 같다.
JWT 만료 시간은 application.properties 파일에서 설정 가능하도록 한다. 기본값은 1시간으로 설정한다.
java @Value("${jwt.expiration}") private long jwtExpiration; // milliseconds
JWT 생성 시 만료 시간을 계산하는 코드는 다음과 같다.
java Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration);
다음 표는 구현에 필요한 파일 및 모듈별 변경 사항을 요약한 것이다.
| 모듈/파일 | 설명 | 변경 사항 |
|---|---|---|
pom.xml |
Maven 의존성 관리 | JWT 관련 라이브러리 추가 (jjwt) |
application.properties |
애플리케이션 설정 | JWT secret key, expiration time 설정 추가 |
src/main/java/com/example/security/JwtTokenProvider.java |
JWT 생성 및 검증 클래스 | JWT 생성, 검증, 파싱 로직 구현 |
src/main/java/com/example/security/JwtAuthenticationFilter.java |
JWT 인증 필터 | API 요청 시 JWT 검증 및 인증 처리 |
src/main/java/com/example/controller/AuthController.java |
인증 컨트롤러 | 로그인 API 엔드포인트 추가 |
src/main/java/com/example/service/UserService.java |
사용자 서비스 | 사용자 인증 로직 구현 |
src/main/java/com/example/config/SecurityConfig.java |
Spring Security 설정 | JWT 인증 필터 등록 및 API 접근 권한 설정 |
java import io.jsonwebtoken.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;
import java.util.Date;
@Component public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String generateToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, jwtSecret)
.compact();
}
public String getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
//Invalid JWT signature
} catch (MalformedJwtException ex) {
//Invalid JWT token
} catch (ExpiredJwtException ex) {
//Expired JWT token
} catch (UnsupportedJwtException ex) {
//Unsupported JWT token
} catch (IllegalArgumentException ex) {
//JWT claims string is empty
}
return false;
}
}
java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 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;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
//log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
주의: 위 코드는 예시이며, 실제 구현 시에는 예외 처리, 로깅, 보안 설정 등을 추가해야 한다. 특히 JWT secret key는 안전하게 관리해야 한다.
다음은 구현된 JWT 인증 시스템의 검증 항목이다.
| Spring Cloud Gateway 설정 파헤치기 (0) | 2026.02.25 |
|---|---|
| Spring Boot Actuator 모니터링 알아보기 (0) | 2026.02.25 |
| Spring Data JPA QueryDSL 사용법 (0) | 2026.02.24 |
| Spring WebFlux vs MVC 비교 (0) | 2026.02.24 |
| Spring Batch 입문 가이드 (0) | 2026.02.24 |