Daily log

개요

본 문서는 Spring Boot 기반 애플리케이션에 JWT(JSON Web Token) 인증 시스템을 구축하기 위한 기술 구현 계획을 담고 있다. 현재 애플리케이션은 인증 기능이 부재하여 보안에 취약하며, 사용자별 권한 관리가 어렵다. JWT 인증 시스템 도입을 통해 다음과 같은 목표를 달성하고자 한다.

  • 보안 강화: JWT를 사용하여 클라이언트 요청의 유효성을 검증하고, 무단 접근을 방지한다.
  • 확장성: stateless한 구조를 통해 서버 확장이 용이하도록 한다.
  • 유지보수성: 표준화된 인증 방식을 적용하여 코드의 가독성을 높이고 유지보수를 용이하게 한다.
  • 사용자 경험 향상: 세션 기반 인증 방식 대비 성능 향상을 통해 사용자 경험을 개선한다.

본 계획은 개발팀 내부 공유를 목적으로 하며, 실제 구현 과정에서 변경될 수 있다.


예상 화면 / 구조

다음은 JWT 인증 시스템의 예상 구조를 ASCII 다이어그램으로 표현한 것이다.

[클라이언트] ──→ [API Gateway] ──→ [인증 서버] ──→ [사용자 DB] │ │ │ │ │ └──→ JWT 발급 │ │ │ └──→ JWT 저장 (Local Storage/Cookie) │ └──→ API 요청 (Authorization 헤더에 JWT 포함)

[API Gateway] ──→ [리소스 서버] ──→ [DB/Cache] │ └──→ JWT 검증

사용자 인증 흐름은 다음과 같다.

  1. 클라이언트는 인증 서버에 사용자 이름과 비밀번호를 전송한다.
  2. 인증 서버는 사용자 정보를 DB에서 조회하여 인증한다.
  3. 인증 성공 시, 인증 서버는 JWT를 생성하여 클라이언트에게 반환한다.
  4. 클라이언트는 JWT를 로컬 저장소(Local Storage, Cookie)에 저장한다.
  5. 클라이언트는 API 요청 시 Authorization 헤더에 JWT를 포함하여 전송한다.
  6. API Gateway 또는 리소스 서버는 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 생성 및 검증에 사용될 알고리즘은 다음과 같다.

  • 서명 알고리즘: HS256 (HMAC SHA256)
  • 페이로드: 사용자 ID, 권한 정보, 발급 시간, 만료 시간

계산 방식

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 접근 권한 설정

코드 예시

JwtTokenProvider.java

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;
}

}

JwtAuthenticationFilter.java

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;
}

}

SecurityConfig.java

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 인증 시스템의 검증 항목이다.

  • 로그인/로그아웃 기능 정상 동작 확인
  • JWT 생성 및 검증 로직 정확성 검증
  • 만료된 JWT로 API 접근 시 에러 처리 확인
  • 사용자 권한에 따른 API 접근 제어 확인
  • JWT secret key 보안 설정 확인
  • CORS 설정 확인 (필요한 경우)
  • API Gateway 연동 테스트 (해당하는 경우)
  • 부하 테스트를 통한 성능 검증 (선택적)
  • XSS/CSRF 공격 방어 대책 마련 (선택적)

'개발 > Spring' 카테고리의 다른 글

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

공유하기

facebook twitter kakaoTalk kakaostory naver band