MSA(Micro  Software Architecture) 구현 중 사용자 로그인과 관련하여 Authentication 부분을 구현해봅니다.

기존에 nest 개발하기 포스트에 이어서 작업한다고 생각하면 되겠습니다.

https://sound-story.tistory.com/3

 

[NestJS] 개발하기

1. Installation Nest CLI를 통해 쉽게 프로젝트 생성할 수 있다. npm i -g @nestjs/cli npm을 통해 @nestjs/cli를 설치한다. -g 옵션을 통해 글로벌 환경에 설치하였다. 글로벌 환경의 설치 경로를 확인할 수 있..

sound-story.tistory.com

 

먼저 인증과 관련된 모듈 설치를 합니다.

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

다음으로 사용할 service와 module을 생성합니다.

인증 관련 서비스를 구현할 auth와 사용자 정보를 가지고 있을 users입니다.

저희는 users와 관련된 부분은 별도의 MSA구조로 가져갈 것이기에 users에는 email, password 정도의 간단한 정보만 가지고 있을 예정입니다.

$ nest g module auth
$ nest g service auth
$ nest g module users
$ nest g service users

위와 같이 generate 하면 아래와 같은 소스 구조를 갖게 됩니다.

├── src
│   ├── auth
│   │   ├── auth.module.ts
│   │   ├── auth.service.ts
│   ├── users
│   │   ├── users.module.ts
│   │   ├── users.service.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

 

users.service.ts를 구현해 보도록 하겠습니다.

추후 AWS Cognito를 연결할 예정이기 때문에 사용자 정보를 임시로 저장하여 구현해 보겠습니다.

사용자 정보는 readonly로 저장이 되어 있고, email 정보로 사용자 정보를 찾는 findOne 메서드를 가지고 있습니다.

import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      userEmail: 'sory5339@gmail.com',
      password: 'sory5339',
    },
  ];

  async findOne(userEmail: string): Promise<User | undefined> {
    return this.users.find((user) => user.userEmail === userEmail);
  }
}

 

UsersService에 대하여 users.module.ts를 수정합니다.

exports를 하였는데 그 이유는 UsersModule 외 다른 모듈에서도 UsersService를 사용하기 위함입니다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

 

다음으로 AuthService를 구현해 보도록 하겠습니다. 먼저 nest의 jwt 사용을 위해 설치해줍니다.

$ npm install --save @nestjs/jwt

auth.service.ts를 살펴보면 UsersService와 JwtService를 사용하여 validateUser와 login메서드를 구현했습니다.

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(userEmail: string, password: string): Promise<any> {
    const user = await this.usersService.findOne(userEmail);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { userEmail: user.userEmail, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
  • validateUser는 사용자의 email, password를 받아와 usersService에 있는 유효한 사용자인지 확인하여 사용자 정보 중 password를 제외한 정보를 result로 return 합니다.
  • login메서드는 사용자의 email과 userId를 payload로 하여 jwtService를 이용하여 token을 생성합니다.

이제 AuthModule을 구현해 봐야겠죠. import부분을 보면 jwtConstants, JwtStrategy, LocalStrategy가 있어요.

Passport strategy를 구현해 봅니다.  auth 폴더에 '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: 'userEmail',
    });
  }

  async validate(userEmail: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(userEmail, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

constructor내의 super 부분을 보면, 저는 usernameField에 userEmail을 이용하도록 하였습니다.

validate 메서드에서는 email과 password로 user가 아니면 Exception을 user이면 user를 return 해줍니다.

 

다음으로 jwt(jason web token) 구현을 위한 준비입니다. import jwtConstants와 관련된 부분이에요.

auth폴더 아래에 'constants.ts' 파일을 추가하여 아래와 같이 구현합니다. jwt를 생성할 때 사용하는 시크릿 키로 외부에 노출되면 안 되는 부분입니다.

export const jwtConstants = {
  secret: 'secretKey', // token 발급 시 사용되는 시크릿 키. 노출되면 안됨.
};

 

마지막으로 JwtStrategy를 위해 'jwt.strategy'를 생성해줍니다. LocalStrategy처럼  JWT를 위한 PassportStrategy확장형 JwtStrategy입니다.

local에서는 매핑할 filed 정도만 정의해 주었는데 jwt에서는 여러 가지 정보가 있어요. 상세 내용은 주석을 참고하세요.

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(), // Request에서 JWT를 추출하는 방법 중 Bearer Token 사용
      ignoreExpiration: false, // jwt 보증을 passport 모듈에 위임함. 만료된 JWT인경우 request거부, 401 response
      secretOrKey: jwtConstants.secret, // token 발급에 사용할 시크릿 키
    });
  }

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

여기까지 import 된 부분에 대한 설명이었고, auth.module의 imports를 보면 UsersModule, PassportModule을 가져왔고 JwtModule은 register를 통해 등록하였어요. register() 안쪽 부분이 JwtModule에 대한 Options이에요.

 

auth module까지 끝났고요. 이제 guards에 대한 부분을 구현해 보도록 하겠습니다.

Guards의 역할은 request가 처리될지 말지를 결정합니다. 예를 들어 로그인에 정상적으로 성공한 경우 이후의 프로세스를 실행할 수 있지만, 로그인에 실패한 경우 더 이상 접근할 수 없도록 하는 것이지요.

 

'app.controllers.ts'를 열어 수정해줍니다. app.으로 왔다니 뭔가 끝이 보이는 거 같네요.

import { Controller, Request, Get, Post, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiParam } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { get } from 'http';
import { AppService } from './app.service';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';

@Controller('auth')
export class AppController {
  constructor(
    private authService: AuthService,
    private appService: AppService,
  ) {}
  // @UseGuards(AuthGuard('local'))
  @UseGuards(LocalAuthGuard)
  @Post('/login')
  async login(@Request() req) {
    // return req.user;
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('/profile')
  getProfile(@Request() req) {
    return req.user;
  }

  @Get('/helloworld')
  getHello(): string {
    return this.appService.getHello();
  }
  @Get('/health')
  @ApiOperation({
    summary: 'auth health check',
    description: 'auth application 이 정상 상태인지 체크한다.',
  })
  getHealth(): string {
    return this.appService.getHealth();
  }
}

guard에 대한 부분도 strategy와 같이 local, jwt가 있어요.

먼저 'local-auth.guard.ts'를 구현합니다.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

'jwt-auth.guard.ts'

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Add your custom authentication logic here
    // for example, call super.logIn(request) to establish a session.
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

 

이제 코딩은 완료되었습니다. 테스트를 진행해 볼까요.

$ yarn start

서비스는 정상적으로 올라왔네요!

이번에는 Postman을 이용하여 사용자 로그인 정보를 넘겨서 테스트해봅니다.

유효한 사용자 정보를 입력하니 access_token이 정상적으로 생성됩니다.

http://localhost:3000/auth/login

이 토큰을 가지고 profile 정보를 조회해 봅니다.

http://localhost:3000/auth/profile

토큰 정보로 사용자 정보를 가져오는 것까지 확인해 보았습니다.

728x90
반응형

'NestJS' 카테고리의 다른 글

[NestJS] Auth Token을 쿠키에 저장하기  (1) 2022.03.25
[NestJS] swagger에서 테스트하기  (0) 2022.03.23
[NestJS] CRUD 구현해보기  (0) 2022.03.07
[NestJS] app.controller 살펴보기  (0) 2022.03.02
[NestJS] 개발하기  (0) 2022.02.25

+ Recent posts