MSA는 각 서비스가 독립적으로 배포되기 통신되므로 보안이 매우 중요하다.
데이터 보호, 인증 및 권한 부여 등을 통해 시스템의 보안성을 확보해야 한다.
📌 OAuth2
`OAuth2`는 `토큰 기반`의 인증 및 권한 부여 프로토콜이다.
클라이언트 어플리케이션이 리소스 소유자의 권한을 얻어 리소스에 접근할 수 있도록 한다.
📌 JWT
`JWT` (Json Web Token)은 JSON 형식의 자가 포함 토큰으로, 클레임을 포함하여 사용자에 대한 정보를 전달한다.
JWT는 헤더, 페이로드, 서명으로 구성되고 암호화를 통해 데이터의 무결성과 출처를 보장한다.
`JWT`의 주요 특징으로는 자가 포함, 간결성, 서명 및 암호화가 있다.
자가 포함 토큰으로 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없다!
https://jwt.io/ 에서 토큰을 decode 할 수 있다.
📌 보안 구성 실습
이번 실습에서는 Cloud Gateway의 Pre 필터에서 JWT 인증을 진행한다.
Auth Service를 생성하여 로그인 기능을 아주 간단하게 구현하고 Pre 필터를 하나 더 생성하여 로그인을 체크한다.
로그인을 담당하는 Auth 서비스를 생성한다.
의존성에 jwt를 추가한다!
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
`application.yml` 파일
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 경로 외의 요청에는 모두 인증이 필요하도록 하였다.
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()
.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();
}
}
JWT 토큰을 생성하고 반환하는 `AuthService`를 작성한다.
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;
}
}
토큰을 생성하는 `AuthController`도 작성한다!
그 후 기존 게이트웨이 설정 파일에 JWT 인증 및 `auth-service` 라우팅 정보를 추가한다.
- id: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/auth/signIn # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
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;
}
}
}
그리고 필터를 추가적으로 작성하여 JWT 토큰을 검증할 수 있도록 한다.
유레카 서버 ⇒ 게이트웨이⇒ 인증 ⇒ 상품 순으로 어플리케이션을 실행한다.
게이트웨이에 로그인(헤더에 토큰) 없이 상품을 요청할 경우 401 에러가 발생한다.
게이트웨이에 로그인을 요청하여 토큰을 발급 받은 뒤 헤더에 넣어 요청하면 정상적으로 응답이 오는 것을 볼 수 있다.
`Bearer`는 OAuth 2.0 프로토콜에서 사용하는 인증 토큰 유형 중 하나로 보호된 리소스에 접근할 수 있도록 한다.
`Bearer` 토큰은 요청 헤더에 포함되어 전달된다.
로그인은 정말 항상 어려운 것 같다.
그래도 반복적으로 학습하니 한결 익숙해지긴 했다..
'공부 > Spring' 카테고리의 다른 글
Springboot JPA N+1 문제와 해결법 (JPQL, FetchType) (0) | 2024.09.02 |
---|---|
[Spring Cloud] API Gateway (Spring Cloud Gateway) (0) | 2024.08.05 |
[Spring Cloud] 서킷 브레이커 (Resilience4j) (0) | 2024.08.05 |
[Spring Cloud] 클라이언트 사이드 로드 밸런싱 (FeignClient와 Ribbon) (0) | 2024.08.05 |
[Spring Cloud] MSA Spring Cloud (Eureka) (0) | 2024.08.01 |