이제는 로그인을 하기 전에 회원가입을 하는 API를 만들어보자
section11 폴더를 만들어서 기존과 같이 복사 붙여넣기 하여 11-01-signup 으로 폴더 이름을 변경해준다.
yarn install 을 통해 필요한 모듈들을 설치한 이후,
user.entity.ts 파일을 다음과 같이 수정해 준다.
// user.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
email: string;
@Column()
@Field(() => String)
password: string;
@Column()
@Field(() => String)
name: string;
@Column()
@Field(() => Int)
age: number;
}
11-01-signup → src → apis → users 에 users.resolver.ts 파일을 만들고
다음과 회원가입 함수를 만들어준다.
// users.resolver.ts
import { Args, Int, Mutation, Resolver } from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver()
export class UsersResolver {
constructor(
private readonly usersService: UsersService, //
) {}
// 회원가입 함수
@Mutation(() => User)
async createUser(
@Args('email') email: string,
@Args('password') password: string,
@Args('name') name: string,
@Args({ name: 'age', type: () => Int }) age: number,
): Promise<User> {
return this.usersService.create({ email, password, name, age });
}
}
@Args 를 통해 createUser 시 필요한 정보들을 받아오고, 받아온 데이터를 users.service.ts 파일로 넘겨준다.return 을 통해 프론트로 유저 정보 객체 결과 값을 보내준다.11-01-signup → src → apis → users 에 users.service.ts 파일을 만들어준다.
DB에 유저정보를 저장하는 비즈니스 로직은 다음과 같다.
// users.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { IUsersServiceCreate, IUsersServiceFindOneByEmail } from './interfaces/users-service.interface';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
findOneByEmail({ email }: IUsersServiceFindOneByEmail) {
return this.usersRepository.findOne({ where: { email } });
}
async create({ email, password, name, age }: IUsersServiceCreate): Promise<User> {
const user = await this.findOneByEmail({ email });
if (user) throw new ConflictException('이미 등록된 이메일입니다.');
// if (user) throw new HttpException('이미 등록된 이메일입니다.', HttpStatus.CONFLICT); // 이렇게도 사용 가능
return this.usersRepository.save({ email, password, name, age});
}
}
findOneByEmail 함수를 통해 찾아와서 중복 이메일이 존재한다면 ConflictException 에러를 던져서 중복 이메일 가입을 방지하는 방식으로 진행해 주었다.return을 통해 users.resolver.ts 로 보내준다.11-01-signup → src → apis → users 폴더 안에 interfaces 폴더를 만들고
해당 폴더 안에 users-service.interface.ts 파일을 만들어 준 이후, 타입에 필요한 인터페이스를 만들어준다.
// users-service.interface.ts
export interface IUsersServiceCreate {
email: string;
password: string;
name: string;
age: number;
}
export interface IUsersServiceFindOneByEmail {
email: string;
}
IUsersServiceCreate 와 IUsersServiceFindOneByEmail 에 대한 타입을 지정했다.이제, 11-01-signup → src → apis → users 에 user.module.ts 파일을 만들어준다.
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
@Module({
imports: [
TypeOrmModule.forFeature([
User, //
]),
],
providers: [
UsersResolver, //
UsersService,
],
})
export class UsersModule {}
module.ts 파일에 조립했다.지금까지 만든 UsersModule을 최종적으로 AppModule에 추가해주는 것도 해주어야한다.
이후, yarn start:dev 를 입력해 서버를 실행하고
플레이그라운드에 접속해 회원가입 api를 요청했을 때, 아래와 같이 결과가 나오고

DBeaver를 실행시켰을 때 다음과 같이 데이터가 잘 생성되었다면 회원가입이 성공한 것이다.

또한 동일한 이메일로 회원가입 시, 아래와 같이 에러 반환이 된다면 성공이다.

회원가입을 하면 프론트 위치에서 비밀번호를 받아올 수 있다.
따라서, DB에 저장된 비밀번호가 노출되지 않도록 user.entity.ts 파일을 아래와 같이 수정해야 한다.
// user.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column()
@Field(() => String)
email: string;
// 수정해준 부분
@Column()
// @Field(() => String) 비밀번호 노출 금지!!
password: string;
@Column()
@Field(() => String)
name: string;
@Column()
@Field(() => Int)
age: number;
}
password는 외부에 노출되면 안 되기 때문에 다음과 같이 graphql 부분을 주석 처리했다.
이렇게 하면, 다시 플레이그라운드에서 회원가입 API를 요청했을 때 아래와 같이 return 값으로 password를 받을 수 없게된다.

