Lumen의 Auth·Guard·User 인증 상세 해설 (3) — 흐름이 전부 풀렸다! (상)

Wake
By Wake2019년 8월 8일14분 읽기
Lumen의 Auth·Guard·User 인증 상세 해설 (3) — 흐름이 전부 풀렸다! (상)

큰 프레임워크나 시스템에 비하면, 저는 작고 가벼운, 갈아 끼우기 쉬운 패키지 쪽을 더 좋아해요. Idiorm & ParisPhinx 같은 것들요. 우선 구조가 이해하기 쉽고, 추상화 계층이 적어요. 그리고 프로젝트 입장에서는 각 기능의 결합도도 낮아져서, 필요할 때 그대로 갈아 끼우거나 조정할 수 있고요(예를 들어 어떤 패키지가 더 이상 유지보수되지 않을 때요).

물론 단점도 있어요. 뭐든 직접 손으로 이어 붙여야 하고, 독립된 패키지는 코어 시스템과 궁합이 그렇게 좋지 않거나, 패키지끼리 부딪히기도 해요(예를 들어 공통 의존성을 서로 다른 버전으로 요구하는 경우요). 게다가 하나의 프로젝트 안에 여러 설계 사상이 뒤섞여서, 하나씩 이해해 나가야 하죠.

그래도 요즘 주요 프레임워크의 설계 사상은 크게 참고할 만한 가치가 있어요. 시스템이 어느 규모까지 자라면, 아키텍처의 추상화나 인터페이스화로 정리하고 유연성을 높이는 게 아무래도 필요해져요. 이건 피할 수 없는 발전의 길이고요.

Auth·Guard·User, 실제 흐름

본론으로 돌아갈게요. 여기서부터는 인증의 전 과정을 한 번 찬찬히 따라가 볼게요. 그림에는 Lumen과 AuthServiceProvider 두 개를 덧붙여서, 연결 관계를 보기 쉽게 했어요.

Lumen → ServiceProvider → AuthManager

FullBasicFlow

Lumen에서 인증 기능을 켜려면, 먼저 AuthServiceProviderLumen Application —— 즉 $app —— 에 등록해야 해요. 여기가 흐름의 출발점이기도 하고요:

$app->register(\App\Providers\AuthServiceProvider::class);

AuthServiceProvider는 관련 서비스를 어떻게 시스템에 엮고 있을까요? 소스를 보면, boot 메서드에 이렇게 돼 있어요:

$this->app['auth']->viaRequest('api', function ($request) {
  ...
});

먼저, 여기서 $this->app['auth']를 가져오려고 해요. app['auth']는 원래는 존재하지 않는 객체인데, 접근된 걸 계기로 AuthManager가 인스턴스화돼요. 그런데 왜 Illuminate\Auth\AuthManager일까요? 이건 코어의 Illuminate\Auth\AuthServiceProvider가 지정하고 있는 거예요. 구체적인 흐름은 다음과 같아요:

  1. 코드가 app['auth']에 접근한다.
  2. appauth가 이미 만들어졌는지 스스로 확인하고, 아직이면 인스턴스화한다.
  3. Lumen Application —— Laravel\Lumen\Application —— 이 auth를 특별한 키워드로 바인딩하고 있어서, auth를 인스턴스화하려고 하면 registerAuthBindings 함수가 호출된다.
  4. registerAuthBindings 함수가 코어 서비스인 Illuminate\Auth\AuthServiceProvider를 등록한다.
  5. Illuminate\Auth\AuthServiceProvider가 그 register 메서드 안에서 싱글턴 패턴 app->singleton을 써서 Illuminate\Auth\AuthManager 클래스를 인스턴스화하고 app['auth']에 할당한다.

그러니까, app['auth']를 호출해서 조금 손을 대는 정도라, App\Providers\AuthServiceProvider 자체가 특별히 뭔가 서비스를 읽어 들이는 건 아니에요. 처리의 대부분은 코어의 auth 관련 바인딩이 맡고 있어요.

뒤쪽의 app['auth']->viaRequest라는 코드는 Guard와 관련 있어서, 다음 절에서 한꺼번에 설명할게요.

AuthManager → XXX (Guard) → GuardDriver → Guard

FullBasicFlow

