오늘의 목표
⏱️ 오늘의 일정
9:00 ~ 21:00 - 팀 프로젝트
📜 팀 프로젝트
9:00 ~ 21:00 - 팀 프로젝트
팀 프로젝트를 진행했다.
테스트용으로 만든 회원가입과 로그인 부분을 내가 생각하는 로직으로 바꿧다.
우선 로그인 전용으로 인증을 검사하는 미들웨어를 만들었다.
export default async function (req, res, next) {
const { authorization } = req.headers;
const { id } = req.body;
do {
// AccessToken이 없음
if (authorization === undefined || authorization.length == 0) {
req.authLoginState = process.env.LOGIN_AUTH_FAIL_AUTHORIZATION_NOT_FOUND;
}
else {
// AccessToken이 있음
const [c2sTokenType, c2sAccessToken] = authorization.split(' ');
if (c2sTokenType !== process.env.TOKEN_TYPE_CHECK) {
req.authLoginState = process.env.LOGIN_AUTH_FAIL_TOKEN_TYPE_NOT_MATCH;
}
const decodedToken = jwt.decode(c2sAccessToken);
if(decodedToken.id !== id)
{
req.authLoginState = process.env.LOGIN_AUTH_FAIL_DIFFERENT_ACCESS_TOKEN;
break;
}
// 토큰 검증
const verifyToken = ValidateToken(c2sAccessToken, process.env.ACCESS_TOKEN_SECRET_KEY);
if (!verifyToken) {
req.authLoginState = process.env.LOGIN_AUTH_FAIL_TOKEN_EXPIRED;
break;
}
req.authLoginState = process.env.LOGIN_AUTH_SUCCESS;
}
} while (0);
next();
}
위 코드에서는 로그인 시도한 유저가 AccessToken을 가지고 있는지 확인한다.
AccessToken이 없거나 빈 내용을 입력하면 req.authLoginState에 LOGIN_AUTH_FAIL_AUTHORIZATION_NOT_FOUND를 저장한다.
AccessToken이 있는데 TokenType이 다를 경우에 LOGIN_AUTH_FAIL_TOKEN_TYPE_NOT_MATCH를 저장한다.
AccessToken이 있는데 다른 유저의 AccessToken을 가지고 왔을 경우에는 LOGIN_AUTH_FAIL_DIFFERENT_ACCESS_TOEKN을 저장한다.
AccessToken이 있는데 만료됐거나, 값이 다를 경우네는 LOGIN_AUTH_FAIL_TOKEN_EXPIRED를 저장한다.
그 외 경우에는 인증 성공으로 판단해서 LOGIN_AUTH_SUCCESS를 저장한다.
이후 메인 Login Router로 진행한다.
// 로그인
usersRouter.post('/Sign-In', loginAuthMiddleware, async (req, res, next) => {
// 아이디, 비밀번호 가져오기
const { id, password } = req.body;
// userDB에 아이디가 있는지 확인
const user = await prisma.users.findFirst({
where: {
id: id
}
});
// 아이디 검사
if (!user) {
return res.status(404).json({ message: `${id}은 존재하지 않는 아이디 입니다.` });
}
// 비밀번호 검사
if (!(await bcrypt.compare(password, user.password))) {
return res.status(404).json({ message: `비밀번호가 일치하지 않습니다.` });
}
// RefreshTokens 테이블에 RefreshToken이 있는지 확인
const refreshTokenExist = await prisma.refreshTokens.findFirst({
where: {
id: id
}
});
switch (req.authLoginState) {
case process.env.LOGIN_AUTH_FAIL_AUTHORIZATION_NOT_FOUND:
// AccessToken이 없음
FirstLogin(id, refreshTokenExist, res);
break;
case process.env.LOGIN_AUTH_FAIL_TOKEN_TYPE_NOT_MATCH:
// AccessToken을 가지고 있지만 토큰 타입이 일치하지 않음
return res.status(404).json({ message: '토큰 타입이 일치하지 않습니다.' });
case process.env.LOGIN_AUTH_FAIL_TOKEN_EXPIRED:
// AccessToken이 만료됨
AccessTokenExpired(id, refreshTokenExist, res);
break;
case process.env.LOGIN_AUTH_FAIL_DIFFERENT_ACCESS_TOKEN:
DifferentAccessToken(refreshTokenExist, res);
break;
case process.env.LOGIN_AUTH_SUCCESS:
// 로그인 성공
return res.status(200).json({ message: `${id}로 로그인에 성공했습니다.` });
}
})
로그인에서는 기본적으로 아이디와 비밀번호를 검사하고,
RefreshToken Table에 RefreshToken이 있는지 찾은 후,
로그인 인증에서 저장한 authLoginState값에 따라 다른 로직으로 처리한다.
//------------------------------------------------------
// 처음 로그인
//------------------------------------------------------
async function FirstLogin(id, refreshTokenDBData, res) {
if (refreshTokenDBData === null) {
// RefreshToken이 Table에 없음
// 처음 로그인 대상으로 해석
// JWT로 AccessToken 생성
const s2cAccessToken = CreateAccessToken(id);
// JWT로 RefreshToken 생성
const s2cRefreshToken = CreateRefreshToken(id);
// 응답 헤더에 accessToken 기록
res.header("authorization", s2cAccessToken);
const nowTime = MakeTime(false);
const expiredAtTokenTime = MakeTime(false, parseInt(process.env.REFRESH_TOKEN_EXPIRE_TIME_DB));
// RefreshToken DB에 저장
const refreshToken = await prisma.refreshTokens.create({
data: {
id: id,
token: s2cRefreshToken,
createdAt: nowTime,
expiredAt: expiredAtTokenTime
}
});
return res.status(200).json({ message: `${id}로 로그인에 성공했습니다.` });
}
else {
const s2cAccessToken = CreateAccessToken(id);
res.header("authorization", s2cAccessToken);
return res.status(200).json({ message: `${id}로 로그인에 성공했습니다.` });
}
}
FirstLogin 에서는 로그인 시도한 대상이 처음으로 로그인한 대상인지를 판단한다.
만약 RefreshToken Table에서 RefreshToken을 찾지 못하면, 해당 유저를 처음 로그인한 대상으로 해석하고,
AccessToken과 RefreshToken을 발행하고, Refresh Token을 Table에 저장한다.
RefreshToken을 이미 가지고 있는 대상이면 AccessToken을 재발행해준다.
async function AccessTokenExpired(id, refreshTokenDBData, res) {
if (refreshTokenDBData !== null) {
// AccessToken을 가지고 있고, AccessToken이 만료됨
// refreshToken을 DB에 가지고 있음
// AccessToken을 재발급
// RefreshToken이 만료되었는지 확인
const refreshTokenCheck = ValidateToken(refreshTokenDBData, process.env.REFRESH_TOKEN_SECRET_KEY);
if (!refreshTokenCheck) {
// RefreshDB createdAt, expiredAt 컬럼 사용 용도 :
// JWT 없이 Refresh Token의 Expire 시간을 제어할 수 있는 용도로 사용
// 예) JWT로 3시간 후 만료로 발급했으나 모종의 이유로 1시간 후 만료로 바꿔야하는 경우,
// JWT는 수정을 할 수 없기 때문에 DB에 있는 시간값으로 판정한다.
// 현재 시간과 db에 저장되어 있는 시간 차를 구함
const dbTimeDifferenceSecond = TimeDifference(refreshTokenDBData.expiredAt, "second");
// 상수값으로 설정해둔 Refresh Token 만료 시간을 지나면
if (dbTimeDifferenceSecond >= parseInt(process.env.REFRESH_TOKEN_EXPIRE_DB_CHECK_TIME)) {
// RefreshToken을 재발급
const s2cRefreshToken = CreateRefreshToken(id);
// 현재 시간을 구함
const nowTime = MakeTime(false);
// Refresh Token 만료 시간을 구함
const expiredTime = MakeTime(false, parseInt(process.env.REFRESH_TOKEN_EXPIRE_TIME_DB));
// Refresh Token을 업데이트
await prisma.refreshTokens.update({
where: { id: id },
data: {
token: s2cRefreshToken,
createdAt: nowTime,
expiredAt: expiredTime
}
});
}
}
const s2cAccessToken = CreateAccessToken(id);
res.header("authorization", s2cAccessToken);
return res.status(200).json({ message: `AccessToken이 재발급 되었습니다. ${id}로 로그인에 성공했습니다. ` });
}
else {
// AccessToken을 가지고 있고, AccessToken이 만료됨
// refreshToken을 DB에 가지고 있지 않음
// 불량 유저로 판정 ( 서버에 문제가 생겨 refreshToken을 기록 못할경우가 있어서 불량 유저로 판정하기엔 무리)
// return res.status(404).json({ message: `비정상 로그인 시도` });
return res.status(404).json({ message: `refreshToken DB Empty` });
}
}
AccessTokenExpired에서는 AccessToken을 재발행 해준다.
만약 RefreshToken을 DB에 가지고 있지 않을 경우에는 불량유저로 판단해 로그인 시켜주지 않는다.
( 이 부분은 실제 이런경우가 있는지 보고 싶어서 따로 처리한 것이고, 원래는 서버에 문제가 생겨 RefreshToken을 DB에 기록하지 못할 경우도 생길 수 있기 때문에 AccessToken을 재발행 해줘야한다고 튜터님에게 답을 얻었다. )
RefreshToken을 DB에 가지고 있으면, RefreshToken이 만료됐는지 JWT로 먼저 확인해주고,
추가로 DB에 있는 시간값을 기준으로 만료됐는지 확인 한 후,
( DB에 있는 시간값을 기준으로 만료됐는지 추가로 확인하는 이유는 다음과 같다. JWT로 발행을하고 나서 모종의 이유로 만료시간을 수정을 해야할 경우, JWT의 만료시간을 수정할 수 없어서 DB값을 기준으로 최종적으로 판단해준다. )
만료됐으면 RefreshToken을 재발행한다.
🌙 하루를 마치며
오늘 생각했던 회원가입과 로그인 부분을 구현할 수 있어서 기분이 좋았다.
로그인 부분에서 불량유저를 판단하는 부분이 애매해 튜터님에게 가서 물어보니,
다른 관점으로 대답을 해주셔서 판단에 많은 도움이 되었다.
주말에는 추가로 맡은 랭킹 부분을 완료해 월요일에 테스트 할 수 있도록 해야겠다.