지금까지의 API로는 회원가입을 하면 DB에 비밀번호가 그대로 저장되었다.
만약 DB가 해킹을 당하게 된다면 해커가 해당 서비스 사이트의 이메일과 비밀번호를 가져갈 수 있는데
일반적으로 많은 사람들이 여러 사이트에 동일한 이메일과 비밀번호를 사용하는 경우가 많으므로, 다른 서비스까지 추가 해킹을 당하는 일이 발생할 수 있다.
따라서, 기밀성을 유지하기 위해 DB에 비밀번호를 저장할 때는 비밀번호를 암호화(Encrypt) 하여 저장하는 것이 일반적이다.
암호화를 하여 저장을 하면 암호화에 사용된 키(비밀번호)를 통해 데이터를 복호화 할 수 있다.
그래서, 완전히 안전한 암호화가 아니기에 해싱이 나타났다.
암호화를 구성하는 요소들을 다음과 같이 정의해보았습니다.
암호화의 종류는 여러 가지가 있다, 그 중에서 2가지는 시험에도 나오기에 유명한데
그 두가지 다 암호화 기법이지만 Hash는 단방향 암호화 기법이고 Encryption은 양방향 암호화 기법이다.
양방향 암호화 : 암호화와 복호화과정을 통해 송 ・ 수신 간 주고받는 메시지를 안전하게 암호화하고 평문으로 복호화하는 과정.단방향 암호화 : 해싱(Hashing) 을 이용한 암호화 방식으로 양방향과는 다른 개념으로, 평문을 암호문으로 암호화는 가능하지만 암호문을 평문으로 복호화 하는 것은 불가능.즉, Hash는 평문을 암호화된 문장(텍스트)으로 만들어주고 Encryption은 평문을 암호화된 문장(텍스트)로 만들어주는 기능을 하고 + 암호화된 문장을 다시 평문으로 만드는 복호화 기능도 한다.
여기에서는 Hash(단방향 암호화)를 사용하기에 이에 대해 조금 더 자세히 알아보자
단방향 해시 함수는 어떤 수학적 연산(또는 알고리즘)에 의해 원본 데이터를 매핑 시켜 완전히 다른 암호화된 데이터로 변환시키는 것을 의미한다.
이 변환을해시라고 하며, 해시에 의해 암호화된 데이터를 다이제스트(digest)라고 한다.
또한 앞서 말했듯 해싱은 단방향이다. 한마디로 단방향 해시 함수는 다이제스트를 복호화 한다는 것, 즉 원본 데이터를 구할 수는 없어야 한다. 말 그대로 단방향성의 특성을 갖기 때문이다.
단방향 암호화를 나타내는 아래 그림을 보면

Password 123456 을 해시 함수에 돌려서 다이제스트인 fs32a3xzz0 을 생성하고 해당 데이터를 DB 에 저장한다.
DB에 저장된 다이제스트가 설령 DB가 누출된다 하더라도 fs32a3xzz0 은 단방향으로 해싱 된 문자라 복호화 할 수가 없다.
이중에서 가장 대표적인 해시 알고리즘인 SHA-256 을 통해 123456 을 해싱하면 다음과 같이 나온다.
8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
만약 조금만 변경하여 123456 다음에 마침표(.) 하나만 더 찍으면 완전히 다른 값이 나온다.
43fae6c11d7632acc6059de1cced9b09a58caaa878071308ad67f32ef6b11691
하지만, 단순히 해시 함수를 이용해서 변환만 한다고 해서 보안이 완벽에 가깝다고 말할 수 없다.
어떠한 알고리즘 암호화를 통해 나온 값을 보고 그 값이 나올 수 있는 모든 경우를 스캔하는 레인보우 테이블을 이용하여 무작정 다 대입해 복호화 하는 경우가 존재하기 때문이다.
이런 약점을 보안하기 위해 생겨난 방법이 키-스트레칭과 솔트이다.
Key Stretching(키-스트레칭)은 개발자가 해싱하는 횟수를 정해서 Hash 함수를 돌리는 방법으로 패스워드를 저장할 때 가장 쉽게 생각 할 수 있는 방법이다.

