개요
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: 클라이언트가 요청한 리소스를 제공.
- 흐름:
- 클라이언트가 인증 서버에서 인증 및 권한 부여를 요청.
- 인증 서버가 Access Token을 발급.
- 클라이언트는 Access Token을 사용하여 리소스 서버에 요청.
- 리소스 서버는 토큰 유효성을 확인한 후 요청을 처리.
JWT(JSON Web Token)
- 역할: JSON 기반으로 정보를 안전하게 전달하는 토큰. OAuth2와 함께 주로 사용되며, 인증 및 권한 정보 저장에 적합.
- 구조:
- Header: 토큰의 타입(JWT)과 해싱 알고리즘.
- Payload: 사용자 정보(클레임, Claim)가 포함됨.
- Signature: Header와 Payload를 합쳐 비밀 키로 서명한 값.
- 장점:
- Stateless: 서버가 세션 상태를 유지할 필요 없음.
- 분산 시스템에서 유용 (e.g., 마이크로서비스).
- 사용:
- OAuth2의 Access Token으로 JWT를 활용하면 클라이언트가 토큰만으로 인증을 수행 가능.
- Resource Server에서 서명을 검증하여 토큰의 유효성을 확인.
Spring Cloud에서의 통합
- Spring Security: OAuth2와 JWT를 통합적으로 지원.
- Spring Boot spring-boot-starter-oauth2-resource-server 사용.
- Spring Cloud Gateway:
- 마이크로서비스 아키텍처에서 Gateway 레벨에서 인증 처리 가능.
- JWT 기반 토큰 검증을 추가하여 요청 필터링.
- Keycloak 같은 ID Provider:
- OAuth2 Authorization Server로 Keycloak을 통합하여 인증 서버로 활용.
핵심 워크플로
- 클라이언트가 인증 서버에 로그인 요청 → JWT Access Token 발급.
- 클라이언트가 API 호출 시 JWT를 포함.
- 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 컴포넌트
주요 역할
- 중앙 집중형 설정 관리:
- 애플리케이션별 설정 파일을 중앙에서 관리.
- 여러 환경(개발, 테스트, 운영 등)에 대한 설정을 통합.
- 동적 설정 업데이트:
- Spring Cloud Bus 또는 Actuator의 /refresh 엔드포인트를 통해 애플리케이션에서 설정을 실시간으로 갱신 가능.
- 구성 소스 외부화:
- 설정 파일을 Git, SVN, 파일 시스템 또는 JDBC 데이터베이스에 저장.
- 애플리케이션 코드와 설정을 분리하여 유지 보수성 향상.
구성 방식
- Config Server:
- 설정 데이터를 제공하는 중앙 서버 역할.
- spring-cloud-config-server 의존성을 추가하여 구성.
- Config Client:
- spring-cloud-starter-config를 사용하여 서버에서 설정 데이터를 가져오는 애플리케이션.
- 저장소:
- Git 또는 다른 외부 소스에서 설정 데이터를 로드.
기본 작동 흐름
- Config Server는 Git 저장소 등의 외부 소스에서 설정 파일(application.yml, application.properties)을 가져옴.
- Config Client는 애플리케이션 실행 시 Config Server에서 해당 설정을 다운로드.
- 설정이 변경되면 클라이언트가 설정을 갱신하도록 /refresh 엔드포인트를 호출하거나 Spring Cloud Bus로 알림.
장점
- 환경별 설정 관리:
- 같은 코드를 여러 환경에서 사용할 수 있도록 환경별 설정 제공.
- 확장성:
- 마이크로서비스 아키텍처에서 각 서비스의 설정을 효율적으로 관리.
- 보안:
- 민감한 정보(예: 데이터베이스 비밀번호, 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 |