티스토리 뷰

728x90

1. 이 포스트는 Email Client를 작성하는 시리즈의 일부이다

  1-1 지금까지의 만든 프로그램은 로그인과 가입하기가 가능하다.

  1-2 하지만 로그인 후에 리프레쉬를 하면 다시 로그아웃이 된다.

  1-3 이것은 세션관리가 안되어서 그런 건데 로그인 성공 후에 헤더를 보면 Response에 쿠키에 값이 있다

    1-3-1 두개의 쿠키에 세션값이 들어있는 것을 확인 할 수 있다.

    1-3-2 이 쿠키를 서버 요청에 붙여 주어야 서버는 로그인 한 사용자를 식별할 수 있다. 

    1-3-3 사용자가 요청을 보낼 때마다 쿠키를 보내야 세션을 유지할 수 있다

  1-4 이 쿠키는 브라우저에서 자동으로 헤더에 붙여서 전송하게 된다.

 

 

  1.5 하지만 Angular는 이 쿠키를 자동으로 붙여주지 않고 option으로 

 

2. 이 문제를 해결하기 가장 쉬운 방법은 모든 요청에 옵션으로 withCredentials에 true를 붙여주는 것이다.

  2-1 아래를 보면 인증관련 요청에 추가적으로 withCredential이 붙어 있다. 

  2-2 이렇게 하면 문제가 해결된다하고 끝내면 좋은데 이게 생각보다 귀찮아서 그냥 intercept로 일괄처리하는 게 좋다.

  2-3 intercept는 node에서 말하는 mediator 개념하고 같다고 보면 된다.

 

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators'
import { AuthCredential, LoginCredential } from './auth-responses';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  signedIn$ = new BehaviorSubject<boolean>(null)
  username: string = ''

  url = 'https://api.angular-email.com/auth'
  constructor(private http: HttpClient) { }

  signedIn(): Observable<SignedInResponse> {
    return this.http.get<SignedInResponse>(`${this.url}/signedIn`, {
      withCredentials: true
    }).pipe(
      tap((response: SignedInResponse)=> {
        this.username = response.username
        this.signedIn$.next(response.authenticated)
      })
    )
  }

  signUp(crediential: AuthCredential): Observable<SignUpResponse> {
    return this.http.post<SignUpResponse>(`${this.url}/signup`, crediential, {
      withCredentials: true
    }).pipe(
      tap(({username})=> {
        this.username = username
        this.signedIn$.next(true)
      })
    )
  }

  signIn(crediential: LoginCredential): Observable<any> {
    return this.http.post(`${this.url}/signin`, crediential, {
      withCredentials: true
    }).pipe(
      tap(({username}) => {
        this.username = username
        this.signedIn$.next(true)
      })
    )
  }

  checkDuplication(username: string) {
    return this.http.post(`${this.url}/username`, { username })
  }
}

interface SignedInResponse {
  authenticated: boolean,
  username: string
}

interface SignUpResponse {
  username: string
}

 

