오늘은 지난 주에 고생했던 내용에 대해서 알아보려고 한다.
nestJS에서 auth 구현은 일반 nodeJS와 유사하다. 사실 장고처럼 특출나게 auth 관리를 하지 않는 이상 다른 프레임워크도 비슷한 수준인 것 같다.
회원가입시에는 User, 현재 구현하고 있는 서비스에서는 Recruiter와 Applier의 엔티티에 새로운 레코드를 추가하는 것이다.
추후 로그인 시 아이디로 businessId, 비밀번호로 password를 사용하기 때문에 회원가입 시 businessId가 겹치게 된다면 오류를 내보내도록 엔티티 단에서 @Unique(['businessId'])
를 설정해주었다.
@Entity()
@Unique(['businessId'])
export class Recruiter {
@PrimaryGeneratedColumn()
id: number;
@Column()
businessId: string;
@Column()
password: string;
...
}
비밀번호를 신규회원이 입력한 그대로 데이터베이스에 저장하게 된다면 보안상 심각한 문제를 초래할 수 있다. 따라서 bcryptJS 모듈을 사용하여 해시된 비밀번호를 데이터베이스에 저장하도록 하였다.
async createRecruiter(signUpRecruiterDto: SignUpRecruiterDto): Promise<void> {
const { businessId, password, managerName, companyName } =
signUpRecruiterDto;
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(password, salt);
...
}
salt를 사용해서 일반적인 decrypt가 이뤄지지 않도록 하여 보안 수준을 높였다.
로그인 시 JWT로 토큰을 발급하고 엔드포인트마다 발급된 토큰을 가지고 권한을 확인하는 Passport + UseGuards 방식을 활용하여 권한 인가를 설정하였다.
JWT(JSON Web Token)은 Header, Payload, Signature 세 부분으로 구성된다. Header에는 주로 해시 알고리즘에 대한 정보가 들어있고 Payload에는 사용자와 관련된 정보가 들어있고 토큰 생성 설정을 통해 구성을 변경할 수 있다. Signature에는 헤더와 페이로드의 인코딩된 정보와 비밀키가 들어있다.
JWT관련 정보는 auth 모듈에서 import 하여 등록한다.
@Module({
imports: [TypeOrmModule.forFeature([Recruiter, Recruitment, Applier]),
JwtModule.register({
secret: Constant.secret,
signOptions: {
expiresIn: '3600s',
},
}), PassportModule.register({ defaultStrategy: 'jwt'})],
controllers:[AuthController],
providers: [AuthService, RecruiterService, ApplierService, JwtStrategy],
exports: [JwtStrategy, PassportModule], //다른 모듈에서도 사용해야하므로 추출해야함
})
export class AuthModule {}
secret에 대한 문자열은 외부로 노출되면 안되기 때문에 반드시 gitIgnore로 관리를 해주어야한다.
passport는 nodeJS에서 인증과 관련된 유명한 미들웨어 중 하나이다. 일반적인 로그인부터 OAUTH까지 구현할 수 있다. 이번 프로젝트에서는 로그인은 직접 따로 service에서 구현을 하였다. JWT에 해당하는 부분만 strategy를 따로 지정하여 @UseGuards(AuthGuard())
로 인증을 관리한다. 다음은 jwt.strategy.ts의 일부이다.
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private recruiterService: RecruiterService,
private applierService: ApplierService) {
super({
secretOrKey: Constant.secret,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
async validate(payload) {
const { businessId, userType } = payload;
if(userType === 'recruiter'){
...
}else if(userType === 'applier'){
...
}
}
}
strategy에서 생성자쪽에 문제가 없으면 validate 함수가 실행되면서 인증 여부를 평가한다.
또하나 추가적으로 한 것은 @UseGuards()
로 request에 실린 user 정보를 가져오기 위한 커스텀 데코레이터를 만든 것이다. 다음과 같이 만들었다.
//get-user.decorator.ts
export const GetUser = createParamDecorator(
(data, ctx: ExecutionContext): Recruiter | Applier => {
const req = ctx.switchToHttp().getRequest();
const { userType, ...user} = req.user;//jwt strategy에서 넣은 userType을 다시 제거해야 쿼리가 정상적으로 동작함
return user; //default로 유저정보가 들어간 것은 user로 되어있기 때문에 이에 맞게 해야함.
},
);
위 과정을 통해 recruiter와 applier별로 엔드포인트의 접근 권한을 구별하여 지정할 수 있었다.