Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>
많은 회사에서 내부 혹은 제 3자의 기술개발과 서비스 확장을 위해서 API를 제공하고 있다. 많은 유형의 API들이 있지만 이 문서는 그 중 웹 API 에 집중한다. 이들 API는 JSON, REST, HTTP와 같이 잘 알려진 프로토콜과 아키텍처를 이용해서 개발한다.

이들 API는 인터넷으로 공개된다. 이는 기능이 외부에 공개된다는 의미로 확장을 위해서는 좋은 생각이지만 보안 측면에서는 나쁜 생각일 수도 있다. 웹은 널리 알려진 기술이고 잘 이해하고 있기 때문에 외부에 공개할 거라면 특히 보안에 신경써야 한다.

Contents

인증과 승인(Authentication vs Authorization)

이 문서는 "인증"을 주로 다룰 건데, 많은 사람이 인증과 승인을 오해하고 있기 때문에 이들 차이점을 설명하려 한다.

인증

인증은 어떤 주체가 사실이라고 주장하는 어떤 것이 진실한지를 확인하는 행위다. 풀어쓰자면 인증이란 주장을 하는 사람이 누구인지를 확인하는 것이다. API에서는 자격증명셋(토큰 형식)을 전달하여 확인 할 수 있다.

예를 들어 여권은 여행시 여행자를 인증하는 수단이다.

 인증

이 문서에서는 토큰을 생성하는 프로세스를 자세히 다루지는 않을 것이다. 토큰을 만들기 위해서는 자격 증명(예. 사용자 이름 과 암호) 집합을 교환하는 로그인 프로세스가 필요하다. 자격증명에 성공하면 JWT(JSON Web Tokens)를 생성한다. 이러한 종류의 토큰은 사용자에 대한 정보를 포함하기 때문에(JSON을 Base64 인코딩) 유용하게 사용할 수 있다. 위조를 방지하기 위해서 개인키로 서명하고 공개키로 유효성을 확인 할 수 있다.

권한

인증은 사용자의 신원을 확인 할 수 있지만, 해당 사용자가 어떤 권한을 가지고 있는지를 확인 할 수는 없다. 예를 들어 접근해서는 안되는 민감한 데이터나, 개인 데이터의 접근을 방지하는 것은 아니다. 인증된 사용자라고 하더라도 개인으로 인증 것과 관리자로 인증된 것은 서로 다르다.

 VISA

정확한 인증을 받았다고 하더라도 비자가 없이는 방문할 권한이 없을 수 있다.

Authorization Logic을 어디에 넣을까

모노리딕 애플리케이션의 경우 유연함은 떨어지지만 인증/권한 서비스를 중앙에서 통합 할 수 있기 때문에 구현에 큰 어려움은 없다. 반면 MSA 애플리케이션의 경우 서비스들이 분리가 되기 때문에 인증/권한과 같은 중앙에서 관리해야 하는 서비스들의 구현에 어려움이 있을 수 있다. 인증과 권한은 "데이터"에 대한 것인데, MSA에서는 서비스가 분리되고 각 서비스마다 데이터를 가지고 있기 때문에 원칙적으로는 각 서비스마다 인증/권한 서비스를 개발해야 한다. 이렇게 할 경우 인증/권한 정책이 변경될 경우 관련된 서비스들의 코드를 모두 바꿔야 하는 어려움이 있다.

MSA에서 이런 문제는 API GateWay 패턴을 이용해서 해결한다. API GateWay는 클라이언트와 서비스를 중계하는 단일 지점이 되기 때문에, 이 단일 지점에 인증/권한과 같은 중앙 집중 적인 서비스를 수행하게 하는 것이다. 아래 그림은 API Gateway 패턴을 보여주고 있다.

 API Gateway 패턴

API Gateway는 Reverse Proxy 형태로 작동하기 때문에 NginX, HAProxy, Kong, Traefik 등을 사용해서 구축한다. 이 문서는 Traefik의 AuthForward 기능을 이용해서 구축할 것이다. 이 구성은 아래와 같다.

 AuthServer와 Traefik

API 요청/처리 흐름을 아래와 같다. 모든 API 요청은 먼저 인증서버로 전달된다. 인증서버가 200 OK를 리턴하면 액세스가 허용되고 클라이언트 요청이 처리된다. 그렇지 않으면 401 에러가 리턴된다.

 Traefik AuthForward Process

Traefik forwardauth

Traefik forwardauth기능을 이용해서 클라이언트 요청을 외부에 있는 인증서버로 보낼 수 있다. 테스트 구조는 아래와 같다.

 Test 구조

  1. Client는 whoami 서비스를 사용하려 한다.
  2. Client 요청은 Traefik이 먼저 받는다.
  3. Traefik는 Auth Middleware인 go-auth 서버로 요청을 전송한다.
  4. 인증이 성공하면 200 OK가 리턴된다. 인증이 실패하면 401 코드가 리턴된다.

