Introduction
이전 jwt포스팅 시리즈중 access토큰까지는 그래도 코드를 봐줄만했으나 refresh토큰이 추가된 순간 아주 코드가 더럽고 불결해졌다. 이걸 깔끔하게 하기위해서 jwt토큰로직을 interceptor, module, middleware을 활용해서 깔끔하고 재사용성있게 고쳐보자.
Interceptor
일단 우리가 흔히 쓰는 axios요청을 보자.
axios.get("/api/test").then((res) => {
console.log(res.data.message);
}).catch((err)=>{
console.log(err.message)
});
내가 지금까지 계속 사용했던 axios요청이다.
근데 /api요청으로 보내는 axios를 아예 변수로 선언할당할수 있다.
const api = axios.create({
baseURL: "/api", // 기본 URL 설정
});
axios는 요청을 보내는 객체를 따로 만드는데 이렇게 코드를 쓰면 api라는 스트링에 /api라는 url로 보내는 객체자체를 api라는 식별자로 꺼내 쓸 수있다.
api
.get("test")
.then((res) => {
console.log(res.data.message);
})
.catch((err) => {
console.log(err.message);
});
이 코드는 내가 자주쓰던 axios코드랑 아예 똑같이 작동하는거다.
console.log(
JSON.stringify(api.get()) == JSON.stringify(axios.get("/api")),
"여기"
); // true출력
궁금해서 출력해봤는데 axios.get자체를 뺴기가 힘들었다. 서로 다른 객체이기때문에 JSON.stringify를 안쓰면 다르나, 똑같은 요청을 보내는거라 단순 json형태로 바꿔서 비교해보면 둘은 똑같다고 나옴. 신기하지?
+ 메모리관점에서도 난 그럼 식별자로 인스턴스를 넣는게 더 낫겠네? 했는데 그냥 axios.get을 쓰는것도 어차피 axios객체 갖다쓰는거라 오히려 이게 더 메모리적으론 좋다고 함. 근데 큰 차이는 안나니까 둘중 아무거나 써도된다.
# 뭐야 인스턴스를 api라는 식별자에 넣어서 쓰는게 더 복잡해보이는데?
const api = axios.create({
baseURL: "/api", // 기본 URL 설정
});
api
.get("test")
.then((res) => {
console.log(res.data.message);
})
.catch((err) => {
console.log(err.message);
});
얘랑
axios.get("/api/test").then((res) => {
console.log(res.data.message);
}).catch((err)=>{
console.log(err.message)
});
얘랑 비교하면 당연 우리가 계속 사용했던 방법이 갠적으로 더 좋아보임
근데 왜 이걸 얘기하냐?
바로 인터셉터를 추가할 수 있기 때문이다.
# 인터셉터를 먼저 코드로 한번 볼까?
const api = axios.create({
baseURL: "/api", // 기본 URL 설정
});
api
.get("test")
.then((res) => {
console.log(res.data);
})
.catch((err) => {
console.log(err.message);
});
백단에선 test라는 요청을 받으면 {message : 'hello'}라는 객체를 보내준다.
음 잘 동작한다.
그럼 코드에 response인터셉터를 추가해보자.
const api = axios.create({
baseURL: "/api", // 기본 URL 설정
});
api.interceptors.response.use((response) => {
response.data.dog = "muzzi";
return response;
});
api
.get("test")
.then((res) => {
console.log(res.data);
})
.catch((err) => {
console.log(err.message);
});
응? 백단에서 안보내줬던 dog : muzzi값이 들어왔다.
플로우로 보면 이렇다. 일단 인터셉터는 클라이언트에서만 존재한다. 그리고 api로 지정된 axios객체로 요청 응답할때 중간에 가로채서 작업을 할 수 있는것이다.
만약 인터셉터가 존재하는 상태로 그냥 axios요청을 하면 받는 데이터는 다르다
api
.get("test")
.then((res) => {
console.log("api axios객체 요청해서 받은건 :", res.data);
})
.catch((err) => {
console.log(err.message);
});
axios
.get("/api/test")
.then((res) => {
console.log("그냥 axios에서 받은건 :", res.data);
})
.catch((err) => {
console.log(err.message);
});
그냥 axios요청한건 인터셉터가 관여하지 않았기 때문이다.
# 그럼 이제 jwt토큰을 기준으로 한번 생각해보자.
우리는 인증이 필요한 요청은 모두 access token을 함께 보낸다. 그리고 만약 access token이 만료되었으면 /refresh 라는 api요청에 access token과 refresh토큰 둘다 보내서 새로운 access, refresh token을 응답받을거다. 백단코드는 일단 생략하고 우리는 interceptor을 생각하고 있으므로 요청보낼때와 요청받을때만 코드로 구현해보자.
const api = axios.create({
baseURL: "/api",
timeout: 10000,
});
api.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("access_token");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
return config;
} else {
alert("로그인하세여!");
return Promise.reject(new Error("토큰없음"));
}
},
(error) => {
return Promise.reject(error);
}
);
요청 인터셉터는 그래도 비교적 간단하다. 로컬스토리지에 access token이 있으면 그걸 헤더값에 추가해서 보내는거구 없으면 로그인하라는 메세지를 alert로 띄우고 에러를 던져줌
코드를 실행해보면
이전 포스팅에서 썼던거 그대로라 토큰을 잘 받아온걸 확인.
만약 로컬스토리지에서 토큰을 없애면?
로그인하라는 alert가 잘 뜬다.
이제 응답받을떄 인터셉터 코드를 보자.
api.interceptors.response.use(
(response) => {
return response; // 응답이 정상인 경우 그대로 응답
},
async (error) => {
const originalRequest = error.config;
// 401 또는 419 상태 코드가 반환된 경우 (토큰 만료 또는 인증 오류)
if ((error.response && error.response.status === 419) && !originalRequest._retry) {
originalRequest._retry = true; // 중복 요청 방지
const refreshToken = localStorage.getItem('refresh_token');
const accessToken = localStorage.getItem('access_token');
if (refreshToken && accessToken) {
try {
// Refresh Token과 Access Token을 함께 보내서 새로운 Access Token을 요청
const response = await axios.post('/api/refresh', { refreshToken, accessToken });
const newAccessToken = response.data.accessToken;
const newRefreshToken = response.data.refreshToken;
// 새로 발급된 Token을 저장
localStorage.setItem('access_token', newAccessToken);
localStorage.setItem('refresh_token', newRefreshToken);
// 원래의 요청에 새 Access Token을 추가하고 재전송
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return api(originalRequest); // 원래 요청을 다시 보냄
} catch (refreshError) {
console.error('리프레시 토큰 갱신 실패:', refreshError);
return Promise.reject(refreshError);
}
} else {
console.log('Refresh Token 또는 Access Token이 없습니다.');
return Promise.reject(error);
}
}
return Promise.reject(error); // 419이 아니면 그대로 에러 처리
}
);
와우
차근차근보면 응답을 잘 받은경우는 그냥 바로 return해서 넘겨준다.
하지만 에러가 발생할경우, 즉 서버쪽에서 에러코드를 전해줬을경우 조건문 처리를 한다. originaRequest._retry는 플래그같은거임
그리고 /refresh에 두 토큰을 담아서 post요청을 보낸다. 그럼 서버쪽에서 두 토큰이 accesstoken을 재발급만한 조건인지 확인하고 응답으로 새로운 access token과 refresh토큰을 보내준다. 그리고 다시 원래의 요청을 새로운 access token으로 보내는것.
아닌경우는 그냥 싹다 에러처리
사실 코드가 복잡하지만 이렇게 인터셉터처리를 해놓고 모든 인증이 필요한 api요청에서 그냥 api라는 식별자에 get post붙이고 요청하면 전부 이 플로우가 적용되서 훨씬 편해짐.
백엔드 / 미들웨어
express 써본 사람들이야 다 활용중이겠지만..
이것도 백단에서의 인터셉터라고 보면 된다.
axios.get("/api/test").then((res) => {
console.log(res.data.message);
});
일단 프론트 코드 기본적인 axios요청을 보자.
app.get("/api/test", (req, res) => {
res.status(200).json({ message: "hello" });
});
백엔드코드는 이렇게 생겼고
요청 잘하고 응답 잘 받는다
그럼 백단에서 미들웨어를 설정해보자.
const imMiddleWare = (req, res, next) => {
res.status(200).json({ message: "hello from middle ware" });
};
이런 미들웨어를 만들어줬다.
const imMiddleWare = (req, res, next) => {
res.status(200).json({ message: "hello from middle ware" });
};
app.get("/api/test", imMiddleWare, (req, res) => {});
그리고 응답을 두번하면 에러나니까 app.get에서 응답해주는걸 뻈다.
실행해보면
미들웨어가 응답해줬다.
이렇게 미들웨어는 백단에서 받는 api요청을 가로채서 동작을 수행한다. req자체를 바로 받아서 res를 바로 응답해줄수도 있다. 객체를 직접 다 받으니까.
인터셉터는 어쨋든 인터셉터를 거치고 요청 응답으로 이루어지는데, 미들웨어는 아예 본 api요청코드객체를 아예 따로 가져와서 응답까지 직접 할 수 있는게 신기함.
또 app.get에 있는 req와 res를 그대로 받아서 처리하는데 특이한게 next가 있다. 이건 미들웨어 여러개를 처리할때 쓰는거다.
또한 미들웨어는 코드의 상단 중반 마지막등에 위치할수있고 미들웨어의 사용목적에 따라 다르다. 근데 이 포스팅은 jwt를 미들웨어로 처리하려고하는거니까 자세한건 생략
그럼 이 미들웨어를 어떻게 사용하려고?
인증이 필요한 api요청에다가 토큰을 검증하는 로직을 갖다 넣을것이다.
일단 인증로직 처리하는 함수들을 만들어서 모듈화해야하기때문에 여기선 코드를 바로 안쓰겠음
인증 로직 모듈화
일단 미들웨어 함수들을 따로 담아서 middleware.js파일을 만들거고, 인증관련 함수들을 따로 담아서 auth.js파일을 만들거다.
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();
// 환경 변수
const ACCESS_KEY = process.env.ACCESS_TOKEN_KEY;
const REFRESH_KEY = process.env.REFRESH_TOKEN_KEY;
// Access Token 검증 미들웨어
const verifyAccessToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Access Token missing or invalid" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, ACCESS_KEY);
req.payloadInfo = decoded.payloadInfo; // payload 정보를 요청 객체에 저장
next(); // 다음 미들웨어로 진행
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(419).json({ message: "Access Token expired" });
}
return res.status(401).json({ message: "Invalid Access Token" });
}
};
// Refresh Token 검증 함수
const verifyRefreshToken = (refreshToken) => {
try {
return jwt.verify(refreshToken, REFRESH_KEY); // 유효한 경우 디코딩된 payload 반환
} catch (err) {
return null; // 유효하지 않은 경우 null 반환
}
};
module.exports = { verifyAccessToken, verifyRefreshToken };
이게 middleware.js라는 모듈파일 내용이다. 미들웨어로 넣어줄거라 토큰 검증이 주 목적이다.
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();
// 환경 변수
const ACCESS_KEY = process.env.ACCESS_TOKEN_KEY;
const REFRESH_KEY = process.env.REFRESH_TOKEN_KEY;
// 토큰 생성 함수
const generateTokens = (payloadInfo) => {
const accessToken = jwt.sign({ payloadInfo }, ACCESS_KEY, {
expiresIn: "15m",
}); // 15분 만료
const refreshToken = jwt.sign({ payloadInfo }, REFRESH_KEY, {
expiresIn: "7d",
}); // 7일 만료
return { accessToken, refreshToken };
};
// 토큰 갱신 함수
const refreshTokens = (accessToken, refreshToken) => {
try {
// Access Token이 만료 상태인지 확인
jwt.verify(accessToken, ACCESS_KEY);
throw new Error("야 꺼져 그냥");
} catch (err) {
if (err.name !== "TokenExpiredError") {
throw new Error("너도 꺼져 그냥");
}
}
// Refresh Token 검증
const decoded = jwt.verify(refreshToken, REFRESH_KEY);
if (!decoded) {
throw new Error("Invalid Refresh Token");
}
// 새 토큰 생성
return generateTokens(decoded.payloadInfo);
};
module.exports = { generateTokens, refreshTokens };
얘는 auth.js파일인데, 토큰을 생성하는 함수랑 토큰을 갱신하는 로직이 담긴 함수가 있다.
refresh토큰 재발급 로직은 지난 포스팅에서 설명했으니 생략함..
const express = require("express");
const bodyParser = require("body-parser");
const dotenv = require("dotenv");
const { verifyAccessToken } = require("./middleware");
const { generateTokens, refreshTokens } = require("./auth");
dotenv.config();
const app = express();
const port = process.env.PORT;
app.use(bodyParser.json());
// 로그인 엔드포인트 (Access Token, Refresh Token 발급)
app.post("/api/login", (req, res) => {
const tokens = generateTokens("무찌엉덩이");
return res.status(200).json({ message: "Login successful", ...tokens });
});
// 테스트 API (보호된 경로)
app.get("/api/test", verifyAccessToken, (req, res) => {
res.status(200).json({ message: `Hello, payload: ${req.payloadInfo}` });
});
// 토큰 갱신 API
app.post("/api/refresh", (req, res) => {
console.log("refresh 요청왔어!");
const { accessToken, refreshToken } = req.body;
if (!accessToken || !refreshToken) {
return res.status(400).json({ message: "Tokens are required" });
}
try {
const tokens = refreshTokens(accessToken, refreshToken);
res.status(200).json(tokens);
} catch (err) {
res.status(401).json({ message: err.message });
}
});
// 서버 시작
app.listen(port, () => {
console.log(`서버가 실행 중입니다.`);
});
마지막으로 서버쪽 코드이다.
/test api요청이 토큰이 필요한 검증이라 미들웨어로 verifyAccessToken을 넣었다.
그리고 refresh token검증 및 재발급은 middleware.js랑 auth.js파일에서 각각 로직을 처리한다.
/refresh api요청에서는 access token이 만료됐다는 조건과 refresh token이 유효하다는 조건 둘 다 체크한다.
이게 auth.js의 refresh token함수에서 이뤄지는데 정상적으로 처리되면 새로운 access token과 refresh토큰을 발급해주며 200응답을 주고 아니면 그냥 꺼지라는 메세지를 에러코드와 함꼐 응답해줄거임
테스트
# 로그인 테스트
const login = () => {
axios.post("/api/login", { id: "무찌", pw: "엉덩이" }).then((res) => {
localStorage.setItem("access_token", res.data.accessToken);
localStorage.setItem("refresh_token", res.data.refreshToken);
alert(res.data.message);
});
};
프론트단에 login버튼이 있고 이곳에 저런 axios요청이 달린 함수를 이벤트로 달아줌
app.post("/api/login", (req, res) => {
//원칙적으로 여기서 보낸 아디 비번정보로 db조회후 조건문으로 응답을 줘야함.
const tokens = generateTokens("무찌엉덩이");
return res.status(200).json({ message: "Login successful", ...tokens });
});
이건 백단 /login api요청 코드이다. 당연히 로그인이니 토큰이 제대로 없을테므로 검증은 하지 않는다.
또한 원칙적으론 받은 아이디 비번 정보로 db조회후 조건적으로 토큰을 생성해줘야한다.
저 generateToken함수는
const generateTokens = (payloadInfo) => {
const accessToken = jwt.sign({ payloadInfo }, ACCESS_KEY, {
expiresIn: "15m",
}); // 15분 만료
const refreshToken = jwt.sign({ payloadInfo }, REFRESH_KEY, {
expiresIn: "7d",
}); // 7일 만료
return { accessToken, refreshToken };
};
auth.js의 이 함수가 처리해준다.
이제 프론트의 버튼을 눌러보면
성공메세지를 받고
따끈따끈한 토큰들을 받았다.
# 검증 미들웨어가 달린 api요청을 해보자
app.get("/api/test", verifyAccessToken, (req, res) => {
res.status(200).json({ message: `Hello, payload: ${req.payloadInfo}` });
});
백단의 /test api요청에는 검증미들웨어가 달려있다.
저 verifyAccessToken미들웨어 함수는
const verifyAccessToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Access Token missing or invalid" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, ACCESS_KEY);
req.payloadInfo = decoded.payloadInfo; // payload 정보를 요청 객체에 저장
next(); // 다음 미들웨어로 진행
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(419).json({ message: "Access Token expired" });
}
return res.status(401).json({ message: "Invalid Access Token" });
}
};
요렇게 생겼다. 토큰까서 확인해보는거임.
프론트단에 이 test button이 있고
const test = () => {
axios
.get("/api/test")
.then((res) => {
console.log("일반 axios 응답메세지 : ", res.data.message);
})
.catch((err) => {
console.log("일반 axios 에러메세지 : ", err.message);
});
api
.get("test")
.then((res) => {
console.log("인터셉터 달린 axios응답메세지 : ", res.data.message);
})
.catch((err) => {
console.log("인터셉터 달린 axios에러메세지 : ", err.message);
});
};
이런 두 axios요청을 하게 해봤다.
하나는 일반 axios요청이다. 얘는 인터셉터를 안거치니 토큰을 안넣고 요청할테고
두번쨰는 인터셉터를 달은 api axios요청이니 토큰을 넣고 요청할거다.
실험해보면?
첫번째는 토큰없이 보냈으니 에러가 떴고
두번째는 토큰 넣고 보내서 응답을 잘 받았다
만약 localStorage에서 토큰을 지우고 다시 테스트해보면?
둘다 인증오류가 뜬다. 첫번째 일반 axios요청은 토큰이 없이 요청한거니 백단을 거쳐서 verifyAccessToken을 거쳐서 백단으로부터 에러를 받은거고,
api.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("access_token");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
return config;
} else {
alert("로그인하세여!");
return Promise.reject(new Error("토큰없음"));
}
},
(error) => {
return Promise.reject(error);
}
);
두번째 api axios요청은 인터셉터에서 토큰없이 하려고한거라 백단을 안거치고 프론트에서 에러를 받은거다.
# access token만료시 새로운 토큰 발급받는거 테스트
const test = () => {
api
.get("test")
.then((res) => {
console.log("인터셉터 달린 axios응답메세지 : ", res.data.message);
})
.catch((err) => {
console.log("인터셉터 달린 axios에러메세지 : ", err.message);
});
};
test 버튼에 붙은 axios요청을 api axios요청만 보내게 해놨다.
const generateTokens = (payloadInfo) => {
const accessToken = jwt.sign({ payloadInfo }, ACCESS_KEY, {
expiresIn: "3s", //여기를 3초로 설정
}); // 15분 만료였음 원래
const refreshToken = jwt.sign({ payloadInfo }, REFRESH_KEY, {
expiresIn: "7d",
}); // 7일 만료
return { accessToken, refreshToken };
};
그리고 새로 토큰을 발급해주는 auth.js의 generateTokens함수에서 만료시간을 3초로 설정했다.
app.post("/api/refresh", (req, res) => {
console.log("refresh 요청왔어!");
const { accessToken, refreshToken } = req.body;
if (!accessToken || !refreshToken) {
return res.status(400).json({ message: "Tokens are required" });
}
try {
const tokens = refreshTokens(accessToken, refreshToken);
res.status(200).json(tokens);
} catch (err) {
res.status(401).json({ message: err.message });
}
});
또한 /refresh요청이 오면 요청이 왔다고 콘솔로 찍어주게했는데
먼저 로그인버튼 눌러서 3초짜리 토큰 발급받고
빠르게 3초안에 test 버튼 눌러서 api요청하면
제대로 응답이 왔다. 또한 백단 출력에서는 아무런 출력이 없다.
근데 3초가 지난 시점에서 다시 누르면
프론트에서 응답을 정상적으로 받았고
refresh api요청이 왔다고 한다.
즉 요청을 보냈을떄 총 세번의 요청을 하고 응답을 처리한건데, 처음요청은 /test로 만료된 access 토큰으로 요청한거니 서버는 refresh토큰검증해야된다고 답장준것. 그래서 인터셉터에서 바로 access refresh토큰 둘다 /refresh요청으로 보내서 새로운 토큰들을 발급받고 로컬스토리지에 저장, 다음 정상적인 /test 요청을 다시 해서 응답을 제대로 받은거다.
이상태에서 또 빠르게 test버튼 누르면 제대로 되고 3초지나면 또 refresh요청가고 그런식이 된다.
사실 코드상으로 설명하면 나도 글만 주구장창 쓸거같아서 이전 refresh토큰 포스팅에서 플로우만 알고 보면된다. gpt가 나름 잘짜줘서 제대로 동작하더라 ㅠ 조금 수정은 헀지만..
이전에 인증 필요한 /mypage api요청을 수정해보자
jwt / refresh token
Introductionjson web token 보안방법에서 refresh token에 대해서 차근차근 포스팅하겠다. 처음부터 와라라락 써버리면 읽는 사람들은 머리가 터질것이기 때문.공부 목적이길래 코드가 매우 더러울것이다
jacobowl.tistory.com
이 포스팅에서 썼던 refresh토큰 재발급 코드를 다시보면
const mypage = () => {
const access_token = localStorage.getItem("access_token");
axios
.get("/api/mypage", {
headers: {
Authorization: JSON.stringify({ access_token: access_token }),
},
})
.then((res) => {
alert(res.data.message);
})
.catch((error) => {
if (error.response.data.code == 419) {
const refresh_token = localStorage.getItem("refresh_token");
alert("만료에러");
axios
.get("/api/refresh", {
headers: {
Authorization: JSON.stringify({
access_token: access_token,
refresh_token: refresh_token,
}),
},
})
.then((res) => {
alert(res.data.message);
console.log(res.data.access_token);
localStorage.setItem("access_token", res.data.access_token);
localStorage.setItem("refresh_token", res.data.refresh_token);
})
.catch((error) => {
alert(error.response.data.code);
});
} else {
alert(error.response.data.code);
}
});
};
mypage 버튼 누르면 실행됐던 프론트단 코드이다.
app.get("/api/mypage", (req, res, next) => {
verifyAT(req, res);
});
const verifyAT = (req, res) => {
const token = JSON.parse(req.headers.authorization).access_token;
const token_key = process.env.ACCESS_TOKEN_KEY;
// 토큰이 없으면
if (!token) {
return res.status(401).json({
code: 401,
message: "Access Token이 없습니다.",
});
}
try {
console.log(jwt.verify(token, token_key));
res.json({ message: "valid token" });
} catch (error) {
console.log("토큰에러야");
if (error.name === "TokenExpiredError") {
return res.status(419).json({
code: 419,
message: "Access Token이 만료되었습니다.",
});
} else if (error.name === "JsonWebTokenError") {
return res.status(401).json({
code: 401,
message: "유효하지 않은 토큰입니다.",
});
} else {
return res.status(500).json({
code: 500,
message: "서버 내부 오류입니다.",
});
}
}
};
해당 /mypage api요청 백단 코드이다.
이걸 오늘 배운 interceptor, module, middleware를 적용해보면?
const mypage = () => {
api
.get("mypage")
.then((res) => {
console.log(res.data.message);
})
.catch((err) => {
console.log(err.message);
});
};
프론트단 코드
app.get("/api/mypage", verifyAccessToken, (req, res) => {
res.status(200).json({ message: `Hello, here is mypage` });
});
백단 코드
요청 제대로 처리됨
놀랍도록 코드가 단순해졌다.
mypage뿐만 아니라 인증이 필요한곳에서는 api axios요청으로 하면되고
인증이 필요없는 api요청은 일반 axios로 요청하면 된다
즉 재사용성, 가독성이 놀랍도록 증가된다는 소리임.
+ 이번 포스팅에서 인터셉터는 따로 모듈화를 안했는데, 원한다면 따로 모듈화를 해서 코드를 더 깔끔하게 하면 된다.
# 전체코드
<template>
<router-view></router-view>
<button v-on:click="login()">login button</button>
<button v-on:click="mypage()">mypage button</button>
<button v-on:click="test()">test button</button>
</template>
<script>
import { reactive } from "vue";
import axios from "axios";
export default {
setup() {
///여기가 데이터
const state = reactive({
data: [],
});
const api = axios.create({
baseURL: "/api",
timeout: 10000,
});
api.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("access_token");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
return config;
} else {
alert("로그인하세여!");
return Promise.reject(new Error("토큰없음"));
}
},
(error) => {
return Promise.reject(error);
}
);
api.interceptors.response.use(
(response) => {
return response; // 응답이 정상인 경우 그대로 응답
},
async (error) => {
const originalRequest = error.config;
// 401 또는 419 상태 코드가 반환된 경우 (토큰 만료 또는 인증 오류)
if (
error.response &&
error.response.status === 419 &&
!originalRequest._retry
) {
originalRequest._retry = true; // 중복 요청 방지
const refreshToken = localStorage.getItem("refresh_token");
const accessToken = localStorage.getItem("access_token");
if (refreshToken && accessToken) {
try {
// Refresh Token과 Access Token을 함께 보내서 새로운 Access Token을 요청
const response = await axios.post("/api/refresh", {
refreshToken,
accessToken,
});
const newAccessToken = response.data.accessToken;
const newRefreshToken = response.data.refreshToken;
// 새로 발급된 Token을 저장
localStorage.setItem("access_token", newAccessToken);
localStorage.setItem("refresh_token", newRefreshToken);
// 원래의 요청에 새 Access Token을 추가하고 재전송
originalRequest.headers["Authorization"] =
`Bearer ${newAccessToken}`;
return api(originalRequest); // 원래 요청을 다시 보냄
} catch (refreshError) {
console.error("리프레시 토큰 갱신 실패:", refreshError);
return Promise.reject(refreshError);
}
} else {
console.log("Refresh Token 또는 Access Token이 없습니다.");
return Promise.reject(error);
}
}
return Promise.reject(error); // 419이 아니면 그대로 에러 처리
}
);
const test = () => {
api
.get("test")
.then((res) => {
console.log("인터셉터 달린 axios응답메세지 : ", res.data.message);
})
.catch((err) => {
console.log("인터셉터 달린 axios에러메세지 : ", err.message);
});
};
const login = () => {
axios.post("/api/login", { id: "무찌", pw: "엉덩이" }).then((res) => {
localStorage.setItem("access_token", res.data.accessToken);
localStorage.setItem("refresh_token", res.data.refreshToken);
alert(res.data.message);
});
};
const mypage = () => {
api
.get("mypage")
.then((res) => {
console.log(res.data.message);
})
.catch((err) => {
console.log(err.message);
});
};
///여기가 메서드
return {
state,
login,
mypage,
test,
};
},
// 여기에 컴포넌트
};
</script>
<style lang="scss" scoped>
.sassTest {
p {
color: blue;
}
}
</style>
프론트 코드
const express = require("express");
const bodyParser = require("body-parser");
const dotenv = require("dotenv");
const { verifyAccessToken } = require("./middleware");
const { generateTokens, refreshTokens } = require("./auth");
dotenv.config();
const app = express();
const port = process.env.PORT;
app.use(bodyParser.json());
app.post("/api/login", (req, res) => {
//원칙적으로 여기서 보낸 아디 비번정보로 db조회후 조건문으로 응답을 줘야함.
const tokens = generateTokens("무찌엉덩이");
return res.status(200).json({ message: "Login successful", ...tokens });
});
// 테스트 API (보호된 경로)
app.get("/api/test", verifyAccessToken, (req, res) => {
res.status(200).json({ message: `Hello, payload: ${req.payloadInfo}` });
});
app.get("/api/mypage", verifyAccessToken, (req, res) => {
res.status(200).json({ message: `Hello, here is mypage` });
});
// 토큰 갱신 API
app.post("/api/refresh", (req, res) => {
console.log("refresh 요청왔어!");
const { accessToken, refreshToken } = req.body;
if (!accessToken || !refreshToken) {
return res.status(400).json({ message: "Tokens are required" });
}
try {
const tokens = refreshTokens(accessToken, refreshToken);
res.status(200).json(tokens);
} catch (err) {
res.status(401).json({ message: err.message });
}
});
// 서버 시작
app.listen(port, () => {
console.log(`서버가 실행 중입니다.`);
});
server.js코드
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();
// 환경 변수
const ACCESS_KEY = process.env.ACCESS_TOKEN_KEY;
const REFRESH_KEY = process.env.REFRESH_TOKEN_KEY;
// Access Token 검증 미들웨어
const verifyAccessToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Access Token missing or invalid" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, ACCESS_KEY);
req.payloadInfo = decoded.payloadInfo; // payload 정보를 요청 객체에 저장
next(); // 다음 미들웨어로 진행
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(419).json({ message: "Access Token expired" });
}
return res.status(401).json({ message: "Invalid Access Token" });
}
};
// Refresh Token 검증 함수
const verifyRefreshToken = (refreshToken) => {
try {
return jwt.verify(refreshToken, REFRESH_KEY); // 유효한 경우 디코딩된 payload 반환
} catch (err) {
return null; // 유효하지 않은 경우 null 반환
}
};
module.exports = { verifyAccessToken, verifyRefreshToken };
middleware.js코드
const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");
dotenv.config();
// 환경 변수
const ACCESS_KEY = process.env.ACCESS_TOKEN_KEY;
const REFRESH_KEY = process.env.REFRESH_TOKEN_KEY;
// 토큰 생성 함수
const generateTokens = (payloadInfo) => {
const accessToken = jwt.sign({ payloadInfo }, ACCESS_KEY, {
expiresIn: "3s", //여기를 3초로 설정
}); // 15분 만료
const refreshToken = jwt.sign({ payloadInfo }, REFRESH_KEY, {
expiresIn: "7d",
}); // 7일 만료
return { accessToken, refreshToken };
};
// 토큰 갱신 함수
const refreshTokens = (accessToken, refreshToken) => {
try {
// Access Token이 만료 상태인지 확인
jwt.verify(accessToken, ACCESS_KEY);
throw new Error("야 꺼져 그냥");
} catch (err) {
if (err.name !== "TokenExpiredError") {
throw new Error("너도 꺼져 그냥");
}
}
// Refresh Token 검증
const decoded = jwt.verify(refreshToken, REFRESH_KEY);
if (!decoded) {
throw new Error("Invalid Refresh Token");
}
// 새 토큰 생성
return generateTokens(decoded.payloadInfo);
};
module.exports = { generateTokens, refreshTokens };
auth.js코드
Conclusion
jwt포스팅을 마치며
jwt토큰 로직은 매우 사람마다 성향이 다르다..
재가 경험한사람들 대충 기억해보면
refresh 토큰을 추가하면 아예 로직이 드러워져서 access 토큰만으로 운영하기도 하고,
아예 토큰을 메모리 db에 저장하여 검증을 엄청나게 까다롭게 하기도한다.
또한 누구는 api요청 보낼때마다 access, refresh토큰 매번 새로 발급해서 준다
페이로드정보와 db조회는 여기서 안적었지만 누구는 여기에 이메일도 담으면 안된다고하고
payload정보로 db정보를 꼭 조회해봐야한다고 하고
누구는 그러면 db에 쓸데없는 과부하일어난다고 하고
정말 의견이 많다.
이번 포스팅에서도 그렇다. /refresh api자체를 만들지말고 다 미들웨어, 인터셉터로 처리해버리면 된다 이러는 사람들도 있을것이다.
중요한건 이거임. 이번 jwt포스팅 시리즈에서 사용한 access토큰 refresh토큰의 플로우는 중간중간 가지가 난 방향만 다를뿐 큰 틀은 유지되고있다. 때문에 각기 다른 개발자마다 다른 flow를 만들어 보안성을 챙긴다.
때문에 이 포스팅을 읽은 사람들은 각기 개성적으로 보안적인 flow를 설계해서 본인 코드에 추가하길 바란다.
이상.
( gpt야 수고했다..)
'security > application' 카테고리의 다른 글
jwt / refresh token (0) | 2024.11.14 |
---|---|
jwt / introduction & access token (0) | 2024.10.24 |