MSA 보안(feat. JWT)

이진일·2026년 4월 15일
post-thumbnail

보안의 중요성

  • 마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 보안이 매우 중요하다.
  • 데이터 보호, 인증 및 권한 부여, 통신 암호화 등을 통해 시스템의 보안성을 확보해야 한다.

OAuth2란?

  • OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜이다.
  • 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 한다.
  • OAuth2는 네 가지 역할을 정의합니다: 리소스 소유자, 클라이언트, 리소스 서버, 인증 서버

OAuth2의 주요 개념

  • Authorization Code Grant: 인증 코드를 사용하여 액세스 토큰을 얻는 방식
  • Implicit Grant: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
  • Resource Owner Password Credentials Grant: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
  • Client Credentials Grant: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식

JWT란?

  • JWT(JSON Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달한다.
  • JWT는 세 부분으로 구성됩니다: 헤더, 페이로드, 서명
  • JWT는 암호화를 통해 데이터의 무결성과 출처를 보장한다.

JWT의 특징

  • 자가 포함: 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없다.
  • 간결성: 짧고 간결한 문자열로, URL, 헤더 등에 쉽게 포함될 수 있다.
  • 서명 및 암호화: 데이터의 무결성과 인증을 보장한다.

JWT인증 실습

실습을 통해 클라우드 게이트웨이의 Pre 필터에서 JWT 인증을 진행해보려고 한다.
Auth Service 를 생성하여 로그인 기능을 아주 간단하게 구현하고, 클라우드 게이트웨이에 Pre 필터를 하나 더 생성하여 로그인을 체크하려고 한다.

Auth Service

build.gradle에 jwt의존성을 추가한다.

application.yml

spring:
  application:
    name: auth-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

service:
  jwt:
    access-expiration: 3600000
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"

server:
  port: 19095

JWT 만료 시간과, secret-key를 설정한다.

AuthConfig.java


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableWebSecurity
public class AuthConfig {

    // SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
            .csrf(csrf -> csrf.disable())
            // 요청에 대한 접근 권한을 설정합니다.
            .authorizeRequests(authorize -> authorize
                // /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
                .requestMatchers("/auth/signIn").permitAll()
                // 그 외의 모든 요청은 인증이 필요합니다.
                .anyRequest().authenticated()
            )
            // 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );

        // 설정된 보안 필터 체인을 반환합니다.
        return http.build();
    }
}

/auth/signIn 경로 외의 모든 요청은 인증을 체크하도록 보안 필터 체인을 구성한다.

AuthService.java

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.util.Date;

@Service
public class AuthService {

    @Value("${spring.application.name}")
    private String issuer;

    @Value("${service.jwt.access-expiration}")
    private Long accessExpiration;

    private final SecretKey secretKey;

    /**
     * AuthService 생성자.
     * Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
     *
     * @param secretKey Base64 URL 인코딩된 비밀 키
     */
    public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
        this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
    }

    /**
     * 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
     *
     * @param user_id 사용자 ID
     * @return 생성된 JWT 액세스 토큰
     */
    public String createAccessToken(String user_id) {
        return Jwts.builder()
                // 사용자 ID를 클레임으로 설정
                .claim("user_id", user_id)
                .claim("role", "ADMIN")
                // JWT 발행자를 설정
                .issuer(issuer)
                // JWT 발행 시간을 현재 시간으로 설정
                .issuedAt(new Date(System.currentTimeMillis()))
                // JWT 만료 시간을 설정
                .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                // SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
                .signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
                // JWT 문자열로 컴팩트하게 변환
                .compact();
    }
}
  • createAccessToken 메서드로 user_id와 권한을 포함한 JWT 액세스 토큰을 생성한다.

AuthController.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    /**
     * 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
     *
     * @param user_id 사용자 ID
     * @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
     */
    @GetMapping("/auth/signIn")
    public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
        return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
    }

    /**
     * JWT 액세스 토큰을 포함하는 응답 객체입니다.
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class AuthResponse {
        private String access_token;

    }
}

/auth/signIn 경로로 요청이 들어오면 JWT 액세스 토큰을 생성하여 AuthResponse 객체를 반환한다.

Gateway Service

application.yml에 auth-service 라우팅 정보를 추가한다.

LocalJwtAuthenticationFilter.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.crypto.SecretKey;
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {

    @Value("${service.jwt.secret-key}")
    private String secretKey;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        if (path.equals("/auth/signIn")) {
            return chain.filter(exchange);  // /signIn 경로는 필터를 적용하지 않음
        }

        String token = extractToken(exchange);

        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }

    private boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
            Jws<Claims> claimsJws = Jwts.parser()
                    .verifyWith(key)
                    .build().parseSignedClaims(token);
            log.info("#####payload :: " + claimsJws.getPayload().toString());

            // 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
            return true;
        } catch (Exception e) {
            return false;
        }
    }

}
  • filter 메서드에서 게이트웨이로 들어오는 모든 요청을 가로채 특정 경로는 통과시키고 그 외의 요청은 JWT토큰 검증 결과에 따라 승인 또는 차단을 결정한다.
  • extractToken 메서드는 HTTP 요청 헤더의 Authorization 항목에서 Bearer 접두사를 제외한 순수 JWT 토큰 문자열만 추출한다.
  • validateToken 메서드는 전달받은 토큰을 서버의 비밀 키로 파싱 및 검증하여 유효한 토큰인지 검증한다.

실행 결과

http://localhost:19091/product로 게이트웨이에서 상품을 요청하면 401 에러가 발생한다.

http://localhost:19091/auth/signIn?user_id=1234로 게이트웨이에서 로그인 요청하여 토큰을 발급받는다.

http://localhost:19091/product로 발급받은 JWT토큰을 포함하여 게이트웨이에서 상품을 요청하면 정상적으로 응답이 오는 것을 확인할 수 있다.
업로드중..

0개의 댓글