본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
사용자 정보는 인증 Middleware(req.user
)를 통해서 전달 받음
허용 역할은 Middleware 사용 시 배열로 전달 받음
// src/middlewares/role.middleware.js
// 미들웨어는 req, res, next를 필요로 하는 함수
// 그렇기에 매개변수를 사용할 수 있는 미들웨어를 만들기 위해 미들웨어를 리턴하는 함수를 만듦
export const requiredRoles = (roles) => {
return async (req, res, next) => {
// 현재 사용자의 역할을 가져옴
const { role } = req.user;
// 배열로 받아온 roles에 현재 사용자의 역할이 포함되는지 확인
if (roles.includes(role)) {
// 역할이 포함되면 다음으로 진행
return next();
}
return res.status(401).json({ status: 401, message: '접근 권한이 없습니다.' });
};
};
사용자 정보는 인증 Middleware(req.user
)를 통해서 전달 받음
이력서 ID를 Path Parameters(req.params
)로 전달 받음
지원 상태, 사유를 Request Body(req.body
)로 전달 받음
이력서 정보 수정과 이력서 로그 생성을 Transaction으로 묶어서 실행
// src/routers/resumes.router.js
// 이력서 지원 상태 변경 API
router.patch('/resumes/:resumeId/state', authMiddleware, requiredRoles(Object.values(USER_ROLE)), async (req, res, next) => {
try {
// 사용자 정보 가져옴
const { userId } = req.user;
// 이력서 ID 가져옴
const { resumeId } = req.params;
//지원 상태, 사유 가져옴
const validation = await resumeStateSchema.validateAsync(req.body);
const { state, reason } = validation;
// 이력서가 존재하는지 조회
const resume = await prisma.resumes.findFirst({ where: { resumeId: +resumeId } });
if (!resume) {
return res.status(401).json({ status: 401, message: '이력서가 존재하지 않습니다.' });
}
let resumeLog; // 이력서 변경 로그
// 트랜젝션을 통해서 작업의 일관성 유지
await prisma.$transaction(
async (tx) => {
// 이력서 수정
const updatedResume = await tx.resumes.update({ where: { resumeId: +resumeId }, data: { state } });
// 이력서 변경 로그 생성
resumeLog = await tx.resumeHistories.create({
data: {
RecruiterId: +userId,
ResumeId: +resumeId,
oldState: resume.state,
newState: updatedResume.state,
reason,
},
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
},
);
return res.status(201).json({ status: 201, message: '지원 상태 변경에 성공했습니다.', data: { resumeLog } });
} catch (err) {
next(err);
}
});
이력서 ID를 Path Parameters(req.params
)로 전달 받음
생성일시 기준 최신순으로 조회
채용 담당자 이름을 반환하기 위해 스키마에 정의 한 Relation을 활용해 조회
// src/routers/resumes.router.js
// 이력서 로그 목록 조회 API
router.get('/resumes/:resumeId/log', authMiddleware, requiredRoles(Object.values(USER_ROLE)), async (req, res, next) => {
// 이력서 ID 가져옴
const { resumeId } = req.params;
// 이력서 로그 조회
const resumeLogs = await prisma.resumeHistories.findMany({
where: { ResumeId: +resumeId },
select: {
resumeLogId: true,
Resume: {
select: {
User: {
select: {
name: true,
},
},
},
},
ResumeId: true,
oldState: true,
newState: true,
reason: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
});
return res.status(200).json({ status: 200, message: '이력서 로그 목록 조회에 성공했습니다.', data: { resumeLogs } });
});
AccessToken 인증 미들웨어와 거의 동일함
RefreshToken을 Request Header의 Authorization 값(req.headers.authorization
)으로 전달 받음
Payload에 담긴 사용자 ID를 이용하여 사용자 정보를 조회
이 때, RefreshToken은 DB에 보관하기 때문에 DB에 접근해서 조회
Payload에 담긴 사용자 ID와 일치하는 사용자가 없는 경우에는 폐기 된 인증 정보입니다
라고 출력
// src/middlewares/auth.refresh.token.middleware.js
// RefreshToken 인증 미들웨어
export default async (req, res, next) => {
try {
// 헤더에서 Refresh 토큰 가져옴
const authorization = req.headers['authorization'];
if (!authorization) throw new Error('인증 정보가 없습니다.');
// Refresh 토큰이 Bearer 형식인지 확인
const [tokenType, token] = authorization.split(' ');
if (tokenType !== 'Bearer') throw new Error('지원하지 않는 인증 방식입니다.');
// 서버에서 발급한 JWT가 맞는지 검증
const decodedToken = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET_KEY);
const userId = decodedToken.userId;
// JWT에서 꺼낸 userId로 실제 사용자가 있는지 확인
const user = await prisma.users.findFirst({ where: { userId: +userId } });
if (!user) {
return res.status(401).json({ status: 401, message: '인증 정보와 일치하는 사용자가 없습니다.' });
}
// DB에 저장된 RefreshToken를 조회
const refreshToken = await prisma.refreshTokens.findFirst({ where: { UserId: user.userId } });
// DB에 저장 된 RefreshToken이 없거나 전달 받은 값과 일치하지 않는 경우
if (!refreshToken || refreshToken.token !== token) {
return res.status(401).json({ status: 401, message: '폐기 된 인증 정보입니다.' });
}
// 조회된 사용자 정보를 req.user에 넣음
req.user = user;
// 다음 동작 진행
next();
} catch (err) {
switch (err.name) {
case 'TokenExpiredError':
return res.status(401).json({ status: 401, message: '인증 정보가 만료되었습니다.' });
case 'JsonWebTokenError':
return res.status(401).json({ status: 401, message: '인증 정보가 유효하지 않습니다.' });
default:
return res.status(401).json({ status: 401, message: err.message ?? '비정상적인 요청입니다.' });
}
}
};
AccessToken 만료 시 RefreshToken을 활용해 재발급
RefreshToken(JWT)을 Request Header의 Authorization 값(req.headers.authorization
)으로 전달 받음
사용자 정보는 인증 Middleware(req.user
)를 통해서 전달 받음
AccessToken(Payload에 사용자 ID
를 포함하고, 유효기한이 12시간
)을 재발급
RefreshToken (Payload: 사용자 ID 포함, 유효기한: 7일
)을 재발급
RefreshToken은 DB에서 보관하기 때문에 DB의 데이터를 갱신
// src/routers/auth.router.js
// 토큰 재발급 API
router.post('/auth/refresh', authRefreshTokenMiddleware, async (req, res, next) => {
try {
// 사용자 정보 가져옴
const user = req.user;
// Access Token 재발급 (12시간)
const AccessToken = jwt.sign({ userId: user.userId }, process.env.ACCESS_TOKEN_SECRET_KEY, { expiresIn: '12h' });
// Refresh Token 재발급 (7일)
const RefreshToken = jwt.sign({ userId: user.userId }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' });
await prisma.refreshTokens.update({
where: { UserId: user.userId },
data: {
token: RefreshToken,
ip: req.ip,
userAgent: req.headers['user-agent'],
createdAt: new Date(Date.now()),
},
});
return res.status(201).json({ status: 201, message: '토큰 재발급에 성공했습니다.', data: { AccessToken, RefreshToken } });
} catch (err) {
next(err);
}
});
RefreshToken(JWT)을 Request Header의 Authorization 값(req.headers.authorization
)으로 전달 받음
사용자 정보는 인증 Middleware(req.user
)를 통해서 전달 받음
RefreshToken은 DB에서 보관하기 때문에 DB의 데이터를 삭제
실제로는 AccessToken이 만료되기 전까지는 AccessToken이 필요한 API는 사용 가능함
// src/routers/auth.router.js
// 로그아웃 API
router.post('/auth/sign-out', authRefreshTokenMiddleware, async (req, res, next) => {
try {
// 사용자 정보 가져옴
const user = req.user;
// DB에서 Refresh Token 삭제
const deletedUserId = await prisma.refreshTokens.delete({
where: { UserId: user.userId },
select: { UserId: true },
});
return res.status(201).json({ status: 201, message: '로그아웃 되었습니다.', data: { deletedUserId } });
} catch (err) {
next(err);
}
});
코드 구현도 끝났으니 과제 발제 문서를 확인하면서 체크하기
배포한 서버 다시 확인하기
과제 질문 작성해서 도메인 주소와 함께 제출하기
그래도 어제 대부분의 기능을 구현해서 오늘은 구현이 수월했음
다만 구현한 내용을 다듬고 Insomnia로 체크하는 시간이 길었음
예비군 때문에 일정이 빠듯했지만 무사히 마무리 될 것 같음
배포는 지난번 과제에서 배포한 EC2 인스턴스를 그대로 사용함
인바운드 규칙이나 대상 그룹 설정은 따로 필요했음
처음에는 로그아웃을 통해 RefreshToken을 삭제하면 진짜 로그아웃 기능처럼 다른 기능들을 사용할 수 없는 줄 알았음
하지만 정상적으로 로그아웃 기능을 구현 후 로그아웃을 진행해도 다른 API들을 사용할 수 있었음
너무 단순하게 생각했음
이번 과제에서 구현한 방식은 AccessToken과 RefreshToken을 사용하는 인증 방식임
AccessToken에는 만료 시간을 짧게 하고, RefreshToken에는 만료 시간을 비교적 길게 만듦
이를 통해 AccessToken이 만료되면 다시 DB에 있는 RefreshToken를 통해 토큰들을 재발급 받는 구조임
여기서 AccessToken은 백엔드에서 처리할 수 있는 토큰이 아님
로그인을 하면 클라이언트에게 AccessToken을 넘겨줌
이후 AccessToken은 클라이언트에서 다뤄지는 데이터임
그렇기에 서버측에서는 AccessToken을 삭제하는 것이 불가능 함