본문 바로가기

자바 심화/TIL

MSA - 기초 4

 

MSA - 기초 3

MSA 기초 MSA - 기초 2MSA MSA - 기초 1개요추후 MSA를 사용한 프로젝트를 진행하기 앞서 MSA가 무엇이고 어떻게 사용하는지에 대해 배워보려고 한다.다만, MSA에 대한 이론부터 완벽하게 익히고 들어

eleunadeu.tistory.com

 

개요

Spring Cloud의 보안 구성(OAuth2, JWT)와 Config Server에 대해 간단한 정리와 함께 사용법을 익힌다.

 

 

Spring Cloud 보안 구성(OAuth2, JWT)

OAuth2(Open Authorization 2.0)

 

  • 역할: 외부 애플리케이션이 사용자 자원에 접근하도록 허용하면서도 사용자 비밀번호를 노출하지 않도록 설계된 인증 및 권한 부여 프레임워크.
  • 구성 요소:
    • Resource Owner: 자원의 소유자 (일반적으로 사용자).
    • Client: 리소스에 접근하려는 애플리케이션.
    • Authorization Server: 인증과 토큰 발급을 담당 (Spring Security Authorization Server 사용 가능).
    • Resource Server: 클라이언트가 요청한 리소스를 제공.
  • 흐름:
    1. 클라이언트가 인증 서버에서 인증 및 권한 부여를 요청.
    2. 인증 서버가 Access Token을 발급.
    3. 클라이언트는 Access Token을 사용하여 리소스 서버에 요청.
    4. 리소스 서버는 토큰 유효성을 확인한 후 요청을 처리.

 

 

JWT(JSON Web Token)

 

  • 역할: JSON 기반으로 정보를 안전하게 전달하는 토큰. OAuth2와 함께 주로 사용되며, 인증 및 권한 정보 저장에 적합.
  • 구조:
    • Header: 토큰의 타입(JWT)과 해싱 알고리즘.
    • Payload: 사용자 정보(클레임, Claim)가 포함됨.
    • Signature: Header와 Payload를 합쳐 비밀 키로 서명한 값.
  • 장점:
    • Stateless: 서버가 세션 상태를 유지할 필요 없음.
    • 분산 시스템에서 유용 (e.g., 마이크로서비스).
  • 사용:
    • OAuth2의 Access Token으로 JWT를 활용하면 클라이언트가 토큰만으로 인증을 수행 가능.
    • Resource Server에서 서명을 검증하여 토큰의 유효성을 확인.

 

Spring Cloud에서의 통합

 

  1. Spring Security: OAuth2와 JWT를 통합적으로 지원.
    • Spring Boot spring-boot-starter-oauth2-resource-server 사용.
  2. Spring Cloud Gateway:
    • 마이크로서비스 아키텍처에서 Gateway 레벨에서 인증 처리 가능.
    • JWT 기반 토큰 검증을 추가하여 요청 필터링.
  3. Keycloak 같은 ID Provider:
    • OAuth2 Authorization Server로 Keycloak을 통합하여 인증 서버로 활용.

 

핵심 워크플로

 

  1. 클라이언트가 인증 서버에 로그인 요청 → JWT Access Token 발급.
  2. 클라이언트가 API 호출 시 JWT를 포함.
  3. Resource Server(Spring Cloud 서비스)가 JWT 유효성을 확인 후 요청 처리.

 

실습 코드

Cloud Gateway의 Pre Filter를 통한 JWT 인증 테스트

 

build.gradle

implementation 'io.jsonwebtoken:jjwt:0.12.6'

// Auth build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'

 

 

 

  • JWT 사용을 위한 dependency 추가

 

auth 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 설정(외부 노출 안 되도록 주의)

 

gateway application.yml

server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service  # 애플리케이션 이름을 'gateway-service'로 설정
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: order-service  # 라우트 식별자
          uri: lb://order-service  # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/order/**  # /order/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: product-service  # 라우트 식별자
          uri: lb://product-service  # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/product/**  # /product/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: auth-service  # 라우트 식별자
          uri: lb://auth-service  # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/auth/signIn  # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/  # Eureka 서버의 URL을 지정
      

service:
  jwt:
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
  • auth 경로를 라우트에 등록 및 JWT Secret-key 등록

 

Auth Config

@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();
    }
}
  • 간단한 Security Filter 설정

 

Auth Controller

@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @GetMapping("/auth/signIn")
    public ResponseEntity<?> createAuthToken(@RequestParam("user_id") String userId) {
        return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(userId)));
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class AuthResponse {
        private String accessToken;
    }
}
  • /auth/signIn으로 user_id 값이 들어오면 JWT 토큰을 반환하는 API 메서드 설정

 

Auth Service

@Service
public class AuthService {

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

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

    private final SecretKey secretKey;

    public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
        this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
    }

    public String createAccessToken(String userId) {
        return Jwts.builder()
                .claim("user_id", userId)
                .claim("role", "ADMIN")
                .issuer(issuer)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                .signWith(secretKey, SignatureAlgorithm.HS512)
                .compact();
    }
}
  • user_id와 고정 role을 바탕으로 JWT 토큰을 생성하고 반환하는 메서드

 