예를 들어 SHA-256 해시함수를 사용한다고 가정할 때, 123456이 입력되었다면 123456의 다이제스트는 아래와 같다.
8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
이 다이제스트를 한 번 더 SHA-256에 돌리면 아래와 같다.
49dc52e6bf2abe5ef6e2bb5b0f1ee2d765b922ae6cc8b95d39dc06c21c848f8c
그러나 Hash 함수를 여러 번 돌리는 만큼 최종 다이제스트를 얻는데 그만큼 시간이 소요되기 때문에 속도 면에서 불리하다는 단점이 있다.
하지만, 키 스트레칭을 통해서 여러 번 해시 함수를 돌리더라도 결국 몇 번 돌렸는지 횟수만 알게 된다면,
공격하는 입장에서 상징성 있는 대표 문자열들을 추려보면 충분히 공격이 가능하다. 또 같은 비밀번호를 사용하는 사용자들이 있다면 하나의 결과를 갖고도 다수 사용자의 password 를 알아낼 수 있다.
이를 방지하기 위해 도입한 것이 바로 솔트다.
Salt(솔트)란 해시함수를 돌리기 전에 원문에 임의의 문자열을 추가로 덧붙이는 것을 말한다. 의미 그대로 요리에 소금을 넣어 맛을 변화하는 것처럼 원문을 변형하는 것을 소금친다(Salting)라고 생각하면 된다.

이렇게 하면 다이제스트를 알아낸다 하더라도 password 를 알아내기 더욱 어려워지며, 사용자마다 다른 Salt 를 사용한다면 설령 같은 비밀번호더라도 다이제스트의 값은 전혀 달라진다. 이는 결국 한 명의 패스워드가 유출되더라도 같은 비밀번호를 사용하는 다른 사용자는 비교적 안전하다는 의미다.
이제, 해싱의 대표적인 라이브러리인 Bcrypt를 사용해서 단방향 암호화를 만들어보자
npm에 등록되어 있는 암호화 모듈인 Bcrypt라는 암호 해싱 기능을 이용하여 데이터베이스에 암호화된 비밀번호로 저장해 보려고한다.
터미널을 통해서 기본 모듈과 타입스크립트 모듈을 모두 설칠해준다.
yarn add bcrypt
yarn add --dev @types/bcrypt
이후, users.service.ts 파일에서 create 함수를 수정해준다.
// users.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { IUsersServiceCreate, IUsersServiceFindOneByEmail } from './interfaces/users-service.interface';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
findOneByEmail({ email }: IUsersServiceFindOneByEmail) {
return this.usersRepository.findOne({ where: { email } });
}
async create({
email,
password,
name,
age,
}: IUsersServiceCreate): Promise<User> {
const user = await this.findOneByEmail({ email });
if (user) throw new ConflictException('이미 등록된 이메일입니다.');
// if (user) throw new HttpException('이미 등록된 이메일입니다.', HttpStatus.CONFLICT); // 이렇게도 사용 가능
const hashedPassword = await bcrypt.hash(password, 10);
return this.usersRepository.save({ email, password: hashedPassword, name, age });
}
}
설치한 bcrypt 모듈을 불러준다.
import * as bcrypt from 'bcrypt' : as를 사용해 bcrypt 모듈의 모든 메서드를 사용할수있게 해준다.bcrypt.hash(password, 10) : hash 알고리즘을 사용해 비밀번호를 암호화하는데 hash 메서드의 두 번째 인자는 salt다. 원본 password를 10회 salt 한다는 의미다.
password: hashedPassword 를 통해서 user DB에 저장될 때, password 값은 hashedPassword 로 hash 된 password가 저장되게 설정했다.
만일, 해시 함수를 다른 곳에서 재사용하게 된다면 해시 함수를 위한 HashService를 따로 만들어서 주입받아서 사용하는 편이 코드 재사용성을 높일 수 있다.
이제 회원가입 시 해시 된 비밀번호로 DB에 저장이 되는지 확인을 해보자
플레이그라운드에서 api 요청해보면

createUser 에 요청을 보내 유저 정보를 생성해주고
DBeaver에서 유저정보가 잘 저장되었는지, 유저의 password가 잘 암호화되었는지 확인하면
이전 시간에 생성된 유저정보와 비교했을 때 암호화된 비밀번호가 잘 적용된 것을 확인할 수 있다.
