NestJS 회원가입 및 로그인(DB저장)

00_8_3·2021년 1월 13일
5

NestJS DB저장하기(TypeORM)

사전 설치

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local

$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt

Passport는 커뮤니티에 잘 알려져 있고 성공적으로 많은 app 제품에 사용된 nodeJS의 가장 인기있는 인증관련 라이브러리이다. passport는 @nestjs/passport 모듈에 통합되었습니다.

회원 가입

CreateUserDto 만들기

경로 : /users/dto/create-user.dto.ts

import { IsEmail, IsNumber, IsString } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  readonly email: string;

  @IsString()
  readonly username: string;

  @IsString()
  readonly password: string;
}

TypeScript는 제네릭 또는 인터페이스에 대한 메타 데이터를 저장하지 않기 때문에 DTO에서 사용할 때 ValidationPipe가 들어오는 데이터의 유효성을 제대로 검사하지 못 할 수 있습니다. 이러한 이유로 DTO에서 구체적인 클래스를 사용하는 것이 좋습니다.

users.controller.ts에 추가 (라우터 생성?)

경로 : /users/users.controller.ts

import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { IdUserDto } from './dto/id-user.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly userService: UsersService) {}
  @Get()
  findAll(): Promise<User[]> {
    return this.userService.findAll();
  }
  @Post()
  async create(@Body() userData: CreateUserDto): Promise<User> {
    return await this.userService.create(userData);
  }
}

users.service.ts에 create 함수 추가

경로 : /users/users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import { UserRepository } from './Repository/UserRepository';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: UserRepository, // 1. DB와의 연결을 정의
  ) {}

  findAll(): Promise<User[]> {
    console.log('유저 전체 불러오기');
    return this.userRepository.find();
  }

  async create(userData: CreateUserDto): Promise<User> { // 2. 
    const { email, username, password } = userData;

    const user = new User();
    user.email = email;
    user.password = password;
    user.username = username;

    await this.userRepository.save(user);
    user.password = undefined;
    console.log(user);

    return user;
  }
}
    1. DB와의 연결을 정의 (Custom Repository란?)
      경로 : /users/Repository/UserRepository.ts
import { EntityRepository, Repository } from 'typeorm';
import { User } from '../entities/user.entity';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  findByName(firstName: string, lastName: string) { // 1-1
    return this.findOne({});
  }
  
  • 1_1.

    DB와 같이 사용할 methods를 포함한 custom repository를 만들수 있다.
    보통 single entity를 위해 만들어지고 그것의 특정 쿼리들을 포함한다.
    Nest : Custom Repository
    Typeorm : custom Repository

    예를 들어 findByName(firstName: string, lastName: string)로 이야기하면 이 method를 놓을 최적의 장소는 Repository(DB와 연결되는 곳)이다. 그래서 우리는 userRepository.findByName(...)와 같이 사용 할 수 있다.

    1. CreateUserDto는 회원가입 할 때 받는 Data의 형식을 정의한다.

회원가입 flow

유저 회원가입 -> users.controller.ts의 create -> users.service.ts의 create 함수 실행 -> DB에 저장(this.userRepository.save(user);) -> 출력

local-passport 로그인

local.strategy.ts 생성

