Authentication와 Guard

권한없는 요청 거부하기

Authentication

Custom Decorator 만들기

handler에게 자동으로 현재 로그인한 유저를 알려주는 기능을 만들려고 한다.

우리는 기존의 @Session() 대신 새로운 우리만의 decorator를 만들 것이다.

우리가 만들 CurrentUser Decorator는 2가지가 필요할 것이다.

  • Session Object : 요청한 유저의 id를 알아야 한다.
  • UsersService Instance : 알아낸 id를 가진 사용자를 찾아야 하기 떄문이다.

우리는 Param decorators로 만들어야 한다.

하지만 Param decorators는 DI(종속성 주입) 시스템 외부에 있어서, UsersService 인스턴스를 받을 수 없다. 이 문제를 해결하기 위해 **interceptor**를 만든다.

인터셉터는 실제로 데이터베이스에서 id를 통해 사용자를 가져오는 역할을 한다. 이후 decorator에게 이를 넘기고 다시 handler에게 넘겨질 것이다.

우선 user 디렉토리에 decorators 폴더를 만들고 내부에 current-user.decorator.ts 파일을 만들자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// current-user.decorator.ts
import {
    createParamDecorator,
    ExecutionContext
} from '@nestjs/common';

// ExecutionContext는 http에서 request라고 생각하면 된다.
// decorators의 첫번째 인수가 data가 된다.
// 우리가 만들 decorator는 인수가 필요 없으므로 안받기 위해 any가 아닌 never로 설정
export const CurrentUser = createParamDecorator(
    (data: never, context: ExecutionContext) => { 
        const request = context.switchToHttp().getRequest();
        request.session.userId
    }
)

이제 인터셉터를 만든다. 기존에 가지고 있는 인터셉터 디렉토리가 아니라 유저 디렉토리에 만들자.

유저 디렉토리에 interceptors 폴더를 만들고 내부에 current-user.interceptor.ts 파일을 만들자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// current-user.interceptor.ts
import {
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Injectable,
} from '@nestjs/common';
import { UsersService } from '../users.service';

@Injectable()
export class CurrentUserInterceptor implements NestInterceptor {
  constructor(private usersService: UsersService) {}

  async intercept(context: ExecutionContext, handler: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const { userId } = request.session || {};

    if (userId) {
      const user = await this.usersService.findOne(userId);
      request.currentUser = user;
    }

    return handler.handle();
  }
}

decorator도 수정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
// user.decorator.ts
import {
    createParamDecorator,
    ExecutionContext
} from '@nestjs/common';

export const CurrentUser = createParamDecorator(
    (data: never, context: ExecutionContext) => { 
        const request = context.switchToHttp().getRequest();
        return request.currentUser;
    }
)

이제 interceptor를 애플리케이션에 연결한다. 저번 Serialize 처럼 전체에 연결하는 것은 비효율적이다. 이 말은 어떤 컨트롤러를 사용하던, 그 이전에 인터셉터가 선행된다는 의미이다.

여기서 조금 바꾸어, 인터셉터를 전역으로 설정해주면 어떤 컨트롤러로 들어오던 요청이든 애플리케이션에 들어오는 모든 요청에 일괄적으로 먼저 인터셉터가 적용된다. 각 컨트롤러마다 설정해 줄 필요가 없으니 코드가 절약된다. 시도해보자

모듈에 추가하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// users.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core'; // 추가한 코드 1
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { CurrentUserInterceptor } from './interceptors/current-user.interceptor';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [
    UsersService,
    AuthService,
    { provide: APP_INTERCEPTOR, useClass: CurrentUserInterceptor }, // 추가한 코드 2
  ],
})
export class UsersModule {}

이제 API Client를 통해 로그인되지 않은 상태에서 현재 로그인된 ID를 요청해보자. 아무것도 돌아오지 않아야 성공.

Guard로 특정 요청의 접근 막기

class AuthGuard

canActivate() : 유저가 route에 접근할 수 있는지 true of false 반환

가드도 인터셉터와 비슷하게 전역으로, 혹은 각 컨트롤러에, 혹은 각 핸들러에 추가할 수 있다.

src폴더에 guards 폴더를 만들고 내부에 auth.guard.ts 파일을 만들자

1
2
3
4
5
6
7
8
9
10
11
// auth.guard.ts
import { CanActivate, ExecutionContext } from '@nestjs/common';

export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();

    return request.session.userId;
  }
}

컨트롤러에 적용해보자. whoamI 에 적용하겠다.

1
2
3
4
5
6
7
8
9
// users.controller.ts
import { Useguard } from '@nestjs/common'; // decorator 추가
import { AuthGuard } from '../guards/auth.guard';;

@Get('/whoami')
  @UseGuards(AuthGuard)
  whoAmI(@Session() session: any) {
    return this.usersService.findOne(session.userId);
  }

API Client로 로그인되지 않은채 저 루트를 실행해보면 작동되지 않는다.

{ “message”: “Forbidden resource”, “error”: “Forbidden”, “statusCode”: 403 }

라고 거부된다.