전제 조건
- cookieParser 를 임포트해 main.ts에 설정을 추가해주었다. : app.use(cookieParser();
- 회원가입 서비스 : ex) signUp, join (메서드 명은 당연히 자유)와 같은 서비스 메서드를 만들어 db에 등록이 가능하다.
- 유저 조회 서비스 : ex) findOne, getUser (메서드 명은 당연히 자유) 와 같은 서비스 메서드를 만들어 가입여부 체크가 가능하다.
왜 쓸까?
사용자의 브라우저에 쿠키만 가지고 인증을 하면 위변조, 탈취의 위험이 있다.
그래서 서버에서 인증하고 인증 정보를 서버의 특정 공간에 저장해두는게 바람직하다. => 이게 세션
쿠키 = 세션을 찾는 정보 : ex) 유저아이디, 유저이메일 같이 단순한 식별 정보만 저장
세션 = 인증에 필요한 중요한 정보 : ex) 패스워드 같은 중요 정보만 사용
의 구조를 만들어 주고 사용해보자. 서버에 부하를 주는 단점이 있지만 보안적으로는 더 안전하다!
Passport.js ?
- Express의 인증 미들웨어 라이브러리
- 인증 로직을 쉽게 분리해서 개발할 수 있다.
- 이 글에서는 안하지만 OAuth, jwt 를 이용한 인증에 사용도 가능하다.
- 스트래티지('전략') 이라는 개념이 있어서 뭔 소리인가 하지만 그냥 인증 로직 수행 클래스(인증 방법) 이라고 이해하면 될 것 같다.
- 이 글에서는 username(아이디), userpassword(비번) 를 이용하는 LocalStrategy 클래스를 만들어 사용한다.
간단한 구조
1. 가드에서 LocalStrategy 클래스에 id, pw로 사용자 검증 요청
2. LocalStrategy에서 Session 에 있는 인증 정보 요청
3. Session에 있는 정보를 LocalStrategy에 전송
4. LocalStrategy가 세션에 있는 정보를 확인해 인증 유무를 가드에 전달
5. 이 구조를 모두 구현할 필요 없이 라이브러리로 편하게 불러올 수 있으니 설정 파일만 조금 바꿔주면 된다!
시작
1. 각종 디펜던시를 설치한다.
- dependencies
"@nestjs/passport",
"express-session",
"passport",
"passport-local"
-devDependencies
"@types/express-session",
"@types/passport-local",
2. 세션을 사용하기 위해 main.ts 파일을 수정
- session, passport 를 import하는 부분, app.use로 사용하는 부분만 보면 된다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { ValidationPipe } from '@nestjs/common';
import * as cookieParser from 'cookie-parser';
import * as session from 'express-session';
import * as passport from 'passport';
dotenv.config({
path: path.resolve(
process.env.NODE_ENV === 'real'
? '.real.env'
: process.env.NODE_ENV === 'stage'
? '.stage.env'
: '.dev.env',
),
});
async function bootstrap() {
console.log(process.env.NODE_ENV);
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.use(cookieParser());
app.use(
session({
secret: process.env.SESSION_SECRET_KEY, // 세션 암호화에 사용하는 키로 절대 노출되서는 안된다.
resave: false, // 세션을 항상 저장할 지 여부라 일단 false
saveUninitialized: false, // 세션이 저장되기 전에는 초기화 하지 않은 상태로 세션을 미리 만들어 저장할지? 일단 false
cookie: { maxAge: 3600000 }, // 쿠키 유효시간 = 일단 1시간 주었다.
}),
);
app.use(passport.initialize());
app.use(passport.session());
await app.listen(3000);
}
bootstrap();
3. 가드 및 스트래지 파일 만들어 놓기
4. auth.guard.ts 작성
- LocalAuthGuard 는 로그인 시 사용할 가드이다.
- AuthenticatedGuard 는 로그인 후 인증 확인 용 가드이다.
- AuthGuard 라이브러리 코드로 타고 가보면 저 'local'이 타입이라는걸 알 수 있다.
- 로컬 스트래티지를 사용하기 위해 'local' 이라는 인자를 던져준다.
- 가드를 사용하려면 반드시 canActivate 메서드를 구현해야 한다.
- super.canActivate 는 passport-local의 로직을 구현한 메서드를 실행한다. Local.strategy.ts에 작성한다.
- super.login 은 로그인 처리를 하고 세션을 저장한다. 저장하고 꺼내오는 방법은 session.serializer.ts 에 정의한다.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: any): Promise<boolean> {
const result = (await super.canActivate(context)) as boolean;
const request = context.switchToHttp().getRequest();
await super.logIn(request); // 세션 저장
return result;
}
}
@Injectable()
export class AuthenticatedGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return request.isAuthenticated(); // 세션에서 정보를 읽어서 인증 확인
}
}
5. session.serializer.ts 작성
- PassportSerializer 를 상속받는데 serializeUser, deserializerUser 는 필수로 정의해줘야 한다.
- 세션에 저장할 때 최소한의 정보이자 중복 불가 값인 USER_ID만 넣어준다.
- 세션에서 정보를 꺼내 올 떄 유저가 있다면 USER_PW를 뺀 나머지 정보만 리턴해준다. (굳이 구조분해 할당한 이유)
import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { UserService } from 'src/user/user.service';
@Injectable()
export class SessionSerializer extends PassportSerializer {
constructor(private userService: UserService) {
super();
}
// 세션 저장 메서드
serializeUser(user: any, done: (err: Error, user: any) => void): any {
done(null, user.USER_ID);
}
// 세션에서 정보를 꺼내올 때 사용하는 메서드
async deserializeUser(
payload: any,
done: (err: Error, payload: any) => void,
): Promise<any> {
const user = await this.userService.findById(payload);
if (!user) {
done(new Error('유저가 없습니다'), null);
return;
}
const { USER_PW, ...userInfo } = user;
done(null, userInfo);
}
}
6. local.strategy.ts 작성
- id, pw 를 이용해 인증하기에 'passport-local' 패키지를 불러온다.
- PassportStrategy(Strategy) -> 믹스인으로 클래스의 일부만 확장시킨다.
- 지금 만드는 클래스는 constructor의 기본값이 usernameField='username', passwordField='password' 이다. 이 말이 무슨 말이냐면 클래스가 생성되고 안에 메서드를 실행할 때 따로 설정을 해주지 않으면 들어오는 인자 ( {} ) 의 'username' , 'password'로 검증을 하기 때문에 내가 쓰는 키 명으로 바꿔주었다. 자신이 만드는 서비스의 Input req가 'username', 'password' 면 그대로 둬도 상관 없다!
- 밑에 validate 메서드로 유저의 정보가 있으면 그대로 유저의 정보를 반환한다.
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({ usernameField: 'USER_ID', passwordField: 'USER_PW' });
}
async validate(userId: string, userPw: string): Promise<any> {
const user = await this.authService.validDateUser(userId, userPw);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
7. auth.module.ts 설정 추가
- 위에 코드들을 처음보면 무슨 소리인가 싶겠지만 한 두 번 백지에서 시작하다보면 어느정도 감이 잡힐 것이다.
- 일단 인증 모듈에 임포트랑, 프로바이더 수정해주자
- 패스포트 모듈의 세션 설정이 기본이 false라 true로 바꿔준다.
- LocalStrategy, SessionSErializer 모두 다른 곳에서 사용하는 클래스라 프로바이더에서 등록해줘야 한다. 이거 안해주면 에러난다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';
@Module({
imports: [UserModule, PassportModule.register({ session: true })],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, SessionSerializer],
})
export class AuthModule {}
8. 테스트해보자.
- auth.controller.ts 에 사용할 라우터 추가해주자.
- sessionLoginTest = 우리가 만든 로그인 가드를 잘타서 정보가 인증이 정상 완료된 사용자면 그대로 유저 정보가 리턴 된다.
- sessionAuthTest = 로그인 된 사용자는 이 주소로 요청하면 그대로 유저 정보를 또 볼 수 있다.
// auth.controller.ts
@UseGuards(LocalAuthGuard)
@Post('/sessionLoginTest')
login3(@Request() req) {
return req.user;
}
@UseGuards(AuthenticatedGuard)
@Get('/sessionAuthTest')
testGuardWithSession(@Request() req) {
return req.user;
}
- 테스트에 대한 결과값은 글을 잘 읽어봤다면 알 것이다 ㅎㅎ
- rest-client를 이용한 테스트에 작성 예시이다.
- 혹시나 안되면 처음부터 천천히 다시 해보길 바란다. 생소한 메서드들이 많아 헷갈릴 수도 있다.
### 세션 로그인 - 맞는 계정으로 시도
POST http://localhost:3000/auth/sessionLoginTest HTTP/1.1
Content-Type: application/json
{
"USER_ID": "회원가입 되어 있는 아이디",
"USER_PW": "회원가입 되어 있는 비밀번호"
}
### 세션 로그인 - 틀린 계정으로 시도
POST http://localhost:3000/auth/sessionLoginTest HTTP/1.1
Content-Type: application/json
{
"USER_ID": "회원가입 되어 있지 않은 아이디",
"USER_PW": "회원가입 되어 있지 않은 비밀번호"
}
### 로그인 되어 있는 상태로 들어오는게 맞는지 아닌지 테스트
GET http://localhost:3000/auth/sessionAuthTest HTTP/1.1
'Node.js > NestJS' 카테고리의 다른 글
[NestJS] ValidationPipe를 이용해 Request 편하게 검증하기 (class-validator) (0) | 2023.08.23 |
---|