개발/Node

# [NestJS] 사용자 인증 모듈 생성 및 회원 가입하기 [2단계]

ForrestPark 2025. 1. 27. 11:33

[NestJS] 사용자 인증 모듈 생성 및 회원 가입하기 [2단계]

🌞 2025.1.23

  • 인증 : 사용자의 자격을 확인
  • 사용자의 자격증명을 기존 정보를 기반으로 확인 후 인증 토큰을 발급함.
  • 사용자에게 부여된 인증 토큰은 특정 기간 동안만 유효
  • 쿠키기반, 토큰기반(쿠키리스) 인증법이 있음.
  • 서버에서 보내준 쿠키를 클라이언트(주로브라우저) 에 저장해 관리함.
  • 토큰은 서버에 상태를 저장할 필요가 없음.
  • 쿠키와 토큰은 서로 장단점이 있음.
  • 토큰은 OAuth 를 사용한 소셜 로긴에서 사용할 예정, 먼저 쿠키 인증을 구현

10.4.1 인증 모듈 만들기 및 설정

(1) 인증 모듈 생성

📌 auth module > service > controller 순 생성

nest g module auth --no-spec
nest g service auth --no-spec
nest g controller auth --no-spec

인증 시스템 논리 구조

(2) UserService 를 AuthService 에서 주입 하도록 user.module.ts 에 exports 설정을 추가함.

📌 user/user.module.ts

// user/user.module.ts
import { Module } from "@nestjs/common";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./user.entity";
@Module({
    imports : [TypeOrmModule.forFeature([User])],
    controllers : [UserController],
    providers : [UserService],
    //UserService 를 외부로 노출해야함. 
    exports: [UserService]
})
export class UserModule {}

10.4.2. 회원 가입 메서드 생성

(1) UserService 클래스의 creatUser 사용 , 비밀번호 같은 정보 암호화

bcrypt 설치

npm install bcrypt
npm install -D @types/bcrypt 

(2) 서비스 -> 컨트롤러 코드 작성

src/auth/auth.service.ts

//src/auth/auth.service.ts 
// ** HTTP , DTO, service import
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from 'src/user/user.dto';
import { UserService } from 'src/user/user.service';
// ** 회원 정보 암호화 라이브러리 
import * as bcrypt from 'bcrypt'
// 
// import { User } from 'src/user/user.schema';
import { User } from 'src/user/user.entity';

@Injectable() // provider 
export class AuthService {
    constructor(private userService: UserService) {}

    async register(userDto : CreateUserDto) {
        // 1. 이미 가입된 유저 있는지 체크 
        const user = await this.userService.getUser(userDto.email)
        if (user) {
            // 이미 가입된 유저 있을 경우 에러 발생 
            throw new HttpException(
                '해당 유저가 이미 있습니다. ',
                HttpStatus.BAD_REQUEST
            )
        }
        // password 암호화 
        const encryptedPassword = await bcrypt.hash(userDto.password, 10)
        // db 저장, 저장중 error 나면 서버 에러 발생 
        try {
            const user = await this.userService.createUser({
                ...userDto,
                password: encryptedPassword
            })
            // 회원 가입 후 반환하는 값에는 password 를 주지 않음. 
            user.password =undefined
            return user
        } catch (error) {
            throw new HttpException('서버 에러 ', 500)
        }
    }
}

(3) 컨트롤러 생성(rough)

//auth.controller.ts

import { Controller, Body, Get, Post } from '@nestjs/common';
import { CreateUserDto } from 'src/user/user.dto';
import { AuthService } from './auth.service';
import chalk from 'chalk';

@Controller('auth')
export class AuthController {
    constructor(private authService : AuthService) {}

    // 등록 요청을 받으면 CreateUserDto 객체 
    @Post('register')
    async register(@Body() userDto: CreateUserDto) {
        console.log(chalk.yellow(" >> register start"))
        return await this.authService.register(userDto)
    }
}
  • 🤔 @Body() userDto: CreateUserDto 해석
    • @Body 데코레이터는 요청 본문에서 데이터를 추출 함.
    • 데코레이터로 추출한 것을 CreateUserDto 타입의 객체로 변환되어 userDto 변수에 할당함.

10.4.3 SQLite 익스텐션으로 테이블 확인

sqlite extension install > user-auth.sqlite check