그럼 AuthManager는 어떻게 Guard와 주고받을까요? Lumen에는 세 종류의 Guard 클래스가 내장돼 있어요:

  • RequestGuard: 요청을 기반으로 한, 확장용 인증 모드. 객체 생성 시에 callback 함수를 넘겨야 해요. 이 callback은 $requestuserProvider 두 개를 받고, 쓸 수 있는 사용자 객체를 반환하도록 요구돼요(인증 결과만 반환하는 게 아니에요).
  • TokenGuard: 마찬가지로 요청을 기반으로, 넘어온 내용으로 인증하는 모드. 예를 들어 header에 특정 token이나 password가 들어 있는지 같은 거요.
  • SessionGuard: Session(과 Cookie)의 데이터를 바탕으로 인증하는 모드. 보통 PHP 프로그램에서 사용자 로그인을 기록·식별할 때 쓰는, 바로 그 방식이에요.

app['auth'] —— AuthManager가 인스턴스화되면, 그걸 통해 위의 Guard 객체(또는 직접 만든 Guard)를 가져올 수 있어요:

  1. app['auth']->guard('XXX')를 호출한다. XXX라는 이름의 Guard 객체가 이미 있으면 그걸 반환하고, 없으면 Guard 객체를 만들 준비에 들어간다. 이름을 넘기지 않으면 config/auth.phpdefaults.guard가 자동으로 쓰이는 점에 주의한다.
  2. Guard 객체를 만들기 전에, config/auth.phpguards에 XXX Guard 설정이 있는지 먼저 확인한다. 없으면 에러를 던진다.
  3. XXX Guard의 설정을 종합해서, 이 Guard에 다음이 있는지 순서대로 확인한다:
    • 커스텀 callback 생성 함수: 코드 안에서 app['auth']->extend()를 써서 callback으로 정의한 생성 함수. 이 정의의 이름을 설정 파일에도 설정해 둬야 한다.
    • AuthManager 안의 드라이버 메서드:
      • AuthManager를 직접 확장했다면, createXXXDriver라는 메서드를 정의해서 XXX Guard를 움직일 수 있다.
      • 내장 Guard의 기본 드라이버 메서드도 같은 방식이라, 예를 들어 SessionGuardcreateSessionDriver, TokenGuard는 createTokenDriver다.
  4. 생성 함수/드라이버 메서드를 호출해서 대상 Guard를 인스턴스화하고, 그 객체를 XXX를 키로 해서 AuthManager 자신의 guards array에 넣는다.
  5. Guard 객체를 반환한다.

이 흐름에서 알 수 있듯이, 직접 Guard를 정의하고 싶다면 가장 간단한 건 app['auth']->extend()로 Guard를 생성하는 함수를 정의하고, config/auth.php에 설정을 써 두는 거예요.

app['auth']->viaRequest로 돌아가면, viaRequest는 사실 RequestGuard를 기반으로 한 extend()를 응용한 거예요. 쉽게 말하면, RequestGuard 객체를 만들어 주고, user callback을 넘기기 위한 함수를 준비해 주는 거죠. 이 두 코드를 봐 볼게요:

AuthServiceProviderboot 메서드:

$this->app['auth']->viaRequest('api', function ($request) {
  if ($request->input('api_token')) {
    return User::where('api_token', $request->input('api_token'))->first();
  }
});

AuthManagerviaRequest:

public function viaRequest($driver, callable $callback) {
  return $this->extend($driver, function () use ($callback) {

    $guard = new RequestGuard($callback, $this->app['request'], $this->createUserProvider());

    $this->app->refresh('request', $guard, 'setRequest');

    return $guard;
  });
}

AuthServiceProvider가 시작되면, viaRequest()extend()를 통해 api라는 이름의 생성/드라이버 메서드를 정의하고, 요청의 api_token을 데이터베이스와 대조해 사용자를 가져오는 callback을 RequestGuard의 생성자 인수로 넘겨요. 그다음 app['auth']->guard('api')가 호출됐을 때 인스턴스화되고, callback에 따라 사용자 객체를 반환해요.

맺음말

AuthManager는 인증 아키텍처 안에서 가장 역할이 복잡한 객체일 거예요. 원래 기능에 더해, __call을 써서 Guard로의 동적 호출도 구현하고 있어요. 즉, app['auth']->guard()->guardFunc() 대신 app['auth']->guardFunc()라고 바로 쓸 수 있다는 거죠. 이게 정말 아키텍처를 따라가기 어렵게 만들어요…

이 시리즈, 쓰면 쓸수록 늘어나는 것 같아요. 다음 글에서는 AuthManager와 UserProvider의 관계에 조금 더 파고들어 볼게요. 읽어 주셔서 고맙습니다. 다음에 또 봬요.

분류:
Wake

Wake

글쓴이 소개

취미 삼아 프론트엔드와 백엔드 기술에 조금 발을 담근 엔지니어예요. 가장 좋아하는 프로그래밍 말고도, 배드민턴, 보드게임, 독서, 요리, 피아노도 좋아해요. 요즘은 AI도 적극적으로 쓰고 있어요.