본문 바로가기
Archive

Passport 와 JWT 인증 & Custom Decorator

by livemehere 2022. 8. 1.

보통서버는 인증된 사용자에게만 리소스를 제공한다.

사용자를 검증하는 방법에는 cookie와 session, 혹은 JWT와 같은 토큰, Oauth 와 같은 써드파티의 인증서비스를 이용한다.

node 생태계에서는 passport라는 인증모듈이 검증되고, 써드파티와의 연동도 손쉽게 제공한다.

또 nest에서는 passport를 Gards에 위임하여 전체 서비스에서 활용할 수 있다.

 

인증 절차 흐름

auth/login -> passport에서 DB의 유저 조회 -> Request.user에 유저정보 추가됨 -> 그정보를 가지고 JWT 생성 후 반환.

 

사용자는 토큰을 header에 담아서 서버 API 사용 -> 사용전에 JWT Guards 가 토큰 검증 -> 검증되면 API로 패싱, 아니면 거절

 

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

임시 User module

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

interface User {
  userId: number;
  username: string;
  password: number;
}

@Injectable()
export class UsersService {
  private users = [
    {
      userId: 1,
      username: 'kong',
      password: 1234,
    },
    {
      userId: 2,
      username: 'ha',
      password: 5678,
    },
  ];

  async getUserById(id: number): Promise<User | undefined> {
    return this.users.find((u) => u.userId === id);
  }
}

 

유저 모듈은 auth(인증)모듈에서 사용하기 때문에 exports

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

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

 

AuthModule

userModule DI 하기

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})

 

비밀번호 검증 service 만들기

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

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

  async validateUser(username: string, password: string) {
    const user = await this.usersService.findByName(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

Passport Local Strategy 구현

DI하여 AuthGuards()에 사용됨

말그대로 전략을 말하는 것인데, 인증할때 어떤 전략을 사용하느냐이다.

local strategy 라고하면, 자체적인 로그인 서비스를 말한다.

자체적으로 인증전략을 사용하려면 디비에 존재하는 유저정보에서 아이디와 패스워드를 검증하는 방식이 일반적이다.

아래 코드는 이제껏 user서비스를 만들고, auth에서 비밀번호를 검증을 거쳐 찾아낸 유저를 반환하는 내용이다.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local'; // 이것을 사용하기 때문에 AuthGuard('local') 사용이 가능함
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { 
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string) {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      return new UnauthorizedException();
    }
    return user;
  }
}

 

auth.module에 직접 작성한 LocalStrategy 를 프로바이더로 제공하고, Passport 를 사용하기위해 모듈을 추가해준다

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

 

 

 

로그인 API 생성

컨트롤러에 AuthGuard('local')을 붙여주면 LocalStrategy의 validate 가 실행된다.

성공한다면 Req객체의 user에 validate에서의 반환값이 담긴다.

import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
  
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Req() req) {
    return req.user; // 최종 반환값인데, 이를 JWT 토큰으로 변경해주어야한다.
  }
}

Jwt 모듈 추가 및 사용

여러곳에서 참조되기 때문에 변수로 분리

export const jwtSecret = {
  secret: 'kong',
};

auth 모듈에 jwtModule imports 하기

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({ // 추가
      secret: 'kong',
      signOptions: { expiresIn: '30s' },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

 

토큰생성 service 생성

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

 

로그인 API 반환값 토큰으로 변경

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

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Req() req) {
    return this.authService.login(req.user);
  }
}

DI하여 AuthGuards()에 사용됨

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtSecret.secret,
    });
  }
  // payload 는 header에 담긴 토큰이 넘어온다.
  async validate(payload: any) {
    return { userId: payload.userId, username: payload.username }; // Req에 주입되는 데이터
  }
}

 

AuthModule에 DI하기

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtSecret.secret,
      signOptions: { expiresIn: '30s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

 

custom guards 만들어서 문자열 입력부 제거하기

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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

 

Controller에 직접 사용하기

아래 가드를 사용하면, header에서 jwt토큰을 검증하고, 통과할 경우에만 다음으로 패싱한다.

인증모듈 완성!

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll() {
    return this.postsService.findAll();
  }

 

보통 모든 Controller에 이렇게 붙여주는것 보다는 전역가드를 설정해둘 수 도 있다.

거의모든 서비스는 인증이 필요하도록 하고, 몇몇을 공개로 두는것이 나으니까!

아래처럼하면 로그인 조차도 영향을 받아버리기 때문에 Public 데코레이터를 만들어주어서 jwt가드의 영향을 안받도록 해주어야한다.

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.useGlobalGuards(new JwtAuthGuard());
  .
  .

혹은 Controller단위로 달아줄 수 도 있다.

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { PostsService } from './posts.service';

@UseGuards(JwtAuthGuard)
@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  @Post()
  create(@Body() body: any) {
    return this.postsService.createOne(body);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() body: any) {
    return this.postsService.updateById(+id, body);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.postsService.removeById(+id);
  }
}

 

Custom Decorator 만들기

헤더의 토큰을 가져오기위한 데코레이터를 만들 수도 있다.

createParamDecorator()로 새로운 객체를 생성하면 return 값이 데코레이터의 리턴값이 된다.

중요한건 data,ctx 이고, ctx 에서 request객체를 꺼내올 수 있다.

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

export const Token = createParamDecorator((data, ctx) => {
  const request = ctx.switchToHttp().getRequest();
  return request.headers.authorization;
});

 

사용하기

  @Get('user')
  getUser(@Req() req, @Token() token) {
    console.log(token);
    return token;
  }

 

반응형

'Archive' 카테고리의 다른 글

TypeORM timezone, charset 설정  (0) 2022.08.01
DB의 시간차이 문제  (0) 2022.08.01
Cookie & Session  (0) 2022.08.01
CORS 와 Cookie  (0) 2022.07.31
static 파일제공 & View Engine & React  (0) 2022.07.31