10.5 쿠키를 사용한 인증 구현

  1. AuthController 에 login 핸들러 메서드 구현

    🤔핸들러란? : 핸들러는 특정 요청(Get,Put,Post,Delete)을 처리하는 역할을 하는 함수이다.

  2. Controller > AuthService 로 email, password 파라미터를 Dto 형태로 넘겨 주면 DB 에 해당 정보 유저가 있는지 유효성 검증을 하는 로직 구현.

  3. 유저 정보의 유효성 검증이 끝나면 응답 값에 쿠키 정보를 추가해 반환함.

  4. NestJS 에서 인증을 구현할때 보통 인증용 미들웨어인 가드를 함께 사용함.

    ✅ 가드는 특정 상황(권한,롤,액세스컨트롤) 에서 받은 요청request 를 가드를 추가한 라우트 메서드에서 처리할지 말지를 결정하는 역할을 함.

10.5.1 AuthService 에 이메일과 패스워드 검증 로직 만들기

(1) 유저의 이메일과 패스워드 검증 로직

📌 auth/auth.service.ts

    // 회원 검증 
    async validateUser(email: string, password: string) {
        const user = await this.userService.getUser(email)
        // 이메일로 유저 정보를 받음. 
        if (!user) { // 유저가 없는 경우 -> 검증 실패 
            return null
        }
        const { password: hashedPassword, ...userInfo } =user

        if (bcrypt.compareSync(password, hashedPassword)) {
            // password 일치 
            return userInfo
        }
        return null
    }

(2) validateUser() 메서드를 AuthController 에서 사용해 인증 결과를 쿠키에 추가

📌 auth/auth.controller.ts

    @Post('login')
    async login(@Request() req, @Response() res) {
        // validateUser
        const userInfo = await this.authService.validateUser(
            req.body.email,
            req.body.password
        )
        // 유저 정보가 있으면, 쿠키 정보를 response 저장 
        if (userInfo) {
            res.cookie('login', JSON.stringify(userInfo), {
                httpOnly: false, 
                maxAge: 1000 * 60 * 60 * 24 * 1 //7 day 단위는 밀리초 쿠키 지속 시간 
            })
        }
        return res.send({ message: 'login success'})
    } 
  • login()은 Request 와 Response를 모두 사용해야 하므로 @Body나 @Param 이 아닌 @Request 를 직접 사용함. Response 객체는 쿠키를 설정할때 사용함.
  • 앞서 만든 authService 의 validateUser를 호출해 패스워드를 제외한 유저 정보를 받음. 유저 정보가 있으면 res.cookie 를 사용해 쿠키를 설정함.
  • httpOnly 를 true 로 설정하여 브라우저에서 쿠키를 읽지 못하게 함.
  • 브라우저에서 쿠키를 읽을수 있으면 XSS(Cross Site Scripting) 등의 공격으로 쿠키 탈취 가 가능함. 명시적으로 false 를 줌. 원래 기본값도 false 임.
  • 쿠키 정보를 브라우저에서 읽지 않아도 된다면 true 설정이 보안에 더 유리

(3) login test

### USER login test
 curl -X POST \
     -H "Content-Type: application/json" \
     -d '{
     "email":"forre@grkcon.com",
     "password":"grkcon2025!"
     }' \
     http://localhost:3000/auth/login
  • curl 에서는 쿠키가 뜨지않고. test.http 파일에서 접근하면 연결자체가 안됨.

  • curl 명령에서 -v 설정을 추가하면 쿠키가 보임, -c cookie.txt 파일에 쿠키 저장 하고 -b cookie.txt 파일에서 쿠키읽어와서 접근하면 오류가 사라짐

  • type script 로 쿠키 확인할수있는 코드

    📌 auth.test.ts

    const data = {
      email: "test1@grkcon.com",
      password: "grkcon2025!"
    };
    
    fetch("http://localhost:3000/auth/login", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
    })
    .then((response) => {
         const setCookieHeaders = [];
         const cookies = [];
    
         response.headers.forEach((value, key) => {
             if (key.toLowerCase() === "set-cookie") {
                setCookieHeaders.push(value);
                 const parsedCookies = value.split(';').reduce((acc, cookie) => {
                    const [name, value] = cookie.trim().split('=');
                    if (name && value) {
                        acc[name] = value;
                    }
                    return acc;
                }, {});
                cookies.push(parsedCookies)
             }
         });
    
         console.log("Set-Cookie 헤더들:", setCookieHeaders);
         console.log("파싱 된 쿠키들:", cookies)
      return response.json();
    })
    .then((result) => console.log("응답 결과:", result))
    .catch((error) => console.error("오류 발생:", error));

