마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 데이터 보호, 인증 및 권한 부여, 통신 암호화 등에 관한 시스템 보안성을 확보해야 합니다.
보안을 확보하기 위해 OAuth2 + JWT를 사용하여 권한 부여 및 토큰을 통한 정보 전달을 하곘습니다.
토큰 기반의 인증 및 권한 부여 프로토콜입니다. 클라이언트가 권한을 얻어 보호된 리소스에 접근할 수 있도록 도와줍니다. 이 방식에는 네가지 역할(리소스 소유자, 클라이언트, 리로스 서버, 인증 서버)을 정의합니다.
JWT(Json Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달합니다. 또한, 세 부분(헤더, 페이로드, 서명)으로 구성됩니다. 이렇게 구성된 JWT는 암호화를 통해 데이터의 무결성과 출처를 보장합니다.
로그인을 담당하는 어플리케이션을 생성합니다. 로그인을 진행하면 토큰을 발급받고 이 토큰을 사용하여 Gateway를 호출합니다.
위와 같은 디펜던시를 생성하고 프로젝트를 생성합니다.
JWT를 사용하기 위해 JWT관련 디펜던시를 추가합니다.
implementation 'io.jsonwebtoken:jjwt:0.12.6'
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka
service:
jwt:
access-expiration: 3600000
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeRequests(
authorize -> authorize
.requestMatchers("/auth/signIn").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
@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.BASE64URL.decode(secretKey));
}
public String createAccessToken(String user_id) {
return Jwts.builder()
.claim("userId", user_id)
.claim("role", "ADMIN")
.issuer(issuer)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
.compact();
}
}
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id) {
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}
기존 게이트웨이에 auth-service 라우팅 정보를 추가합니다.
jwt 의존성을 추가합니다.
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive
application:
name: gateway-service
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/signIn
discovery:
locator:
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
auth-service
, jwt secret-key
를 추가해줍니다.
@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;
}
}
}
eureka server ⇒ gateway ⇒ auth ⇒ product 순으로 어플리케이션을 실행합니다.
http://localhost:19090에 접속하여 각 인스턴스를 확인합니다.
권한이 없기때문에 401에러가 발생하는 것을 볼 수 있습니다.
이렇게 auth의 인증을 통해 보안을 알아봤습니다.
이상으로 보안구성 구현을 마무리 하겠습니다.