Nest 프로젝트 생성
nest new [프로젝트 명]
필자는 session라는 이름으로 프로젝트를 만들었고 npm을 이용했다.
auth module, service, controller를 만들고 local.serializer.ts, local.strategy.ts, localAuth.guard.ts와 common폴더 안에 user.decorator.ts를 추가해준다.
라이브러리 설치
npm install --save @nestjs/passport passport passport-local express-session
passport와 세션사용을 위해 express-session을 설치해준다.
코드
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as passport from 'passport';
import * as session from 'express-session';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
session({
resave: false,
saveUninitialized: false,
secret: 'this_is_secret',
cookie: {
httpOnly: true,
},
}),
);
app.use(passport.initialize());
app.use(passport.session());
await app.listen(3000);
}
bootstrap();
session, passport.initialize(), passport.session() 미들웨어를 붙혀준다. session에서 option으로 session저장방식, 키 등을 지정할 수 있다.
//auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { LocalSerializer } from './local.serializer';
import { AuthController } from './auth.controller';
@Module({
imports: [PassportModule.register({ session: true })],
providers: [AuthService, LocalStrategy, LocalSerializer],
controllers: [AuthController],
})
export class AuthModule {}
provider들을 불러오고 session을 true로 지정해준다.
//auth.controller.ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './localAuth.guard';
import { User } from 'src/common/decorator/user.decorator';
@Controller('auth')
export class AuthController {
@Post()
@UseGuards(LocalAuthGuard)
logIn() {
return 'login ok';
}
@Get()
getUserInfo(@User() user) {
console.log(user);
return user;
}
}
간단하게 2개의 라우터만 만들었다. Post를 이용해서 로그인을 하고 Get에서는 connect.sid를 이용해서 유저정보를 불러올 수 있는지 확인해볼것이다. getUserInfo에서 보이는 User데코레이터는 직접 제작할 것이다.
// common/decorator/user.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
express를 해봤다면 알겠지만 그냥 req.user를 return해주는 decorator이다. 그렇다면 왜 이 코드를 controller에서 @Req req로 받아와서 하지않고 따로 커스텀 데코레이터로 만들까?
nestjs 공식문서에서 볼 수 있듯 nestjs는 express뿐만 아닌 fastify로도 쉽게 전환할 수 있도록 만들어져있다. 때문에 이러한 express의 코드들이 계속해서 사용되다보면 추후 확장성에서 문제가 생길 수 있다. 때문에 이러한 커스텀 데코레이터를 만들어서 express와의 결합을 줄인다.
이제 LocalAuthGuard를 살펴보자
//localAuth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const can = await super.canActivate(context);
if (can) {
const request = context.switchToHttp().getRequest();
await super.logIn(request);
}
return true;
}
}
passport모듈의 AuthGuard를 상속받아오는 것을 볼 수 있다. AuthGuard('local')이 express의 passport.authenticate('local')라고 볼 수 있다.
AuthGuard가 실행된 이후에는 PassportStrategy가 실행된다.
//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({ usernameField: 'email', passwordField: 'password' });
}
async validate(email: string, password: string, done: CallableFunction) {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return done(null, user);
}
}
passportstrategy를 상속받아 진행한다. usernameField는 email이라는 이름으로 passwordField는 password라는 이름으로 받아온다. 받아온 email과 password를 authService.validateUser로 보내서 존재하는 사용자인지 확인한다. 없다면 UnauthorizedException을 터뜨리고 존재한다면 done으로 다음작업으로 넘겨준다.
//auth.service.ts
import { Injectable } from '@nestjs/common';
const User = {
id: 1,
email: 'moonstone1126@naver.com',
password: '1234',
};
@Injectable()
export class AuthService {
async validateUser(email: string, password: string) {
if (User.email === email && User.password === password) {
const { password, ...userWithoutPassword } = User;
return userWithoutPassword;
}
return null;
}
}
원래는 DB에서 불러와야하는 값이지만 일시적으로 객체를 만들어놨다. user가 있다면 password를 제외한 정보들을 return해준다.
이렇게 strategy의 작업이 끝나면 serializer의 작업이 실행된다.
//local.serializer.ts
import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { AuthService } from './auth.service';
const User = {
id: 1,
email: 'moonstone1126@naver.com',
password: '1234',
};
@Injectable()
export class LocalSerializer extends PassportSerializer {
constructor(private readonly authService: AuthService) {
super();
}
serializeUser(user, done: CallableFunction) {
console.log(user);
done(null, user.id);
}
async deserializeUser(userId: string, done: CallableFunction) {
if (User.id == +userId) {
done(null, User);
return;
} else {
done(new Error('User Not Found'));
return;
}
}
}
serializeUser와 deserializeUser가 있다. serialize가 되면 사용자를 구분할 수 있는 유니크한 값을 done(null, 값)으로 넘겨준다. deserialize할 때는 비교 후 정보를 done에 넘겨준다.
Test
로그인 요청을 보내면 cookie를 받아온다. cookie를 갖고 정보요청을 해보자
cookie를 이용해서 user info를 받아올 수 있다.
https://github.com/P4i1l0/nestjs-passportjs-starter-kit
GitHub - P4i1l0/nestjs-passportjs-starter-kit
Contribute to P4i1l0/nestjs-passportjs-starter-kit development by creating an account on GitHub.
github.com