Photogram 프로젝트에서는 인증 방식으로 JWT(Json Web Token)를 사용한다.
Gateway에서 JWT를 검증 후 요청 헤더에 User-Id를 추가하고 , 마이크로서비스 간에는 이 헤더 값을 통해 인증 여부를 판단한다.
https://github.com/setung/photogram
인증 흐름
- 사용자가 로그인 시도
- user-service에서 로그인 처리
- 로그인 성공 시 JWT 발급
- JWT를 포함한 요청
- 클라이언트는 이후 요청 시 JWT를 헤더에 담아 Gateway로 전달
- Gateway에서 JWT 검증
- JWT의 유효성 확인
- 유효할 경우, JWT에서 userId를 추출하여
user-id라는 이름의 헤더로 마이크로서비스에 전달
- 마이크로서비스에서 user-id 판단
- Gateway가 전달한 user-id 헤더 존재 여부로 인증 여부를 판단
마이크로서비스 간 통신 시 인증 처리
마이크로서비스 간 통신은 Gateway를 거치지 않고 직접 통신한다.
따라서, 내부 통신 시에는 user-id를 직접 헤더에 명시하거나, 필요한 경우 별도의 인증 없이 진행된다.
이유
- 프라이빗 네트워크 환경
- 마이크로서비스들은 내부 네트워크에 위치해 있어, 외부에서는 직접 접근할 수 없다.
- 중복된 인증 로직 방지
- Gateway에서 이미 JWT 검증을 완료했기 때문에, 각 서비스에서 다시 검증하는 것은 불필요한 중복이다.
- JWT 만료 이슈
- JWT의 유효기간이 짧은 경우, 내부 서비스 간 호출에서 토큰 만료로 인한 예외가 발생할 수 있다.
익명 사용자 처리
일부 API는 사용자의 로그인 여부와 관계없이 접근이 가능해야 한다.
익명 사용자가 필요한 경우
- JWT 유무와 관계없이 요청을 허용해야 하는 대표적인 예는 유저 상세 조회 API 가 있다.
- 조회 대상 사용자의 상태가 공개(public) 일 경우, 로그인하지 않은 사용자도 접근 가능해야 한다.
- 반면, 상태가 비공개(private)인 경우, 요청자가 해당 사용자를 팔로우하고 있고, 팔로우가 승인된 상태여야만 조회가 가능하다. 이 경우에는 요청자의 팔로우 상태를 확인해야 하기 때문에 로그인이 필요하다.
익명 사용자 처리 방식
- 인증 필터(JWT 필터)에서 익명 사용자 허용 설정이 활성화된 API에 한해, JWT가 없는 요청을 허용한다.
- JWT가 존재하지 않는 경우, 내부적으로 user-id를 -1로 설정하여 익명 사용자를 나타낸다.
JwtAuthGatewayFilter
@Component
class JwtAuthGatewayFilter(
private val jwtProvider: JwtProvider
) : AbstractGatewayFilterFactory<JwtAuthGatewayFilter.Config>(Config::class.java) {
override fun apply(config: Config?): GatewayFilter {
return GatewayFilter { exchange, chain ->
val request = exchange.request
if (!request.headers.containsKey(HttpHeaders.AUTHORIZATION)) {
if (config!!.allowAnonymous) {
val anonymousRequest = request.mutate()
.header(HttpHeader.USER_ID.value, LoginStatus.ANONYMOUS.id.toString())
.build()
return@GatewayFilter chain.filter(exchange.mutate().request(anonymousRequest).build())
}
return@GatewayFilter onError(exchange, "No authorization header")
}
val authorizationHeader = request.headers[HttpHeaders.AUTHORIZATION]!![0]
val jwt = authorizationHeader.replace("Bearer ", "")
if (!jwtProvider.validateToken(jwt)) {
return@GatewayFilter onError(exchange, "JWT token is not valid")
}
val newRequest = exchange.request.mutate()
.headers {
it.remove(HttpHeaders.AUTHORIZATION)
it.set(HttpHeader.USER_ID.value, jwtProvider.getUserId(jwt).toString())
}
.build()
return@GatewayFilter chain.filter(exchange.mutate().request(newRequest).build())
}
}
private fun onError(exchange: ServerWebExchange, err: String): Mono<Void> {
val response = exchange.response
response.statusCode = HttpStatus.UNAUTHORIZED
val buffer = response.bufferFactory().wrap(err.toByteArray(Charsets.UTF_8))
return response.writeWith(Mono.just(buffer))
}
class Config {
var allowAnonymous: Boolean = false
}
}
Gateway Routes
spring:
cloud:
gateway:
routes:
- id: user-service-private-get
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users/me
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- JwtAuthGatewayFilter
- id: user-service-allow-anonymous-get
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users/*
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- name: JwtAuthGatewayFilter
args:
allowAnonymous: true
- id: user-service-public-get
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
user-service-private-get
- JwtAuthGatewayFilter를 사용해 JWT 인증 필수
user-service-allow-anonymous-get
- JwtAuthGatewayFilter 필터의 상태값 allowAnonymous을 true로 설정
- 기본적으로는 JWT 인증을 시도하지만, 토큰이 없어도 허용
- JWT가 있을 경우 user-id 헤더 추가, 없으면 user-id의 값을 -1(익명사용자) 추가
user-service-public-get
- JwtAuthGatewayFilter 필터를 사용 안 함→ 완전 공개 API
'project > Photogram' 카테고리의 다른 글
Photogram Push 기반 피드 구현 (0) | 2025.04.14 |
---|---|
Photogram 태그 기반 게시글 검색 기능 (0) | 2025.04.13 |
Photogram 아키텍처 (0) | 2025.04.13 |
댓글