Authentication

Authentication

인증은 대부분의 응용 프로그램에서 필수적인 부분입니다. 인증을 처리하기위한 다양한 접근 방식과 전략이 있습니다. 모든 프로젝트에 대한 접근 방식은 특정 응용 프로그램 요구 사항에 따라 다릅니다. 이 장에서는 다양한 요구 사항에 맞출 수있는 인증에 대한 여러 가지 접근 방식을 제시합니다.

Passport는 가장 널리 사용되는 node.js 인증 라이브러리이며 커뮤니티에서 잘 알려져 있으며 많은 프로덕션 응용 프로그램에서 성공적으로 사용됩니다. 이 라이브러리를 @nestjs/passport 모듈을 사용하여 Nest 애플리케이션과 통합하는 것은 간단합니다. 높은 수준에서 Passport는 다음과 같은 일련의 단계를 수행합니다.

  • "신임 정보(credentials)"(예 : 사용자 이름(username)/비밀번호(password), JSON 웹 토큰 (JWT) 또는 자격 증명 공급자의 자격 증명 토큰)를 확인하여 사용자를 인증합니다.

  • 인증 된 상태 관리 (JWT와 같은 휴대용 토큰을 발행하거나 Express session 생성)

  • 라우트 핸들러에서 추가로 사용하기 위해 인증된 사용자에 대한 정보를 Request 오브젝트에 첨부하십시오.

Passport에는 다양한 인증 메커니즘을 구현하는 풍부한 전략 생태계가 있습니다. 개념은 간단하지만 선택할 수 있는 Passport 전략 세트는 크기가 크며 다양한 형태를 제공합니다. Passport는 이러한 다양한 단계를 표준 패턴으로 추상화하고, @nestjs/passport 모듈은 이 패턴을 친숙한 Nest 구성으로 랩핑하고 표준화합니다.

이 장에서는 이러한 강력하고 유연한 모듈을 사용하여 RESTful API 서버를 위한 완벽한 엔드 투 엔드 인증 솔루션을 구현합니다. 여기에 설명된 개념을 사용하여 Passport 전략을 구현하여 인증 체계를 사용자 정의할 수 있습니다. 이 장의 단계에 따라 이 완전한 예제를 빌드할 수 있습니다. 완성된 샘플 앱이 있는 저장소를 찾을 수 있습니다.

Authentication Requirements

우리의 요구 사항을 해결합시다. 이 사용 사례의 경우 클라이언트는 사용자 이름과 비밀번호로 인증하여 시작합니다. 인증되면 서버는 인증 요청을 위해 후속 요청에서 권한 헤더의 베어러 토큰으로 보낼 수있는 JWT를 발행합니다. 또한 유효한 JWT가 포함된 요청에만 액세스할 수 있는 보호된 경로를 만듭니다.

첫 번째 요구 사항인 사용자 인증부터 시작하겠습니다. 그런 다음 JWT를 발행하여 이를 확장할 것입니다. 마지막으로 요청에서 유효한 JWT를 확인하는 보호된 경로를 만듭니다.

먼저 필요한 패키지를 설치해야합니다. Passport는 passport-local이라는 전략을 제공하여 사용 사례의 이 부분에 대한 사용자 이름/비밀번호 인증 메커니즘을 구현합니다.

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local

Warning 알림 어떤 패스포트 전략을 선택하려면 항상 @nestjs/passportpassport 패키지가 필요합니다. 그런 다음 구축중인 특정 인증 전략을 구현하는 전략 별 패키지 (예: passport-jwt 또는 passport-local)를 설치해야 합니다. 또한 위의 @types/passport-local과 같이 패스포트 전략에 대한 유형 정의를 설치하여 TypeScript 코드를 작성하는 동안 도움을 줄 수 있습니다.

Implementing Passport strategies

이제 인증 기능을 구현할 준비가되었습니다. 먼저 Passport 전략에 사용되는 프로세스에 대한 개요부터 시작하겠습니다. Passport 자체를 미니 프레임 워크로 생각하면 도움이 됩니다. 프레임 워크의 우아함은 인증 프로세스를 구현하는 전략에 따라 사용자 정의하는 몇가지 기본 단계로 추상화한다는 것입니다. 패스포트 함수가 적절한 시간에 호출하는 콜백 함수 형태로 사용자 정의 매개 변수 (일반 JSON 오브젝트로) 및 사용자 정의 코드를 제공하여 구성하므로 프레임 워크와 같습니다. @nestjs/passport 모듈은이 프레임 워크를 Nest 스타일 패키지로 포장하여 Nest 애플리케이션에 쉽게 통합할 수 있습니다. 아래에 @nestjs/passport를 사용하지만 먼저 vanilla Passport의 작동 방식을 고려해 보겠습니다.

