JS/Nest.js

[Nest] JWT 토큰발급 구현, 외부 모듈 import-export 하는법

Mini_96 2024. 9. 18. 16:44

* 의존성 주입

  • authModule에서 import => DI 컨테이너에 올라감
@Module({
  imports:[
    JwtModule.register({}),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
  • 서비스에서 생성자 주입받고 사용할수 있음.
@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService
  ) {
  }

 

* jwt 의사코드

/**
 * 1) register with email
 *    - email, nickname, password 입력받고 사용자 생성
 *    - 완료되면, accessToken과 refreshToken을 반환
 *      => 회원가입 후 다시 로그인 방지
 *
 * 2) loginWithEmail
 *    - email, password를 입력하면 사용자 검증을 진행한다.
 *    - 검증이 완료되면 accessToken과 refreshToken을 반환한다
 *
 * 3) loginUser
 *    - (1)과 (2)에서 필요한 acessToken, refreshToken을 반환
 *
 * 4) signToken
 *    - (3)에서 필요한 토큰들 sign
 *
 * 5) authenticateWithEmailAndPassword
 *    - (2)에서 로그인을 진행할때 필요한 기본적인 검증 진행
 *      1. 사용자가 존재하는지 확인 (email)
 *      2. 비밀번호가 맞는지 확인
 *      3. 모두 통과되면 찾은 사용자 정보 반환
 *      4. loginWithEmail에서 반환된 데이터를 기반으로 토큰 생성
 */

 

* 토큰 sign 함수

/**
 * Payload(실제 데이터)에 들어갈 정보
 *
 * 1) email
 * 2) sub == 사용자의 id
 * 3) type : access토큰인지, refresh 토큰인지
 *
 * Pick문법 : 가독성이 좋음 // email:string, id: string과 기능은 동일
 */
signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean){
  const payload = {
    email: user.email,
    sub: user.id,
    type: isRefreshToken ? 'refresh' : 'access',
  };

  return this.jwtService.sign(payload, {
    secret: 'sample',
    expiresIn: isRefreshToken? 3600 : 300,
  });
}

 

* authenticationWithEmailAndPassword 구현'

  • findUserByEmail 함수필요
async getUserByEmail(email: string){
  await this.usersRepository.findOne({
    where:{
      email,
    }
  })
}
  • 유저 서비스를 auth 서비스에서 사용해야함.
    • 임포트, 익스포트, 생성자 주입 다해야함
    • import시 모듈을 임포트해야함!! // 모듈을 export했으니 당연함 
@Module({
  imports:[
    JwtModule.register({}),
    UsersService,
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

모듈을 임포트
usersModule 내에서, UsersService라는 기능만 export 하겠다.

@Module({
  imports:[
    TypeOrmModule.forFeature([UsersModel]),
  ],
  exports:[
    UsersService
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}
@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
  ) {}

 

* registerWithEmail 구현

  • user service > create시 중복확인 추가
async createUser(user: Pick<UsersModel, 'email'|'nickname'|'password'>) {
  /**
   * 중복확인
   */
  const nicknameExists = await this.usersRepository.exists({
    where: {
      nickname: user.nickname,
    }
  });
  if(nicknameExists){
    throw new BadRequestException('이미 존재하는 nickname 입니다.');
  }

  const emailExists = await this.usersRepository.exists({
    where: {
      email: user.email,
    }
  });
  if(emailExists){
    throw new BadRequestException('이미 존재하는 email 입니다.');
  }
  • auth service
async registerWithEmail(user: Pick<UsersModel, 'nickname'| 'email'| 'password'>){
  const hash = await bcrypt.hash( //salt는 자동생성됨
    user.password,
    HASH_ROUNDS,
  )

  const newUser = await this.usersService.createUser(user);

  return this.loginUser(newUser);

}

 

* 컨트롤러 구현

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


  @Get('/login/email')
  loginEmail(
    @Body('email')email :string,
    @Body('password')password : string,){
    return this.authService.loginWithEmail({
      email,
      password,
    });
  }

  @Post('/register/email')
  registerEmail(
    @Body('nickname')nickname :string,
    @Body('email')email :string,
    @Body('password')password : string,){
    return this.authService.registerWithEmail({
      nickname,
      email,
      password,
    });
  }

 

* postman으로 테스트

{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImJpc3VAbmF2ZXIuY29tIiwic3ViIjoyLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzI2NjQzOTY3LCJleHAiOjE3MjY2NDQyNjd9._3Xj4JAYtRcFXqgBPZRUmNHUq6l7CnFDstKO00pSz6k",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImJpc3VAbmF2ZXIuY29tIiwic3ViIjoyLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcyNjY0Mzk2NywiZXhwIjoxNzI2NjQ3NTY3fQ.JZmeT4HxD1WY2EsCvQFCFjqkipxZfWnRY0i3MKm-qBc"
}

access token 검증, return된 token 값 == 사이트에서 만든 토큰값
refresh token 검증

 

* 디버그

  • 문제 : 비밀번호 맞게 입력했는데, 안됨
  • 원인 : await로 결과를 기다려야함
async authenticationWithEmailAndPassword(user: Pick<UsersModel, 'email' | 'password'>){
  const existingUser = await this.usersService.getUserByEmail(user.email);

  if(!existingUser){
    throw new UnauthorizedException('존재하지 않는 사용자 입니다.');
  }

  const passOk = await bcrypt.compare(user.password, existingUser.password);
  • 문제 : 그래도 미인증 사용자
  • 원인 : 123123\n도 안됨. 순수 123123만 있어야함