[NestJS] - 18. JWT 토큰 인증 - 회원가입/로그인 구현

2023. 3. 4. 03:44
반응형

 

거의 모든 웹 어플리케이션에서 인증과 인가의 구현은 빠질 수 없는 존재이다.

이번 포스팅부터는 JWT 토큰 인증 방식을 NestJS에서 구현해보는 것을 목표로 하려고 한다.

JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다.

https://sjh9708.tistory.com/46

 

[Web] 인증과 인가 - JWT 토큰 인증

앞 포스팅에서 세션 방식의 인증과, 성능 개선을 위한 방법들에 대해서 다루어 보았었는데 이번에는 언급했던 토큰 인증 방식에 대해서 알아보려고 한다. 토큰 인증 세션 인증 방식과 달리 인증

sjh9708.tistory.com

 

 


회원 엔티티 생성 및 회원가입 구현

 

user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column()
  email: string;

  @Column()
  password: string;

  @Column()
  name: string;

  @Column()
  age: number;

  @Column({ default: 0 })
  point: number;
}

회원 엔티티를 간단하게 만들어 주었다.

 

 

 user.controller.ts

@Controller('user')
export class UserController {
  PASSWORD_SALT = 10;
  constructor(
    private readonly userService: UserService, //
  ) {}

  @Post()
  async createUser(@Body() input: CreateUserInput): Promise<User> {
    const hashedPassword = await bcrypt.hash(
      input.password,
      this.PASSWORD_SALT,
    );
    const user = {
      ...input,
      password: hashedPassword,
    };
    return this.userService.create(user);
  }

}
  • POST : /user 요청에 대한 회원 가입 컨트롤러이다.
  • 비밀번호와 같은 민감정보는 암호화하여 데이터베이스에 저장해야 한다. bcrypt를 통해 인코딩해주었다. 
  • bcrypt의 인자로는 평문과, 해시 알고리즘에 사용될 Salt값을 작성해준다.
  • bcrypt 패키지는 다음의 패키지를 설치해주면 된다.

 

yarn add bcrypt @types/bcrypt

 

 

 user.service.ts

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  // 유저 회원가입
  async create(input: CreateUserInput): Promise<User> {
    const { name, password, email, age } = input;
    const user = await this.userRepository.findOne({ email });
    if (user) {
      throw new ConflictException('이미 등록된 이메일입니다.');
    }
    const result = await this.userRepository.save({
      email,
      password,
      name,
      age,
    });

    return result;
  }

}

암호화한 비밀번호를 포함하여 데이터베이스에 Insert한다. 중복된 이메일이라면 ConfilctException을 반환해주었다.

 

 

 

 

 


로그인 구현 : Access Token과 Refresh Token 생성

 

이제 위에서 가입한 회원 정보로 로그인하면 JWT 토큰을 발급하는 API를 만들 것이다. JWT 토큰 생성을 도와주는 아래의 패키지를 설치하자

 

yarn add @nestjs/jwt

 

 

로그인할때 사용될 이메일로 유저를 조화하는 서비스를 user.service.ts에 미리 만들어두자.

 

 user.service.ts

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  // 유저 회원가입
  async create(input: CreateUserInput): Promise<User> {
  // ...
  }
  
    //유저 조회
  async findOne(email): Promise<User> {
    return await this.userRepository.findOne({ email });
  }

}

 

 

이제 인증을 담당할 Auth 모듈 구조를 생성하자

 

 

auth.module.ts

@Module({
  imports: [
    JwtModule.register({}), //
    TypeOrmModule.forFeature([User]), //유저 관련 DB 엑세스에 필요
  ],
  controllers: [AuthController],
  providers: [AuthService, UserService],
})
export class AuthModule {
  //
}

아까 설치한 패키지의 Jwt 모듈을 사용하기 위해서 Import해주었다.

 

 

 auth.service.ts

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


@Injectable()
export class AuthService {
  constructor(
    //주입받은 JWT Module의 서비스 이용
    private readonly jwtService: JwtService,
  ) {}

