passport에는 다양한 인증 메커니즘을 구현하는 전략이 있다. 개념은 단순하지만 선택할 수 있는 passport 전략 세트는 다양함. passport는 이런 다양한 단계를 표준 패턴으로 추상화하고 @nestjs/passport
모듈은 이 패턴을 익숙한 Nest 구성으로 래핑하고 표준화한다.
passport는 이러한 과정을 실행한다.
1. "credentials(자격 증명)"-> 사용자 이름/비밀번호, JSON 웹 토큰(JWT) 또는 ID제공자의 ID 토큰)을 확인해 사용자를 인증한다.
요구 사항을 구체화. 사용자 이름과 비밀번호로 인증을 시작한다. 인증되면 서버는 인증을 증명하기 위한 후속 요청에서 인증 헤더의 전달자 토큰으로 보낼 수 있는 JWT를 발행한다. 또한 유효한 JWT를 포함하는 요청에만 엑세스 할 수 있는 보호된 경로를 생성한다.
필요한 패키지 설치. Passport는 사용 사례의 이 부분에 대한 요구 사항에 맞는 사용자 이름/암호 인증 매커니즘을 구현하는 Passport-local 이라는 전략을 제공한다.
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
> Notice
선택한 Passport 전략에는 항상 `@nestjs/passport`및`passport`패키지가 필요하다. 그런 다음 구축 중인 특정 인증 전략을 구현하는 전략별 패키지(ex. `passport-jwt`)를 설치해야한다. 또한 TypeScript 코드를 작성하는 동안 도움을 제공하는 `passport-local`와 함께 위에 표시된 것 처럼 모든 Passport 전략에 대한 유형 정의를 설치할 수 도 있다. (ex.@types/passport-local)
## passport 전략 구현
- Passport 전략에 사용되는 프로세스의 개요부터 시작. Passport를 그 자체로 미니 프레임워크로 생각하면 도움이 된다. 프레임워크의 장점은 인증 프로세스를 구현 중인 전략에 따라 사용자가 지정하는 몇 가지 기본단계로 추상화한다는 것이다. Passport가 적절한 시간에 호출하는 콜백 함수 형태로 사용자 매개변수(일반 JSON 개체) 및 사용자 정의 코드를 제공하여 구성하기 때문에 프레임워크와 같다. 모듈은 `@nestjs/passport` 이 프레임워크를 Nest 스타일 패키지로 래핑하여 Nest 애플리케이션에 쉽게 통합할 수 있다. 아래에서 사용할 것이지만 먼저 vanilla Passport(NodeJs에 기본제공되는 passport로 추정)`nestjs/passport`가 작동하는 방식을 살펴 보겠다.
### vanilla Passport 작동방식
- vanilla Passport에서의 두가지 제공 전략
1. 해당 전략에 특정한 옵션 집합. 예를 들어, JWT 전략에서 토큰에 서명하기 위한 secret 을 제공할 수 있다.
2. Passport에게 사용자 저장소(사용자 계정을 관리하는 곳)와 상호작용하는 방법을 알려주는 "verify callback"이다. 여기에서 사용자의 존재여부(또는 새 사용자 생성)와 자격 증명이 유효한지 확인한다. Passport 라이브러리는 이 콜백이 유효성 검사가 성공하면 전체 사용자를 반환하거나 실패하면 null을 반환할 것으로 예상한다.(실패는 사용자를 찾을 수 없거나 passport-local의 경우 비밀번호가 일치하지 않는 것으로 정의된다.)
이를 사용하면 클래스 `@nestjs/passport`를 확장하여 Passport 전략을 구성할 수 있습니다. 하위 클래스에서 메서드를 호출하고 선택적으로 옵션 개체를 `PassportStrategy` 전달하여 vanilla Passport 작동방식 1번 전략 옵션을 전달합니다. 하위 클래스에서 메서드를 `super()`구현하여 vanilla Passport 작동방식 2번 확인 콜백을 제공합니다.
`AuthModule`을 위한 `AuthService`를 생성합니다.
$ nest g module auth
$ nest g service auth
이를 구현할 때 `AuthService` User 작업을 캡슐화하는 것이 유용하기 때문에 `UserService`와 `UserModule`을 생성한다.
$ nest g module users
$ nest g service users
```user//user.service.ts
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
@Injectable()
export class UserService {
private readonly user = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
},
];
async findOne(username: string): Promise<User | undefined> {
return this.user.find(user => user.username === username);
}
}
UserModule
에 필요한 변경 사항은 이 모듈을 외부에서 볼 수 있도록 데코레이터 UserService의 export 배열에 추가하는 것이다.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
AuthService
는 사용자를 검색하고 암호를 확인하는 작업을 가지고 있다. validateUser()
이를 위해 매서드를 만듭니다. 아래 코드에서는 편리한 ES6 스프레드 연산자를 사용하여 변환하기 전에 사용자 개체에서 암호 속성을 제거합니다. validateUser()
잠시 후 Passport 로컬 전략에서 매서드를 호출한다.
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private 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;
}
}
경고!
실제 응용 프로그램에서는 암호를 일반 텍스트로 저장하지 않는다. 단방향 해시 알고리즘과 함께 bcrypt와 같은 라이브러리를 사용합니다. 이 접근 방식을 사용하면 해시된 암호만 저장한 다음 저장된 암호를 수신하고 이를 해시된 버전과 비교하므로 사용자 암호를 일반 텍스트로 저장하거나 노출하지 않는다.
이제 AuthModule
에서 UserModule
을 가져오도록 한다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
auth
폴더에 local.strategy.ts
라는 파일을 생성 한 후 다음 코드를 추가한다.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 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;
}
}
Passport-local을 사용하는 경우에는 구성 옵션이 없으므로 생성자 super()
는 옵션 객체 없이 단순하게 호출만 합니다.
힌트!
super()
에서 passport strategy 동작을 정의하기 위해 options 객체를 전달 할 수 있다. 이예제에서 passport-local strategy은 기본적으로 요청 본문에 호출된 속성(username, password)을 예상한다. options 객체를 전달하여 다른 속성 이름을 지정한다. (ex. super({uesrnmaeField: 'email'}) 자세한 내용은 passport 문서를 참고하세요
또한 validate()
메서드를 구현했습니다. 각 전략에 대해 Passport는 적절한 전략별 매개 변수 집합을 사용하여 확인 기능(@nestjs/passport
의 validate()
메서드로 구현)을 호출합니다. 로컬 전략의 경우 Passport는 validate() 메서드에 validate(사용자 이름: 문자열, 암호: 문자열): any 서명을 사용할 것으로 예상됩니다.