/routes/login.ts 의 코드 변경
기존의 코드 accesstoken만 사용
-> access token, refresh token 으로 변경
잘 정리되어있는 참고 자료 : Access Token & Refresh Token 원리_ Inpa Dev
Access Token만을 통한 인증방식의 문제는 제 3자에게 탈취당할 경우 보안에 취약하다는 점이다. Access Token은 발급된 이후, 서버에 저장되지 않고 토큰 자체로 검증을 하며, 사용자 권한을 인증하기 때문에 Access Token이 탈취되면 만료되기 전까지 토큰을 획득한 사람은 누구나 권한 접근이 가능해지기 때문
JWT는 발급한 후 삭제가 불가능하기 때문에, 접근에 관여하는 토큰에 유효시간을 부여하는 식으로 탈취 문제에 대해 대응함
이처럼 토큰 유효기간을 짧게하면 토큰 남용을 방지하는 것이 해결책이 될 수 있지만, 유효기간이 짧은 Token의 경우 그만큼 사용자는 로그인을 자주 해서 새롭게 Token을 발급받아야 하므로 불편하다는 단점이 있다. 그렇다고 무턱대고 유효기간을 늘리자면, 토큰을 탈취당했을 때 보안에 더 취약해지게 된다.
이때 “그러면 유효기간을 짧게 하면서 좋은 방법이 있지는 않을까?”라는 질문의 답이 바로 Refresh Token이다.
이름이 다르지만 형태 자체는 Refresh Token은 Access Token과 똑같은 JWT다. 단지 Access Token은 접근에 관여하는 토큰이고, Refresh Token은 재발급에 관여하는 토큰 이므로 행하는 역할이 다르다고 보면 된다.
import express, { Request, Response, NextFunction } from 'express';
import prisma from '../utils/prisma/index';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const router = express.Router();
/** 사용자 로그인 API - 비즈니스 로직
1. `email`, `password`를 **body**로 전달받습니다.
2. 전달 받은 `email`에 해당하는 사용자가 있는지 확인합니다.
3. 전달 받은 `password`와 데이터베이스의 저장된 `password`를 bcrypt를 이용해 검증합니다.
4. 로그인에 성공한다면, 사용자에게 JWT와 name을 발급합니다.
*/
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
const { email, password } = req.body;
const user = await prisma.users.findFirst({
where: { email },
});
// 사용자가 있는지 확인합니다.
if (!user) {
return res.status(412).json({ message: '이메일을 확인해주세요.' });
}
// bcrypt를 이용해 패스워드를 검증합니다.
if (!(await bcrypt.compare(password, user.password))) {
return res.status(412).json({ message: '비밀번호를 확인해주세요.' });
}
// 로그인에 성공하면, 사용자의 userId를 바탕으로 JWT 토큰을 발급합니다.
const token = jwt.sign(
{
userId: user.userId,
},
process.env.SECRET_KEY as string, // 비밀키를 입력
{ expiresIn: '30d' }
);
// Authorization 쿠키에 Bearer 토큰 형식으로 JWT를 저장합니다.
res.cookie('Authorization', `Bearer ${token}`);
// 로그인 성공시 JWT와 name을 응답합니다.
const response = {
token,
name: user.name,
};
return res.status(200).json(response);
});
export default router;
accesstoken, refreshtoken 발급으로 변경
import express, { Request, Response, NextFunction } from 'express';
import prisma from '../utils/prisma/index';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const router = express.Router();
/** 사용자 로그인 API
1. `email`, `password`를 **body**로 전달받습니다.
2. 전달 받은 `email`에 해당하는 사용자가 있는지 확인합니다.
3. 전달 받은 `password`와 데이터베이스의 저장된 `password`를 bcrypt를 이용해 검증합니다.
4. 로그인에 성공한다면, 사용자에게 JWT와 name을 발급합니다.
*/
interface LoginRequest {
email: string;
password: string;
}
router.post(
'/login',
async (req: Request, res: Response, next: NextFunction) => {
const { email, password } = req.body as LoginRequest;
try {
const user = await prisma.users.findFirst({
where: { email },
});
// 사용자가 있는지 확인합니다.
if (!user) {
return res.status(412).json({ message: '이메일을 확인해주세요.' });
}
if (!email || !password) {
return res
.status(400)
.json({ message: '이메일과 비밀번호를 작성해주세요.' });
}
// bcrypt를 이용해 패스워드를 검증합니다.
if (!(await bcrypt.compare(password, user.password))) {
return res.status(412).json({ message: '비밀번호를 확인해주세요.' });
}
// JWT를 발급합니다.
const accessToken = await jwt.sign(
{
userId: user.userId,
},
process.env.ACCESS_SECRET_KEY as string,
{ expiresIn: '1h' },
);
const refreshToken = await jwt.sign(
{
userId: user.userId,
ip: req.ip,
userAgent: req.headers['user-agent'],
},
process.env.REFRESH_SECRET_KEY as string,
{ expiresIn: '1d' },
);
// Access, Refresh Token을 HttpOnly Cookie에 저장합니다.
// 보안을 강화하기 위한 중요한 옵션, https로 배포시 secure: true로 설정
res.cookie('accessToken', accessToken, { httpOnly: true });
res.cookie('refreshToken', refreshToken, { httpOnly: true });
// Refresh Token을 데이터베이스에 저장합니다.
await prisma.users.update({
where: { userId: user.userId },
data: {
refreshToken: refreshToken,
},
});
return res.status(200).json({ message: '로그인에 성공했습니다.' });
} catch (err) {
next(err);
}
},
);
/** Accsess Token 인증 API */
router.post(
'/token',
async (req: Request, res: Response, next: NextFunction) => {
try {
const { accessToken } = req.cookies;
if (!accessToken) {
return res
.status(401)
.json({ message: '엑세스 토큰이 존재하지 않습니다.' });
}
//Access Token이 서버가 발급한 것이 맞는지 검증합니다.
const { userId } = (await jwt.verify(
accessToken,
process.env.ACCESS_SECRET_KEY as string,
)) as { userId: number };
const user = await prisma.users.findUnique({
where: {
userId: +userId,
},
});
if (!user) {
res.clearCookie('accessToken');
return res.status(404).json({
message: '해당하는 사용자를 찾을 수 없습니다.',
});
}
return res
.status(200)
.json({ message: '엑세스 토큰 인증에 성공하였습니다.' });
} catch (err) {
// accessToken 쿠키를 삭제합니다.
res.clearCookie('accessToken');
console.error(err);
return res.status(400).json({
message: '엑세스 토큰 인증에 실패하였습니다.',
});
}
},
);
/** Access Token 재발급 API */
router.post(
'/refresh',
async (req: Request, res: Response, next: NextFunction) => {
try {
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res
.status(401)
.json({ message: '리프레시 토큰이 존재하지 않습니다.' });
}
// Refresh Token이 서버가 발급한 것이 맞는지 검증합니다.
const { userId } = jwt.verify(
refreshToken,
process.env.REFRESH_SECRET_KEY as string,
) as { userId: number };
const user = await prisma.users.findUnique({
where: { userId: +userId },
select: { refreshToken: true },
});
// 사용자가 존재하지 않거나, RefreshToken이 일치하지 않으면 에러를 발생시킵니다.
if (!user || refreshToken !== user.refreshToken) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
return res
.status(401)
.json({ message: '리프레시 토큰 인증에 실패하였습니다.' });
}
// Access Token을 새롭게 생성한 후, 사용자 쿠키에 설정해줍니다.
const accessToken = jwt.sign(
{ userId: userId },
process.env.ACCESS_SECRET_KEY as string,
{
expiresIn: '1h',
},
);
res.cookie('accessToken', accessToken, { httpOnly: true });
return res
.status(200)
.json({ message: '리프레시 토큰 검증에 성공하였습니다.' });
} catch (err) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
console.error(err);
return res
.status(400)
.json({ message: '리프레시 토큰 검증에 실패하였습니다.' });
}
},
);
/** 사용자 로그아웃 API */
router.post('/logout', (req: Request, res: Response) => {
// 쿠키에서 accessToken과 refreshToken을 제거
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
return res.status(200).json({ message: '로그아웃에 성공했습니다.' });
});
export default router;
➕ 여기서 로그아웃시 refresh token 내역도 삭제하는게 더 안정성에 좋다고 하는데, 이건 나중에 하고 업데이트하기로 !