로그인에 JWT를 사용할 경우, 아래와 같은 시나리오를 생각할 수 있다.
클라이언트에서 ID, Password로 로그인 요청을 보내면 서버에서 확인 후 토큰을 발급한다.
이 토큰이 "이 클라이언트가 로그인에 성공했음을 증명함" 증명서같은 역할을 하는 것이다.
그 이후에 클라이언트에서 요청을 할 때는 요청 헤더에 발급받은 토큰을 붙여서 보내면 서버에서는 해당 토큰이 유효한지만 검사하면 정보를 확인할 수 있다.
클라이언트에서 요청을 보낼 때 헤더에 토큰을 넣어서 보내려면, 토큰을 가지고 있어야 한다. 그러니까 어딘가에 저장해놓아야 한다는 뜻이다.
브라우저 메모리(react에서는 state)에 저장하면 한 번 렌더링될 때마다 다시 로그인을 해야 한다. local storage에 저장해놓으면, 따로 지우기 전까지는 클라이언트 쪽에 토큰이 계속 남아있는 데다 JS로 접근할 수 있어 보안에 취약하다🙄
여기저기 검색을 해보니 보통 httpOnly cookie에 저장해서 사용한다는 것을 알게 되었다.
1. 그냥 쿠키로 저장하기
우선 백엔드 쪽 코드를 조금 수정해서, cookie-parser를 미들웨어로 등록한 후 로그인 POST 시
res.cookie("loginToken", token);
로 쿠키를 설정해주었다!
이런 식으로. 클라이언트 쪽에서는 react-cookie의 useCookies 훅을 사용해서
const [token, setToken, removeToken] = useCookies(["loginToken"]);
이렇게 하면 token.loginToken으로 슥삭💨 가져와서 헤더에 넣어줄 수 있겠지?
그런데
쿠키가 애초에 세팅 자체가 안 되는 것이다. 로그인은 되는데 (response가 ok로 옴) 쿠키가 설정이 안 되는 게 대체 뭐가 문제인지 모르겠어서 며칠을 여기에 매달렸다.
이유가 너무 간단했다. cookie를 설정해주는 api 서버랑 클라이언트가 도메인이 달라서 그랬던 것이다. 클라이언트는 http://localhost:3000이고 서버는 http://localhost:4000이라서💢
var cors = require("cors");
const corsOptions = {
origin: ["http://localhost:3000"],
credentials: true,
};
app.use(cors(corsOptions));
서버 쪽에서 이렇게 origin과 credentials를 설정해주고,
const response = await fetch(`${process.env.REACT_APP_API_SERVER}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: username,
password,
}),
credentials: "include",
});
클라이언트에는 credentials: "include"로 fetch해주면
작동은 되는데
그러면 안된다.
2. httponly 쿠키로 저장하기
위처럼 하면
JS에서 접근이 가능하다. 그냥 개발자도구로도 수정할 수 있다. 딱 봐도 보안적으로 좋지 않아 보인다.
그래서 쿠키를 세팅할 때 httpOnly 옵션을 true로 줘야 한다.
그런데 httpOnly 쿠키는 JS에서 읽을 수가 없다. 그러니까 클라이언트에서 쿠키를 읽어다가 헤더에 넣는 게 안 된다는 뜻이다.
jwt 검사를 할 때 헤더가 아니라 쿠키를 검사하는 것으로 수정할 필요가 있다.
app.use(
jwt({
secret: process.env.JWT_SECRET,
getToken: req => req.cookies.token
})
);
일반적으로는 이렇게 하면 되고, 우리 백엔드는 passport를 사용해서 passport-jwt-cookiecombo를 이용할 예정이다. 그리고 쿠키의 크로스 도메인 문제를 방지하기 위해 클라이언트의 package.json에
{
...
"proxy": "http://localhost:4000",
}
프록시를 추가해 주고, fetch에는
const response = await fetch(`/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: username,
password,
}),
credentials: "include",
});
상대경로로 수정해준다.
'Web' 카테고리의 다른 글
[Typescript] 타입 단언, 타입 가드 (0) | 2023.11.21 |
---|---|
[인증] JWT로 로그인 인증 2. Access Token과 Refresh Token 사용하기 (Koa, next-auth) (1) | 2023.06.09 |
[Network] HTTP에 대해서 (0) | 2023.06.01 |
[웹] 혹시 당신… 웹 개발이 처음인가요? (0) | 2023.03.21 |
[기타] URL과 URI의 차이 (3) | 2020.04.28 |
댓글