개발/Node

#[node] 패스포트와 세션을 사용한 인증 구현

ForrestPark 2025. 1. 27. 16:27

10.6 패스포트와 세션을 사용한 인증 구현

2025.1.27

  • 서버에서 인증을 하고 해당 정보를 서버의 특정공간에 저장.(세션 이용)
  • 쿠키는 세션을 찾는 정보만 저장(세션의 아이디값) 중요 정보는 세션에 모두 넣음.
  • 세션은 서버의 자원을 사용하여 서버에 부하를 주지만 위조,변조,탈취가 불가능하여 보안적임.
  • 가드하나로 로그인과 인증 모두 사용했지만 가드 두개와 인증 처리를 하기위한 파일을 여러개 만들것.
  • 로그인에 사용할 가드.
  • 인증 로직 구현 부분은 패스포트 라는 인증 로직을 쉽게 분리해서 개발하는 라이브러리 사용
  • 패스포트 사용시 인증 로직은 스트래티지 파일을 생성해서 사용.
  • 패스포트는 인증 로직 수행을 담당하는 클래스를 의미함.
  • 다양한 인증을 위한 스트래티지 패키지를 같이 설치해 인증을 쉽게 구현가능.
  • 가드 안에 인증 로직을 두는것이 아닌 인증로직을 처리하는 별도의 스트래티지 파일 작성
  • id, password 주었을때 올바른 정보인지 판단하는로직, 쿠키에서 값을 읽어 인증을 ㅟ한 올바른 데이터가 있는지 검증하는 로직 의미
  • 세션에서 데이터를 읽어오고 저장하므로 세션에 데이터 저장하고 읽어올 세션 시리얼라이저(session serializer) 파일도 필요함.
  • 가드 패스포트 스트래티지, 세션 시리얼라이저가 서로 협력하여 사용자 신원을 확ㅇ니하고 이증 정보를 저장하고 읽어와서 다시 인증하는 작업을 함. 역할 분담이 잘되어 있어서 유지보수에 유리함.

10.6.1 라이브러리 설치 및 설정

(1) passport 라이브러리 설치

passport-local : username 과 password 로 인증전략 모듈
세션 저장에는 express-session 사용
개발할때 유용하므로 개발 환경 패키지를 설치하는 -D 옵션을 주어 설치함.

npm i @nestjs/passport passport passport-local express-session
npm i -D @types/passport-local @types/express-session 

(2) 세션을 사용하려면 main.ts 파일에 설정 추가

📌 src/main.ts

// 로그인 인증을 위한 세션, passport 라이브러리 임포트 
import * as session from 'express-session'
import * as passport from 'passport'

//(생략)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // session 설정 
  app.use(
    session({
      secret: 'very-important-secret', // 세션 암호화 키 
      resave: false, // 세션을 항상 저장할지 여부 
      // 세션 저장되기 전 초기화 되지 않은 상태로 세션을 미리 저장 
      saveUninitialized: false, 
      cookie: {maxAge: 3600000} // 쿠키 유효시간 1시간
    })
  )
  // passport 시작, session  선언
  app.use(passport.initialize())
  app.use(passport.session())

10.6.2 로그인과 인증에 사용할 가드 구현

로그인에 사용할 가드와 로그인후 인증에 사용할 가드를 별개로 생성하여 사용
loginAuthGuard 는 HTTp 요청을 받은 email 과 password 정보로 유효한 user 정보가 있는지 확인해 유효할 경우 유저 정보를 세션에 저장.
AuthenticatedGuard 는 HTTP 요청에 잇는 쿠키를 찾아 쿠키에 있는 정보로 세션을 학인해 로그인이 완료된 사용자인지 판별

  • LoginAuthGuard 와 AuthenticatedGuard 가드를 auth.guard.ts 에 추가
/// src/auth/auth.guard.ts

// Guard 를 사용하기 위한 임포트 
import { CanActivate, ExecutionContext, Injectable, Request} from '@nestjs/common'

