프로젝트
트러블슈팅 3 .refreshToken 재발급
늘곰's
2023. 10. 10. 09:54
리프레시 토큰을 재발 급 받는 과정에서의 트러블 슈팅
서비스 부분에서 리프레시 토큰을 발급받는 로직을 우선적으로 만듬
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from './../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import {
IAuthServiceGetAccessToken,
IAuthServiceGetRefereshToken,
IAuthServiceLogin,
} from './interface/auth-service.interface';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async login({ email, password }: IAuthServiceLogin): Promise<{
accessToken: string;
refreshToken: string;
}> {
// 1. 이메일이 일치하는 유저를 DB에서 찾기
const user = await this.usersService.findByEmail({ email });
// 2. 일치하는 유저가 없으면 에러
if (!user) throw new NotFoundException('이메일이 없습니다.');
// 3. 일치하는 유저는 있지만 비밀번호가 틀렸다면 에러
const isAuth = await bcrypt.compare(password, user.password);
if (!isAuth)
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
// 4. 리프레시 토큰 생성
const refreshToken = this.generateRefreshToken({ user });
// 5. 리프레시 토큰을 DB 또는 다른 안전한 저장소에 저장 (여기서는 저장하지 않고 클라이언트로 직접 보냄)
// 6. 액세스 토큰 및 리프레시 토큰을 반환
const accessToken = await this.getAccessToken({ user });
return { accessToken, refreshToken };
}
async getAccessToken({ user }: IAuthServiceGetAccessToken): Promise<string> {
const accessToken = this.jwtService.sign(
{ sub: user.userId },
// secret 및 expiresIn 설정을 JwtModule.register에서 관리하도록 수정
{ secret: process.env.JWT_ACCESS_KEY, expiresIn: '15s' },
);
return accessToken;
}
generateRefreshToken({ user }: IAuthServiceGetRefereshToken): string {
// 리프레시 토큰을 생성하는 로직을 구현
const refreshToken = this.jwtService.sign(
{ sub: user.userId },
{ secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' },
);
return refreshToken;
}
async refreshAccessToken(refreshToken: string): Promise<string> {
try {
// 리프레시 토큰의 유효성을 검증
const decodedToken = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_REFRESH_KEY,
});
// 리프레시 토큰이 유효하다면 새로운 액세스 토큰을 발급
const newAccessToken = await this.getAccessToken({ user: decodedToken });
return newAccessToken;
} catch (error) {
// 검증 실패 시 (예: 서명이 유효하지 않은 경우) 에러 처리
throw new UnauthorizedException('Invalid refresh token');
}
}
}
그 후에 컨트롤러 부분에서 코드를 처리하던중 headers에서 정보가 제대로 안들어오고 있었다..
'{ new (init?: HeadersInit): Headers; prototype: Headers; }' 형식의 값은 호출할 수 없습니다. 'new'를 포함하려고 했습니까?ts(2348)
이러한 오류가 생기면서 값이 제대로 들어오지 않았음.
import { Body, Controller, Post, Res } from '@nestjs/common';
import { AuthService } from './auth.service';
import {
ApiOkResponse,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';
import { Response } from 'express';
@Controller('users')
@ApiTags('Auth')
@ApiOkResponse({ type: AuthEntity })
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: '로그인' })
@ApiResponse({ status: 200, description: '로그인에 성공하셨습니다.' })
@ApiResponse({ status: 400, description: '이메일이 없습니다.' })
@ApiResponse({ status: 401, description: '비밀번호가 일치하지 않습니다.' })
@Post('login')
async login(
@Body() { email, password }: LoginDto,
@Res({ passthrough: true }) res: Response, // Response 객체 주입
): Promise<void> {
const { accessToken, refreshToken } = await this.authService.login({
email,
password,
});
// 엑세스 토큰을 HTTP 응답 헤더에 추가
res.header('access-token', accessToken);
// 리프레시 토큰을 쿠키로 설정하여 클라이언트에게 전달
//httpOnly : javascript 로 쿠키에 접근 할 수 없는 옵션
//secure : true 일 시 https 연결에서만 전송된다.
//리프레시 토큰을 HTTP 응답 헤더에 추가
res.header('refreshToken', refreshToken);
//res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: false });
// 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
res.status(200).json({ accessToken }); // 클라이언트에게 JSON 응답을 보냄
}
// 리프레시 토큰을 사용하여 엑세스 토큰 재발급을 위한 엔드포인트 추가
@ApiOperation({ summary: '리프레시 토큰을 사용하여 엑세스 토큰 재발급' })
@ApiResponse({
status: 200,
description: '엑세스 토큰 재발급에 성공하셨습니다.',
})
@ApiResponse({
status: 401,
description: '리프레시 토큰이 유효하지 않습니다.',
})
// ...
@Post('refresh')
async refreshAccessToken(
@Headers('refresh-token') refreshToken: string, // 요청 헤더에서 refresh-token 값을 추출
@Res({ passthrough: true }) res: Response,
): Promise<void> {
try {
// 리프레시 토큰을 사용하여 새로운 엑세스 토큰 발급
const newAccessToken =
await this.authService.refreshAccessToken(refreshToken);
// 새로운 엑세스 토큰을 HTTP 응답 헤더에 추가
res.header('access-token', newAccessToken);
// 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
res.status(200).json({ accessToken: newAccessToken });
} catch (error) {
// 리프레시 토큰이 유효하지 않은 경우 에러 처리
res
.status(401)
.json({ errorMessage: '리프레시 토큰이 유효하지 않습니다.' });
}
}
// ...
}
위에 Headers 를 응답을 받아오면서 해결
하지만 왜 기존처럼 컨트롤 + i로 응답이 안받아 졌던 건지는 모르겠음...
빠른수정에서 호출에 누락된new 연산자를 추가하겠냐는 말뿐이고 헤더를 추가해주는건 없어서
찾는데 시간이 걸린것 같다.
import { Body, Controller, Post, Res, Headers } from '@nestjs/common'; // Headers 추가
import { AuthService } from './auth.service';
import {
ApiOkResponse,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';
import { Response } from 'express';
@Controller('users')
@ApiTags('Auth')
@ApiOkResponse({ type: AuthEntity })
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: '로그인' })
@ApiResponse({ status: 200, description: '로그인에 성공하셨습니다.' })
@ApiResponse({ status: 400, description: '이메일이 없습니다.' })
@ApiResponse({ status: 401, description: '비밀번호가 일치하지 않습니다.' })
@Post('login')
async login(
@Body() { email, password }: LoginDto,
@Res({ passthrough: true }) res: Response, // Response 객체 주입
): Promise<void> {
const { accessToken, refreshToken } = await this.authService.login({
email,
password,
});
// 엑세스 토큰을 HTTP 응답 헤더에 추가
res.header('access-token', accessToken);
// 리프레시 토큰을 쿠키로 설정하여 클라이언트에게 전달
// httpOnly : javascript 로 쿠키에 접근 할 수 없는 옵션
// secure : true 일 시 https 연결에서만 전송된다.
// 리프레시 토큰을 HTTP 응답 헤더에 추가
res.header('refreshToken', refreshToken);
// res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: false });
// 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
res.status(200).json({ accessToken }); // 클라이언트에게 JSON 응답을 보냄
}
// 리프레시 토큰을 사용하여 엑세스 토큰 재발급을 위한 엔드포인트 추가
@ApiOperation({ summary: '리프레시 토큰을 사용하여 엑세스 토큰 재발급' })
@ApiResponse({
status: 200,
description: '엑세스 토큰 재발급에 성공하셨습니다.',
})
@ApiResponse({
status: 401,
description: '리프레시 토큰이 유효하지 않습니다.',
})
@Post('refresh')
async refreshAccessToken(
@Headers('refreshToken') refreshToken: string, // 요청 헤더에서 refresh-token 값을 추출
@Res({ passthrough: true }) res: Response,
): Promise<void> {
try {
// 리프레시 토큰을 사용하여 새로운 엑세스 토큰 발급
const newAccessToken =
await this.authService.refreshAccessToken(refreshToken);
// 새로운 엑세스 토큰을 HTTP 응답 헤더에 추가
res.header('access-token', newAccessToken);
// 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
res.status(200).json({ accessToken: newAccessToken });
} catch (error) {
// 리프레시 토큰이 유효하지 않은 경우 에러 처리
res
.status(401)
.json({ errorMessage: '리프레시 토큰이 유효하지 않습니다.' });
}
}
}