바닐라 패스포트에서는 다음 두가지를 제공하여 전략을 구성합니다.

  1. 해당 전략에 특정한 옵션 세트. 예를 들어, JWT 전략에서 토큰에 서명하는 비밀을 제공할 수 있습니다.

  2. "콜백 확인": Passport에서 사용자 계정을 관리하는 사용자 저장소와 상호 작용하는 방법을 알려줍니다. 여기서 사용자가 존재하는지 (및/또는 새 사용자를 작성), 신임 정보가 유효한지 검증합니다. Passport 라이브러리는 유효성 검사에 성공하면 이 콜백이 전체 사용자를 반환하거나 실패하면 null을 반환합니다 (실패는 사용자를 찾을 수 없거나 Passport 로컬의 경우 비밀번호가 일치하지 않는 것으로 정의됨) .

@nestjs/passport를 사용하면 PassportStrategy 클래스를 확장하여 Passport 전략을 구성합니다. 서브 클래스에서 super()메소드를 호출하고 선택적으로 옵션 객체를 전달하여 전략 옵션 (위의 항목 1)을 전달합니다. 서브 클래스에 validate()메소드를 구현하여 verify 콜백 (위의 항목 2)을 제공합니다.

먼저 AuthModuleAuthService를 생성하는 것으로 시작하겠습니다.

$ nest g module auth
$ nest g service auth

AuthService를 구현할 때 사용자 작업을 UsersService에 캡슐화하는 것이 유용하다는 것을 알게되었으므로 이제 해당 모듈과 서비스를 생성하겠습니다.

$ nest g module users
$ nest g service users

아래와 같이 생성된 파일의 기본 내용을 바꾸십시오. 샘플 앱의 경우, UsersService는 하드 코딩된 인 메모리 사용자 목록과 사용자 이름으로 검색하는 find 메소드를 유지합니다. 실제 앱에서는 선택한 라이브러리 (예: TypeORM, Sequelize, Mongoose 등)를 사용하여 사용자 모델 및 지속성 계층을 구축 할 수 있습니다.

@@filename(src/users/users.service)
import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {
  private readonly users: User[];

  constructor() {
    this.users = [
      {
        userId: 1,
        username: 'john',
        password: 'changeme',
      },
      {
        userId: 2,
        username: 'chris',
        password: 'secret',
      },
      {
        userId: 3,
        username: 'maria',
        password: 'guess',
      },
    ];
  }

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }
}
@@switch
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor() {
    this.users = [
      {
        userId: 1,
        username: 'john',
        password: 'changeme',
      },
      {
        userId: 2,
        username: 'chris',
        password: 'secret',
      },
      {
        userId: 3,
        username: 'maria',
        password: 'guess',
      },
    ];
  }

  async findOne(username) {
    return this.users.find(user => user.username === username);
  }
}

UsersModule에서 필요한 유일한 변경은 UsersService@Module 데코레이터의 exports 배열에 추가하여 이 모듈 외부에서 볼 수 있도록 하는 것입니다 (우리는 곧 AuthService에서 사용할 것입니다).

@@filename(src/users/users.module)
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
@@switch
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

AuthService에는 사용자를 검색하고 비밀번호를 확인하는 작업이 있습니다. 이를 위해 validateUser()메소드를 만듭니다. 아래 코드에서는 편리한 ES6 스프레드 연산자를 사용하여 비밀번호 속성을 사용자 객체에서 반환하기 전에 제거합니다. 우리는 Passport 로컬 전략에서 validateUser()메소드를 곧 호출할 것입니다.

@@filename(src/auth/auth.service)
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
@Dependencies(UsersService)
export class AuthService {
  constructor(usersService) {
    this.usersService = usersService;
  }

