spring:
application:
name: eureka-server
server:
port: 190090
eureka:
client:// 클라이언트로써의 역할 / 기본적으로 유레카서버도 자신을 레지스트리에 등록시키기 때문에 작성해야 함
register-with-eureka: false// 다른 eureka 서버에 이 서버를 등록하지 않음(자신이 서버여서 가져올 필요 없음)
// -> 유레카 서버에 자신을 등록할지 여부 설정
fetch-registry: false// 다른 eureka 서버의 레지스트리를 가져오지 않음(자신이 서버여서 가져올 필요 없음)
// eureka 서버로부터 다른 서비스 인스턴스 목록을 가져오는지 여부
service-url:
defaultZone: http://localhost:190090/eureka// 자신의 호스트주소 설정
server:// 서버로써의 역할
enable-self-preservation: false// 자기 보호모드 비활성화
instance:
hostname: localhost// 호스트 이름 설정
//logging:
// level:
// com.netflix.eureka: OFF
// com.netflix.discovery: OFF
// 이 서비스가 EurekaServer임을 선언
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
spring:
application:
name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
port: 190091
// 여기까지만으로도 유레카 서버에 등록됨--------------------------------------------------
eureka:
client:
service-url:
defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
register-with-eureka: true// Eureka 서버에 등록
fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
instance:
hostname: localhost// 호스트 이름 설정
prefer-ip-address: true// IP 주소 사용 선호
lease-renewal-interval-in-seconds: 30// lease 갱신 간격
lease-expiration-duration-in-seconds: 90// lease 만료 기간
3) Eureka 클라이언트 설정 [ 조회할 서비스 요청 준비 ]
Eureka 서버에서 필요한 서비스의 위치를 조회해야 한다.(RestTemplate 사용하는 방법, FeignClient를 사용하는 방법)
1_ RestTemplate을 사용하는 경우
// @SpringBootApplication이 있는 곳에 @Bean으로 등록한 RestTemplate에
// @LoadBalanced를 붙여 로드밸런싱 기능 추가한다.
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
//사용할 Controller에 서비스url을 restTemplate로 불러와서 사용한다.
@RestController
public class MyRestTemplateController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/get-data-rest")
public String getDataWithRestTemplate() {
String serviceUrl = "http://my-service/api/data"; //my-service 위치가 애플리케이션 이름
return restTemplate.getForObject(serviceUrl, String.class);
}
}
2_ FeignClient를 사용하는 경우
// FeignClient를 사용하용하도록 표시해야 한다.
@SpringBootApplication
@EnableFeignClients
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// FeignClient에 필요한 서비스 이름을 추가한다
// RestTemplate 방식에서 serviceUrl이 여기서 인터페이스로 만들어진다.
// 사용하는 url은 상대 서비스에 보낼 것으로 작성하면 됨
@FeignClient(name = "my-service") // 애플리케이션 이름
public interface MyServiceClient {
@GetMapping("/api/data")
String getData();
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회해 온다
@RestController
public class MyFeignClientController {
@Autowired
private MyServiceClient myServiceClient;
@GetMapping("/get-data-feign")
public String getDataWithFeignClient() {
return myServiceClient.getData(); //api가 아닌 메서드로
}
}
3_ DiscoveryClient와 RestClient를 활용하는 방법
package com.example.serviceb.controller;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
@RestController
public class ServiceBRestController {
private final DiscoveryClient discoveryClient;
private final RestClient restClient;
public ServiceBRestController(DiscoveryClient discoveryClient, RestClient.Builder restClientBuilder) {
this.discoveryClient = discoveryClient;
restClient = restClientBuilder.build();
}
@GetMapping("helloEureka")
public String helloWorld() {
ServiceInstance serviceInstance = discoveryClient.getInstances("servicea").get(0);
// 인스턴스명이 "servicea"인 인스턴스 중 처음 것을 가져옴
String serviceAResponse = restClient.get()
.uri(serviceInstance.getUri() + "/helloWorld")
.retrieve()
.body(String.class);
return serviceAResponse;
}
}
4) Eureka 서버에서 헬스 체크 및 장애 처리
- 헬스 체크 : 주기적으로 서비스 인스턴스 상태를 확인하여 가용성 확인
- 장애 처리 : 서비스 장애 시 해당 인스턴스를 레지스트리에서 제거하여 해당 서비스로의 다른 서비스 접근을 차단한다
5) Eureka 클라이언트 설정 [ Eureka의 고가용성 구성 ] - 클러스터 구성(역할이 같다는 것으로 등록)
// 유레카 클라이언트를 여러 유레카 서버에 등록
eureka:
client:
service-url:
defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/
// 유레카 서버 URL을 여러 개로 구성
FeignClient는 Spring Cloud에서 제공하는 HTTP클라이언트, 선언적으로 RESTful 웹 서비스를 호출할 수 있다.
Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행
주요 특징
선언적 HTTP 클라이언트 : 인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있다.
Eureka 연동 : Eureka 서버에서 인스턴스 목록을 가져와 로드 밸런싱을 수행
자동 로드 밸런싱 : Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행
Ribbon
클라이언트 사이드 로드 밸런서 MSA에서 서비스 인스턴스 간의 부하를 분산시켜 줌
다양한 로드 밸런싱 알고리즘 지원, 서비스 디스커버리와 연동하여 사용
주요 특징
다양한 로드 밸런싱 알고리즘 지원
Failover : 요청 실패 시 다른 인스턴스로 자동 전환
1) Eureka클라이언트 설정 [ FeignClient와 Ribbon 설정 ]
build.gradle 에 의존성을 추가한다.
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' //유레카 클라이언트
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' //FeignClient 추가
//FeignClient 선언적 사용
@SpringBootApplication
@EnableFeignClients
public class MyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyServiceApplication.class, args);
}
}
// FeingClient 선언적 사용 : interface로 상대 서비스 인스턴스 선언
// Controller와 같이 선언
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "request-service")
public interface MyServiceClient {
@GetMapping("/endpoint")
String getResponse(@RequestParam(name = "param") String param);
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회 구현
@RestController
@RequiredArgsConstructor
public class MyFeignClientController {
private MyServiceClient myServiceClient;
@GetMapping("/get-data-feign")
public String callService(@RequestParam String param) {
return myServiceClient.getResponse(param);
}
}
spring:
application:
name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
port: 190091
eureka:
client:
service-url:
defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
register-with-eureka: true// Eureka 서버에 등록
fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
instance:
hostname: localhost// 호스트 이름 설정
prefer-ip-address: true// IP 주소 사용 선호
lease-renewal-interval-in-seconds: 30// lease 갱신 간격
lease-expiration-duration-in-seconds: 90// lease 만료 기간
request-service:
ribbon:
eureka:
enabled: true//FeignClient에서 제공하는 서비스 인스턴스를 사용하여 로드 밸런싱을 수행
2) 요청받을 서비스 인스턴스 여러 개 등록(Intellij IDE 한정) - Ribbon에서 로드 밸런싱 알고리즘에 맞게 자동 호출
위 사진에서 옵션 수정 클릭 - VM옵션 추가 - "-Dserver.port=19093(실행할 포트)" 입력
FeignClient와 Ribbon 동작 원리
@FeignClient(name="request-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조
서비스 인스턴스 조회
로드 밸런싱(Ribbon) : 서비스 인스턴스 선택 및 요청 분배
4.Resilience4j 사용하기 [ 서킷 브레이커 ]
프로젝트 생성 - 생성시 dependencies에 resilience4j는 추가하지 않는다
기본 설정(build.gradle) resilience4j를 spring starter에서 추가하지 않고 gradle dependecy에서 추가한다. ( spring starter에서 제공하는 dependency는 구현체가 아니어서 github의 resilience4j로 추가한다.)
// Spring Cloud와 Resilience4j 연동 - Eureka와 Ribbon 등 다른 Spring Cloud 구성요소와 쉽게 통합
spring:
application:
name: my-service
cloud:
circuitbreaker:
resilience4j:
enabled: true
server:
port: 19090
// Resilience4j 설정
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
# 슬라이딩 윈도우의 크기를 설정
# COUNT_BASED일 경우: 최근 N번의 호출을 저장
# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
// Resilience4j Dashboard를 위한 설정 - http://${hostname}:${port}/actuator/prometheus 에 접속하여 서킷브레이커 항목을 확인 가능
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true
Fallback 적용
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstruct
public void registerEventListener() {
circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
.onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
.onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
.onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
.onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
}
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
public Product getProductDetails(String productId) {
log.info("###Fetching product details for productId: {}", productId);
if ("111".equals(productId)) {
log.warn("###Received empty body for productId: {}", productId);
throw new RuntimeException("Empty response body");
}
return new Product(
productId,
"Sample Product"
);
}
public Product fallbackGetProductDetails(String productId, Throwable t) {
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
return new Product(
productId,
"Fallback Product"
);
}
// 이벤트 설명 표
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생 | CircuitBreaker State Transition: ... |
// | 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | CircuitBreaker Failure Rate Exceeded: ... |
// | 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생 | CircuitBreaker Call Not Permitted: ... |
// | 오류 발생 | 서킷 브레이커 내부에서 호출이 실패하면 발생 | CircuitBreaker Error: ... |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 메서드 호출 | 제품 정보를 얻기 위해 메서드를 호출 | ###Fetching product details for productId: ... |
// | (성공 시) 서킷 브레이커 내부에서 호출 성공 | 메서드 호출이 성공하여 정상적인 응답을 반환 | |
// | (실패 시) 서킷 브레이커 내부에서 호출 실패 | 메서드 호출이 실패하여 예외가 발생 | #######CircuitBreaker Error: ... |
// | (실패 시) 실패 횟수 증가 | 서킷 브레이커가 실패 횟수를 증가시킴 | |
// | (실패율 초과 시) 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | #######CircuitBreaker Failure Rate Exceeded: ... |
// | (실패율 초과 시) 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨 | #######CircuitBreaker State Transition: Closed -> Open at ... |
// | (오픈 상태 시) 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨 | #######CircuitBreaker Call Not Permitted: ... |
// | (오픈 상태 시) 폴백 메서드 호출 | 메서드 호출이 차단될 경우 폴백 메서드 호출 | ####Fallback triggered for productId: ... due to: ... |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
}
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/** 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
필터 작성
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest response = exchange.getRequest();
logger.info("Pre Filter: Request URI is " + response.getURI());
// Add any custom logic here
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter: Response status code is " + response.getStatusCode());
// Add any custom logic here
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
@Component
public class PreFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 요청 로깅
System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
6.OAuth2 + JWT 사용하기 [ 보안 구성 ]
Auth 서비스 생성
build.gradle 설정 - jwt dependencies 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
application.yml 설정
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000
secret-key: #key값을 임의의 값에 대한 base64로 암호화한 값으로 추가
server:
port: 19095
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();
}
}
AuthoService.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();
}
}
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;
}
}
6-1.Gateway 추가 [ 보안 구성 ] - 직접 접근하는 곳인 게이트웨이에서 Filter로 추가
build.gradle 설정 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
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: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/auth/signIn # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
service:
jwt:
secret-key: #key값을 임의의 값에 대한 base64로 암호화한 값으로 추가
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @RefreshScope 애노테이션은 Spring 애플리케이션의 빈이 설정 변경을 반영할 수 있도록 하는 역할을 합니다.
* 기본적으로 Spring 애플리케이션의 빈은 애플리케이션이 시작될 때 초기화되고, 설정 값이 변경되더라도 해당 빈은 갱신되지 않습니다.
* 이 애노테이션을 사용하면 /actuator/refresh 엔드포인트를 호출하여 설정 변경 사항을 동적으로 반영할 수 있습니다.
*/
@RefreshScope
@RestController
@RequestMapping("/product")
public class ProductController {
@Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
private String serverPort;
@Value("${message}")
private String message;
@GetMapping
public String getProduct() {
return "Product detail from PORT : " + serverPort + " and message : " + this.message ;
}
}
7-3) 설정 반영하기[ 수동으로 /actuator/refresh 엔드포인트를 호출하는 방법 ]