Nest. bcypt & JWT

cm·2024년 1월 20일

Nest

목록 보기
5/7

1. 암호화 방법

  • 원본 비밀번호 저장 : 최악
  • 비밀번호를 암호화 키와 함께 암호화 (양방향) : 암호화키 노출되면 위험도 높음
  • SHA256 등으로 hash해서 저장 (단방향) : 복호화 불가, 레인보우 테이블로 위험도 높음
  • 레인보우 테이블 : 암호화 해시 함수에서 사용될 수 있는 가능한 모든 평문 문자열과 해당 문자열의 해시 값을 사전에 계산하여 저장해 둔 테이블
  • salt + 비밀번호를 hash해서 저장 : bryptjs을 이용

    참고: bcrypt의 작동원리와 노드에서 사용하는 방법


2. bcryptjs : 암호화

npm install bcryptjs --save
# import * as bcrypt from 'bcryptjs'
  • repository
    async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> {
      const { username, password } = authCredentialsDto;
      const salt = await bcrypt.genSalt();
      const hashedPassword = await bcrypt.hash(password, salt);
      const user = this.create({ username, password: hashedPassword });
      ...
      }

2-1. 로그인 기능 구현하기

  • service 저장된 해시 값에서 솔트를 추출한다. 추출된 솔트를 사용하여 제공된 평문 비밀번호를 다시 해시하고 두 해시 값을 비교한다. 따라서, 별도로 솔트를 저장하지 않아도 해시와 비교 과정에서 자동으로 솔트를 사용할 수 있다.
  async signIn(authCredentialsDto: AuthCredentialsDto): Promise<string> {
    const { username, password } = authCredentialsDto;
    const user = await this.userRepository.findOne({ where: { username } });
    if (user && (await bcrypt.compare(password, user.password))) {
      return 'logIn success';
    } else {
      throw new UnauthorizedException('logIn failed');
    }
  }
  • controller
   @Post('/signin')
  signIn(@Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto) {
    return this.authService.signIn(authCredentialsDto);
  }

3. JWT (JSON Web Token)

  • JWT : 정보를 안전하게 전달하기 위한 작은 크기의 토큰으로 인증 및 정보교환에 사용된다.
  • Passport : JWT를 이용해서 인증 처리를 쉽게 만들어주는 모듈.
    • 역할 : JWT 토큰을 받은 유저가 JWT 토큰을 포함한 HTTP 헤더와 유저 이름을 넣은 payload를 넣어서 서버에 요청을 보낸다. 서버에서는 JWT 토큰이 유효한지 secret text를 이용해서 확인한다. 동일하면 payload 내 유저이름을 이용해서 Database 안에 있는 유저 이름에 해당하는 유저 정보를 모두 가져올 수 있다. 이러한 처리를 쉽게 해주는 모듈이다.

pnpm install @nestjs/jwt @nestjs/passport passport passport-jwt @types/passport-jwt --save

3-1. JWT/Passport 모듈 등록하기

  • PassportModule : jwt로 설정
  • JwtModule : secret과 signOptions 설정
    • secret : secret text 설정
    • signOptions: JWT 서명에 사용되는 옵션
      • algorithm: JWT 서명에 사용할 알고리즘(default:'HS256')
      • expiresIn: 토큰 유효기간 (초 단위로 지정/3600초 === 1시간)
      • notBefore: 토큰사용을 시작할 수 있는 시간
      • issuer 및 audience: 토큰의 발급자와 대상
      • jwtid: JWT의 고유 식별자(ID)
      • subject: 토큰주제
@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 3600,
      },
    }),
    TypeOrmModule.forFeature([UserRepository]),
  ],
  controllers: [AuthController],
  providers: [AuthService, UserRepository],
})
export class AuthModule {}

3-2. 로그인 성공시 JWT를 이용해서 토큰 생성해주기

  • service

    • 생성자에 jwtService 추가

        private jwtService: JwtService,
    • jwtService.sign(payload) : sign 메서드는 내부적으로 header, payload, secret text(환경설정에서 정의된)를 사용하여 JWT 생성한다. (내부적으로 복잡한 과정을 단순화)

      async signIn(
        authCredentialsDto: AuthCredentialsDto,
      ): Promise<{ accessToken: string }> {
        const { username, password } = authCredentialsDto;
        const user = await this.userRepository.findOne({ where: { username } });
        if (user && (await bcrypt.compare(password, user.password))) {
          const payload = { username };
          const accessToken = await this.jwtService.sign(payload);
          return { accessToken };
        } else {
          throw new UnauthorizedException('logIn failed');
        }
      }
    • controller : 반환타입 추가

      @Post('/signin')
      signIn(
        @Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto,
      ): Promise<{ accessToken: string }> {
        return this.authService.signIn(authCredentialsDto);
      }