인증서버 개발

Traefik forwardauth 기능 테스트를 위해서 간단한 Go 코드를 만들었다.
package main

import (
	"log"
	"net/http"
)

func main() {

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		name := r.Header.Get("X-AUTH")
		if name != "success" {
			w.WriteHeader(http.StatusUnauthorized)
		}
	})
	http.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	})
	if err := http.ListenAndServe(":8888", nil); err != nil {
		log.Fatal(err)
	}
}
테스트를 용이하게 하기 위해서 클라이언트 요청 헤더의 "X-AUTH"의 값이 "success" 이면 200 OK를 아니면 401을 반환하게 했다. 빌드 후 테스트해보자.
# go build -o go-auth
# ./go-auth
curl로 테스트했다.
# curl -i -H "X-AUTH: yundream" localhost:8888   
HTTP/1.1 401 Unauthorized
Date: Sun, 20 Dec 2020 15:41:25 GMT
Content-Length: 0

# curl -i -H "X-AUTH: success" localhost:8888 
HTTP/1.1 200 OK
Date: Sun, 20 Dec 2020 15:41:32 GMT
Content-Length: 0

테스트는 Docker로 진행 할 것이다. Dockerfile을 만들었다.
FROM golang:1.12

COPY go-auth /opt/joinc/go-auth
EXPOSE 8888 
CMD ["/opt/joinc/go-auth"]
Docker 이미지를 만들자.
# docker build . -t joinc/go-auth:1.0  
# docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
joinc/go-auth             1.0                 6805c4bb8ab6        13 minutes ago      816MB

Traefik Docker-Compose 생성

Docker Compose 파일을 만들었다.
version: '3'

services:
  traefik:
    image: traefik:v2.2
    command: --providers.docker
    ports:
      - "8085:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  go-auth:
    image: joinc/go-auth:1.0
    environment:
      - PROVIDERS_GOOGLE_CLIENT_ID=your-client-id
      - PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret
      - SECRET=something-random
      - INSECURE_COOKIE=true # Example assumes no https, do not use in production
    labels:
      - "traefik.http.middlewares.go-auth.forwardauth.address=http://go-auth:8888"
      - "traefik.http.middlewares.go-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
      - "traefik.http.services.go-auth.loadbalancer.server.port=8888"

  whoami:
    image: containous/whoami
    labels:
      - "traefik.http.routers.whoami.rule=Host(`whoami.mycompany.com`)"
      - "traefik.http.routers.whoami.middlewares=go-auth"
  • 처음 계획대로 traefik, go-auth middleware, whoami service 로 구성된다.
  • traefik의 서비스포트는 8085다.
  • go-auth.environment는 신경 쓸 필요 없다. Google 인증을 붙여볼 목적으로 만들었는데, 지금은 하지 않을 거라서.
  • go-auth.labels를 보자. forwardauth.address를 go-auth:8888로 설정했다.
서비스를 실행해보자.
# docker-compose up                                             
Creating network "docker_default" with the default driver
Creating docker_go-auth_1 ... done
Creating docker_traefik_1 ... done
Creating docker_whoami_1  ... done
Attaching to docker_traefik_1, docker_go-auth_1, docker_whoami_1
traefik_1  | time="2020-12-20T15:53:00Z" level=info msg="Configuration loaded from flags."
whoami_1   | Starting up on port 80

테스트를 진행했다.
# curl -i -H "Host:whoami.mycompany.com" -H "X-AUTH: fail" localhost:8085   
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Sun, 20 Dec 2020 15:54:57 GMT

# curl -i -H "Host:whoami.mycompany.com" -H "X-AUTH: success" localhost:8085 
HTTP/1.1 200 OK
Content-Length: 378
Content-Type: text/plain; charset=utf-8
Date: Sun, 20 Dec 2020 15:55:37 GMT

Hostname: 6f2bffa1c178
IP: 127.0.0.1
IP: 172.27.0.3
RemoteAddr: 172.27.0.2:44238
GET / HTTP/1.1
Host: whoami.mycompany.com
User-Agent: curl/7.68.0
Accept: */*
Accept-Encoding: gzip
X-Auth: success
X-Forwarded-For: 172.27.0.1
X-Forwarded-Host: whoami.mycompany.com
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: e3687d555261
X-Real-Ip: 172.27.0.1
테스트 성공이다. 이제 실제 로그인 작업을 수행하도록 코드를 보강하면 된다.

참고