경로 : /auth/passport/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) { // 1
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    console.log('로컬 스트레티지', username);
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

auth.module.ts에 추가

경로 : /auth/auth.module.ts

imports에 PassportModule를
providers에 LocalStrategy를 추가한다.

@Module({
  imports: [
    UsersModule,
    PassportModule,
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

auth.controller.ts에 추가

경로 : /auth/auth.controller.ts

  @UseGuards(AuthGuard('local')) // 1
  @Post('local')
  async login(@Req() req) {
     return req.user;
  }
    1. Guard는 NestJS 미들웨어 중 하나로, 특정 라우트를 통과해서 서버에 누가 들어왔는지 누가 못들어왔는지 알려줍니다. (Express는 안알려 준다)
      "local"을 명시 해줌으로써 local.strategy.ts를 실행 후 req.user에 반환 해줍니다.

what is guard? stackoverflow
nestJS guard org

로그인 해보기

post : localhost:3000/auth/local, {email: "admin@test.com, password: "admin"}

Postman으로 로그인 해보길 바랍니다.

local-passport Flow

유저 로그인 -> /auth/local 접근 -> Guard 실행 -> req.user 반환

google-passport 로그인

google.strategy.ts 생성

경로 : /auth/passport/google.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { config } from 'dotenv';
import { Injectable } from '@nestjs/common';

config();

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,  // 1
      clientSecret: process.env.GOOGLE_SECRET,
      callbackURL: 'http://localhost:3000/auth/google/callback',
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ): Promise<any> {
    const { name, emails } = profile;
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      accessToken,
      refreshToken,
    };
    done(null, user);
  }
}
  • 1 google develper 들어가서 web api 생성 해주면 된다.

auth.module.ts에 추가

@Module({
  imports: [
    UsersModule,
    PassportModule,
  ],
  providers: [AuthService, GoogleStrategy, LocalStrategy],
  controllers: [AuthController],
})

auth.controller.ts에 추가

  @Get('google')  // 1
  @UseGuards(AuthGuard('google'))
  async googleAuth(@Req() req) {}

  @Get('google/callback') // 2
  @UseGuards(AuthGuard('google'))
  googleAuthRedirect(@Req() req) {
    return this.authService.googleLogin(req);
  }
    1. express에서 사용 하던 것과 비슷하다. localhost:3000/auth/google로 접속하여 로그인 성공 시 localhost:3000/auth/google/callback으로 콜백 된다.
    1. 접속 성공 하여 콜백 되었으면 로그인 후 처리를 한다.

auth.service.ts에 추가

googleLogin(req) {
    if (!req.user) {
      return 'No user from google';
    }
    return {
      message: 'User information from google',
      user: req.user,
    };
  }

구글 로그인에 성공 하면 localhost:3000/auth/google/callback 링크에서 해당 함수를 실행한다.(나중에 JWT 발급 처리)

구글 로그인 Flow

유저 구글 로그인 -> localhost:3000/auth/google 접속 -> 성공 시 localhost:3000/auth/google/callback 접속 -> req.user를 JSON으로 반환(나중에 JWT 발급 추가)

JWT-passport

jwt.strategy.ts 생성

경로 : /auth/passport/jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from '../constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

auth.module.ts에 추가

경로 : /auth/auth.module.ts

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, GoogleStrategy, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})

constants.ts 생성

경로 : /auth/constants.ts

export const jwtConstants = {
  secret: 'secretKey',
};

auth.controller.ts에 추가 및 수정

경로 : /auth/auth.controller.ts

  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Req() req) {
    return req.user;
  }
  @UseGuards(AuthGuard('local'))
  @Post('local')
  async login(@Req() req) {
    return this.authService.login(req.user); // 1
    // return req.user;
  }
    1. authService.login method를 실행하게 수정한다.

auth.service.ts에 추가

경로 : /auth/auth.service.ts

async login(user: any) {
    const payload = {
      username: user.username,
      sub: user.userId,
    };
    return {
      user,
      access_token: this.jwtService.sign(payload),
    };
  }

JWT Flow

유저 local 로그인 -> authService.login 실행 -> user와 jwt Token반환 -> localhost:3000/auth/profile 접속(Token이 유효한지 확인) -> req.user 반환

DTO란?

NestJS docs pipe
Should use DTO?

Shared Module이란?

User 모듈을 Auth 모듈에서 Import할 때
User 모듈에서 UserService를 Export하면
Auth 모듈에서 사용 가능하다.

6개의 댓글

comment-user-thumbnail
2021년 1월 15일

DTO가 정확히 뭔가요..? 필수적으로 사용해야 하는건가요??

1개의 답글
comment-user-thumbnail
2021년 1월 15일

수많은 어노테이션이 js 인데 java 코드를 보는 것 같네요.! 다음 플젝엔 nest를 쓸건데 제대로 시작하기전에 코드를 간단히 훑어보기 좋네요! 잘봤어요 ;)

1개의 답글
comment-user-thumbnail
알 수 없음
2021년 7월 29일
수정삭제

삭제된 댓글입니다.

1개의 답글