3개의 포스팅을 거치면서 우린 JWT 생성부터 권한관리까지의 내용을 진행중에 있고, 이전 포스팅에선 권한 부여를 하기에 앞서 db에 사용자의 권한을 관리하기 위한 테이블을 생성하고 기존 유저의 데이터를 담은 테이블과 JOIN 하는 작업까지 진행하였다.
이번 포스팅에선 권한 부여를 위한 RolesGuard
를 생성해 볼 것이다. 이전에 생성해보았던 AuthGuard
보단 더 추가할 로직이 많을 것으로 예상된다. 그럼 한번 진행해보자.
기존 페이로드 인터페이스에 우리가 User
엔티티에 추가해 준 authorities
를 추가해 준다.
// payload.interface.ts
export interface Payload {
id: number;
username: string;
authorities?: any[]; // authorities가 있을 수도 있고, 그렇지 않을수도 있으므로 "?"이용
}
앞서 생성하였던 user_authority
테이블이다. 역할을 담고 있는 authority_name
엔 두 가지의 역할인 ROLE_USER
와 ROLE_ADMIN
이 있다.
나중에 컨트롤러에 해당 Role을 적용시키기에 앞서 Role의 타입을 미리 선언해 줄 필요가 있다. 어떤 타입이 가능한 지 지정해 주는 것이다.
우리는 Role Type을 TS의 enum
객체를 사용하여 지정해 줄 것이다. enum
에 대한 설명은 생략하겠다. 생성할 Role Type은 DB에 정의된 것과 꼭 동일하게 만들어준다.
// role-type.ts
export enum RoleType {
USER = 'ROLE_USER',
ADMIN = 'ROLE_ADMIN',
}
컨트롤러에서 메뉴의 사용 권한을 표시해 주기 위해 데코레이터를 생성할 것이다. 즉, 우리가 바로 위에서 지정해 준 RoleType
을 읽어올 수 있는 데코레이터를 만들어 주는 것이다.
// role.decorator.ts
import { SetMetadata } from "@nestjs/common";
import { RoleType } from "../role-type";
export const Roles = (...roles: RoleType[]): any => SetMetadata('roles', roles);
즉, 위와 같은 데코레이터는 나중에 컨트롤러에서 @Roles
와 같은 형식으로 POST 방식에 쓰일 것이다.
본인도 그렇고, Nest의 구조를 처음 접한 사람에게는 갑자기 Roles
데코레이터를 왜 만드는 것이고 어떠한 역할을 가지냐 에 관해 의문을 품을 것이다. 익숙치 않기 때문이다.
해당 데코레이터를 생성하기 까진 일련의 단계가 존재한다. 우리는 그것을 Nest 공식 문서를 통해 직접 확인할 수 있다.
공식 문서는 전부 영어로 되어있어서 이해하는데 어려움이 있을 수 있다. 그래서 RolesGuard
생성부터 Roles
데코레이터 생성까지 그 과정을 번역하여 우리의 진행 과정에 앞서 알아보고자 한다.
해당 내용은 개별의 포스팅으로 진행하고자 한다. ⬇⬇⬇ 아래 포스팅 참고
Role Guard 생성 __공식문서 번역
위에 ⬆⬆ 걸어둔 "Role Guard 생성 __공식문서 번역" 을 먼저 보고 해당 내용으로 넘어가는 것을 추천한다. 공식문서의 코드 베이스를 바탕으로 진행이 될 것이다.
// role.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import { User } from "../entity/user.entity";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]> ('roles', context.getHandler());
if(!roles) { // roles가 아니면 true를 리턴하고 진행한다.
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user as User;
return user && user.authorities && user.authorities.some(role => roles.includes(role));
}
}
공식문서에서도 나와있듯이 constructor
내부에 프라이빗 생성자로 담은 Reflector
를 통해 메타데이터를 런타임에서 가져오도록 한다.
우린 Reflector
를 통해 roles
를 string 타입의 배열 값으로 불러올 수 있다.
만약 roles
가 지정이 되있다면 실행 컨텍스트에서 요청 객체인request
를 불러온다.
이 request
는 주의깊게 볼 필요가 있다. 우리가 지난 포스팅 "토큰 검증 - AuthGuard를 컨트롤러에 적용" 부분에서
@Get('/authenticate')
@UseGuards(AuthGuard)
isAuthenticated(@Req() req: Request): any {
const user: any = req.user;
return user;
}
isAuthenticated
의 파라미터로 Request
객체를 받고, 해당 요청 객체의 user
값을 받는 작업을 수행해 보았었다. 이렇게 반환된 user
는 "토큰 인증이 확인" 된 객체이다. 조금 더 자세히 말하면 우리가 앞서 작성한 JwtStrategy
의 메서드 validate()
에서 반환하였던
// passport.jtw.strategy.ts
// ~~~~
async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
const user = await this.authService.tokenValidateUser(payload);
if(!user) {
return done(new UnauthorizedException({message: 'user does not exist'}), false)
}
return done(null, user);
}
user
객체인 것이다. 앞서 컨트롤러에서 const user: any = req.user
을 통해 req.user
의 타입이 any였다면 Role Guard에서 아래와 같이
const user = request.user as User
User
타입으로 "타입 단언"을 해줌으로써 타입시스템의 장점을 가질 수 있게 된다.
그렇게 최종 RolesGuard
는 아래와 같은 값들을 리턴하는데
return user && user.authorities && user.authorities.some(role => roles.includes(role));
User
를 타입으로 받는 user
객체와 User
엔티티 내에 정의해 준 authorities
, 그리고 javascript의 some()
메서드를 사용하여 authorities
배열이 실행 컨텍스트에서 얻게 되는 roles
를 포함하고 있는지에 관한 불리언 값을 리턴 받는다.
💨 참고 !
Array.prototype.some()
: some()
메서드는 배열 안의 어떤 요소라도 주어진 판별 함수를 통과하는지 테스트한다.
인자로는 callback
을 받고 callback
이 어떤 배열 요소에 대해서라도 참인 값을 반환하는 경우 true
, 그 외엔 false
를 가진다.
이번 포스팅에선 권한 부여를 위한 RolesGuard
를 구현하였고 어떠한 원리로 원하는 user
객체를 전달하고 반환하는지에 대해 알아보았다.
지난 포스팅들에 비해 짧게 작성하였는데 이번 포스팅에서 사실 가장 중요한 부분은 오히려 이번 포스팅의 작성물이 아니라 내용 중간에 링크로 걸어준 "Nest 공식 문서 번역 __RolesGuard" 일지도 모른다. 꼭 먼저 보고 오길 추천한다.
다음 포스팅에선 우리가 생성한 RolesGuard
를 본격적으로 사용하기 위해 서비스와 컨트롤러에 적용하는 과정을 가져보도록 하자.