본문 바로가기
project/Photogram

Photogram 인증 구현하기

by setung 2025. 4. 13.

Photogram 프로젝트에서는 인증 방식으로 JWT(Json Web Token)를 사용한다.
Gateway에서 JWT를 검증 후 요청 헤더에 User-Id를 추가하고 , 마이크로서비스 간에는 이 헤더 값을 통해 인증 여부를 판단한다.

https://github.com/setung/photogram

 

로그인을 성공하면 jwt을 반환 받는다.
gateway에서 jwt가 검증이 되면 user-id 헤더가 추가된다.

 

인증 흐름

  1. 사용자가 로그인 시도
    • user-service에서 로그인 처리
    • 로그인 성공 시 JWT 발급
  2. JWT를 포함한 요청
    • 클라이언트는 이후 요청 시 JWT를 헤더에 담아 Gateway로 전달
  3. Gateway에서 JWT 검증
    • JWT의 유효성 확인
    • 유효할 경우, JWT에서 userId를 추출하여
      user-id라는 이름의 헤더로 마이크로서비스에 전달
  4. 마이크로서비스에서 user-id 판단
    • Gateway가 전달한 user-id 헤더 존재 여부로 인증 여부를 판단

 

마이크로서비스 간 통신 시 인증 처리

마이크로서비스 간 통신은 Gateway를 거치지 않고 직접 통신한다.

따라서, 내부 통신 시에는 user-id를 직접 헤더에 명시하거나, 필요한 경우 별도의 인증 없이 진행된다.

 

이유

  1. 프라이빗 네트워크 환경
    • 마이크로서비스들은 내부 네트워크에 위치해 있어, 외부에서는 직접 접근할 수 없다.
  2. 중복된 인증 로직 방지
    • Gateway에서 이미 JWT 검증을 완료했기 때문에, 각 서비스에서 다시 검증하는 것은 불필요한 중복이다.
  3. 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

댓글