그동안은 passport와 세션을 이용하여 로그인을 구현해왔었는데, jwt도 이번 기회에 익혀둘려고한다. Nest로 구현하면서 생각보다 조금 해맸기에, 블로그에 정리하려고 한다.
우선 어떤 순서로 로직이 작동되는지 이해해야한다. access token, refresh token 개념은 다른 곳에도 정리가 잘 되어있어 자세한 설명은 생략한다.
작동 순서
- 로그인시 access token, refresh token 발급, refresh token은 DB에 저장
- access token, refresh token 모두 httpOnly 쿠키로 클라이언트에 전송
- 클라이언트에서 페이지 이동할 때마다 access token을 서버에 보내 토큰 유효성 확인
- acces token 불일치 시, refresh token 유효 여부 확인
- refresh token 유효할 시, access token 발급 / 유효하지 않을 시 재로그인 요청
이 순서로 작동된다고 보면 된다. 우선 로그인부터 구현해보자. user모듈과 auth모듈을 분리해서 작성하였다.
user.service에서는 간단하게 유저 정보 db 존재 여부를 return해주고, 이 return된 데이터를 바탕으로 auth 부분이 작동한다. 순서대로 코드를 설명하자면
Module 설정
일단 jwt를 사용하기 위해 auth.module에 설정을 해준다.
// auth.module.ts
import { Module } from "@nestjs/common"
import { AuthService } from "./auth.service"
import { UserModule } from "../user/user.module"
import { PassportModule } from "@nestjs/passport"
import { LocalStrategy } from "./local.strategy"
import { AuthController } from "./auth.controller"
import { jwtConstants } from "./constants"
import { JwtModule } from "@nestjs/jwt"
import { JwtStrategy } from "./jwt.strategy"
import { TypeOrmModule } from "@nestjs/typeorm"
import { UserInfoEntity } from "../user/entities/user.entity"
import { RefreshJwtStrategy } from "./refresh-jwt.strategy"
@Module({
imports: [UserModule,
TypeOrmModule.forFeature([UserInfoEntity]),
PassportModule.register({
defaultStrategy: "jwt"
}),
JwtModule.register({
secret: jwtConstants.ACCESS_TOKEN_SECRET,
signOptions: { expiresIn: "5m" }
})],
providers: [AuthService, LocalStrategy, JwtStrategy],
controllers: [AuthController]
})
export class AuthModule {
}
여기서는 jwt를 사용할 것이므로, passportModule의 기본 Strategy를 jwt로 설정한다. 그리고 jwt의 secret키와 만료 시간을 설정한다. 여기서는 access token에 관한 옵션을 설정하고, refresh token 옵션은 따로 설정한다.
Controller 설정
// auth.controller.ts
import { Controller, Request, Res, Post, UseGuards, Get } from "@nestjs/common"
import { AuthGuard } from "@nestjs/passport"
import { LocalAuthGuard } from "./local-auth.guard"
import { AuthService } from "./auth.service"
import { InjectRepository } from "@nestjs/typeorm"
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {
}
@UseGuards(LocalAuthGuard)
@Post("/login")
async login(@Request() req: any, @Res({ passthrough: true }) res: any) {
const { access_token, refresh_token }: { access_token: string, refresh_token: string }
= await this.authService.login(req.user)
res.cookie("access_token", access_token, {
httpOnly: true,
secure: true,
// maxAge: 5 * 60 * 1000
// maxAge: 10 * 1000
})
res.cookie("refresh_token", refresh_token, {
httpOnly: true,
secure: true,
// maxAge: 24 * 60 * 60 * 1000
})
return req.user.user_name
}
}
- @UseGuards(LocalAuthGuard): LocalAuthGuard라는 가드에서 전달받은 값의 유효성을 검사한다. (아래 코드)
- @Request() req: Guard에서 return된 값이 넘어오게 된다.
- @Res({ passthrough: true }) res: 클라이언트에 쿠키를 전달하기 위해 설정한다. passthrough: true를 설정해야 쿠키를 넘길 수 있다.
클라이언트에서 값을 받아오고, Guard를 통해 유효성을 검증한 뒤, 통과되면 access_token, refresh_token을 쿠키로 전달해주는 코드다. 유저 정보는 body에 담아서 보내줬다.
Guard 설정
그 다음으로 LocalAuthGuard를 참고해보면, passport모듈을 사용하였다.
// local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
// local.strategy.ts
import { Strategy } from "passport-local"
import { PassportStrategy } from "@nestjs/passport"
import { Injectable, UnauthorizedException } from "@nestjs/common"
import { AuthService } from "./auth.service"
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
// 받아오는 key 이름 설정
usernameField: "user_name",
passwordField: "user_pwd"
})
}
// validate라는 함수이름은 변경하면 에러 발생.
async validate(user_name: string, user_pwd: string): Promise<any> {
const user = await this.authService.validateUser(user_name, user_pwd)
if (!user) {
throw new UnauthorizedException()
}
return {
"user_UUID": user.user_UUID,
"user_name": user.user_name
}
}
}
local-auth.guard.ts를 보면 AuthGuard('local')이라는 코드가 존재하는데, 이 코드는 passport-local모듈의 Strategy를 사용하는 파일을 찾아서 연결해준다. 아래와 같이 이름도 설정할 수 있다. 이름을 설정하면 AuthGuard에 설정한 이름을 넣으면 된다.
PassportStrategy(Strategy, 'setName')
/auth/login으로 전송된 user_name, user_pwd를 Guard를 사용해 local.strategy.ts로 가져왔다. 가져온 값들은 validate 함수의 매개변수로 가져올 수 있다.
가져온 값들을 authService에 정의해논 validateUser 메서드를 통해 이름과 비밀번호가 유효한지 확인한 후, 없을 시, 401에러, 있을 시 유저 정보를 가져온다. 그리고 여기서 user정보를 return해준다. 여기서 return된 값은 auth.controller.ts의login 메서드로 가게되고, 이 메서드에서 req 매개변수로 받게되고, 이 값들을 통해, access token, refresh token을 발급받는다.
Service 설정
login 메서드에서 사용했던, access token, refresh token을 발급해주는 authService의 login 메서드 코드는 아래와 같다.
// auth.service.ts
import { Injectable, UnauthorizedException } from "@nestjs/common"
import { UserService } from "../user/user.service"
import { JwtService } from "@nestjs/jwt"
import * as bcrypt from "bcrypt"
import { jwtConstants } from "./constants"
import { In, Repository } from "typeorm"
import { InjectRepository } from "@nestjs/typeorm"
import { UserInfoEntity } from "../user/entities/user.entity"
import { UserInfoDto } from "./dto/user-info-dto"
@Injectable()
export class AuthService {
constructor(private userService: UserService,
private jwtService: JwtService,
@InjectRepository(UserInfoEntity) private userInfoRepository: Repository<UserInfoEntity>
) {
}
async validateUser(user_name: string, user_pwd: string): Promise<any> {
const user = await this.userService.userLogin(user_name)
if (user && await bcrypt.compare(user_pwd, user.user_pwd) === true) {
return user
}
return null
}
async generateAccessToken(user: UserInfoDto) {
const payload = { user_name: user.user_name }
return this.jwtService.sign(payload)
}
async generateRefreshToken(user: UserInfoDto) {
const payload = { user_name: user.user_name }
return this.jwtService.sign(payload, {
secret: jwtConstants.REFRESH_TOKEN_SECRET,
expiresIn: "1d"
})
}
async login(user: any) {
const payload = { user_name: user.user_name }
const access_token: string = await this.generateAccessToken(user)
const refresh_token: string = await this.generateRefreshToken(user)
await this.userInfoRepository.update({ user_UUID: user.user_UUID }, {
refresh_token: refresh_token
})
return {
user_name: user.user_name,
access_token,
refresh_token
}
}
}
controller에서 받아온 user정보로 payload를 설정한다. 그리고 이를 바탕으로 access token과 refresh token을 발급한다. 이 두 토큰을 생성하는 함수를 분리한 이유는 이후 재발급할 때도 사용하기 위함이다. 그리고 refresh 토큰의 secret키와 만료 시간을 설정하고, 나중에 유효성 검사를 하기 위해, db에 저장해둔다. 여기서 return된 토큰과 유저 정보가 controller에 return 되고, 클라이언트로 전송된다.
이제 남은건 기능은 api요청이 들어올 때마다 access token, refresh token을 검증하는 기능이다.
깃허브 주소
Anhye0n - Overview
anhye0n.me. Anhye0n has 10 repositories available. Follow their code on GitHub.
github.com
'coooding > NestJS' 카테고리의 다른 글
| [NestJS] cors 해결 (0) | 2024.02.24 |
|---|