3. 인터셉터를 작성한다. 

  3-0 우선 auth 모듈에 interceptor을 생성한다.

    3-0-1 인터셉터의 용도는

      3-0-1-1 url 변경 - http를 https로 변경하는 등등

      3-0-1-2 Loader 생성 - 사용자가 결과를 기다리는 동안 모래시계나 로딩바 같은 것을 보여 줄 수 있다.

      3-0-1-3 포멧전환 - json을 xml로 변경한다든지 하는 기능도 가능하다.

      3-0-1-4 헤더추가 - 우리가 지금 하고 있는 거다.

      3-0-1-5 알림 - 결과를 중간 중간 마다 알려줄 수 있으므로 어떻게 돌아가는지 팝업을 통해 알려줄 수도 있다.

      3-0-1-6 에러 - 에러 발생 시 다시 request를 보내거나 체증이 있는 곳에 흐름 제어도 할 수 있다.

      3-0-1-7 로깅 - 로그를 남기거나 메소드 수행시간을 측정하여 성능평가에도 사용할 수 있다.

      3-0-1-8 가짜 서버 - 서버기능을 흉내내어 테스트를 수행할 수 있다.

      3-0-1-9 캐싱 - 캐싱처리로직을 넣어 이미 있는 정보는 request대신 바로 반환 할 수 있다.

      3-0-1-10 인증 - 이것도 정말 많이 쓰인다. token을 저장하여 인증을 자동화하는 기능을 예를 들 수 있다.

 

 

 

  3-1 단순히 모든 request에 쿠키정보를 추가해주는 기능을 한다.

  3-2 인터셉터를 작성하려면 @Injection을 수동으로 관리해야 한다.

  3-3 AuthInterceptor 코드

    3-3-1 기본 템플릿을 보면 단순히 request를 가로채어 추가기능을 수행한 후 다시 절차로 밀어넣는다.

    3-3-2 next는 다음 intercept라고 보면 되고 next.handle로 전달한다. intercept가 더 없으면 네트워크로 전송한다.

 

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor() {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const clonedRequest = request.clone({
      withCredentials: true
    })
    return next.handle(clonedRequest);
  }
}

 

  3-4 문제는 이 코드를 어디에다가 넣는다인데, @Injectable이라서 자동으로 생성되어야 하는데

    3-4-1 서비스처럼 어디에서 제공하는지 출처가 없다.

    3-4-2 출처가 없으면 provider를 통해서 생성요청을 해야 한다. 그리고 별 말이 없으면 app.module이 정답이다.

      3-4-2-1 물론 auth 모듈에 지정할 수 있는데, 그러면 auth모듈에 한정된다. app.module에 정의하면 범용이다.

더보기
더보기

A lazy-loaded module has its own injector, typically a child of the app root injector. Lazy-loaded services are scoped to the lazy-loaded module's injector. If a lazy-loaded module also provides the UserService, any component created within that module's context (such as by router navigation) gets the local instance of the service, not the instance in the root injector. Components in external modules continue to receive the instance provided by their injectors.

 

    3-4-3 app.module.ts 파일이다.

      3-4-3-1 아래 코드는 AuthInterceptor를 HTTP_INTERCEPTOR라는 이름으로 등록하고 있다.

      3-4-3-2 HTTP_INTERCEPTOR는 HttpInterceptor를 implement 한 클래스타입의 alias이고

        3-4-3-2-1 Angular에서 저정된 타입이다. 이 형식을 요청이 들어오면 AuthInterptor가 실행된다는 의미다.

        3-4-3-2-2 같은 이름으로 여러개 등록도 가능하고 등록한 순서대로 실행된다.

      3-4-3-1 일반적으로는 그냥 provider 없이 클래스 이름만 등록하는데 그러면 클래스 이름으로 등록이 된다.

        3-4-3-1-1 서비스가 사용하는 방식이 이 방식이다.

 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HomeComponent } from './home/home.component';
import { NotFoundComponent } from './not-found/not-found.component';
import { AuthInterceptor } from './auth/auth.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    NgbModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

    3-4-4 이제 auth 서비스에 withCredentials는 다 지워야 한다.

 

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators'
import { AuthCredential, LoginCredential } from './auth-responses';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  signedIn$ = new BehaviorSubject<boolean>(null)
  username: string = ''

  url = 'https://api.angular-email.com/auth'
  constructor(private http: HttpClient) { }

  signedIn(): Observable<SignedInResponse> {
    return this.http.get<SignedInResponse>(`${this.url}/signedIn`).pipe(
      tap((response: SignedInResponse)=> {
        this.username = response.username
        this.signedIn$.next(response.authenticated)
      })
    )
  }

  signUp(crediential: AuthCredential): Observable<SignUpResponse> {
    return this.http.post<SignUpResponse>(`${this.url}/signup`, crediential).pipe(
      tap(({username})=> {
        this.username = username
        this.signedIn$.next(true)
      })
    )
  }

  signIn(crediential: LoginCredential): Observable<any> {
    return this.http.post(`${this.url}/signin`, crediential).pipe(
      tap(({username}) => {
        this.username = username
        this.signedIn$.next(true)
      })
    )
  }

  checkDuplication(username: string) {
    return this.http.post(`${this.url}/username`, { username })
  }
}

interface SignedInResponse {
  authenticated: boolean,
  username: string
}

interface SignUpResponse {
  username: string
}

 

4. 이제는 리프레쉬를 해도 로그인 상태가 유지된다.

728x90
댓글