프로젝트를 진행하며 모든 데이터에 대한 CRUD API를 만들었다. 기능을 추가해 가면서 CRUD API를 관리자 전용으로 바꾸기로 해서 모든 User에 Role을 부여하고 이를 통한 인가를 구현하기로 했다.
인증? 인가?
인증이란, 사용자의 신원 그 자체를 검사하는 프로세스를 말한다. 로그인을 인증의 예시로 들 수 있다.
인가란, 요청을 보낸 클라이언트를 식별하여 해당 클라이언트의 요청에 대한 권한을 검사하는 것을 말한다. 관리자 전용 API 등을 예시로 들 수 있다.
User Entity
User Entity를 다음과 같이 정의할 수 있다. User Entity의 role을 사용해 인가를 구현할 것이다. (예시 코드)
/**
* @description
* 일반 사용자 - USER
* 관리자 - ADMIN
* 모든 권한 - ANY
*/
export enum UserRole {
USER = 'USER',
ADMIN = 'ADMIN',
ANY = 'ANY',
}
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
@IsString()
nickname!: string;
@Column({ unique: true })
@IsEmail()
email!: string;
@Column({ select: false })
@IsString()
@Matches(/^(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/, {
message:
'Password must be at least 8 characters(en) long, contain 1 number',
})
password!: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
@IsEnum(UserRole)
role!: UserRole;
}
Role 데코레이터
@Role() Decorator를 사용하여 route Handler에 Custom Metadata를 붙이고, 특정 Route에 접근하기 위한 Role을 지정할 수 있게 해 줄 것이다. SetMetadata를 통해 roles라는 metadata를 사용할 수 있는 기능을 구현한다. 아까 User Entity에 정의해 두었던 UserRole enum을 사용해 다음과 같이 구현할 수 있다.
//role.decorator.ts
export const Role = (...roles: UserRole[]) => SetMetadata('roles', roles);
Role Guard
route Handler에 roles라는 Metadata를 붙였으니 이제 이를 사용해서 해당 User에 대한 validation을 해 주면 된다.
//role.guard.ts
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<UserRole>('roles', context.getHandler());
if (!roles) {
throw new NotFoundException('Roles not defined');
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
throw new NotFoundException('User not found');
}
if (roles.includes('ANY')) {
return true;
}
if (user.role !== roles[0]) {
throw new ForbiddenException('Access denied');
}
return roles.includes(user.role);
}
}
Relfector를 사용하여 Role 데코레이터를 만들면서 정의한 roles Custom Metadata에 접근한 후 roles에 할당해 준다.
JwtAuthGuard를 통과한 후 request객체에 있을 user 정보를 추출한다.(JwtAuthGuard는 다른 포스팅에서 다룰 예정)
상황에 따른 적절한 예외를 반환했다.
이제 Role 데코레이터와 RoleGuard를 사용할 수 있다.
적용
//example.controller.ts
@UseGuards(JwtAuthGuard, RoleGuard)
@Role(UserRole.ADMIN)
@Get()
async findAll(): Promise<FindAllResponseDto> {
return this.exampleService.findAll();
}
@Role()에 있는 UserRole을 통해 RoleGuard에서 인가를 한다.
데코레이터를 하나로
데코레이가 JwtAuthGuard, RoleGuard, Role로 3개인데, 이 데코레이터들을 하나로 합칠 수 있다.
Auth 데코레이터로 합쳐 보자.
//auth.decorator.ts
export function Auth(roles: UserRole) {
return applyDecorators(
Role(roles),
UseGuards(JwtAuthGuard),
UseGuards(RoleGuard),
);
}
주의해야 할 점은 RoleGuard가 실행되기 전 request 객체에 user가 있어야 하므로, RoleGuard는 JwtAuthGuard보다 뒤에 실행돼야 한다는 점이다.
다음과 같이 적용할 수 있다.
//example.controller.ts
@Auth(UserRole.ADMIN)
@Get()
async findAll(): Promise<FindAllResponseDto> {
return this.exampleService.findAll();
}
참고: https://velog.io/@hkja0111/NestJS-12-Role-Based-Authorization
'NestJS' 카테고리의 다른 글
CORS, credentials (0) | 2023.08.30 |
---|---|
NestJS에서 openAI API 연동으로 ChatGPT 활용하기 (0) | 2023.08.30 |
Throttler를 사용한 Rate Limiting (0) | 2023.08.24 |
Provider (0) | 2023.07.24 |
Controller (0) | 2023.07.20 |