Nest.js 심화
2. JWT 발급하기
Nest.js에서의 jwt 패키지는 @nestjs/jwt
@nestjs/jwt 설치
npm i @nestjs/jwt
Auth 에 대해 알아보기
- Authentication (인증)
- 인증은 login 함수와 같은 함수를 통해 사용자 ID와 암호와 같은 데이터를 요구하여 사용자의 신원을 파악하는 프로세스에요. 이 인증 절차를 통과한 유저만이 해당 유저임을 증빙할 수 있는 JWT 토큰을 받아요.
- Authorization (승인 또는 인가)
- 승인은 해당 사용자가 특정 함수 혹은 리소스에 접근할 수 있는지를 파악하는 프로세스에요. 발급받은 JWT 토큰을 토대로 서버는 특정 사용자임을 알아내고 특정 사용자에 관련된 액션은 전부 허가를 해줘요.
user.module.ts 1차
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from 'src/board/article.entity';
import { JwtConfigService } from 'src/config/jwt.config.service';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
imports: [
TypeOrmModule.forFeature([User]), // 이건 TypeORM 강의 시간에 배웠죠?
JwtModule.register({
secret: 'secret', // 일단은 시크릿키를 하드코딩한 상태
signOptions: { expiresIn: '3600s' }, // 토큰의 만료시간은 1시간
}),
],
providers: [UserService],
exports: [UserService],
controllers: [UserController],
})
export class UserModule {}
위 코드는 UserService에서 JWT 패키지를 사용할 수 있게 JwtModule.register 함수를 통해서 주입
또한 위에 시크릿키를 하드코딩할 경우 해커에게 시크릿키가 유출될 경우 계정 해킹이 될수있다.
저번에 배운것 처럼 @nest.js/config 패키지를 통해 비밀키를 캡슐화 한다.
config/jwt.config.service.ts 생성
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModuleOptions, JwtOptionsFactory } from "@nestjs/jwt";
@Injectable()
export class JwtConfigService implements JwtOptionsFactory {
constructor(private readonly configService: ConfigService) {}
createJwtOptions(): JwtModuleOptions {
return {
secret: this.configService.get<string>("JWT_SECRET"),
signOptions: { expiresIn: "3600s" },
};
}
}
user.module.ts 2차
JwtModule.registerAsync({
imports: [ConfigModule],
useClass: JwtConfigService,
inject: [ConfigService],
}),
로그인 함수에서 JWT 발급받기
user.service.ts
import {
ConflictException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
private jwtService: JwtService,
) {}
async login(userId: string, password: string) {
const user = await this.userRepository.findOne({
where: { userId, deletedAt: null },
select: ['id', 'password'],
});
if (_.isNil(user)) {
throw new NotFoundException(`User not found. userId: ${userId}`);
}
if (user.password !== password) {
throw new UnauthorizedException(
`User password is not correct. userId: ${userId}`,
);
}
const payload = { id: user.id };
const accessToken = await this.jwtService.signAsync(payload);
return accessToken;
}
async createUser(userId: string, name: string, password: string) {
const existUser = await this.getUserInfo(userId);
if (!_.isNil(existUser)) {
throw new ConflictException(`User already exists. userId: ${userId}`);
}
const insertResult = await this.userRepository.insert({
userId,
name,
password,
});
const payload = { id: insertResult.identifiers[0].id };
const accessToken = await this.jwtService.signAsync(payload);
return accessToken;
}
updateUser(userId: string, name: string, password: string) {
this.userRepository.update({ userId }, { name, password });
}
async getUserInfo(userId: string) {
return await this.userRepository.findOne({
where: { userId, deletedAt: null },
select: ['name'], // 이외에도 다른 정보들이 필요하면 리턴해주면 됩니다.
});
}
}
일단, createUser가 async 함수로 바뀌었습니다. 저는 회원가입을 완료하면 곧바로 로그인 처리가 되길 원하기 때문에 바로 JWT를 발급하게끔 바꾸었어요! 그러기 위해서는 insert가 되었을 때 유저에 해당되는 id를 알아야 하기 때문에 async 함수로 바뀐것이에요!
또한, DI를 통해 주입된 this.jwtService의 signAsync 함수로 JWT를 발급을 하는 코드를 넣었습니다. 이제 회원가입 및 로그인에 성공하면 클라이언트는 JWT를 받을 수 있어요!
3. JWT 검증하기
JWT를 검증하는 Auth 미들웨어를 만들자
auth/auth.middleware.ts
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private jwtService: JwtService) {}
async use(req: any, res: any, next: Function) {
const authHeader = req.headers.authorization;
if (!authHeader) {
throw new UnauthorizedException("JWT not found");
}
let token: string;
try {
token = authHeader.split(" ")[1];
const payload = await this.jwtService.verify(token);
req.user = payload;
next();
} catch (err) {
throw new UnauthorizedException(`Invalid JWT: ${token}`);
}
}
}
클라이언트가 헤더에 Authorization 필드로 Bearer {JWT} 를 보내면 ({JWT}에는 서버에서 실제로 받은 JWT를 채워넣어야 합니다) AuthMiddleware는 JWT를 파싱하여 특정 유저임을 파악할 수 있습니다.
AuthMiddleware는 모듈의 형태가 아니고 독립적인 서비스이기 때문에
JwtService를 DI하기 위해서 AppModule에서 JwtService를 주입해야 한다.
AppModule에도 JwtModule를 import 시켜야 한다.
app.module.ts 수정
JwtModule.registerAsync({ // AuthMilddleware에서도 사용할 수 있게 import
imports: [ConfigModule],
useClass: JwtConfigService,
inject: [ConfigService],
}),
User 컨트롤러 추가
nest g co user
user.controller.ts
import { Controller, Get, Post, Put } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}
@Post("/login")
async login() {
return await this.userService.login("userId", "password");
}
@Post("/signup")
async createUser() {
return await this.userService.createUser("userId", "name", "password");
}
@Put("/update")
updateUser() {
this.userService.updateUser("userId", "new_name", "new_password");
}
}
올바른 JWT를 갖고있는 사용자만이 호출할 수 있도록 설정해야 합니다.
app.module.ts 2차
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BoardModule } from './board/board.module';
import { TypeOrmConfigService } from './config/typeorm.config.service';
import { UserModule } from './user/user.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { JwtConfigService } from './config/jwt.config.service';
import { AuthMiddleware } from './auth/auth.middleware';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useClass: TypeOrmConfigService,
inject: [ConfigService],
}),
JwtModule.registerAsync({
imports: [ConfigModule],
useClass: JwtConfigService,
inject: [ConfigService],
}),
BoardModule,
UserModule,
],
controllers: [AppController],
providers: [AppService, AuthMiddleware], // AuthMiddleware 추가
})
export class AppModule implements NestModule {
// NestModule 인터페이스 구현
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware) // 미들웨어 적용!
.forRoutes({ path: 'user/update', method: RequestMethod.PUT });
}
}
providers 에 AuthMiddleware를 추가하고
NestModule 인터페이스를 구현해야한다.
위의 코드에서는 PUT /user/update에 해당되는 API에 AuthMiddleware를 적용하겠다
유저 정보를 업데이트를 할 때 올바른 JWT를 넘겨야 유저 정보를 업데이트 할 수 있다.
4. JWT 기능 테스트
테스트를 하기 위해 user.controller.ts
import { Controller, Get, Post, Put } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}
@Post("/login")
async login() {
return await this.userService.login("test_id", "test_pw");
}
@Post("/signup")
async createUser() {
return await this.userService.createUser("test_id", "test_name", "test_pw");
}
@Put("/update")
updateUser() {
this.userService.updateUser("test_id", "test_new_n", "test_new_p");
}
}
테스트 해볼려고 하니까 수정해야 될 부분이 더 있었다..
.env
JWT_SECRET="SECRET"
추가
typeorm.config.service.ts 의 엔티티에 User 추가
import { User } from 'src/user/user.entity';
entities: [Article, User],
'코딩캠프 > 내일배움캠프' 카테고리의 다른 글
[ TIL ] 02.24(금) 73일차 (0) | 2023.02.24 |
---|---|
[ TIL ] 02.23(목) 72일차 (0) | 2023.02.23 |
[ TIL ] 02.21(화) 70일차 (0) | 2023.02.21 |
[ TIL ] 02.20(월) 69일차 (0) | 2023.02.20 |
[ WIL ] 02.13~17 14주차 (0) | 2023.02.19 |