auth-service는 유저의 로그인, 회원가입 등을 다루는 서비스입니다. 이 서비스에서 로그인을 거쳐 jwt토큰으로 유저에 대한 인증과 인가가 허락이 되어야 서비스를 이용할 수 있게 해주는 기능을 가지고 있습니다.
루트 디렉토리로 가서 auth-service를 설치하겠습니다.
nest new auth-service
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(7000);
}
bootstrap();
ValidationPipe를 이용하여 REST 통신 시 VO객체에 존재하는 IsString(), IsNumber()등 과 같이 해당 값들이 올바른 타입을 가지는지 검증을 진행합니다.
컨트롤러를 작성하여 테스트 드라이븐 형태로 코드를 작성하도록 하겠습니다.
vo객체에 담길 변수에 대한 데이터 타입 검증을 위해 다음의 라이브러리를 설치하도록 하겠습니다.
npm install --save class-transformer class-validator builder-pattern
import { Body, Controller, Get, HttpStatus, Param, Post } from "@nestjs/common";
import { Builder } from "builder-pattern";
import { AuthService } from "./auth/auth.service";
import { statusConstants } from "./constants/status.constant";
import { UserDto } from "./dto/user.dto";
import { UserService } from "./user/user.service";
import { RequestLogin } from "./vo/request.login";
import { RequestRegister } from "./vo/request.register";
import { ResponseUser } from "./vo/response.user";
@Controller('auth-service')
export class AppController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
@Get('status')
public async status(): Promise<string> {
return "auth-service is working successfully";
}
@Post('login')
public async login(@Body() requestLogin: RequestLogin): Promise<any> {
try {
const result = await this.authService.login(Builder(UserDto).email(requestLogin.email)
.build());
if(result.status === statusConstants.ERROR) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + result.message,
});
}
return await Object.assign({
status: HttpStatus.OK,
token: result.access_token,
payload: Builder(ResponseUser).email(result.payload.email)
.nickname(result.payload.nickname)
.phoneNumber(result.payload.phoneNumber)
.userId(result.payload.userId)
.build(),
message: 'Successfully Login'
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + err
});
}
}
@Post('register')
public async register(@Body() requestRegister: RequestRegister): Promise<any> {
try {
const result: any = await this.userService.register(Builder(UserDto).email(requestRegister.email)
.password(requestRegister.password)
.nickname(requestRegister.nickname)
.phoneNumber(requestRegister.phoneNumber)
.build());
if(result.status === statusConstants.ERROR) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + result.message,
});
}
return await Object.assign({
status: HttpStatus.CREATED,
payload: result.payload,
message: "Successfully register"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + err,
});
}
}
@Get(':userId')
public async getUser(@Param('userId') userId: string): Promise<any> {
try {
const result: any = await this.userService.getUser(userId);
if(result.status === statusConstants.ERROR) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + result.message,
});
}
return await Object.assign({
status: HttpStatus.OK,
payload: Builder(ResponseUser).email(result.payload.email)
.nickname(result.payload.nickname)
.phoneNumber(result.payload.phoneNumber)
.userId(result.payload.userId)
.build(),
message: "Get User Information",
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Error message: " + err,
});
}
}
}
주요 메서드들을 살펴 보겠습니다.
1) POST /auth-service/login
로그인을 위한 메서드입니다. dto객체를 builder패턴을 이용하여 authService의 login 비즈니스 로직으로 보내고, 이에 대한 반환 값을 받습니다. 그리고 이 값의 status(비즈니스 로직에서의)가 ERROR라면 오류 메시지를 담은 객체를 반환하고, 아니라면 성공 메시지 혹은 컨트롤러에서의 오류 메시지를 반환합니다.
2) POST /auth-service/register
회원가입을 위한 메서드입니다. 1)과 동일한 흐름을 갖습니다.
3) GET /auth-service/:userId
회원정보를 얻는 메서드입니다. 1)과 동일한 흐름을 갖습니다.
컨트롤러의 오류를 해결하면서 코드를 작성하겠습니다. 다음의 모듈들을 만들겠습니다.
nest generate module auth
nest generate controller auth
nest generate service auth
nest generate module user
nest generate controller user
nest generate service user
dto 객체입니다. dto는 data transfer object로 controller -> service -> repository / repository -> service -> controller와 같이 레이어들 간에서 움직일 수 있는 객체입니다.
import { IsString } from "class-validator";
export class UserDto {
@IsString()
email: string;
@IsString()
password: string;
@IsString()
nickname: string;
@IsString()
phoneNumber: string;
@IsString()
userId: string;
@IsString()
createdAt: string;
}
앞서 설치했던 패키지의 @Is~ 데코레이터를 이용해서 속성의 값들이 올바른 타입을 받았는지 검증합니다.
vo객체입니다. vo는 value object의 약자를 가지고 있습니다. dto와의 차이점이 있다면 vo는 단순 값만 가지는 객체입니다. 즉, dto는 레이어들을 오가는 과정 중 비즈니스 로직을 거쳐 데이터의 변형이 일어나지만 vo는 단순히 값만 가지는 객체이기 때문에 read only의 특성을 가집니다.
import { IsNotEmpty, IsString } from 'class-validator';
export class RequestLogin {
@IsString()
@IsNotEmpty()
readonly email: string;
@IsString()
@IsNotEmpty()
readonly password: string;
}
@IsNotEmpty는 값을 필수적으로 받아야 하는 데코레이터입니다. 만일 값을 받지 못한다면 에러가 발생합니다.
import { IsString, IsNotEmpty } from 'class-validator';
export class RequestRegister {
@IsString()
@IsNotEmpty()
readonly email: string;
@IsString()
@IsNotEmpty()
readonly password: string;
@IsString()
@IsNotEmpty()
readonly nickname: string;
@IsString()
@IsNotEmpty()
readonly phoneNumber: string;
}
response 객체를 만들어 vo로 사용해야 하지만 예외적으로 값을 사용자에게 반환시킬 객체이기 때문에 read only를 사용하지 않도록 하겠습니다.
import { IsString } from "class-validator";
export class ResponseUser {
@IsString()
email: string;
@IsString()
password: string;
@IsString()
nickname: string;
@IsString()
phoneNumber: string;
@IsString()
userId: string;
@IsString()
createdAt: string;
}
export const statusConstants = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
};
성공, 오류 값을 나타내는 상태값을 가진 상수입니다.
mongodb는 mysql과 같은 관계형 데이터베이스와 달리 NoSQL로 문서의 형태로써 데이터를 저장하는 데이터베이스입니다. 이 데이터베이스는 하나의 컬렉션(테이블)을 만들고 그 안에 문서(튜플)를 저장하는 형식입니다.
mongodb는 낮은 지연 시간, 자유로운 데이터 모델, 수평 확장 가능한 분산 아키텍쳐라는 장점을 가집니다.
그러면 mongodb를 설치해보고 nest js와 연동을 진행해보겠습니다.
설치 참조 - https://m.blog.naver.com/wideeyed/221815886721
위의 블로그 링크를 참조하여 설치를 한 후 MongoDB Compass프로그램을 이용하여 AUTHSERVICE라는 이름의 데이터베이스를 만들겠습니다. 완료되었으면 아래의 패키지를 설치하도록 하겠습니다.
npm install --save @nestjs/mongoose mongoose
패키지가 설치되었으면 AppModule에 mongoose를 import하겠습니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
MongooseModule.forRoot("mongodb://localhost:27017/AUTHSERVICE?readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false"),
AuthModule,
UserModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
document의 형식이 되는 스키마를 만들도록 하겠습니다.
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop({ required: true })
email: string;
@Prop({ required: true })
nickname: string;
@Prop({ required: true })
userId: string;
@Prop({ required: true })
phoneNumber: string;
@Prop({ required: true })
encryptedPwd: string;
@Prop({ required: true })
createdAt: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
이 스키마를 import하여 user와 auth 모듈에서 UserSchema를 사용하도록 하겠습니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from 'src/schema/user.schema';
import { UserService } from './user.service';
@Module({
imports: [MongooseModule.forFeature([{
name: User.name,
schema: UserSchema
}])],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { jwtConstant } from 'src/constants/jwt.constant';
import { User, UserSchema } from 'src/schema/user.schema';
import { JwtStrategy } from 'src/strategy/jwt.strategy';
import { LocalStrategy } from 'src/strategy/local.strategy';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: jwtConstant.secret,
signOptions: { expiresIn: '43200s' },
}),
MongooseModule.forFeature([{
name: User.name,
schema: UserSchema
}])
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
],
exports: [AuthService],
})
export class AuthModule {}
여기까지 완료되었다면 nestjs에서 mongoose를 이용하여 mongodb 접근이 가능합니다.
다음의 패키지를 설치하여 uuid, bcrypt암호화 라이브러리를 사용해보겠습니다. uuid는 범용고유식별자라는 뜻으로 유니크한 값을 갖게 하고자할 때 주로 사용되는 방법입니다.
npm install --save uuid
npm install -D @types/uuid
npm install --save bcrypt
npm install --save-dev @types/bcrypt
비밀번호 암호화를 위해 util 디렉토리를 만들어 다음의 유틸 클래스를 만들도록 하겠습니다.
import * as bcrypt from 'bcrypt';
export const hash = async(password: string): Promise<string> => {
const saltOrRound = 10;
return await bcrypt.hash(password, saltOrRound);
};
export const isHashValid = async(password, encryptedPwd): Promise<boolean> => {
return await bcrypt.compare(password, encryptedPwd);
};
hash메서드는 비밀번호의 암호화를 하는데 사용되고, isHashValid는 추후에 로그인 시 입력되는 비밀번호와 암호화된 비밀번호를 비교하는 메서드입니다.
그러면 mongoose를 이용하여 service레이어에서 데이터베이스에 대한 접근과 userId, 비밀번호의 암호화를 사용하도록 하겠습니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument, UserSchema } from 'src/schema/user.schema';
import { Model } from 'mongoose';
import { UserDto } from 'src/dto/user.dto';
import { statusConstants } from 'src/constants/status.constant';
import { Builder } from 'builder-pattern';
import { v4 as uuid } from 'uuid';
import { hash } from 'src/util/util.hash';
@Injectable()
export class UserService {
constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
public async register(userDto: UserDto): Promise<any> {
const user = new this.userModel(Builder(User).email(userDto.email)
.encryptedPwd(await hash(userDto.password))
.nickname(userDto.nickname)
.phoneNumber(userDto.phoneNumber)
.userId(uuid())
.createdAt(new Date().toDateString())
.build());
if(await this.userModel.findOne({ email: userDto.email })) {
return Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Duplicated Email! retry register"
});
}
try {
const result = await user.save();
return Object.assign({
status: statusConstants.SUCCESS,
payload: result,
message: "Successfully save in database"
});
} catch(err) {
return Object.assign({
status: statusConstants.ERROR,
payload: null,
message: err
});
}
}
...
}
UserService의 코드를 살펴보겠습니다.
1) 생성자에서 @InjectModel이란 데코레이터를 이용하여 mongodb사용을 위한 userModel객체를 생성합니다.
2) register메서드에서는 이 userModel에 User객체(encryptedPwd: hash메서드 사용, userId: uuid라이브러리 사용)를 만들어 인자로 넘겨주고, 결과값을 반환합니다.
여기까지 서비스까지 구현을 해보았습니다. 회원가입의 흐름을 잠깐 살펴 보겠습니다.
위의 흐름대로 컨트롤러부터 데이터베이스까지 저장되는 모습을 볼 수 있습니다.
다음 포스트에서는 로그인을 위한 인증과 인가에 대한 부분을 구현하도록 하겠습니다.