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 }
라고 거부된다.