3-3. Passport, JWT 이용해서 토큰 인증 후 유저 정보 가져오기

  • jwt.strategy.ts 파일 생성
// jwt.strategy를 다른곳에서도 사용할 수 있도록 @Injectable()
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(UserRepository)
    private userRepository: UserRepository,
  ) {
    super({
      secretOrKey: 'Secret1234', // 토큰 유효성 체크할 때 쓰는 secret text
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // auth header에서 토큰타입이 bearerToken인 토큰을 추출했다.
    });
  }

  // 위에서 토큰이 유효한지 체크되면 validate 메소드에서 payload에 있는 유저이름이 데이터베이스에서 있는 유저인지 확인후 있다면 유저객체를 return 해준다.
  // return 값은 @UseGuards(AuthGuard())를 이용한 모든 요청의 Request Object에 들어간다.
  async validate(payload) {
    const { username } = payload;
    const user: User = await this.userRepository.findOne({
      where: { username },
    });
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

모듈 'passport-jwt'의 Strategy의 생성자에 포함된 secretOrKey와jwtFromRequest의 값을 상속받아 입력해준다. secretOrKey에 입력된 secret text을 통해 토큰 유효성을 체크하고 jwtFromRequest에는 헤더에서 토큰타입이 bearerToken인 토큰을 추출한다. 토큰이 유효한 경우, validate 메소드가 실행된다. validate는 유효한 토큰의 payload를 받아와서 해당 정보를 사용하여 사용자를 조회하고, 조회된 사용자 객체를 반환한다. 반환된 사용자 객체는 @UseGuards(AuthGuard())를 이용한 모든 요청의 Request Object에 들어간다. 실행 시점은 Passport의 AuthGuard에서 관리된다. @UseGuards(AuthGuard())를 사용하는 라우터나 컨트롤러 메소드에서 해당 전략을 사용하고자 할 때 Passport가 JwtStrategy를 호출하고 유효성 검사를 수행하게 된다. 실패 시에는 Passport에서 자동으로 Unauthorized 예외를 발생시키게 되고, 해당 예외는 Express 미들웨어에서 처리할 수 있다.

  • auth.module.ts에 추가

    • providers: Auth 모듈에서 사용할 수 있도록 JwtStrategy 등록
    • exports: 다른 모듈에서 사용할 수 있도록 JwtStrategy, PassportModule 등록
  • controller

    • @UseGuards : Guard 미들웨어를 처리해주는 데코데이터
    • AuthGuard() : @nestjs/passport에서 가져온 메소드. JWT 토큰을 해석하고 이를 기반으로 사용자를 인증하는 역할. (토큰이 있다면 유저객체를, 토큰이 없거나, 잘못되면 Error!)
    @Post('/authTest')
    @UseGuards(AuthGuard()) //@UseGuards 안에 @nestjs/passport에서 가져온 AuthGuard()를 이용하면 요청안에 유저 정보를 넣어줄 수 있다.
    authTest(@Req() req) {
      console.log(req.user); //req.user: 유저객체
    }

3-4. 커스텀 데코레이터

  • req.user가 아닌 바로 user를 가져올 수 있도록 하려면 커스텀 데코레이터를 사용하면 된다.
  • get-user.decorator.ts 생성
    • data: 사용자가 데코레이터를 사용할 때 전달할 수 있는 추가 데이터
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { User } from './user.entity';

export const GetUser = createParamDecorator(
  (data, ctx: ExecutionContext): User => {
    const req = ctx.switchToHttp().getRequest();
    return req.user;
  },
);
  • controller
  @Post('/authTest')
  @UseGuards(AuthGuard())
  test(@GetUser() user: User) {
    console.log('user: ', user);
  }

3-5. 인증된 유저만 게시물 보고 쓸 수 있게 만들기

  • 유저에게 게시물 접근 권한 주기
    • board에서 인증된 유저인지 확인하기 위해서는 우선 AuthModule import
    • import하면 AuthModule에서 하는 어떠한 것이든 BoardModule에서 사용가능하다.
    @Module({
      imports: [TypeOrmModule.forFeature([BoardRepository]), AuthModule],
      controllers: [BoardsController],
      providers: [BoardsService, BoardRepository],
    })
    export class BoardsModule {}
    • board.controller.ts : 핸들러 레벨에서 모두 적용되도록 @UseGuards(AuthGuard()) 추가
    @Controller('boards')
    @UseGuards(AuthGuard())
    • 올바른 토큰을 주지 않으면 401 Unauthorized Error 발생한다.
profile
나를 위한 기록

0개의 댓글