-
Access Token
사용자의 권한이 확인, 즉 로그인 되었을 경우 해당 사용자를 인증하는 용도로 발급한다.
Cookie로 jwt를 발급하고 설정한 Expires 기간이 지날 때 인증이 만료되게 하는것 또한 Access Token이다.
사용자가 Access Token을 가지고 인증을 요청할 경우 Token을 생성할 때 사용한 비밀키(Secret Key)를 가지고 인증하기 때문에, 복잡한 설계없이 코드를 구현할 수 있고, 여러 분기를 거치지 않아도 된다는 장점이 있다.
Access Token의 경우 Stateless(무상태) 즉, Node.js 서버가 죽었다 살아나더라도 동일한 동작을하는 방식이다. 즉, jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만 처음 발급한 사용자 본인인지 확인할 수는 없다는 단점이 있다.
Access Token은 그 자체로도 사용자를 인증하는 모든 정보를 가지고 있다. 그렇기 때문에 토큰을 가지고 있는 시간이 늘어날 수록 탈취되었을 때는 피해가 더욱 커지게 된다.
만약 토큰이 탈취된 사실을 알더라도, 우리는 해당 토큰이 탈취된 토큰인지 알 수 없고, 고의적으로 만료를 시킬 수도 없다. 그러므로 언제든지 사용자의 토큰이 탈취될 수 있다고 생각을 하고, 피해를 최소화 할 수 있는 방향으로 개발을 진행해야 한다.
Refresh Token
Refresh Token은 Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌, 특정한 사용자가 Access Token을 발급받기 위한 용도로만 사용된다.
Refesh Token은 사용자의 인증정보를 사용자가 가지고 있는 것이 아닌, 서버에서 해당 사용자의 정보를 저장소 또는 별도의 DB에 저장하여 관리한다. 그렇기 때문에, 서버에서 특정 Token 만료가 필요할 경우 저장된 Token을 제거하여 사용자의 인증 여부를 언제든지 제어가 가능하다는 장점이 있다.
사용자 인증 시 Access Token을 발급하지 않고, Refresh Token을 거쳐서 Access Token을 발급하는 이유는? 사용자에게 발급한 Token이 탈취당할 경우 피해를 최소화 하기 위해서이다.
실제 세계에서 사용하는 OTP와 같이 짧은 시간 내에서만 인증 정보를 사용할 수 있게하고, 주기적으로 재발급하여, 토큰이 유출되더라도 오랜 기간동안 피해를 입는것이 아닌, 짧은 기간동안만 사용가능하도록 하여 피해를 최소화할 수 있게 된다.
코드
const jwt = require("jsonwebtoken"); const cookieParser = require("cookie-parser"); const express = require("express"); const app = express(); const port = 3002; const SECRET_KEY = `HangHae99`; app.use(cookieParser()); let tokenObject = {}; // Refresh Token을 저장할 Object // ★ 토큰을 발급하는 API : GET http://localhost:3002/set-token/:id app.get("/set-token/:id", (req, res) => { const id = req.params.id; const accessToken = createAccessToken(id); // AccessToken 생성함수 아래에 나옴. (파라미터 id값) const refreshToken = createRefreshToken(); // RefreshToken 생성함수 아래에 나옴. tokenObject[refreshToken] = id; // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다. res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다. res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다. // 해당 유저에게 accessToken과 refreshToken 두개를 발급한다. return res.status(200).send({ "message": "Token이 정상적으로 발급되었습니다." }); }) // Access Token 생성. function createAccessToken(id) { const accessToken = jwt.sign( // JsonWebToken 암호화 (sign) { id: id }, // JWT 데이터 SECRET_KEY, // 비밀키 (위에 전역변수로 설정) { expiresIn: '60s' }) // Access Token이 60초 뒤에 만료되도록 설정. return accessToken; } // Refresh Token 생성. function createRefreshToken() { const refreshToken = jwt.sign( // JsonWebToken 암호화 (sign) {}, // JWT 데이터가 없다. 서버의 Refresh Token이 원래 갖고있는 정보를 사용함. SECRET_KEY, // 비밀키 (위에 전역변수로 설정) { expiresIn: '7d' }) // Refresh Token이 7일 뒤에 만료되도록 설정. return refreshToken; } // Refresh Token는 계속 사용하면서, Access Token을 재발급하기 위한 용도로 사용. // 그래서 만료시간이 긺. // ★ 토큰을 검증하는 API : GET http://localhost:3002/get-token app.get("/get-token", (req, res) => { const accessToken = req.cookies.accessToken; // accessToken의 쿠키를 변수할당 const refreshToken = req.cookies.refreshToken; // refreshToken의 쿠키를 변수할당 if (!refreshToken) return res.status(400).json({ "message": "Refresh Token이 존재하지 않습니다." }); if (!accessToken) return res.status(400).json({ "message": "Access Token이 존재하지 않습니다." }); const isAccessTokenValidate = validateAccessToken(accessToken); // AccessToken 유효한토큰 (아래에 유효성검사 함수 나옴) const isRefreshTokenValidate = validateRefreshToken(refreshToken); // RefreshToken 유효한토큰 (아래에 유효성검사 함수 나옴) if (!isRefreshTokenValidate) return res.status(419).json({ "message": "Refresh Token이 만료되었습니다." }); if (!isAccessTokenValidate) { const accessTokenId = tokenObject[refreshToken]; // tokenObject(위에선언된 전역변수)에 refreshToken을 할당해서 처음 저장된 id값을 가져온다 if (!accessTokenId) return res.status(419).json({ "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }); // 토큰은 정상적이지만 서버의 토큰과 일치하지않는 경우. 또는, 존재하지 않거나, 저장소에 있는 토큰이 유실되어 서버에서 고의적으로 만료시킨 경우. const newAccessToken = createAccessToken(accessTokenId); // 토큰 새로발급 res.cookie('accessToken', newAccessToken); // accessToken은 위에서 설정된 똑같은 key인데, 여기에 newAccessToken을 발급함. return res.json({ "message": "Access Token을 새롭게 발급하였습니다." }); } // accessToken과 refreshToken 모두 정상적인 경우. response에 아이디 출력함. 아래에 getAccessTokenPayload 함수 나옴 const { id } = getAccessTokenPayload(accessToken); return res.json({ "message": `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.` }); }) // Access Token을 검증. function validateAccessToken(accessToken) { try { jwt.verify(accessToken, SECRET_KEY); // JWT를 검증합니다. return true; } catch (error) { return false; } } // Refresh Token을 검증합니다. function validateRefreshToken(refreshToken) { try { jwt.verify(refreshToken, SECRET_KEY); // JWT를 검증합니다. return true; } catch (error) { return false; } } // Access Token의 Payload를 가져옵니다. function getAccessTokenPayload(accessToken) { try { const payload = jwt.verify(accessToken, SECRET_KEY); // jwt.verify의 결과값(Payload)를 가져옴. return payload; } catch (error) { return null; } } // app.get("/", (req, res) => { res.status(200).send("Hello Token!"); }) app.listen(port, () => { console.log(port, '포트로 서버가 열렸어요!'); })
https://tansfil.tistory.com/59
'Today I Learned > TIL 07' 카테고리의 다른 글
2023 - 07 - 07 Json Web Token (JWT)의 장단점, 토큰과 DB 사용자 인증, 쿠키, 세션 (면접 단골질문) (0) 2023.07.07 2023 - 07 - 06 트랜젝션 (Transaction) ACID (원자성, 일관성, 격리성, 지속성), 락(Lock), 락킹 수준, 교착상태 (Deadlock), 트랜잭션의 격리 수준 (0) 2023.07.06 2023 - 07 - 04 TCP, UDP, 소켓(socket.io), 웹소켓, 패킷 (0) 2023.07.04 2023 - 07 - 03 코드 가독성을 위한 코드서식 관리도구 Prettier (0) 2023.07.04 2023 - 07 - 02 Ajax 매서드 (JQuery) (0) 2023.07.03