1. Auth Resource 생성
nest g resource
// 다음 'Auth' 로 생성
2. JWT 패키지 설치
yarn add @nestjs/jwt bcrypt
// 혹은
npm i @nestjs/jwt bcrypt
jwt와 bcrypt 패키지를 설치한다.
3. Token 발급 메서드
signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean) {
const payload = {
email: user.email,
sub: user.id,
type: isRefreshToken ? 'refresh' : 'access',
};
return this.jwtService.sign(payload, {
secret: JWT_SECRET,
//seconds
expiresIn: isRefreshToken ? 3600 : 300,
})
}
참고로 auth.service.ts 에 작성해야하며, 해당 로직에는 비밀번호가 포함되지 않아야 한다.
4. loginUser 메서드
loginUser(user: Pick<UsersModel, 'email' | 'id'>) {
return {
accessToken: this.signToken(user, false),
refreshToken: this.signToken(user, true),
}
}
loginUser 메서드는 사용자가 로그인을 하거나 회원가입을 할 때, AccessToken과 RefreshToken을 발급받는 공통적인 과정을 하나의 메서드로 작성한 것이다.
5. 회원가입
@Post('register/email')
postRegisterEmail(
@Body('nickname') nickname: string,
@Body('email') email: string,
@Body('password') password: string,
) {
return this.authService.registerWithEmail({
nickname,
email,
password,
});
}
auth.controller.ts 에 위와 같이 라우터를 구현한다.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModel } from './entities/users.entity';
@Module({
imports: [TypeOrmModule.forFeature([UsersModel])],
exports: [UsersService],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
exports: [UsersService],
우선 로직을 작성하기에 앞서 UsersModel에 접근해야 한다.
그러나 Auth에서 UsersModel에 직접 접근하기보다는 이미 만들어둔 메서드를 사용하기 위해 users.service.ts 에 있는 메서드를 가져오도록 한다.
그러기 위해선 users.module.ts 내에 exports로 UsersService를 등록한다.
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService,
) {}
이후 auth.service.ts 내의 생성자에 UsersService를 등록하여 사용할 수 있다.
async registerWithEmail(user: Pick<UsersModel, 'nickname' | 'email' | 'password'>) {
const hash = await bcrypt.hash(
user.password,
HASH_ROUNDS,
);
const newUser = await this.usersService.createUser({
...user,
password: hash,
});
return this.loginUser(newUser);
}
registerWithEmail 메서드는 클라이언트로부터 받은 nickname, email, password를 받아온 뒤 password와 HASH_ROUNTDS(salt)를 이용해 bcrypt 해쉬를 만들고, 이것을 usersService 내에 만들어져있는 createUser 메서드를 호출하여 유저를 DB에 등록한다.
그리고 loginUser 메서드를 호출하여 AccessToken과 RefreshToken을 생성하여 클라이언트에 반환한다.
6. 이메일 로그인
@Post('login/email')
postLoginEmail(
@Headers('authorization') rawToken: string,
) {
// email:password -> base64
// 디코딩 후 email과 password를 나눠야함
const token = this.authService.extractTokenFromHeader(rawToken, false);
const credentials = this.authService.decodeBasicToken(token);
return this.authService.loginWithEmail(credentials);
}
auth.controller.ts 에 위와 같이 라우터를 구현한다. rawToken은 클라이언트에서 'email:password' 로 만든 후 base64 인코딩한 문자열이다.
/**
* Header로 부터 토큰을 받을 때
*
* {authorization: 'Basic {token}'}
* {authorization: 'Bearer {token}'}
*/
extractTokenFromHeader(header: string, isBearer: boolean) {
const splitToken = header.split(' ');
const prefix = isBearer ? 'Bearer' : 'Basic';
if(splitToken.length !== 2 || splitToken[0] !== prefix) {
throw new UnauthorizedException('잘못된 토큰입니다.');
}
const token = splitToken[1];
return token;
}
extractTokenFromHeader는 클라이언트에서 보낸 헤더를 기반으로 Basic (토큰생성), Bearer (토큰사용)을 구분하여 토큰을 반환한다.
/**
* Basic asdlkfjadlfkjadflkjasdf
*
* 1) asdlkfjadlfkjadflkjasdf -> email:password (디코딩)
* 2) email:password -> [email, password]
* 3) {email: email, password: password}
*/
decodeBasicToken(base64String: string) {
const decoded = Buffer.from(base64String, 'base64').toString('utf8');
const split = decoded.split(':');
if(split.length !== 2) {
throw new UnauthorizedException('잘못된 유형의 토큰입니다.');
}
const email = split[0];
const password = split[1];
return {
email,
password,
}
}
decodedBasicToken은 받아온 token(email:password)을 디코딩하는 과정이다. 객체로 만들어 email, password을 반환한다.
async loginWithEmail(user: Pick<UsersModel, 'email' | 'password'>) {
const existingUser = await this.authenticateWithEmailAndPassword(user);
return this.loginUser(existingUser);
}
async authenticateWithEmailAndPassword(user: Pick<UsersModel, 'email' | 'password'>) {
const existingUser = await this.usersService.getUserByEmail(user.email);
if (!existingUser) {
throw new UnauthorizedException('존재하지 않는 사용자입니다.');
}
/**
* 파라미터
*
* 1) 입력된 비밀번호
* 2) 기존 해시(hash) -> 사용자 정보에 저장되어있는 hash
*/
const passOk = await bcrypt.compare(user.password, existingUser.password);
if (!passOk) {
throw new UnauthorizedException('비밀번호가 틀렸습니다.');
}
return existingUser;
}
사용자가 이메일과 비밀번호로 로그인을 진행하면 입력된 이메일로 유저를 DB에서 가져온 뒤, DB에 저장된 패스워드(base64 암호화)와 입력된 비밀번호를 비교하여 일치하는 경우 유저를 반환하고, 이 유저를 가지고 loginUser 메서드를 실행하여 AccessToken과 RefreshToken을 클라이언트에 반환한다.
7. AccessToken 갱신
@Post('token/access')
postTokenAccess(
@Headers('authorization') rawToken: string,
) {
const token = this.authService.extractTokenFromHeader(rawToken, true);
const newToken = this.authService.rotateToken(token, false);
/**
* {accessToken: {token}}
*/
return {
accessToken: newToken,
}
}
auth.controller.ts 에 postTokenAccess 메서드를 라우팅한다.
/**
* Header로 부터 토큰을 받을 때
*
* {authorization: 'Basic {token}'}
* {authorization: 'Bearer {token}'}
*/
extractTokenFromHeader(header: string, isBearer: boolean) {
const splitToken = header.split(' ');
const prefix = isBearer ? 'Bearer' : 'Basic';
if(splitToken.length !== 2 || splitToken[0] !== prefix) {
throw new UnauthorizedException('잘못된 토큰입니다.');
}
const token = splitToken[1];
return token;
}
로그인때와 마찬가지로 클라이언트에서 보낸 헤더를 분해하여 토큰을 반환한다.
rotateToken(token: string, isRefreshToken: boolean) {
const decoded = this.verifyToken(token);
/**
* sub: id
* email: email,
* type: 'access' | 'refresh'
*/
if(decoded.type !== 'refresh') {
throw new UnauthorizedException('토큰 재발급은 Refresh 토큰으로만 가능합니다!');
}
return this.signToken({
...decoded
}, isRefreshToken);
}
이후 토큰을 검증 후 signToken을 통해 토큰을 재발급한다.
/**
* 토큰 검증
*/
verifyToken(token: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.jwtService.verify(token, {
secret: JWT_SECRET,
});
}
verifyToken은 jwtService.verify 를 통해 AccessToken과 RefreshToken을 검증할 수 있다.
8. RefreshToken 갱신
@Post('token/refresh')
postTokenRefresh(
@Headers('authorization') rawToken: string,
) {
const token = this.authService.extractTokenFromHeader(rawToken, true);
const newToken = this.authService.rotateToken(token, true);
/**
* {refreshToken: {token}}
*/
return {
refreshToken: newToken,
}
}
AccessToken 갱신과 다른 부분은 rotateToken에 두 번째 매개변수 isRefreshToken: boolean차이 뿐이다.
'NestJS' 카테고리의 다른 글
NestJS - Pipe (0) | 2025.02.26 |
---|---|
VSC 디버거 사용하기 (0) | 2025.02.26 |
Authentication - Encryption (암호화) (0) | 2025.02.26 |
Authentication - JWT (0) | 2025.02.26 |
TypeORM - Repository 메서드 종류 (0) | 2025.02.25 |