7. JWT(JSON Web Token)
1. JWT
로그인을 할 때 로그인한 유저를 위한 토큰을 생성한 후 제공해야 한다. 이 때 JWT라는 모듈을 사용한다. JWT는 정보를 JSON 객체로 안전하게 전송하기 위한 스펙이다. 정보를 안전하게 전송할 때 혹은 유저의 권한과 같은 것을 검사하기 위해 사용하는 것이다.
JWT의 구조는 Header, Payload, Verify Signature로 이루어져 있다. Header는 타입, 해싱 알고리즘 등 토큰에 대한 메타데이터를 포함하고 있다. Payload는 유저 정보, 만료 기간, 주제 등을 포함하고 있다. Verify Signature는 토큰이 보낸 사람에 의해 서명되었으며 어떤 식으로든 변경되지 않았는지 확인하는 데 사용된다. 이 Verify Signature는 Header 및 Payload, 서명 알고리즘, 비밀키 또는 공개키를 사용하여 생성된다.
요청으로부터 온 headers
및 payload
에, 서버에서 가지고 있는 Secret을 결합해 Verify Signature를 다시 생성한다. 처음 생성했던 JWT의 Verify Signature와 동일한지 여부를 확인한다.
JWT를 이용해서 인증을 처리하기 위해 다음과 같은 모듈을 설치한다.
Auth
모듈에 PassportModule
및 JwtModule
을 등록한다.
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { User } from "./user.entity";
@Module({
imports: [
PassportModule.register({ defaultStrategy: "jwt" }),
JwtModule.register({
secret: "Secret1234",
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([User]),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
2. 로그인 성공 시 JWT를 이용해서 토큰 생성해 주기
Auth
모듈에 JwtModule
을 등록했기 때문에 AuthService
에서 가져와 사용할 수 있다.
import {
ConflictException,
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { User } from "./user.entity";
import * as bcrypt from "bcryptjs";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
private jwtService: JwtService
) {}
async signUp(authCredentialDto: AuthCredentialDto): Promise<void> {
const { username, password } = authCredentialDto;
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(password, salt);
const user = this.userRepository.create({
username,
password: hashedPassword,
});
try {
await this.userRepository.save(user);
} catch (error) {
if (error.code === "23505") {
throw new ConflictException("Existing username");
} else {
throw new InternalServerErrorException();
}
}
}
async signIn(
authCredentialDto: AuthCredentialDto
): Promise<{ accessToken: string }> {
const { username, password } = authCredentialDto;
const user = await this.userRepository.findOneBy({ username });
if (user && (await bcrypt.compare(password, user.password))) {
const payload = { username };
const accessToken = this.jwtService.sign(payload);
return { accessToken };
} else {
throw new UnauthorizedException("login failed");
}
}
}
import { Body, Controller, Post, ValidationPipe } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@Post("/signup")
signUp(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<void> {
return this.authService.signUp(authCredentialDto);
}
@Post("/signIn")
signIn(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<{ accessToken: string }> {
return this.authService.signIn(authCredentialDto);
}
}
3. Passport를 사용해서 요청 내의 유저 정보 얻기
이전 작업은 토큰을 생성해서 제공하는 목적이었고, 지금부터 할 것은 토큰을 인증하는 작업이다. PassportModule
은 요청 내의 유저 정보를 이용해서 인증 처리 등의 작업을 쉽게 처리할 수 있게 한다.
먼저 다음과 같이 필요한 모듈을 설치한다.
다음과 같이 JwtStrategy
를 정의한다.
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { InjectRepository } from "@nestjs/typeorm";
import { ExtractJwt, Strategy } from "passport-jwt";
import { Repository } from "typeorm";
import { User } from "./user.entity";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(User) private userRepository: Repository<User>
) {
super({
secretOrKey: "Secret1234",
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
async validate(payload: any) {
const { username } = payload;
const user: User = await this.userRepository.findOneBy({ username });
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
이제 이 JwtStrategy
를 사용하기 위해 Auth
모듈의 providers
배열에 넣어준다. 그리고 다른 모듈에서 JwtStrategy
와 PassportModule
을 사용해야 하기 때문에 exports
배열에 넣어준다.
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { User } from "./user.entity";
@Module({
imports: [
PassportModule.register({ defaultStrategy: "jwt" }),
JwtModule.register({
secret: "Secret1234",
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([User]),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
4. 요청 안에 유저 정보(User
객체)가 들어가게 하는 방법
현재 요청 내에는 유저 정보가 존재하지 않기 때문에 인증 처리를 할 수 없다. 이를 해결하기 위해서 라우트 핸들러에 @UseGuards
데코레이터와 AuthGuard()
를 이용한다.
Guard, Interceptor 등 미들웨어가 불리는 순서는 다음과 같다.
Middleware → Guard → Interceptor (before) → pipe → controller → service → controller → interceptor (after) → filter → client
import {
Body,
Controller,
Post,
Req,
UseGuards,
ValidationPipe,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@Post("/signup")
signUp(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<void> {
return this.authService.signUp(authCredentialDto);
}
@Post("/signIn")
signIn(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<{ accessToken: string }> {
return this.authService.signIn(authCredentialDto);
}
@Post("/test")
@UseGuards(AuthGuard())
test(@Req() req) {
console.log("req", req);
}
}
test()
라우트 핸들러에 요청을 보내면 다음과 같이 user
프로퍼티의 값으로 User
객체가 확인된다.
...
user: User {
id: 3,
username: 'user2',
password: '$2a$10$yW.CLGOfiCGHwHG00miri.Yc8EVlBy71AupqsKsFlifp/vMLZ3bCG'
},
...
5. 커스텀 데코레이터 생성하기
위의 user
프로퍼티에 접근하려면 항상 req.user
처럼 접근해야 한다. user
라는 파라미터로 바로 가져올 수 있게 커스텀 데코레이터를 구현해 보자.
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { User } from "./user.entity";
export const GetUser = createParamDecorator(
(data, ctx: ExecutionContext): User => {
const req = ctx.switchToHttp().getRequest();
return req.user;
}
);
이 데코레이터를 라우트 핸들러에 달아준다.
import {
Body,
Controller,
Post,
UseGuards,
ValidationPipe,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { AuthCredentialDto } from "./dto/auth-credential.dto";
import { GetUser } from "./get-user.decorator";
import { User } from "./user.entity";
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@Post("/signup")
signUp(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<void> {
return this.authService.signUp(authCredentialDto);
}
@Post("/signIn")
signIn(
@Body(ValidationPipe) authCredentialDto: AuthCredentialDto
): Promise<{ accessToken: string }> {
return this.authService.signIn(authCredentialDto);
}
@Post("/test")
@UseGuards(AuthGuard())
test(@GetUser() user: User) {
console.log("user", user);
}
}