JWT

(Json Web Token)

헤더/페이로드/시그니처

이런식으로 구성되어 있음

[로그인]

  1. 프론트엔드에서 Login Request (id, pw)
  2. 시크릿 키로 JWT 를 만들어서 전달
  3. FE쿠키에 저장

[인증(글쓰기)]

  1. 글쓰기 API req
  2. 헤더에 JWT를 실어서 보내줌
  3. JWT 가드
  4. JWT Strategy (시크릿 키 디코딩 => 해당하는 User를 저장)
  5. 이 User에 대한 정보를 알아서 비즈니스 로직 수행하여 Response 를 보냄

Passport 를 거쳐서 감.

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

서비스, Module 만들기

$ nest g module auth
$ nest g service auth

jwt.guard.ts 만들기

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

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

여기서 AuthGuard는 stategy 를 자동으로 실행시키게 된다.

그러므로 jwt.strategy.ts 파일을 만들어 준다.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Payload } from './jwt.payload';
import { CatsRepository } from 'src/cats/cats.repository';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly catsRepository: CatsRepository) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
      ignoreExpiration: false,
    });
  }

  async validate(payload: Payload) {
    const cat = await this.catsRepository.findCatByIdWithoutPassword(
      payload.sub,
    );

    if (cat) {
      return cat; // request.user
    } else {
      throw new UnauthorizedException('접근 오류');
    }
  }
}

여기에는 전략과 연관된 정보가 들어가는 것이다.

scret key 같은 경우는 env 파일에서 지정해주는 것을 알 수 있음.

ignoreExpiration : 만료기간을 줄 수 있는데, 여기서 false 만료 없이

validate 를 수행을 해줌.

auth.module.ts

import { forwardRef, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt/jwt.strategy';
import { CatsModule } from 'src/cats/cats.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),

    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '1y' },
    }),

    forwardRef(() => CatsModule),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

PassportModule.register 여기서 strategy 설정 jwtModule 은 인증 쪽임

forwardRef(() => CatsModule)

여기서 CatsRepository 를 가져옴.

login.request.dto.ts

import { PickType } from '@nestjs/swagger';
import { Cat } from '../../cats/cats.schema';

export class LoginRequestDto extends PickType(Cat, [
  'email',
  'password',
] as const) {}

auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { CatsRepository } from 'src/cats/cats.repository';
import { LoginRequestDto } from './dto/login.request.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly catsRepository: CatsRepository,
    private jwtService: JwtService,
  ) {}

  async jwtLogIn(data: LoginRequestDto) {
    const { email, password } = data;

    //* 해당하는 email이 있는지
    const cat = await this.catsRepository.findCatByEmail(email);

    if (!cat) {
      throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
    }

    //* password가 일치한지
    const isPasswordValidated: boolean = await bcrypt.compare(
      password,
      cat.password,
    );

    if (!isPasswordValidated) {
      throw new UnauthorizedException('이메일과 비밀번호를 확인해주세요.');
    }

    const payload = { email: email, sub: cat.id };

    return {
      token: this.jwtService.sign(payload),
    };
  }
}

findCatByEmail 는 repository 에서 구현해줌

아래와 같이 함.

async findCatByEmail(email: string): Promise<Cat | null>{
  const cat = await this.catModel.findOne({ email });
  return cat;
}

그리고 passwordValidated 로 비밀번호 유효성 검증

JwtService 사용하기

auth.service.ts 파일에서

const payload = { email: email, sub: cat.id };

return {
  token: this.jwtService.sign(payload),
};

마지막에 return 할 때, jwtService를 호출해준다. (payload를 사용해줌.)

payload에서

  • sub : 토큰 제목을 의미함.

그러면 token을 만들어줌.

이러려면 당연히 상단에

constructor(
    private readonly catsRepository: CatsRepository,
    private jwtService: JwtService,
  ) {}

위와 같이 의존성 주입을 해주어야 하고

위에서 사용하는 JwtService는

auth.module.ts 에서 imports 시킨

JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '1y' },
    }),

에서 등록된 애를 사용하는 것이다.

순환 종속성 참조

forwardRef(() => CatsModule)

를 사용하는 이유임.

근데 작동을 안하고 있었음…

왜그런가 보니…

오류나는 화면

@InjectModel 을 빼니까 Repository를 제대로 참조해 오는 것을 보게 된다.

애초에 @InjectModel 이 무슨 역할을 해주는지도 모르겠다.

단순히 github에 올라가 있는 코드 따라 다른 것들을 적용해본거라…

아무튼 된다…

결과

{
    "success": true,
    "data": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im5hdnNraEBnbWFpbC5jb20iLCJzdWIiOiI2M2FiZThiNGU3NzEwZTE4MmFiYTI3YjEiLCJpYXQiOjE2NzIyMTI2NjIsImV4cCI6MTcwMzc3MDI2Mn0.JDN0s3FKEzdFXSsGKiPt4LJZByW9L2EPafZGWYpw5KA"
    }
}

위와 같이 token이 생성되어 오는 것을 확인하면 끝!

Alt text

sub에 아이디 나오고 exp 에 만료기간 (1년정도) 로 나오는 것을 확인할 수 있다.