[완] 개인서버 개발/공통 서비스 개발(완)

#6 Webflux기반 OAuth2서버 + gateway 구축

북극곰은콜라 2023. 5. 14. 19:14
반응형


개요

서비스를 위해서  서버에 접근제어를 위한 인증서버가 필요했다.
개인적으로 정리한 요구사항:
 - 개인 서버의 계정을 기반으로 인증을 수행할 수 있어야 한다.
 - 내부적인 서비스 호출에 인가를 받을 수 있는 구조여야 한다.
 - OAuth2.0 최소 스펙 이상 동작할 수 있어야 한다.
 - 추후 외부 OAuth 프로토콜과 연계 될 수 있는 구조여야 한다.

 


Gateway OAuth 2.0 서버 개요


Server Architecture

전체적인 서버구성에 추가적인 요소들이 있다.
1. Nginx의 추가
 - WebServer의 역할을 하며, 개인 서버의 유일한 Entry Point
 - URL Prefix를 통해서 각 서버로 연동을 해준다.
 - Web Resource (static page)에 대한 제공을 한다.

2. Gateway Auth Server
 - API 서버로 접근하기 위한 Entry Point 역할을 한다.
 - Nginx로 /gateway로 접근 가능
 - API 서버로 접근할 수 있는 유일한 루트
 - API에 대한 인증 및 인가를 수행한다. (OAuth 2.0 기반)
 - Client에 대한 관리도 진행한다.

3. Redis
 - 토큰 등 캐싱을 위한 데이터를 임시 보관한다.
 - 캐시 DB로 역할을 FIX

Gateway OAuth 서버 구축

앞으로 늘어날 서비스들로 라우팅할 수 있는 Gateway 서버가 필요했다.
또한 해당 포인트에서 인증 및 인가를 수행하는 것이 효율적이라는 판단으로 해당 서비스에 구현했다.
이 아이디어는 Line에서 소개한 다음 글에서 얻었다.
https://engineering.linecorp.com/ko/blog/service-authentication-sidecar-proxy

개인서버를 스케일아웃 할 수 없는 구조에서, 인증서버를 따로 두는 것은 비효율적이라 판단하고
gateway에서 인증 / 인가 서비스를 직접 하여 불필요한 트레픽을 줄였다.

난관

Spring Cloud Gateway를 기반으로 서버를 구축했다.
문제는 Webflux 기반 Spring Security 프로젝트에서, 더 이상 인증서버에 대한 기능 (토큰 발급 등 OAuth2.0 스펙)의 지원을 중단했다는 점이다.
따라서 Spring 프로젝트로 따로 OAuth2.0 스펙을 구현해야하는 상황이다.

OAuth2.0은 오래된 스펙인 만큼 해당 서버를 구축할 수 있는 라이브러리가 있을 것이라 기대했다.
하지만 2가지 문제가 있었다.
1. OAuth 2.0 을 사용하는 client 입장의 라이브러리와 SDK가 대부분
2. 대부분의 라이브러리는 블로킹 기반의 서버에서 활용 가능하도록 API가 구성되어있음

처음 시도했던 방법은 블로킹 기반으로 설계된 라이브러리를 비동기 방식으로 Wrapping해서 구현해 보려 했다.
하지만, 대부분 Datasource를 제공하는 방식에서 문제가 생겼다.
결국 DataSource로 부터 데이터를 동기적으로 세팅하도록 설계가 되어있어서, Integrate하는데 문제가 발생했다.

그래도 직접 구현하기에는 스펙 자체가 작지 않아서 다른 방식을 찾아보았다.

해결

Nimbus라는 OAuth2.0 기반 엔터프라이즈 인테그레이션 서비스를 제공하는 회사?단체?가 있다.
Numbus에서 자신들의 OAuth2.0 서버를 기존 서버에 인테그레이션 할 수 있는 SDK를 제공한다.
해당 오픈소스에는 OAuth2.0의 인터페이스를 전체적으로 제공하고있다.
하지만 인증 로직에 대한 부분은 구현되어있지 않았다.

해당 SDK를 활용해서
인증 요청 -> (SDK) -> 인증로직 (구현) -> (SDK) -> 응답
하는 방식으로 최소한의 로직 구현으로 인증서버를 구축하기로 결정했다.

 


OAuth 2.0 서비스 구현

주요 라이브러리

implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")

