1)

npm install --save @nestjs/passport passport

npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

nest g module auth
nest g service auth

jwt.guard.ts

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

export class JwtAuthGuard extends AuthGuard('jwt') {

}

jwt.strategy.ts

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretKey: 'secretKey',
      ignoreExpiration: false,
    });
  }
}

인증/인가 구현을 위해서 passport library를 설치하고 auth 폴더와 아래 jwt 폴더를 생성했다. app.module의 imports에는 AuthModule이 자동으로 추가된다. passport는 인증을 위해 사용하는 것이고 JwtModule은 토큰 생성에 기여한다.

AuthGuard 역시 의존성 주입 되어질 수 있는 class인데 type인 IAuthGuard의 CanActivate와 ExecutionContext는 request 부분을 담당하고 IAuthModuleOptions에서 구체적인 strategy를 선택한다. token 검사 시, guard가 strategy를 부르는 구조를 기억하자.

JwtStrategy가 기본적으로 Passport library의 strategy를 따르되, 생성자 밑에 정의되어 있는 jwt 옵션을 사용하므로 JwtStrategy로 정의하는 구도로 보인다.

ExtractJwt에는 header, body, query 등 request의 어떤 부분에서 Jwt를 추출할 것인지 method가 나열돼있다.

2)

auth.service.ts

import { Injectable } from '@nestjs/common';
import { CatsRepository } from 'src/cats/cats.repository';

@Injectable()
export class AuthService {
  constructor(private readonly CatsRepository: CatsRepository) {}
}

auth.module.ts

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    JwtModule.register({
      secret: 'secret',
      signOptions: { expiresIn: '1y' },
    }),
    CatsModule,
  ],
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

cats.module.ts

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
  exports: [CatsService, CatsRepository],
})

CatsRepository -> AuthService -> cats.controller 순으로 의존성이 주입되는데 auth.module의 provider에 먼저 CatsRepository를 등록해줘야 한다.

prodivers에 직접 등록할 수도 있다.(확실해?) 그런데 cats.module에서 CatsRepository를 공급 받는 providers 영역은 이미 '캡슐화' 되어 있어서 exports에 CatsRepository를 추가하고, CatsModule 자체를 exports/imports하는 방향을 택했다.

PassportModule, JwtModule 같이 이미 library 내에 심어진 모듈들은 동적 모듈화 옵션이 잘 제공되는 것 같다.

3)

auth.service.ts

@Injectable()
export class AuthService {
  constructor(private readonly catsRepository: CatsRepository,
    private jwtService: JwtService,
    ) {}
...
    const payload = {
      email: email,
      sub: cat.id,
    };
    return {
      token: this.jwtService.sign(payload),
    };

auth.module.ts

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    JwtModule.register({
      secret: 'secret',
      signOptions: { expiresIn: '1y' },
    }),
    CatsModule,
  ],

의존성 주입되는 JwtService는 auth.module에 등록한 JwtModule에서 제공한다. 유효성 검사를 통과한 email과 cat의 고유 식별자로 payload를 만들고 서명한 후 토급을 발급한다.

4)

cats.controller.ts

@Controller('cats')
@UseInterceptors(SuccessInterceptor)
@UseFilters(HttpExceptionFilter)
export class CatsController {
  constructor(
    private readonly catsService: CatsService,
    private readonly authService: AuthService,
  ) {}

cats.module.ts

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]),
    forwardRef(() => AuthModule),
  ],
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
  exports: [CatsService, CatsRepository],
})

AuthService를 cats.controller에서 사용하기 위해서 의존성을 주입해주고 cats.module에 module 자체를 imports 해주는데 여기서 순환 참조 문제가 발생한다. 이를 cats.module, auth.module 양쪽에서 forwardRef() method로 처리한다.

순환 참조는 (module이나 provider에서 사용되는) class를 참조해서 instance를 만들어야 하는데 (서로 참조 관계일 때) metadata가 전제되지 않아서 instance를 뱉어내지 못하는 상황으로 보인다. forwardRef()말고 Module ref 방식도 있다고 한다.

https://docs.nestjs.com/fundamentals/circular-dependency 참조

5)

cats.repository.ts

  async findCatByIdWithoutPassword(catId: string): Promise<Cat | null> {
    const cat = await this.catModel.findById(catId).select('-password');
    return cat;
  }

select()를 사용하면 찾은 model에서 property를 선택적으로 가져올 수 있다.

6)

cats.controller.ts

  @ApiOperation({ summary: '현재 고양이 정보를 가져옵니다..' })
  @UseGuards(JwtAuthGuard)
  @Get()
  getCurrentCat(@Req() req: Request) {
    return req.user;
  }

JwtAuthGuard가 확장하는 AuthGuard의 handleRequest 부분에서 request에 user를 심어주기 때문에 req.user를 반환할 수 있는 것으로 보인다.

7)

user.decorator.ts

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

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

cats.controller.ts

 @ApiOperation({ summary: '현재 고양이 정보를 가져옵니다..' })
  @UseGuards(JwtAuthGuard)
  @Get()
  getCurrentCat(@CurrentUser() cat) {
    return cat.readOnlyData;
  }

custom decorator를 사용해서 추상화를 진행할 수 있다.

https://docs.nestjs.com/custom-decorators 참조

8)

npm install express-basic-auth

main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter());
  app.use(
    ['/docs', '/docs-json'],
    expressBasicAuth({
      challenge: true,
      users: {
        [process.env.SWAGGER_USER]: process.env.SWAAGGER_PASSWORD,
      },
    }),
  );

auth.module.ts

@Module({
  imports: [
    ConfigModule.forRoot(),
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '1y' },
    }),
    forwardRef(() => CatsModule),
  ],

swagger 노출을 막기 위해 BasicAuth를 사용했다. .env는 secret key도 기입했는데 auth.module에 ConfigModule.forRoot() 설정이 필요하다. 환경변수 설정에 따라다니는 module로 기억하자.

0개의 댓글