10.5.2 가드를 사용해 인증됬는지 검사

  • Nest.js 인증시 가드라는 미들웨어를 보편적으로 사용함.
  • 가드는 @Injectable() 데코가 붙어있고 CanActive 인터페이스를 구현한 클래스임.
  • @UseGuard 로 사용할수도 있음.
  • 클라이언트의 요청을 @Get, @Post 등이 붙어있는 핸들러 메서드에 넘기기 전에 인증에 관련된 처리를 할수 있음.
  • CanActivate 인터페이스를 구현하려면 canActivate() 메서드를 구현해야함.
  • CanActiavet 는 boolean or Promise을 반환 true 인경우 핸들러 메서드 실행, false 이면 403 forbidden 에러를 반환

    NestJS 가드 인증 논리구조

(1) 서버측에서 http 헤더에 있는 쿠키를 읽는 코드 작성.

  • cookie-parser 패키지 설치
    npm install cookie-parser

메인 에 코드 추가

📌 src/main.ts

// cookie 
import * as cookieParser from 'cookie-parser'
///( 생략 ..)
async function bootstrap() {
  // Cookie parser 사용 
  app.use(cookieParser())
}
  • 쿠키 파서는 request 객체에서 읽어오는데 사용하는 미들웨어임
  • NestFactory.create로 만든 NestApplication 의 객체인 app에서 use() 함수를 사용해 미들웨어를 사용하도록 한줄만 추가하면 됨.

(3) auth.guard.ts 작성

  • authService 의 validateUser 사용하여 가드 생성
  • src/auth 아래에 auth.guard.ts 파일 생성
/// src/auth/auth.guard.ts

import { CanActivate, ExecutionContext, Injectable} from '@nestjs/common'

import { AuthService } from './auth.service'
import { Observable } from 'rxjs'

@Injectable()
export class LoginGuard implements CanActivate {

    constructor(private authService: AuthService) {}
    // CanActivate 인터페이스의 메서드 
    async canActivate(context: any): Promise<boolean> {
        // 컨텍스트에서 리퀘스트 정보를 가져옴
        const request =  context.switchToHttp().getRequest()
        // 쿠키가 있으면 인증된 것
        if (request.cookies['login']) {
            return true
        }
        // 쿠키가 없으면 request 의 body정보 확인 
        if (!request.body.email || !request.body.password) {
            return false 
        }

        //기존의 authService.validateUser 를 사용하여 인증
        const user = await this.authService.validateUser(
            request.body.email,
            request.body.password
        )
        // 유저 정보 없을시 false
        if (!user) {
            return false 
        }
        // 유저정보가 있으면 request 에 user 정보 추가후 true 
        request.user = user
        return true 
    }
}
  • @Injectable 이 있으므로 다른 클래스 주입가능 , CanActive 있으므로 가드 클래스임.
  • 인증시 authService 객체 주입, canActivate() 는 추상 메서드이므로 사용할 클래스에서 구현해야함. 반환 타입 은 async 이므로 Promise boolean 타입으로 사용
  • true: 인증됨, false: 인증 안됨.

(4) auth.controller 에 useGuard 를 활용한 login2 함수 작성

auth.controller.ts

   // 사용자 인증 
    @UseGuards(LoginGuard)
    @Post('login2')
    async login2(@Request() req, @Response() res) {
        // 쿠키정보는 없지만 request에 user 정보가 있다면 응답값에 쿠키 정보 추가 
        if (!req.cookies['login'] && req.user) {
            // 응답에 쿠키 정보 추가 
            res.cookie('login', JSON.stringify(req.user), {
                httpOnly: true,
                maxAge: 1000 * 10 // test 용 
            })
        }
        return res.send({message: 'login2 success'})

    }
    // 로그인을 한 때만 실행되는 메서드 
    @UseGuards(LoginGuard)
    @Get('test-guard')
    testGuard() {
        return '로그인 된 떄만 이 글이 보입니다. '
    }
}

(5) 쿠키 로그인 인증 test

✅ 기존에 create 로 생성된 아이디들은 테스트를 할수없음 auth/register 로 생성된 아이디들만 auth guard 에 인식이 되며 쿠키가 생성됨

로그인 > login2(by쿠키) Curl test

### 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/login

### USER login 가드 테스트 ( cookie 읽어서 login2 쿠키 확인 )
curl -X POST \
    -H "Content-Type: application/json" \
    -d '{
    "email":"test1@grkcon.com",
    "password":"grkcon2025!"
    }' \
    -b cookies.txt \
    http://localhost:3000/auth/login2

### USER login  쿠키 인증 test
curl -X GET -b cookies.txt http://localhost:3000/auth/test-guard

TEST RESULT