// 패스포트 사용하는 AuthGuard 임포트 
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service'

@Injectable()
// AuthGuard 상속 
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()
  }
}
  • 패스 포트 인증에 가드를 사용할수 있도록 감싸둔 AuthGuard 를 제공하는 라이브러리
  • 패스포트는 인증로직을 스트래티지 개념으로 구현.
  • 이외에 스트레티지로 passport-jwt 와 passport-google-oauth20 이 있음.
  • 가드를 사용하려면 canActivate를 구현
  • AuthGuard 상속 하여 super.canActivate() 에 서 passport-local 로직을 구현할 메서드 실행함.
  • local.startagy.ts 파일이 localStrategy 클래스 생성한후 valdiate() 메서드 구현
  • super.logIn()에서 로그인 처리, 세션저장함. 세션에서 꺼내오는 방법은 session.serializaer.ts 파일에서 작성
  • AuthenticatedGuard 가 로그인 후 인증되었는지 확인할때 사용.
  • 세션에 덷이터를 저장하고 돌려주는 응답값에 connect.sid 라는 이름의 쿠키를 만듬.
  • 이후 요청에 해당 쿠키값을 같이 전송하면 세션에 있는 값을 읽어 인증 여부를 확인 할떄 사용함.

10.6.3 세션에 정보를 저장하고 읽는 세션 시리얼라이저 구현

(1) request.isAuthenticated() 함수가 세션에서 정보를 읽어옴.

import { Injectable } from "@nestjs/common";
import { PassportSerializer } from "@nestjs/passport";
// userService 주입 -> 유저정보를 검증 
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.email) // 세션에 저장할 정보 
    }
    // 세션에서 정보를 꺼내올떄 사용 
    async deserializeUser(
        payload: any,
        done: (err: Error, payload: any) => void,
    ): Promise<any> {
        const user = await this.userService.getUser(payload)
        // 유저 정보가 없는경우 done()함수에 에러 전달
        if(!user) {
            done(new Error('No User'), null)
            return 
        }
        const { password, ...userInfo} = user
        done(null,userInfo)
    }
}
  1. SessionSerializer : 로그인 성공후 사용자정보를 세션에 저장. 이메일(최소한의 정보)만 추출하여 세션에 저장. 이후 요청에서 세션정보를 이용하여 사용자 정보를 복원함.
    serializeUser 함수가 이메일을 세션에 저장하는 작업 완료후 그결과를 Passport 에 알림.
    user 정보는 LocalAuthGuard 의 canActive() 메서드 의 super.logIn(request)를 호출 할때 내부적으로 request에 있는 user 정보를 꺼내서 전달하면서 serializeUser 실행

  2. done(err:Error, user :any): Passport.js 에서 비동기 작업의 결과를 처리하기 위해 사용하는 콜백 , err 는 Error 타입의 객체임. 결과 void 이므로 done 은 어떤 값도 반환 하지 않음.

  3. (user: any ..)
    : user 는 매개변수 이름. 로그인전략( local,google,facebook) 성공시 전달해주는 사용자 정보를 의미함!! LocalAuthGuard 사용시 valildateUser 메소드에서 반환된 유저정보가 serializeUser 함수의 user인자로 전달됨.

  4. getPassportInstance() : 패스포트 인스턴스를 가져옴, 패스포트 인스턴스의 데이터가 필요한 경우 사용

  5. deserializeUser() : 인증되었는지 세션에 있는 정보로 검증할때
    payload(세션에서 꺼내온값.전달되는 데이터의 핵심 부분 ) : deserializeUser 함수에서 세션에 저장된 사용자 식별 정보(이메일)를 전달 받아 해당 사용자의 정보를 조회하고 복원하는데 사용하는 값.

10.6.4 email,password 인증 로직이 있는 LocalStrategy 파일 작성

  • 인증 방법이 다양함. 패스포트에서 strategy(인증전략) 이라는 별개의 패키지로 분리해 담음.
  • id, password 로 인증하는 기능은 passport-local 패키지에서 제공함.