implementation("com.nimbusds:c2id-server-sdk:4.53")
implementation("org.mariadb:r2dbc-mariadb:1.0.3")
Kotlin으로 구현된 서버이며
Spring Webflux 기반으로 한정된 서버 리소스를 최대할 활용하고자 했다.
Spring Security로 인증 / 인가에 대한 기능을 구현하고
nimbus 라이브러리를 활용해 인증을 위한 서비스를 구현했다.
Database를 논블로킹으로 활용 할 수 있게 R2DBC 인터페이스를 사용했으며
연동되는 DB는Redis와 MariaDB 이다.

Client Service

OAuth 2.0 의 Client에 대한 create, delete, verify 기능을 제공할 수 있도록 구현했다.

패키지 외부적으로 ClientHandler를 통해서 Client에 대한 CRD를 제공한다.
ClientAuthenticationVerifier는 Nimbus의 Client Verifier를 위한 모듈이며, 이는 Nimbus의 Verifier를 논블로킹 방식으로 레핑했다.

OAuth Service

OAuthHandler: 토큰 발급 요청을 적절하게 파싱하는 핸들러, Nimbus SDK를 적극 활용, Authorize 요청을 위한 로직은 이쪽에 구현
TokenService: 토큰 발급을 위한 로직들이 구현되어있는 핸들러, 구현된 사항으로는 Authorize Code, Authorize Token, Resource Owner 방식 3가지 이다.
TokenStore: 토큰에 대한 CRUD를 담당하는 클래스.
OAuthRedisRepository: 토큰을 Redis에 CRD를 하는 레파지토리

인증 / 인가 서비스 구현

@Bean
fun securityWebFilterChain(
    serverHttpSecurity: ServerHttpSecurity,
    authManager: AuthManager,
    authenticationConverter: AuthenticationConverter): SecurityWebFilterChain {
    val filter = AuthenticationWebFilter(authManager)
    filter.setServerAuthenticationConverter(authenticationConverter)

    val authorizeExchangeSpec = serverHttpSecurity
        .authorizeExchange()

    // permitAll 세팅
    permitAllApiMap.forEach{ entry ->
        entry.value.forEach { uri ->
            authorizeExchangeSpec.pathMatchers(entry.key, uri).permitAll()
        }
    }

    authorizeExchangeSpec
        // main 서버 기본 scope -> main:*
        .pathMatchers("/main/**").hasAuthority("SCOPE_main:*")

    return authorizeExchangeSpec
        .and()
        .addFilterAfter(filter, SecurityWebFiltersOrder.AUTHENTICATION)

        .csrf().disable()
        .formLogin().disable()
        .httpBasic().disable()
        .logout().disable()
        .cors().configurationSource(corsConfigurationSource())
        .and()
        .build()
}

 

Spring Security를 활용해서 인가에 대한 컨트롤을 수행한다.
회원가입, 토큰 발급 등에 대해서 permitAll(), 나머지는 서비스별로 SCOPE를 확인 하도록 설정

 


GateWay Service

@Bean
fun proxyRouterFunction(routeLocatorBuilder: RouteLocatorBuilder): RouteLocator = routeLocatorBuilder.routes()
    .route("main") { predicateSpec ->
        predicateSpec
            .path("/main/**")
            .filters { it
                .filter(apiAccessControlFilter.apply(ApiAccessControlFilter.Config()))
                .rewritePath("^/main", "")
            }
            .uri(mainRouteUrl)
    }
    .build()
Spring Cloud Gate를 활용하여 구성
/main으로 들어오는 요청을 main 서비스로 라우팅 한다.
prefix는 제거해서 요청

전체 flow 예시로
/gateway/main/api/xxx
Nginx에서 gateway가 지워지면서 gateway서버로 접근
gateway filter단에서 토큰 검증
/main을 지우면서 main 서버로 /api/xxx로 요청

 


추후 계획

기본 로그인 / 회원가입 페이지를 Niginx를 통해 제공하며
묵시적 코드 플로우를 통해 토큰을 제공할 예정이다.
전체적인 구성은
개별 서비스 -> login 페이지 (redirectURI 포함) -> 로그인 수행 -> 개별 서비스로 redirect
로 구성 되며, 로그인 페이지에 구글 연동 정도 추가 할 예정

 


REFERENCE

https://oauth.net/2/

https://www.rfc-editor.org/rfc/rfc6749

https://connect2id.com/products/server

 

 

 

 

 

반응형