API를 만들기 위해서는 인증의 구현을 필수 사항입니다.
node.js의 인증 라이브러리인 Passport를 이용합니다.
사용자의 이름과 암호로 인증을 합니다.
인증이 되면 서버는 인증을 증명하기 위해 후속 요청에서 인증 헤더에 전달자 토큰으로 전송할 수 있는 JWT(Json Web Token)를 발행합니다. 또한 유효한 JWT가 포함 된 요청에만 엑세스 할 수 있는 보호 경로를 생성합니다.
사용자이름과 암호를 통해 인증을 구현하기 위해 [passport-local] 패키지를 설치해야합니다.
@types/passport-local을 설치하는 이유는 Typescript 작성에 도움을 주기 때문입니다.
$ npm i --save @nestjs/passport passport passport-local
$ npm i --save-dev @types/passport-local
auth의 내용을 작성하기 위하여 auth module,service를 만들어 줍니다. 쉽게 만들기 위하여 nest cli를 사용합니다.
$ nest g module auth
$ nest g service auth
인증을 하기 위해서는 사용자이름과 암호를 사용합니다.
그렇기 때문에 이전에 사용하던 user entity의 코드를 수정해야합니다.
username으로 검색을 하기 때문에 username은 유일성을 가지도록합니다.
/src/users/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
password: string;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
}
현재 작성 중인 예제는 사용자이름을 통해 검색하기 때문에 전에 만든 findOne을 사용하지 못합니다.
그래서 해당 요청에 대한 함수를 새로 만들어 줘야합니다.
/src/users/users.service.ts
...생략
@Injectable()
export class UsersService {
...생략
async find(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ username: username });
}
}
username 컬럼에서 username 변수의 값으로 검색할 수 있습니다.
users.service.ts 의 find 메소드를 auth에서 사용하기 위해서 모듈 외부에서 볼 수 있도록합니다.
/src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
exports 배열에 내보내고 싶은 user.service.ts를 넣어 주시면 됩니다.
AuthService에서는 사용자를 검색하고 암호를 확인합니다.
/src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async vaildateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.find(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
validateUser는 username을 통해 UserService에서 user 정보를 가져와 password가 맞는지 확인합니다.
일치하는 경우 user의 password와 분리한 나머지 부분을 반환합니다.
인증 전략을 구현하기 위하여 [local.strategy.ts]에 만들어줍니다.
/src/auth/strategies/local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
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.vaildateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
현재 passport-local의 사용 사례에는 구성 옵션이 없으므로 생성자 super()는 옵션 객체없이 단순히를 호출 합니다.
passport 전략의 동작을 사용자화 하기 위해 옵션 개체를 전달 할 수 있습니다.
validate 메소드는 passport에서 validate라는 이름을 찾아 호출합니다. 즉, 이름이 다를 경우 동작이 제대로 되지 않습니다. validate에서는 사용자가 존재하고 유효한지 확인합니다. 만약 유효하지 않을 경우 예외를 던집니다.
passport기능을 사용할 수 있도록 모듈에 등록해줘야합니다.
/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
/auth/login 경로에 해당하는 것을 구현합니다.
/src/app.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LocalAuthGuard } from './auth/local-auth.guard';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}
passport 로컬 전략의 기본 이름은 'local'입니다. @UseGuards()에서 해당 이름을 참조하여 passport-local 패키지에서 제공하는 코드와 연결해줍니다. AuthGuard 안에 현재 한개의 전략이 있지만 추가될 수 있습니다.
해당 전략이 여러개가 될 수 있기 때문에 따로 클래스를 만들어서 관리하는 편이 좋습니다.
/src/auth/guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
수정된 코드에 맞게 AppController도 수정해줍니다.
/src/app.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/guards/local-auth.guard';
@Controller()
export class AppController {
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}
일단 테스트로 요청을 보내기 위하여 전에 추가했던 데이터를 전부 삭제해줍니다. 간단하게 DB user테이블을 지운후 다시 실행해주면 됩니다.
새로운 데이터를 추가합니다.
Request
[
{
"username": "test1",
"password": "qwer1234@",
"firstName": "a",
"lastName": "bc",
"isActive": true
},
{
"username": "test2",
"password": "qwer",
"firstName": "b",
"lastName": "cd",
"isActive": false
},
{
"username": "test3",
"password": "qwer12",
"firstName": "c",
"lastName": "de",
"isActive": true
}
]
Request
{
"username": "test1",
"password": "qwer1234@"
}
Response
{
"id": 1,
"username": "test1",
"firstName": "a",
"lastName": "bc",
"isActive": true
}
Request
{
"username": "test2",
"password": "qwer1234@"
}
Response
{
"statusCode": 401,
"message": "Unauthorized"
}
정상적인 요청에 대한 응답이 올 경우 비밀번호를 제외한 나머지 정보를 보내줍니다.