  async validateUser(username, pass) {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

Warning 경고 물론 실제 응용 프로그램에서는 비밀번호를 일반 텍스트로 저장하지 않습니다. 대신 Salt된 단방향 해시 알고리즘과 함께 bcrypt와 같은 라이브러리를 사용합니다. 이 방법을 사용하면 해시된 비밀번호만 저장한 다음 저장된 비밀번호를 입력된 비밀번호의 해시된 버전과 비교하여 사용자 비밀번호를 일반 텍스트로 저장하거나 노출하지 않습니다. 샘플 앱을 단순하게 유지하기 위해 우리는 절대적인 의무를 위반하고 일반 텍스트를 사용합니다. 실제 앱에서는이 작업을 수행하지 마십시오!

이제 AuthModule을 업데이트하여 UsersModule을 가져옵니다.

@@filename(src/auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})
export class AuthModule {}

Implementing Passport local

이제 Passport 로컬 인증 전략을 구현할 수 있습니다. auth 폴더에 local.strategy.ts라는 파일을 만들고 다음 코드를 추가하십시오.

@@filename(src/auth/local.strategy)
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
@@switch
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException, Dependencies } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
@Dependencies(AuthService)
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(authService) {
    this.authService = authService
    super();
  }

  async validate(username, password) {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

우리는 모든 패스포트 전략에 대해 앞에서 설명한 레시피를 따랐습니다. Passport-local의 유스 케이스에는 구성 옵션이 없으므로 생성자가 옵션 객체없이 super()를 호출하기만하면 됩니다.

우리는 또한 validate()메소드를 구현했습니다. 각 전략에 대해 Passport는 적절한 전략별 매개 변수 세트를 사용하여 verify 함수 (@nestjs/passportvalidate()메소드로 구현)를 호출합니다. 로컬 전략의 경우 Passport는 validate(username: string, password: string): any서명이 있는 validate()메소드를 예상합니다.

대부분의 검증 작업은 AuthService (UserService의 도움으로)에서 수행되므로 이 방법은 매우 간단합니다. any Passport 전략에 대한 validate()메소드는 자격 증명이 표시되는 방법에 대한 세부 사항만 다를뿐 유사한 패턴을 따릅니다. 사용자가 발견되고 자격 증명이 유효한 경우 Passport가 해당 작업을 완료할 수 있도록 (예:Request 객체에서 user 속성 작성) 요청 처리 파이프 라인을 계속할 수 있도록 사용자가 반환됩니다. 발견되지 않으면 예외가 발생하여 예외 계층에서 처리하도록 합니다.

일반적으로 각 전략에 대한 validate() 메소드의 중요한 차이점은 사용자가 존재하고 유효한지 여부를 결정하는 방법입니다. 예를 들어, JWT 전략에서 요구 사항에 따라 디코딩 된 토큰에 포함 된 userId가 사용자 데이터베이스의 레코드와 일치하는지 또는 해지된 토큰 목록과 일치하는지 평가할 수 있습니다. 따라서 이 하위 분류 및 전략 별 유효성 검사 패턴은 일관되고 우아하며 확장 가능합니다.

방금 정의한 Passport 기능을 사용하려면 AuthModule을 구성해야합니다. auth.module.ts를 다음과 같이 업데이트하십시오 :

@@filename(src/auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

Built-in Passport Guards

가드 챕터는 Guards의 주요 기능을 설명합니다: 요청이 라우트 핸들러에 의해 처리 될지 여부를 결정합니다. 그것은 사실이며, 우리는 곧 그 표준 기능을 사용할 것입니다. 그러나 @nestjs/passport모듈을 사용하는 상황에서 처음에는 혼란스러울 수 있는 약간의 새로운 주름이 생길 것이므로 지금 논의하겠습니다. 앱은 인증 관점에서 두 가지 상태로 존재할 수 있습니다.

  1. 사용자/클라이언트가 로그인되어 있지 않음 (인증되지 않음)

  2. 사용자/클라이언트 로그인 (인증 됨)

첫 번째 경우 (사용자가 로그인하지 않은 경우) 두 가지 고유 한 기능을 수행해야합니다.

  • 인증되지 않은 사용자가 액세스할 수 있는 경로를 제한합니다 (예: 제한된 경로에 대한 액세스 거부). 보호된 경로에 보호대를 배치하여 이 기능을 처리 할 수 있는 친숙한 기능으로 Guards를 사용합니다. 예상한 대로 이 Guard에서 유효한 JWT가 있는지 확인하므로 JWT를 성공적으로 발급한 후에 나중에 이 Guard에서 작업할 것입니다.

  • 이전에 인증되지 않은 사용자가 로그인을 시도 할 때 인증 단계 자체를 시작하십시오. 이것은 유효한 사용자에게 JWT를 발행하는 단계입니다. 이에 대해 잠시 생각하면 인증을 시작하기 위해 사용자 이름/비밀번호 자격 증명이 필요하다는 것을 알고 있으므로 이를 처리하기 위해 POST /api/login 경로를 설정합니다. 이것은 우리가 그 노선에서 패스포크 로컬 전략을 정확히 어떻게 호출 하는가 하는 의문을 제기합니다.

대답은 간단합니다. 약간 다른 유형의 Guard를 사용하면됩니다. @nestjs/passport 모듈은 이를 위해 내장 Guard를 제공합니다. 이 Guard는 Passport 전략을 실행하고 위에서 설명한 단계 (신임 정보 검색, 확인 기능 실행, user 특성 작성 등)를 시작합니다.

위에 나열된 두 번째 사례 (사용자 로그인)는 로그인한 사용자가 보호된 경로에 액세스할 수 있도록 이미 논의한 표준 Guard 유형에 의존합니다.

Login route

이 전략을 적용하여 이제 베어 본 /api/login경로를 구현하고 내장 가드를 적용하여 패스포트 로컬 흐름을 시작할 수 있습니다.

app.controller.ts 파일을 열고 그 내용을 다음과 같이 바꾸십시오 :

@@filename(src/app.controller)
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('api')
export class AppController {
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return req.user;
  }
}
@@switch
import { Controller, Bind, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('api')
export class AppController {
  @UseGuards(AuthGuard('local'))
  @Post('login')
  @Bind(Req())
  async login(req) {
    return req.user;
  }
}

@UseGuards(AuthGuard('local'))를 사용하면 패스포트 로컬 전략을 확장할 때 @nestjs/passport자동으로 프로비저닝 된 AuthGuard를 사용합니다. 그것을 분해해 봅시다. Passport 로컬 전략의 기본 이름은 local '입니다. @UseGuards()데코레이터에서 이 이름을 참조하여 passport-local 패키지에서 제공한 코드와 이름을 연결합니다. 앱에 여러 패스포트 전략이있는 경우 (각 전략별로 AuthGuard를 프로비저닝할 수있는 경우) 호출할 전략을 명확하게 지정하는 데 사용됩니다. 지금까지는 그러한 전략이 하나 뿐이지만 곧 추가될 예정이므로 명확성을 기하기 위해 필요합니다.

경로를 테스트하기 위해 /api/login 경로를 사용하여 사용자를 반환합니다. 이것은 또한 또 다른 Passport 기능을 보여줍니다. Passport는 우리가 validate()메소드에서 반환한 값에 따라 user 객체를 자동으로 생성하고 이를 Request 객체에 req.user로 할당합니다. 나중에 이 코드를 코드로 대체하여 대신 JWT를 작성하고 리턴합니다.

이러한 경로는 API 경로이므로 일반적으로 사용 가능한 cURL 라이브러리를 사용하여 테스트합니다. UsersService에 하드 코딩된 user 객체로 테스트할 수 있습니다.

$ # POST to /api/login
$ curl -X POST http://localhost:3000/api/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"userId":1,"username":"john"}

JWT functionality

인증 시스템의 JWT 부분으로 넘어갈 준비가되었습니다. 요구 사항을 검토하고 수정합시다.

  • 사용자가 사용자 이름/비밀번호로 인증할 수 있게 하여 보호된 API 엔드 포인트에 대한 후속 호출에 사용할 JWT를 리턴합니다. 우리는이 요구 사항을 충족시키는 길을 가고 있습니다. 이를 완료하려면 JWT를 발행하는 코드를 작성해야합니다.

  • 베어러 토큰으로 유효한 JWT가 있는지에 따라 보호되는 API 라우트 작성

JWT 요구 사항을 지원하려면 몇 가지 패키지를 추가로 설치해야합니다.

$ npm install @nestjs/jwt passport-jwt
$ npm install @types/passport-jwt --save-dev

@nest/jwt 패키지 (자세한 내용은 여기 참조)는 JWT 조작을 돕는 유틸리티 패키지입니다. passport-jwt 패키지는 JWT 전략을 구현하는 Passport 패키지이며 @types/passport-jwt는 TypeScript 유형 정의를 제공합니다.

POST /api/login 요청이 어떻게 처리되는지 자세히 살펴 보자. 패스포트 로컬 전략에서 제공하는 내장된 AuthGuard를 사용하여 경로를 장식했습니다. 이것은 다음을 의미합니다.

  1. 경로 핸들러는 사용자의 유효성이 검증된 경우에 만 호출됩니다.

  2. req 매개 변수는 user 특성을 포함합니다 (패스포트 로컬 인증 플로우 동안 Passport에 의해 채워짐)

이를 염두에 두고 마침내 실제 JWT를 생성하고 이 경로로 리턴할 수 있습니다. 서비스를 깔끔하게 모듈화하려면authService에서 JWT 생성을 처리합니다. auth 폴더에서 auth.service.ts 파일을 열고 login()메소드를 추가한 후 다음과 같이 JwtService를 가져 오십시오.

@@filename(src/auth/auth.service)
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Dependencies(UsersService, JwtService)
@Injectable()
export class AuthService {
  constructor(usersService, jwtService)
  ) {
    this.usersService = usersService;
    this.jwtService = jwtService;
  }

  async validateUser(username, pass) {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

우리는 @nestjs/jwt 라이브러리를 사용하는데, sign()함수를 제공하여 user 객체 속성의 하위 집합에서 JWT를 생성한 다음 단일 access_token 속성을 가진 간단한 객체로 반환합니다. 참고: JWT 표준과 일관성을 유지하기 위해 userId값을 보유하기 위해 sub속성 이름을 선택합니다. JwtService 제공자를 AuthService에 주입하는 것을 잊지 마십시오.

이제 새로운 의존성을 가져오고 JwtModule을 구성하기 위해 AuthModule을 업데이트해야 합니다.

먼저, auth 폴더에 constants.ts를 생성하고 다음 코드를 추가하십시오 :

@@filename(src/auth/constants)
export const jwtConstants = {
  secret: 'secretKey',
};
@@switch
export const jwtConstants = {
  secret: 'secretKey',
};

이를 사용하여 JWT 서명과 확인 단계 사이에 키를 공유합니다.

Warning 경고 이 키를 공개적으로 노출하지 마십시오 . 코드의 기능을 명확하게하기 위해 여기에서 그렇게했지만 프로덕션 시스템에서는 비밀 금고, 환경 변수 또는 구성 서비스와 같은 적절한 조치를 사용하여이 키를 보호해야합니다.

이제 auth 폴더에서 auth.module.ts를 열고 다음과 같이 업데이트하십시오 :

@@filename(src/auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

register()를 사용하여 JwtModule을 설정하고 구성 객체를 전달합니다. Nest JwtModule여기 및 사용 가능한 구성 옵션에 대한 자세한 내용은 여기를 참조하십시오.

이제 우리는/api/login 경로를 업데이트하여 JWT를 반환할 수 있습니다.

@@filename(src/app.controller)
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller('api')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}
@@switch
import { Controller, Bind, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller('api')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  @Bind(Req())
  async login(req) {
    return this.authService.login(req.user);
  }
}

cURL을 다시 사용하여 경로를 테스트 해 봅시다. UsersService에 하드 코딩된 user 객체로 테스트할 수 있습니다.

$ # POST to /api/login
$ curl -X POST http://localhost:3000/api/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: above JWT truncated

Implementing Passport JWT

이제 최종 요구 사항을 해결할 수 있습니다. 요청에 유효한 JWT가 있어야 엔드 포인트를 보호할 수 있습니다. 패스포트도 우리를 도울 수 있습니다. JSON 웹 토큰으로 RESTful 엔드 포인트를 보호하기 위한 passport-jwt 전략을 제공합니다. auth 폴더에 jwt.strategy.ts라는 파일을 만들어 시작하고 다음 코드를 추가하십시오.

@@filename(src/auth/jwt.strategy)
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
@@switch
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload) {
    return { userId: payload.sub, username: payload.username };
  }
}

우리는 JwtStrategy를 사용하여 앞에서 설명한 모든 Passport 전략에 대해 동일한 레시피를 따랐습니다. 이 전략은 약간의 초기화가 필요하므로 super()호출에서 옵션 객체를 전달하여 이를 수행합니다. 사용 가능한 옵션에 대한 자세한 내용은 여기를 참조하십시오. 우리의 경우 이러한 옵션은 다음과 같습니다.

  • jwtFromRequest:Request에서 JWT를 추출하는 방법을 제공합니다. API 요청의 Authorization 헤더에 베어러 토큰을 제공하는 표준 방식을 사용합니다. 다른 옵션은 여기에 설명되어 있습니다.

  • ignoreExpiration: 명시적으로 표현하기 위해 기본 false설정을 선택합니다.이 설정은 JWT가 Passport 모듈에 만료되지 않았는지 확인하는 책임을 위임합니다. 즉, 경로에 만료된 JWT가 제공되면 요청이 거부되고 401 Unauthorized응답이 전송됩니다. Passport는 편리하게 자동으로 이를 처리합니다.

  • secretOrKey: 토큰 서명을 위해 대칭 비밀을 제공하는 편리한 옵션을 사용하고 있습니다. PEM으로 인코딩 된 공개 키와 같은 다른 옵션이 프로덕션 앱에 더 적합 할 수 있습니다. (자세한 내용은 여기 참조). 어쨌든 이전에 주의 한대로 이 비밀을 공개적으로 노출하지 마십시오 .

validate() 메소드는 약간의 토론이 필요합니다. jwt-전략의 경우 Passport는 먼저 JWT의 서명을 확인하고 JSON을 디코딩합니다. 그런 다음 디코딩된 JSON을 단일 매개 변수로 전달하는 validate()메서드를 호출합니다. JWT 서명이 작동하는 방식에 따라 우리는 이전에 서명하고 유효한 사용자에게 발행한 유효한 토큰을 받고 있음을 보증합니다.

이 모든 것의 결과로, validate()콜백에 대한 우리의 응답은 사소한 것입니다: 우리는 단순히 userIdusername 속성을 포함하는 객체를 반환합니다. Passport는 우리의 validate()메소드의 반환 값을 기반으로 user 객체를 빌드하고 이를 Request 객체의 속성으로 첨부합니다.

또한 이 접근 방식은 프로세스에 다른 비즈니스 로직을 주입할 수 있는 여지를 남겨둔다는 점을 지적할 가치가 있습니다. 예를 들어, validate()메소드에서 데이터베이스 조회를 수행하여 사용자에 대한 추가 정보를 추출하여 Request에서 보다 풍부한 사용자 오브젝트를 사용할 수 있습니다. 또한 해지된 토큰 목록에서 userId를 찾아서 토큰 해지를 수행할 수 있도록 토큰 유효성 검사를 추가로 결정할 수도 있습니다. 샘플 코드에서 구현한 모델은 빠른 "상태 비 저장 JWT" 모델로, 여기서 각 API 호출은 유효한 JWT의 존재에 따라 즉시 승인되며 요청자에 대한 약간의 정보는 요청 파이프 라인에서 사용할 수 있습니다.

새로운 JwtStrategyAuthModule의 제공자로 추가하십시오.

@@filename(/src/auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

JWT에 서명 할 때 사용한 것과 동일한 암호를 가져 와서 Passport에서 수행하는 확인 단계와 AuthService에서 수행 된 sign 단계에서 공통의 암호를 사용하도록합니다.

Implement protected route and JWT strategy guards

이제 보호된 경로와 관련 Guard를 구현할 수 있습니다.

app.controller.ts 파일을 열고 아래와 같이 업데이트하십시오 :

@@filename(src/app.controller)
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller('api')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('me')
  getProfile(@Request() req) {
    return req.user;
  }
}
@@switch
import { Controller, Bind, Get, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller('api')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  @Bind(Req())
  async login(req) {
    return this.authService.login(req.user);
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('me')
  @Bind(Req())
  getProfile(req) {
    return req.user;
  }
}

다시 한 번, Passport-jwt 모듈을 구성 할 때 @nestjs/passport 모듈이 자동으로 프로비저닝 된 AuthGuard를 적용합니다. 이 가드는 기본 이름 인 jwt로 참조됩니다. GET /api/me경로에 도달하면 Guard는 자동으로 passport-jwt 사용자 정의 구성 논리를 호출하고 JWT를 유효성 검증하며 user 특성을 Request 오브젝트에 지정합니다.

앱이 실행 중인지 확인하고cURL을 사용하여 경로를 테스트하십시오.

$ # GET /api/me
$ curl http://localhost:3000/api/me
$ # result -> {"statusCode":401,"error":"Unauthorized"}
$ # POST /api/login
$ curl -X POST http://localhost:3000/api/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
$ # GET /api/me using access_token returned from previous step as bearer code
$ curl http://localhost:3000/api/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # result -> {"userId":1,"username":"john"}

AuthModule에서 JWT의 만료 시간이 60 초가 되도록 구성했습니다. 이것은 만료 기간이 너무 짧을 수 있으며 토큰 만료 및 새로 고침에 대한 세부 사항을 다루는 것은 이 기사의 범위를 벗어납니다. 그러나 우리는 JWT의 품질과 passport-jwt 전략을 보여주기 위해 이를 선택했습니다. GET /api/me 요청을 시도하기 전에 인증 후 60 초 동안 대기하면 401 Unauthorized 응답이 수신됩니다. 이는 Passport가 JWT의 만료 시간을 자동으로 확인하여 애플리케이션에서 이를 수행하는 데 따른 문제점을 저장하지 않기 때문입니다.

이제 JWT 인증 구현을 완료했습니다. JavaScript 클라이언트 (예: Angular/React/Vue) 및 기타 JavaScript 앱은 이제 API 서버를 통해 안전하게 인증하고 통신할 수 있습니다. 이 장 여기에서 완전한 코드 버전을 찾을 수 있습니다.

Default strategy

우리의 AppController에서 우리는 @AuthGuard()데코레이터에서 전략의 이름을 전달합니다. 우리는 여기 패스포트 전략 (패스포트 로컬 및 passport-jwt)을 도입 했으므로 다양한 패스포트 구성 요소의 구현을 제공하기 때문에 이를 수행해야 합니다. 이름을 전달하면 연결되는 구현이 명확해집니다. 애플리케이션에 여러 전략이 포함된 경우 기본 전략을 사용할 경우 더 이상 @AuthGuard 데코레이터에서 이름을 전달할 필요가 없도록 기본 전략을 선언할 수 있습니다. PassportModule을 가져올 때 기본 전략을 등록하는 방법은 다음과 같습니다. 이 코드는 AuthModule에 들어갑니다.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy ';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' }), UsersModule],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