LocalJwtAuthenticationFilter

@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;
        }
    }
}
  • signIn 경로를 제외한 다른 모든 경로에 대한 호출 시 JWT 토큰을 검증하여 없거나 유효하지 않을 경우 401 Unauthorized 반환하고 통과 시 체인을 통해 요청을 다음 단계로 전달

 

실습 과정

유레카 서버 → 게이트웨이 → 인증 → 상품 순으로 어플리케이션 실행 후 SignIn API를 통해 JWT 토큰을 발급 받고, 게이트 웨이를 통해 상품 애플리케이션에 요청을 보내 JWT 인증이 제대로 진행 되는지 확인

  • SignIn을 통해 발급된 JWT 토큰을 헤더에 넣고 요청을 보냈을 때 인증이 성공되는 것을 확인할 수 있다.

  • JWT 토큰이 없는 경우 401 Unauthorized를 반환하는 것을 확인할 수 있다.

 

컨피그 서버(Config Server)

분산 시스템에서 외부화된 설정을 중앙에서 관리하고 제공하기 위해 설계된 Spring Cloud 컴포넌트

주요 역할

 

  1. 중앙 집중형 설정 관리:
    • 애플리케이션별 설정 파일을 중앙에서 관리.
    • 여러 환경(개발, 테스트, 운영 등)에 대한 설정을 통합.
  2. 동적 설정 업데이트:
    • Spring Cloud Bus 또는 Actuator의 /refresh 엔드포인트를 통해 애플리케이션에서 설정을 실시간으로 갱신 가능.
  3. 구성 소스 외부화:
    • 설정 파일을 Git, SVN, 파일 시스템 또는 JDBC 데이터베이스에 저장.
    • 애플리케이션 코드와 설정을 분리하여 유지 보수성 향상.

 

구성 방식

 

  1. Config Server:
    • 설정 데이터를 제공하는 중앙 서버 역할.
    • spring-cloud-config-server 의존성을 추가하여 구성.
  2. Config Client:
    • spring-cloud-starter-config를 사용하여 서버에서 설정 데이터를 가져오는 애플리케이션.
  3. 저장소:
    • Git 또는 다른 외부 소스에서 설정 데이터를 로드.

 

 

 

기본 작동 흐름

  1. Config Server는 Git 저장소 등의 외부 소스에서 설정 파일(application.yml, application.properties)을 가져옴.
  2. Config Client는 애플리케이션 실행 시 Config Server에서 해당 설정을 다운로드.
  3. 설정이 변경되면 클라이언트가 설정을 갱신하도록 /refresh 엔드포인트를 호출하거나 Spring Cloud Bus로 알림.

 

장점

 

  1. 환경별 설정 관리:
    • 같은 코드를 여러 환경에서 사용할 수 있도록 환경별 설정 제공.
  2. 확장성:
    • 마이크로서비스 아키텍처에서 각 서비스의 설정을 효율적으로 관리.
  3. 보안:
    • 민감한 정보(예: 데이터베이스 비밀번호, API 키)를 암호화하여 안전하게 저장.

 

실습 코드

Config Server build.gradle

implementation 'org.springframework.cloud:spring-cloud-config-server'

 

  • Config Server를 사용하기 위한 depnedency 추가

 

application.java

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {

    public static void main(String[] args) {
       SpringApplication.run(ConfigApplication.class, args);
    }

}
  • EnableConfigServer 애너테이션으로 Config Server 사용

 

Config server application.yml

spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/config-repo
  • Config Server의 설정 정의
    • config server의 이름, 로컬에서 설정 파일을 검색할 경로 지정

 

Config Client build.gradle

implementation 'org.springframework.cloud:spring-cloud-starter-config'
  • denpendency 추가

 

Config client application.yml

spring:
  profiles:
    active: local
  application:
    name: product-service
  config:
    import: "configserver:"
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server

management:
  endpoints:
    web:
      exposure:
        include: refresh
  • 설정 정보를 Config Server로부터 가져오도록 지정, 서비스 디스커버리를 통해 Config Server를 찾도록 설정
    • Actuator의 /refresh 엔드포인트를 노출해 Config Server에서 변경된 설정 정보를 다시 로드해 반영

 

실습 과정

Eureka Server → Config Server → Product 실행

  • Config Server 업데이트 전 메시지 출력

  • /actuator/refresh로 post 요청을 보내 메시지 업데이트 확인

  • 다시 product를 호출해 메시지가 변경된 것을 확인

 

정리

  • Spring Cloud에서 보안 설정과 Config Server를 사용하는 방법에 대해 배웠다.

'자바 심화 > TIL' 카테고리의 다른 글

Docker - 기본 사용 및 Cl / CD  (5) 2024.11.28
MSA - 기초 5  (0) 2024.11.27
MSA - 기초 3  (0) 2024.11.25
MSA - 기초 2  (1) 2024.11.22
MSA - 기초 1  (1) 2024.11.21