공부 시작한지도 4개월이 훌쩍 넘었고..첫 프로젝트로 했었던 간단한 게시판 crud를 만들면서 와.. 이걸 나중에 혼자서 만들 수 있을까? 하는 생각들은 절대 가능 하지 않을 거란 생각을 가졌었다.
하지만 결국 비슷한 crud를 수없이 많이 만들어보고 그 중 특히 모든 서비스에서 빠지지 않는 로그인과 관련된 인증
, 인가
와 같은 것들은 이제 몸에 익숙해졌다.
E-Commerce의 기본 제품 요소인 로그인, 회원가입 기능을 바닥부터 만들어 주는 것이다.
이는 E-Commerce형태를 띠는 거의 모든 제품의 기본적인 기능으로, 개발자로서 기본을 탄탄히 만들어가는데 매우 도움이 된다고 생각한다.
나는 여기서 특히 강점은 생각을 구체적으로 만들어낼 수 있는 구현력 이었던 것 같다.
항상 상상을 많이 하는 성격 덕분에 상상력이 뛰어나서 상상한대로 한글로 손코딩 하고, 코드 구현에 돌입하면 결국 내가 생각했던 상상을 바탕으로 코드가 완성되고, 그 코드가 실제로 작동이 결국엔 된다.
그럴때 마다 신기했지만 또 한가지 부족한 점이라고 한다면. 말 그대로 "구현" 하는데에만 급급해 성능적으로나 코드 리팩토링적 측면에서 바라봤을때 코드가 가독성이 좋지도 않고, 구조 아키텍처에 근거해서 잘 나눠져서 모듈화가 이루어져 있지도 않고, 코드들이 일관성 있게 모여있지 않아서 나중에 디버깅할 때 정말 어려워 질 수 있는 반쪽짜리구나 라는 내 약점을 항상 느낄 수 있었다.
그 점들을 해결하기 위하여 기본적으로 내가 사용하고 있는 도구들인 언어와 프레임워크 SQL과 같은 기술적인 부분에서는 충분히 기본기를 다지고 사용법을 잘 숙지해야지 내가 그때 그때 필요하다고 느끼는 부분에서 적재적소에 알맞게 잘 사용할 수 있을 것이라고 생각한다.
nest.js
세팅 및 필요한 리소스들을 제네레이터 해 준다.TypeORM
을 db로 사용하기 위해서 서버와 연결을 한다.환경변수
: jwt시크릿 코드나 db 연결코드들을 등록해주기 위해서 .env
에 저장하고 .gitignore
에 등록해준다.// app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { ConfigModuleValidationSchema } from 'configs/env.valid';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmModuleOptions } from 'configs/database.config';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { SecurityMiddleware } from './middleware/security.middleware';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: ConfigModuleValidationSchema,
}),
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SecurityMiddleware).forRoutes('*');
}
}
내가 이번에 만들 것들은 8시간 안에 로그인과 회원가입을 완벽하게 만드는 것이다.
시간이 부족하다면 기본 기능만 구현하고 시간이 낮으면 회원crud를 모두 다 할 예정이다.
회원가입을 먼저 구현하기 위해서 User의 엔티티를 먼저 만들어준다.
또한 만들어 주면서 미리 swagger
를 작성할 수 있는 데코레이터를 사용한다.
기본적인 crud하듯 body로 만든 값을 db에 저장해주는 코드는 금방 짜게 되었다.
여기서 초보자들은 햇갈리거나 깜빡할 수 있는데 자기가 사용할 repository가 있다면
꼭 데코레이터로 엔티티를 호출하면서 레포지를 연결 한 다음, 각 기능에 맞는 module에서 엔티티를 추가시켜 줘야한다.
db연결이 안되서 안되면 순간적으로 뇌 정지 와서 짜증날 수 있음.
이번 프로젝트의 가장 꽃은 사실 jwt이다.
jwt는 실제 서비스에서 로그인 인증,인가에서 가장 중요한 역할을 하는 아주 효자 녀석이다.
이 친구를 이용해서 accessToken과 refreshToken을 사용하여 클라이언트가 로그인 한 상태임을 서버가 알 수 있도록 해줄 수 있고 accessToken의 유효기간이 지나서 로그아웃되는 불편함을 없애기 위해서 리프래쉬토큰이 새로 생기고.. 어쨋든 이런 인증, 인가에 관련된 부분에선 모두가 인정할 만큼 중요한 부분이다.
jwt를 가드로 사용하기 위해서 전략파일을 만들어주었다
// src/starategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_ACCESS_SECRET'),
});
}
validate(payload: JwtPayload) {
return payload;
}
}
이 녀석의 역할은 리퀘스트로 받아온 bearer에 해당하는 토큰을 가져와서 등록했던 jwt 시크릿 코드로 payload를 읽을 수 있게 하는 부분이다.
그렇게 되면 payload는 로그인 성공한 클라이언트 유저가 jwt등록 할 때 넣었던 payload가 담겨져서 나올 것 이다.
여기서 엑세스 토큰과 리프래쉬 토큰에 대해서 이야기를 했을 때 나는 더이상 입 아플 정도로 많이 읽고, 또한 블로그에도 몇 번 포스팅을 한 적 있어서 역할에 대해서 나름 깊은 이해를 가지고 있다고 생각한다.
먼저 엑세스 토큰은 유저가 로그인한 상태임을 서버에게 알려주기 위해서 가지고 있는 인증키 같은 개념이다.
리프래쉬 토큰은 유저가 가지고 있는 엑세스 토큰이 탈취나 보안의 위험성 때문에 유효기간이 있는데 이 유효기간이 지나게 되면 그 토큰은 아에 사용하지 못하게 폐기가 되어버리는 것이다.
따라서 그때 그때 있을 수 있는 예외처리들을 미리 생각해두고 만들어 주는 방법도 좋았었다.
// 엑세스토큰 재발급
@ApiBearerAuth('access_token')
@ApiOperation({
summary: '액세스 토큰 갱신',
description: '리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다.',
})
@ApiResponse({ status: 401, description: '리프레시 토큰 유효하지 않음' })
@Post('/refresh')
@UseGuards(AuthGuard('jwt'))
async refreshAccessToken(@Req() req) {
const user = req.user;
const currentTime = Math.floor(Date.now() / 1000);
// if (user.exp < currentTime) {
const refreshToken = await this.userRepository.findOne({
where: { userId: user.userId },
select: ['refreshToken'],
});
const refreshTokenVerify = await this.jwtService.verify(
refreshToken.refreshToken,
{
secret: this.configService.get('JWT_REFRESH_SECRET'),
},
);
console.log('래프래쉬검증', refreshTokenVerify);
if (refreshTokenVerify.exp < currentTime) {
throw new UnauthorizedException('재 로그인이 필요합니다.');
}
const newAccessToken =
await this.authService.generateAccessToken(refreshTokenVerify);
return {
HttpStatus: 200,
newAccessToken,
// };
};