현재 프로젝트에서는 Node.js 를 활용중이며, ES6문법을 사용하고, 이를 Babel을 통해 트랜스컴파일 한다.
두기자의 선택지가 있었다.
이 두가지의 선택지 중에서 mysql2를 사용했고, 이는 Promise 객체를 사용하기 위해서이다.
Promise를 반환하는 async와 await을 적절히 사용하여 callback을 사용하는 mysql 방식을 벗어났다.
// pool 을 사용한 이유 -> Connection 계속 유지하므로 부하 적어짐. (병렬 처리 가능)
const pool = mysql.createPool(
process.env.JAWSDB_URL ?? {
host: "3.34.97.140",
user: "mixbowl",
database: "Mixbowl",
password: "swe302841",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
}
);
createPool
을 사용하면, connectionLimit
만큼 미리 connection을 Pool에 만들어 놓는다. 따라서 createConnection
이라는 단일 커넥션보다 훨씬 부담이 적을 수 밖에 없다.
다음은 공식문서이다.
https://www.npmjs.com/package/mysql
Once pool.end is called, pool.getConnection and other operations can no longer be performed. Wait until all connections in the pool are released before calling pool.end. If you use the shortcut method pool.query, in place of pool.getConnection → connection.query → connection.release, wait until it completes.
pool.end calls connection.end on every active connection in the pool. This queues a QUIT packet on the connection and sets a flag to prevent pool.getConnection from creating new connections. All commands / queries already in progress will complete, but new commands won't execute.
즉,pool.query
를 사용하면,pool.getConnection
을 사용하여connection.query
를 사용한후connection.release
를 사용하는 것보다 훨씬 간편하게 사용할 수 있다. 알아서 connection이 release되기 때문이다.
const promisePool = pool.promise();
이렇게 사용하면, promise 객체를 반환해서 async, await를 쉽게 사용할 수 있다.
해당 작업이 필요한 이유는, Refresh Token을 DB에 저장하기 위해서이다. DB에 refresh token을 담는 것이 heavy 할 수도 있지만, 일단은 그렇게 하기로 했다.
Refresh Token이 필요한 이유
Access Token만으로도 구현할 수 있지만, 이는 보안상으로 문제가 있을 수 있다. 탈취되면 끝이기 때문이다. 따라서 Access Token의 유효기간을 적게 하고, Refresh Token을 하나 더 발급하여서, Access Token의 유효기간이 끝나면 DB에 개인별로 저장된 Refresh Token을 이용하여 새로운 Access Token을 발급한다.
따라서 로그인을 할 때, 두가진 토큰을 발급하는데, Access Token과 Refresh Token 을 발급하여 클라이언트에게 전송한다. 이 때, 서버에서는 DB에 Refresh Token을 저장하는 과정을 거쳐야한다.
loginUser: async (req) => { const { nickname, password } = req.body; try { const [username] = await promisePool.query(` SELECT NICKNAME FROM Mixbowl.USER WHERE '${nickname}' = NICKNAME AND '${password}' = PASSWORD ; `); console.log("in sql", username); const accessToken = await jwt_module.sign(username[0]["NICKNAME"]); const refreshToken = await jwt_module.refresh(); //refresh token sql 업데이트 await promisePool.query(` UPDATE USER SET TOKEN = '${refreshToken}' WHERE NICKNAME = '${nickname}'; `); return { code: 200, message: "토큰이 발급되었습니다.", token: { accessToken, refreshToken, }, }; } catch (error) { console.log(error.message); } }
Access Token을 발급하는 코드이다.
export function sign(username) {
// Access 토큰 생성 코드
const payload = {
type: "JWT",
nickname: username,
};
return jwt.sign(payload, process.env.SECRET_KEY, {
expiresIn: "1h",
issuer: "MixBowl",
});
}
Access Token이 유효한지 확인하는 코드이다.
export function accessVerify(token) {
//Access 토큰 확인 코드
let decoded = null;
try {
decoded = jwt.verify(token, process.env.SECRET_KEY);
return {
ok: true,
nickname: decoded.nickname[0]["NICKNAME"],
};
} catch (error) {
return {
ok: false,
message: error.message,
};
}
}
.env에 저장한 SECRET_KEY 와 함께, 클라이언트에서 HTTP HEADER에 보낸 token 정보를 파라미터로 받아와서 verify
코드를 사용하여 해당 JWT를 해석하여
{
ok:true,
nickname:~~~
}
를 클라이언트에게 전송한다.
해당 모듈 함수는, 사용자 인증이 필요한 express router에 적용할 미들웨어를 작성하는 함수에 사용된다.
아래는 그 함수이다.
// middleware/checkAccessToken.js
export default (req, res, next) => {
// 인증 완료
try {
//req.headers.authorization = access Token일 경우
req.decoded = jwt.verify(req.headers.authorization, process.env.SECRET_KEY);
return next();
} catch (error) {
//유효시간 만료
if (error.name === "TokenExpiredError") {
return res.status(419).json({
ok: false,
message: "토큰이 만료되었습니다.",
});
}
//비밀키 일치 오류
if (error.name === "JsonWebTokenError") {
return res.status(401).json({
ok: false,
message: "유효하지 않은 토큰입니다",
});
}
}
};
여기에서 verify
함수를 호출하면서 req.headers.authorization
을 인자로 주어서, Access Token 정보를 받아온다. 그 다음 라우터에게 콜백으로 할 일을 넘긴다.
Refresh Token을 발급하는 함수이다.
export function refresh() {
// Refresh 토큰 생성 코드
return jwt.sign({}, process.env.SECRET_KEY, {
expiresIn: "14d",
issuer: "MixBowl",
});
}
이 때, payload에는 아무것도 존재하지 않아도 된다. 단지 기간만 좀 길게 설정했다.
이는 Refresh Token의 유효성을 확인하는 코드이다.
sql 모듈의 getToken함수를 이용하여 username 인자로 들어온 유저의 Token 정보를 찾아 refToken에 저장하여, 이를 클라이언트에서 전송한 Token정보와 비교한다.
//토큰 header에 주고, db 내 refresh 토큰으로 확인
export async function refreshVerify(token, username) {
//Refresh 토큰 확인 코드
//redis 도입하면 좋을듯
try {
const refToken = await sql.getToken(username);
if (token === refToken) {
try {
jwt.verify(token, process.env.SECRET_KEY);
return true;
} catch (error) {
return false;
}
} else {
return false;
}
} catch (error) {
return false;
}
}
새로운 AccessToken을 발급해주기 위한 함수이다.
클라이언트에서 RefreshToken 과 AccessToken의 정보를 HTTP HEADER에 담아 전송해야한다.
HTTP HEADER에
“authorization” : Access Token 정보
“refresh”: Refresh Token 정보
를 담아 보낸다.
// header에 "authorization", "refresh"에 각각 토큰정보 넣어줄 것
export const refresh_new = async (req, res) => {
if (req.headers.authorization && req.headers.refresh) {
const access = req.headers.authorization;
const refresh = req.headers.refresh;
const accessResult = accessVerify(access);
const decodeAccess = jwt.decode(access);
if (decodeAccess === null) {
res.status(401).send({
ok: false,
message: "No Authorization for Access Token",
});
}
const refreshResult = refreshVerify(refresh, decodeAccess.nickname);
if (accessResult.ok === false && accessResult.message === "jwt expired") {
if (refreshResult.ok === false) {
res.status(401).send({
ok: false,
message: "No Authorization, MAKE A NEW LOGIN",
});
} else {
//refresh token이 유효하므로, 새로운 access token 발급
const newAccessToken = sign(req.body.nickname);
res.stauts(200).send({
ok: true,
nickname: req.body.nickname,
});
}
} else {
res.status(400).send({
ok: false,
message: "Access Token is not expired",
});
}
} else {
res.status(400).send({
ok: false,
message: "Access token and Refresh Token are needed for refresh",
});
}
};