Today I Learned
오늘은 보안을 구성하는 Security를 학습하였다. Security는 배워도 배워도 끝없이 어려운 거 같다 😭
🔥 나중에 다시 봐도 이해하기 쉽게 정리를 해보도록 하자
[ 보안의 중요성 ]
- 마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 보안이 매우 중요합니다.
- 데이터 보호, 인증 및 권한 부여, 통신 암호화 등을 통해 시스템의 보안성을 확보해야 합니다.
[ OAuth2 ]
- OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜입니다.
- 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 합니다.'
- OAuth2는 네 가지 역할을 정의합니다: 리소스 소유자, 클라이언트, 리소스 서버, 인증 서버
[ JWT의 주요 특징 ]
- 자가 포함: 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없습니다.
- 간결성: 짧고 간결한 문자열로, URL, 헤더 등에 쉽게 포함될 수 있습니다.
- 서명 및 암호화: 데이터의 무결성과 인증을 보장합니다.
[ 동작 실습 ]
이번 강의에서는 실습을 통해 Cloud Gateway의 Pre 필터에서 JWT 인증을 진행해 보았습니다.
- 로그인을 담당하는 서비스 어플리케이션 Auth Service 를 생성하고 로그인 기능을 아주 간단하게 구현한다.
- Cloud Gateway에 Pre 필터를 하나 더 생성하여 로그인을 체크 한다.
- 동작 정리: 로그인을 진행하면 auth를 통하여 토큰을 발급받고, 이 토큰을 사용하여 Gateway를 호출한다.
[ Auth ]
Auth의 yaml 설정
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000 # 만료시간 1시간
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
server:
port: 19095
Dependencies
build.gradle에 jwt 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
AuthService
- 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
public String createAccessToken(String user_id){
return Jwts.builder()
.claim("user_id", user_id)
.claim("role","ADMIN") // 권한
.issuer(issuer) // 토큰 발급자
.issuedAt(new Date(System.currentTimeMillis())) // 발행 날짜
.expiration(new Date(System.currentTimeMillis() + accessExpiration)) // 만료 날짜
.signWith(secretKey, SignatureAlgorithm.HS512) // 알고리즘 서명
.compact();
}
AuthController
- 사용자 ID를 받아 JWT 액세스 토큰을 생성하고 응답합니다.
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthToken(
@RequestParam("user_id")
String user_id
){
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
[ Cloud Gateway ]
- 기존 사용하던 Gateway 코드에 JWT인증 및 auth-service 라우팅 정보를 추가합니다.
build.gradle에 jwt 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
yaml 수정
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을 지정
# Auth에서 access_token을 만들때 사용한 동일한 secret-key가 있어야 사용자로 부터 요청이 들어왔을 때 Gateway에서 토큰 유효성 검사 가능
service:
jwt:
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
LocalJwtAuthenticationFilter.java
- 헤더에서 토큰을 가져오고, 토큰에 대한 유효성 검사를 해줍니다.
- 로그인 페이지 접근에 대해서는 토큰을 검사할 필요가 없기 때문에 다음 필터로 이동합니다.
- https://http.dog/ 에서 HTTP 에러를 강아지의 귀여운 사진으로 볼 수 있습니다.
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// signIn(로그인)페이지 접근에 대해서는 토큰을 검사를 하지 않고 넘겨준다.
String path = exchange.getRequest().getURI().getPath();
if (path.equals("/auth/signIn")){
return chain.filter(exchange);
}
String token = extractToken(exchange);
// 잘못된 토큰일 경우 / State: 401
if (token == null || !validateToken(token)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 제대로 된 토큰일 경우 다음 필터로 이동
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange){
// 헤더에서 토큰을 가져오기 / Authorization: {Key:Value}형식의 Key 값
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
// 토큰이 null이 아니고, "Bearer "로 시작하는지
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);
// payload에 들어있는 데이터 확인
log.info("#####payload :: " + claimsJws.getPayload().toString());
// 추가적인 검증 로직 추가 가능(예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
return true;
} catch (Exception e) { // 토큰 검증 중 예외가 발생할 경우 false 반환
return false;
}
}
[ Run ]
- 유레카 서버, 게이트웨이, 인증, 상품 순으로 어플리케이션을 실행합니다.
- Talend: https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ko
http://localhost:19090에 접속하여 각 인스턴스를 확인합니다.
[ 엑세스 토큰 발급 전 ]
Gateway '19091' 에서 상품을 요청해 봅니다. 401 에러가 발생하는 것을 볼 수 있습니다.
- 엑세스 토큰을 발급받지 않고 접근
[ 엑세스 토큰 발급 ]
Gateway에서 로그인을 요청하여 토큰을 발급받아봅니다.
해당 토큰을 상품요청에 헤더에 넣어서 요청합니다.
- Gateway를 통하여 상품 요청 성공, 헤더에 엑세스 토큰 넣어주기
보안 구성!! Gateway를 통한 Security의 동작을 알아 보았다. 아직 Security를 완벽하게 이해한다고는 못하지만, 확실한 것은 들을 때마다 조금씩 쉬워지는 느낌이 있는 것 같다.
읽어 주셔서 감사합니다 😊
Eureka 서버
😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-eureka.server
Gateway Repository
😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-client.gateway
Auth Repository
😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-client.auth
'해피 코딩 > Today I Learned' 카테고리의 다른 글
[TIL 6] 컴퓨터는 죄가 없다, Redis정리 (1) | 2024.08.07 |
---|---|
[TIL5] MSA 마무리 (0) | 2024.08.06 |
[TIL 3] CircuitBreaker와 Resilience4j (0) | 2024.08.02 |
[TIL 2] MSA와 Spring Cloud (0) | 2024.08.01 |
[TIL 1] 어떻게든 배포만 하면 된다!! (2) | 2024.07.31 |