오늘은 로그인 기능 중 JWT에 대해 공부하고 구현을 했습니다
JWT 는 JSON Web Token의 약자로 전자 서명 된 URL-safe (URL로 이용할 수있는 문자 만 구성된)의 JSON입니다.
전자 서명은 JSON 의 변조를 체크 할 수 있게되어 있습니다.
JWT는 속성 정보 (Claim)를 JSON 데이터 구조로 표현한 토큰으로 RFC7519 표준 입니다.
JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증합니다.
이때 사용되는 JSON 데이터는 URL-Safe 하도록 URL에 포함할 수 있는 문자만으로 만듭니다.
JWT는 HMAC 알고리즘을 사용하여 비밀키 또는 RSA를 이용한 Public Key/ Private Key 쌍으로 서명할 수 있습니다
JWT는 세 가지 파트로 구성되어 있습니다:
Header (헤더): 토큰의 유형과 해싱 알고리즘을 지정합니다. 일반적으로는 alg (알고리즘)와 typ (토큰 유형)을 포함합니다.
Payload (페이로드): 토큰에 포함될 클레임 정보를 담고 있습니다. 클레임은 토큰에 대한 속성과 값을 나타냅니다. 클레임은 세 가지 종류로 나뉩니다:
Registered Claims (등록된 클레임): 토큰에 대한 일반적인 속성을 나타냅니다. 예를 들어, iss (발급자), exp (만료 시간), sub (주제) 등이 있습니다.
Public Claims (공개 클레임): 클라이언트와 서버 사이의 공유 정보를 나타냅니다. 사용자 정의 클레임으로, 필요에 따라 추가할 수 있습니다.
Private Claims (비공개 클레임): 클라이언트와 서버 간에 합의된 정보를 나타냅니다. 비공개 정보로, 표준이 아닌 클레임입니다.
Signature (서명): 헤더와 페이로드를 Base64 URL로 인코딩한 후, 비밀 키를 사용하여 서명합니다. 서명을 통해 토큰이 변조되지 않았음을 검증할 수 있습니다.
JWT는 토큰 기반의 인증 방식으로 사용되며, 클라이언트가 인증을 요청할 때 서버는 JWT를 발급합니다. 클라이언트는 발급받은 JWT를 이후의 요청에 포함시켜 서버에 전달합니다. 서버는 전달받은 JWT를 검증하여 클라이언트의 인증 여부를 확인합니다.
JWT는 다양한 플랫폼과 언어에서 지원되며, JWT를 사용하여 보안적으로 효율적이고 확장 가능한 인증 시스템을 구축할 수 있습니다.
nest g resource auth
nest g resource user
[ user/entities/user.entitiy.ts ]
import {
BeforeInsert,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
@Entity({ name: 'user' })
export class UserEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
password: string;
@Column()
nickname: string;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
@DeleteDateColumn()
deleted_at: Date;
@BeforeInsert()
private beforeInsert() {
this.password = bcrypt.hashSync(this.password, 10);
}
}
@BeforeInsert() 데코레이터는 TypeORM에서 제공하는 데코레이터 중 하나이며, 데이터베이스에 새로운 엔티티가 저장되기 전에 자동으로 실행되는 메서드를 정의할 수 있습니다.
bcrypt.hashSync() 함수는 bcrypt 라이브러리에서 제공하는 함수 중 하나로, 주어진 문자열을 해시화하여 반환합니다. 이 함수는 동기식으로 작동하므로, 해시화 작업이 완료될 때까지 대기합니다.
[ user/user.service.ts ]
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from 'src/user/entities/user.entity';
import { AuthDTO } from 'src/auth/dto/authDto';
@Injectable()
export class UserService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) { }
async create(authDTO: AuthDTO.SignUp) {
const userEntity = await this.userRepository.create(authDTO);
return await this.userRepository.save(userEntity);
}
async findById(id: number) {
return await this.userRepository.findOne({
where: {
id,
},
});
}
async findByEmail(email: string) {
return await this.userRepository.findOne({
where: {
email,
},
});
}
async findByNickname(nickname: string) {
return await this.userRepository.findOne({
where: {
nickname,
},
});
}
}
[ user/user.controller.ts ]
import {
Body,
ConflictException,
Controller, Post,
} from '@nestjs/common';
import { UserService } from './user.service';
import { AuthDTO } from 'src/auth/dto/authDto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
@Post('/signup')
async signup(@Body() authDTO: AuthDTO.SignUp) {
const { email, nickname } = authDTO;
const hasEmail = await this.userService.findByEmail(email);
if (hasEmail) {
throw new ConflictException('이미 사용중인 이메일 입니다.');
}
const hasNickname = await this.userService.findByNickname(nickname);
if (hasNickname) {
throw new ConflictException('이미 사용중인 닉네임 입니다.');
}
const userEntity = await this.userService.create(authDTO);
return '회원가입성공';
}
}
회원가입코드는 user crud와 관련된것이기 때문에 userController에서 작성해주었습니다.
throw error를 controller에서 처리하는 이유는, controller는 API 엔드포인트와 직접적으로 연결되어 있어서 클라이언트에게 에러 메시지를 응답으로 반환할 수 있기 때문입니다.
반면에, throw error를 service나 다른 모듈에서 처리하게 되면, 이 에러를 controller에서 캐치할 수 없기 때문에 클라이언트에게 적절한 응답을 보내줄 수 없습니다. 따라서 일반적으로 throw error는 controller에서 처리하는 것이 좋습니다.
[ user/user.module.ts ]
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserEntity } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])], //userrepository사용할수있도록 주입
exports: [UserService], //userService 다른곳에서 사용할수 있도록 exports
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
auth
인증에 관련한 것들은 auth에서 구현하였습니다.
[ auth/dto/authDto.ts ]
import { IsEmail,IsString ,Length} from "class-validator";
export namespace AuthDTO {
export class SignUp {
@IsEmail()
email: string;
@IsString()
@Length(4, 20)
password: string;
@IsString()
nickname: string;
}
export class SignIn {
@IsEmail()
email: string;
@IsString()
@Length(4, 20)
password: string;
}
}
[ auth/auth.controller.ts ]
import {
Controller,
Post,
Body,
Get,
Req,
UseGuards,
UnauthorizedException
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt/dist';
import * as bcrypt from 'bcrypt';
import { AuthDTO } from './dto/authDto';
import { UserService } from 'src/user/user.service';
@Controller()
export class AuthController {
constructor(private readonly userService: UserService) { }
@Post('/signin')
async signin(@Body() authDTO: AuthDTO.SignIn) {
const { email, password } = authDTO;
const user = await this.userService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('이메일 또는 비밀번호를 확인해 주세요.');
}
const isSamePassword = bcrypt.compareSync(password, user.password);
if (!isSamePassword) {
throw new UnauthorizedException('이메일 또는 비밀번호를 확인해 주세요.');
}
return "로그인 완료"
}
}
bcrypt.compareSync() 함수는 bcrypt 라이브러리에서 제공하는 함수 중 하나로, 주어진 문자열과 해시화된 문자열을 비교하여 일치 여부를 반환합니다. 이 함수는 동기식으로 작동하므로, 비교 작업이 완료될 때까지 대기합니다.
[ auth.module.ts ]
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
@Module({
imports: [UserModule],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
[ app.module.ts ]
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
import { UserEntity } from './user/entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [`${__dirname}/**/entities/*.entity.{ts,js}`],
synchronize: Boolean(process.env.DB_SYNC),
}),
AuthModule,
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule
여기까지 진행했는데 잘 안되는 거 같습니다...ㅠㅠㅠ
내일 차근차근 다시 해볼 거 같습니다