  //Access Token 발급
  getAccessToken({ user }): String {
    return this.jwtService.sign(
      {
        email: user.email,
        sub: user.id,
      },
      {
        secret: process.env.ACCESS_TOKEN_SECRET_KEY,
        expiresIn: '5m',
      },
    );
  }
}

Access Token을 생성하는 로직이다.

  • 컨트롤러에서 사용될 JWT 토큰을 생성하는 로직을 서비스에 구현하였다.
  • 모듈에서 주입받은 jwtService.sign()을 이용하여 JWT 토큰을 생성한다.
  • 인자로는 JWT Token의 Payload에 해당하는 부분과 옵션을 작성하였다.
  • 페이로드에는 중요한 정보를 담지 않으므로 email이나 유저ID정도를 담아주었다.
  • 옵션에서는 JWT 서명에 필요한 비밀키와 토큰의 만료시간을 설정해주었다. 
  • Access Token을 발급할 것이므로 만료시간은 짧게 주었다.

 

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


@Injectable()
export class AuthService {
  constructor(
    // 주입받은 JWT Module의 서비스 이용
    private readonly jwtService: JwtService,
  ) {}

  //Access Token 발급
  getAccessToken({ user }): String {
  //...
  }
  
  setRefreshToken({ user, res }) {
    const refreshToken = this.jwtService.sign(
      {
        email: user.email,
        sub: user.id,
      },
      {
        secret: process.env.REFRESH_TOKEN_SECRET_KEY,
        expiresIn: '2w',
      },
    );
    //배포환경에서는 쿠키 보안옵션과 CORS 추가해주어야함
    res.setHeader('Set-Cookie', `refreshToken=${refreshToken}`);
    return;
  }  
}

Refresh Token을 생성하는 로직도 함께 만들어주었다.

  • 기본적으로 Access Token과 마찬가지로 JWT이므로 사용법은 같다.
  • Refresh Token을 발급할 것이므로 만료시간은 길게 주었다.
  • Refresh Token은 문자열을 반환하지 않고 클라이언트의 쿠키에 저장시킬 것이다. 따라서 Response 객체에서 쿠키를 설정해주었다.

 

 

 auth.controller.ts

@Controller('auth')
export class AuthController {
  constructor(
    private readonly userService: UserService, //
    private readonly authService: AuthService,
  ) {}

  @Post('login')
  async login(@Body() input: LoginInput, @Res() res: Response) {
    const { email, password } = input;
    //1. 이메일, 비밀번호 일치 유저 찾기
    const user = await this.userService.findOne(email);

    //2. 일치하는 유저 없으면 에러
    if (!user) {
      throw new UnprocessableEntityException('이메일이 없습니다.');
    }

    //3. 일치하는 유저가 있지만 비밀번호 틀린경우 에러
    const isAuth = await bcrypt.compare(password, user.password);

    if (!isAuth) {
      throw new UnprocessableEntityException('비밀번호가 일치하지 않습니다.');
    }

    //4. 모두 일치 유저가 있다면 JWT Refresh Token 쿠키에 발급
    this.authService.setRefreshToken({ user, res });

    const jwt = this.authService.getAccessToken({ user });
    //5. 모두 일치 유저가 있다면 JWT Access Token 발급
    return res.status(200).send(jwt);
  }
}

이제 만들어둔 서비스 로직들을 이용해서 로그인 컨트롤러를 작성해보자,

  • 우선 아이디와 비밀번호가 일치하는 유저를 찾는다. 유저가 없다면 에러
  • 모두 일치한다면 Response 객체에 Refresh Token을 쿠키설정을 해준다.
  • 응답으로는 Access Token의 문자열을 반환한다.
  • @Res()를 통해 Response 객체를 이용할 때에는 res.status().send()나 res.status().json()을 통해 응답해야 한다.

 

 

 


로그인 결과

Access Token이 발급되었다.
쿠키에는 RefreshToken이 저장되었다.

 

 

클라이언트는 발급받은 토큰들을 App 내부, 쿠키, 스토리지 등에 저장해두고 나서 인가에 사용하게 된다.

다음 포스팅에서는 해당 토큰들을 이용한 인가 과정에 대해서 포스팅해보려고 한다.

반응형

BELATED ARTICLES

more