Customize Passport

모든 표준 패스포트 사용자 정의 옵션은 register()메소드를 사용하여 동일한 방식으로 전달될 수 있습니다. 사용 가능한 옵션은 구현중인 전략에 따라 다릅니다. 예를 들면 다음과 같습니다.

PassportModule.register({ session: true });

Named strategies

전략을 구현할 때 PassportStrategy함수에 두 번째 인수를 전달하여 전략의 이름을 제공할 수 있습니다. 이렇게하지 않으면 각 전략의 기본 이름 (예: jwt-strategy의 경우 'jwt')이 됩니다.

export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')

그런 다음 @AuthGuard ('myjwt')와 같은 데코레이터를 통해 이를 참조합니다.

GraphQL

GraphQL에서 AuthGuard를 사용하려면 내장 AuthGuard 클래스를 확장하고 getRequest() 메소드를 대체하십시오.

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

위의 구문을 사용하려면 GraphQL 모듈 설정에서 컨텍스트 값의 일부로 요청 (req) 객체를 전달해야합니다.

GraphQLModule.forRoot({
  context: ({ req }) => ({ req }),
});

graphql 리졸버에서 현재 인증된 사용자를 얻으려면 사용자 데코레이터를 정의할 수 있습니다.

import { createParamDecorator } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data, [root, args, ctx, info]) => ctx.req.user,
);

리졸버에서 위의 데코레이터를 사용하려면 쿼리 또는 돌연변이의 매개 변수로 포함시킵니다.

@Query(returns => User)
@UseGuards(GqlAuthGuard)
whoAmI(@CurrentUser() user: User) {
  return this.userService.findById(user.id);
}

Last updated