어노테이션과 메타데이터를 이용해서 , 유저가 Admin인지 일반 User인지에따른 api 인가를 설정해보도록 하자.
이글을 통해서 얻어갈 수 있는점
- nestjs SetMetadata 를 통해서 어노테이션과, 메타데이터 설정
- 가드에서 accessToken의 유저정보를 확인후에 접근 제한 하기
- getAllAndOverride 메서드를 통해 클래스 레벨과 메소드 레벨 중 메소드 레벨에 우선순위두기
이 글의 내용들은 고스락 티켓예매 프로젝트(이하 고티켓) 에서 진행하는 백엔드에서 적용하고 사용중인 소스들의 예시이다.
1.0 버전의 ( 21번째 공연 예매에선 ) 어드민 유저와 일반 유저를 아예 분리를 시켰는데 role 기반 인가 연습도 할겸해서 한 테이블 내에 유저의 권한을 두었다.
관리자 페이지가 존재하고 , 주문 입금확인이나 조회등에서 관리자만 접근해야할 api들이 있다.
고티켓의 유저 모델에는
@Entity()
export class User {
@ApiProperty({
description: '유저의 권한입니다.',
enum: Role
})
@Expose()
@Column({
type: 'enum',
enum: Role,
default: Role.User
})
public role: Role;
// 간략화된 소스입니다.
}
enum Role {
User = 'User',
Admin = 'Admin'
}
이렇게 유저정보에 role 정보를 저장하는데
@ApiUnauthorizedResponse({
status: 401,
description: 'AccessToken 권한이 없을 경우'
})
@Roles(Role.Admin)
@Delete('/:id/comment')
async deleteComment(@Param('id') id: number)
위와 같은 방식으로 Role을 적어주면 접근할수있는 api에대해서 제한을 할수있다.
이제 적용하는 방법을 알아보
nestjs SetMetadata 를 통해서 어노테이션과, 메타데이터 설정
메타데이터 나 데코레이터 부분은 공식문서를 한번읽으면 도움이 된다.
컴파일 타임에서 런타임으로 넘어갈때 적어놓은 어노테이션으로 메타데이터를 설정할 수 있는데
어떠한 클래스나 클래스의 프로퍼티에 키값을 가지고 정보를 저장 할 수 있다.
주의할점은 메타데이터가 한번 설정한뒤에 그뒤에 동적으로 변화할 순없다.
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
common/decorators/roles.decorator.ts
롤들을 인자로 받는다. ...roles 는 @Roles("admin" ,"user" ) 처럼 이런형식으로 인자를 넘기면
["admin" ,"user" ] 로 변환을 해준다고 보면된다.
nest js 에서 제공해주는 SetMetadata를 활용해서 "roles" 라는 키에 권한 정보를 저장할 수 있다.
이렇게 컨트롤러 메서드나 컨트롤러 클래스에 적어주면 "누가 접근 할수 있는 메소드" 인지에 대한 정보를 얻을 수있다.
가드에서 인가 확인하기
import {
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from 'src/common/consts/enum';
@Injectable()
export class AccessTokenGuard implements CanActivate {
constructor(private authService: AuthService, private reflector: Reflector) {}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request, context);
}
private async validateRequest(request: Request, context: ExecutionContext) {
// 커스텀 데코레이터로 만든 롤을 가져옴
// 클래스와 함수 둘다 가져와야함.
// 함수 레벨한테 우선순위를 줌 ( 스프링처럼 자세한 부분이 더 우선순위 높도록)
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass()
]);
// accessToken payload 에서 유저 role 조회
const user = this.authService.verifyAccessJWT(jwtString);
// 적혀진 roles 정보가 없을때
if (!roles) {
return true;
}
// 적혀진 roles 정보 [] 일때 @Roles() 만준경우
if (!roles.length) {
return true;
}
else {
// 적힌 롤에 포함되는 권한을 가지고 있으면 통과
if (roles.includes(user.role) === true) {
return true;
}
// 어드민이면 다통과
else if (user.role === Role.Admin) {
return true;
}
// 모든케이스 위반시 403
else {
throw new ForbiddenException(
AuthErrorDefine['Auth-3000'],
'어드민 롤에 일반유저가 접근한 경우'
);
}
}
}
}
auth/guards/AccessToken.guard.ts
가드에서 제일 중요한점은 실행 컨텍스트를 가져올 수있다는 점인데.
현재클라이언트에서 요청한 api의 컨트롤러 클래스와 핸들러 ( 컨트롤러 메소드 ) 에 접근을 할 수있다는 점이다.
위에서 말했듯이 @Roles() 는 컨트롤러 클래스 에도 , 핸들러에도 적힐 수 있기 때문에
( 컨트롤러 클래스에 적히는경우 모든 해당 컨드롤러의 메소드를 admin레벨로 설정하는 등 )
+ 또한 실행컨텍스트를 통해서 클라이언트의 요청 헤더나 바디등에도 접근할수있다! 매우중요 ArgumentHost를 봐라!
네스트에서 주입받은 reflector를 사용하여 ( 가드 생성자에 있다 ) 현재의 실행되고있는 api의 클래스와 메소드 참조값을 넘긴다.
아마 내부적으론 reflect-metadata 활용해서 해당 참조값에 "roles"라는 키값을 가지고있는 메타데이터를 꺼내온다.
이때 getAllOverride 라는 메소드를 사용하고 있는데 이건
우선순위를 생각해서 가령 ( admin 컨트롤러 내부에 진짜 최종 꼭대기 관리자인 superAdmin 이 써야하는 api 가 있다면 )
메소드를 최상위 우선순위로 두고 덮어쓰기를 해야할듯 싶어서
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass()
]);
위와 같은 코드를 작성했다.
위는 우리가 생성자에서 주입받았던 리플렉터 인터페이스 이다 중요한부분은 저 targets 인데
클래스의 인스턴스를 넘겨줘야만 ( 플레인 객체 넘기면 아무것도없다 ) 리플렉션을 이용해서 메타데이터를 빼내올 수 있다는 점이고
우리는 실행컨텍스트에서 api요청이왔을때 어느 컨트롤러인지, 어느 컨트롤러의 메소드가 실행중인지 에 대한 target 을 가져와야 메타데이터를 빼내올 수 있다는 점이다.
그 메타데이터는 컴파일타임에서 런타임으로 넘어갈때 정해진다는 점. 꼭 기억해두길바란다.
구현체보면 뭐 리플렉트 메타데이터 써서 타겟에서 정보를 가져오고있다.
이렇게되면
@controller("admin")
@Roles("admin")
class AdminClass {
@Roles("superAdmin")
@Delete('admin/:id')
async deleteAdminUser(@Param('id') id: number)
}
위에 예시일때 delete api는 superAdmin만 실행시킬수있다.
현재 그리고 @Roles() 가 적혀있지 않다면 모든 사람 ( 즉 일반유저도 ) 실행시킬수있게 구성해놨다.
간단하다.
네스트 공식문서에는 setMatadata까진 나와있긴한데, role을 가지고 우선순위를 정해서 고티켓 프로젝트에 맞게 설정하는 구성을 해보았다. 내부적으로 어떻게 돌아가는지 이해하다보면 커스텀하기 점점 편해질 것이다.
'nest.js' 카테고리의 다른 글
[고스락 티켓 2.0] nestjs db rollback repository 테스트 (0) | 2022.08.14 |
---|---|
[고스락 티켓 2.0] nestjs transaction with repository (0) | 2022.08.14 |
[고스락 티켓 2.0] nestjs redis forRootAsync 모듈 만들기 (0) | 2022.08.13 |
[고스락 티켓 2.0] nest js ValidationError custom (1) | 2022.08.13 |
[티키타카] nestjs - mongoDB 적용하기 (4) | 2022.08.12 |