인증방법 패키지 설명
Local passport-local 유저명, 패스워드를 사용
OAuth passport-oauth 구글,페이스북 등 외부 서비스에서 인증
SAML passport-saml SAML 신원제공사에서 인증, OneLogin, Okta
JWT passport-jwt JSON web token 인증
AWS Cognito passport-cognito AWS Cognito user pool 인증
LDAP passport-ldapauth LDAP 디렉터리 사용

(1) email, password 인증 로직이 있는 localStrategy 파일 작성

//auth/local.strategy.ts

//auth/local.strategy.ts

import { Injectable } 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) {
    // 1. PassportStrategy 믹스 인 
    constructor( private authService: AuthService) {
        // 기본값이 username 이므로 email 로 변경함. 
        super({usernameField :'email'})
    }
    // 유저 정보의 유효성 검증 
    async validate(email: string, password: string) : Promise<any> {

        const user = await this.authService.validateUser(email,password)

        if (!user) {
            return null // null -> 404 에러 
        }
        return user  // user 정보 반환 
    }
}
  • PassportStrategy(Strategy) : 믹스인

  • 컴포는트를 재사용할 때 상속을 많이 사용하지만 해당 클래스의 모든것을 재사용해야하는 불편함. 클래스의 일부만 확장하고 싶을 때는 믹스인 을 사용

    🤔 믹스인(mixin) / 트레잇(trait)
    클래스에 새로운 기능을 추가하기 위해, 필요한 메서드를 가지고 있는 작은 클래스들을 결합해 기능을 추가하는 방법

  • local-strategy 에는 인증시 사용하는 필드명이 username, password 로 정해져있으므로 usernameField 이름을 email 로 바꾸어줌.

  • validate() 메서드에서는 전달한 email과 password 가 올바른지 검증함.(이미 있는 authSErvice 의 validateUser() 사용 )

10.6.5 auth.module.ts 에 설정 추가

만들어둔 LocalStrategy, SessionSerializer 를 다른 클래스에서 사용하도록 프로바이더 등록, passportModule에 세션을 추가 등록

// auth.module.ts

// Passport, serializer , local strategy 추가 
import { PassportModule } from '@nestjs/passport';
import { SessionSerializer } from './session.serializer';
import { LocalStrategy } from './local.strategy';

console.log(chalk.red('AuthModule Start[[[인증]]]]'))
@Module({
  imports: [
    UserModule,
    PassportModule.register({session: true}),
  ],
  providers: [
    AuthService,
    LocalStrategy,
    SessionSerializer
  ],
  controllers: [AuthController]
})
  • PassportModule 의 기본설정은 세션 설정이 false 로 되어있어서 true 로 설정.
  • LocalStrategy ,SessionSerializer 프로바이더 등록 필요 다른데서 주입하지 않아도 프로바이더 등록안하면 클래스를 찾지 못해 에러남.

10.6.6. 테스트

// auth.controller.ts

// 가드 사용 임포트 
import { AuthenticatedGuard, LocalAuthGuard, LoginGuard } from './auth.guard';

    // Login 3. 가드, 세션, 쿠키 사용 
    @UseGuards()
    @Post('login3')
    login3(@Request() req ) {
        return req.user
    }

    @UseGuards(AuthenticatedGuard)
    @Get('test-guard2')
    testGuardWithSession(@Request() req){
        return req.user
    }
}
### USER login 가드 테스트 ( cookie 기록 )
curl -X POST \
    -H "Content-Type: application/json" \
    -d '{
    "email":"test1@grkcon.com",
    "password":"grkcon2025!"
    }' \
    -c cookies.txt \
    http://localhost:3000/auth/login3
  • 틀린 패스워드로 하면 401 에러가 난다. (auth.service validateUser() 동작 )
  • 인증이 성공하면 유저정보가 나온다.
  • 서버 재시작 하면 세션은 초기화 됨.

10.6.7 로그인과 세션 저장까지 순서