쿠키는 어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터다.
서버가 웹 브라우저(클라이언트)에 정보를 저장하고 불러올 수 있는 수단으로
해당 도메인에 쿠키가 존재하면 웹 브라우저는 도메인에게 http 요청 시 쿠키를 함께 전달한다.
서버가 클라이언트에게 응답 헤더를 전달할 때 위와 같이 Set-Cookie를 전달한다.
클라이언트는 이후 매 요청 시마다 헤더에 쿠키의 이름과 정보를 전달한다.
쿠키는 클라이언트에 남아서 로그인 유지나 테마 유지 등의 기능을 한다.
- Domain : 서버와 요청의 도메인이 일치하는 경우 쿠키 전송
http://www.localhost.com:3000/users/login 에서 localhost.com이 도메인으로써
요청과 응답이 같은 도메인이여야 전송됨
- Path : 서버와 요청의 세부 경로가 일치하는 경우 쿠키 전송
http://www.localhost.com:3000/users/login 에서 라우팅단인 /users/login이 Path로써 최상위 경로만 알맞다면 쿠키 전송이 가능하다
ex) cookie path : /users 일 경우 /users/option 같은 경로면 쿠키 전송 가능
하지만, /items/option 같이 상위 경로가 다르면 전송 불가능.
- MaxAge or Expires : 쿠키의 유효기간을 설정한다.
쿠키는 이 옵션의 여부에 따라 세션 쿠키와 영속성 쿠키로 나뉜다.
세션 쿠키 : 위 옵션이 없는 쿠키,브라우저가 실행 중일때 사용할수 있는 쿠키로 브라우저 종료시 쿠키도 삭제된다.
영속성 쿠키 : 위 옵션에 지정된 시간만큼 사용가능하다.
- Secure :
Https
프로토콜에만 쿠키 전송 여부 결정
- SameSite : CORS 요청의 옵션 및 메서드에 따라 쿠키 전송 여부 결정
- Lax : GET 메서드 요청만 쿠키 전송 가능
- Strict : 쿠키 전송 불가
- None : 모든 메서드 요청에 대해 쿠키 전송 가능, 단 Secure 쿠키 옵션이 필요
위와 같이 CSRF 공격을 받았을 경우에 효과적이다.
로그인 상태 유지를 하는 기능을 만들어보려 한다.
한 파일에 서버/클라이언트가 있으며, 각각의 요구사항을 충족하면 된다.
서버의 index.js에서 cors 설정을 해야한다.
cors 설정은 클라이언트와 서버가 서로 쿠키를 주고받기 위해 꼭 필요한 설정이다.
// server/index.js
const corsOptions = {
// TODO
// CORS 설정
origin: "http://localhost:3000", // 접근 권한을 허용하는 도메인
credentials: true, // Access-Control-Allow-Origin 접근 허용
methods: ["GET", "POST", "OPTION"], // 허용할 메소드
};
app.use(cors(corsOptions));
// cors 설정을 사용함
app.post("/login", controllers.login);
app.post("/logout", controllers.logout);
app.get("/userinfo", controllers.userInfo);
// 각 컨트롤러마다 endpoint를 연결해줌
총 3개의 state가 있다.
const [loginInfo, setLoginInfo] = useState({
userId: "",
password: "",
}); // 로그인 정보
const [checkedKeepLogin, setCheckedKeepLogin] = useState(false); // 로그인 유지 할건지
const [errorMessage, setErrorMessage] = useState(""); // 에러 발생 시 로그인 버튼 밑에 에러메시지 표시 여부
첫 번째로 ID와 Password가 업데이트 되는 LoginInfo
두 번째로 로그인 유지 할건지에 대한 상태 checkedKeepLogin
세 번째로 에러 발생시 에러 메시지은 errorMessage
이다.
로그인 에러가 발생하는 경우는 아이디 또는 비밀번호가 입력되지 않았을 경우다.
아이디와 비밀번호가 전부 잘 입력되었으면, 서버에 로그인 데이터를 보낸다.
const loginRequestHandler = () => {
// 1. 아이디,비밀번호 중 하나라도 입력이 되지 않았다면 에러를 띄워줌
if (!loginInfo.userId || !loginInfo.password) {
setErrorMessage("아이디와 비밀번호를 입력하세요");
return;
}
// 2. 입력이 전부 들어왔으면, axios.post를 통해 정보를 전달해줌
// endpoint는 server/users/index.js를 보면 login이라고 적혀있다.
// endpoint의 login은 controllers의 user에 login 컨트롤러로 데이터가 전달된다.
return axios
.post("http://localhost:4000/login", { loginInfo, checkedKeepLogin })
.then((res) => {
return res;
});
};
위 상태로 로그인정보를 보내고,
// server/controller/login.js
const { USER_DATA } = require("../../db/data");
module.exports = (req, res) => {
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
const userInfo = {
...USER_DATA.filter(
(user) => user.userId === userId && user.password === password // req로 들어온 데이터와 같은 id,password인 user를 가져옴.
)[0],
};
console.log(req.body);
console.log(userInfo);
}
database에 있는 USER_DATA를 가져온 다음 입력된 정보와 비교해본다.
userInfo는 req.body에 있는 loginInfo와 비교하여 알맞은 요소를 추출한다.
콘솔창을 띄워보면 위와 같이 req.body의 정보와 userInfo를 확인할 수 있다.
2번에서 userInfo는 올바른 아이디와 비밀번호가 들어오면 객체를 리턴한다.
하지만, 올바르지 않은 아이디 또는 비밀번호가 들어오면 빈 배열을 리턴한다.
이것을 이용해 로그인 성공 여부를 가릴 수 있다.
로그인 실패 시 401 상태코드와 함께 Not Authorized
라는 문구를 보내야하고
로그인 성공 시 로그인 유지를 체크하여 쿠키를 전송하면 된다.
다만, 클라이언트에 바로 쿠키를 보내지 않고 userInfo.js 로 리다이렉트 시킨 후 검증해서 보낸다.
// server/controller/login.js
// 쿠키의 옵션 설정해줌
const cookiesOption = {
domain: "localhost",
path: "/",
secure: true,
httpOnly: true,
sameSite: "strict",
};
if (userInfo.id === undefined) {
// 로그인 실패!
res.status(401).send("Not Authorized");
} else if (checkedKeepLogin === true) {
// 로그인 유지가 true일 경우 cookiesOption의 max-age와 expires 옵션을 추가해준다.
const time = 1000 * 60 * 30;
cookiesOption.maxAge = time; // 단위 ms, 해당 시간 동안 쿠키 "유지"
cookiesOption.expires = new Date(Date.now() + time); // 지금시간+넣은 시간 후에 쿠키 "삭제"
// 로그인 성공!
// express에서 쿠키를 전송하려면 cookie() 메소드를 사용하면 된다.
// cookie 메서드는 전달인자로 쿠키 이름,값,옵션을 받는다.
res.cookie("cookieId", userInfo.id, cookiesOption);
res.redirect("/userinfo");
} else {
// 로그인 유지를 안 하고 싶을때
res.cookie("cookieId", userInfo.id, cookiesOption); // 추가적인 옵션 없이 쿠키 설정
res.redirect("/userinfo");
// userinfo controller로 redirect
}
};
// 여기선 쿠키만 생성 !
// userInfo에서 쿠키 검증
이후 userInfo에서 쿠키를 검증한다.
//server/controller/userInfo
const { USER_DATA } = require("../../db/data");
module.exports = (req, res) => {
const userInfo = {
...USER_DATA.filter(
(user) => user.id === cookieId // id와 비교
)[0],
};
if (!cookieId || !userInfo.id) {
res.status(401).send("Not Authorized");
} else {
// 비밀번호는 민감한 정보라서 삭제 후에 보내야 함.
delete userInfo.password;
res.send(userInfo); // 클라이언트로 다시 보내주기.
}
};
이러면 userInfo에서 쿠키 검증 후 다시 클라이언트로 보내지게 된다.
userInfo에서 받아온 쿠키는 어떤 모습인지부터 확인해보겠다.
위 정보를 가지고 React의 상태를 업데이트 해보겠다.
우선, App.js에서 각각 Login과 Mypage에 setIsLogin과 setUserInfo를 넘겨준다.
//client/Login.js
return axios
.post("http://localhost:4000/login", { loginInfo, checkedKeepLogin })
.then((res) => {
setUserInfo(res.data);
setIsLogin(true);
setErrorMessage("");
})
.catch((err)=>{
setErrorMessage("로그인 실패");
});
로그인이 성공해 쿠키가 담긴 res가 오면 사용자 정보에 res.data를 넘겨주고 setIsLogin을 통해 현재 로그인 상태를 변경시켜준다.
client의 userInfo는 받아온 쿠키를 가지고 로그인 페이지를 렌더링시킨다.
//client/userInfo
return (
<div className='container'>
<div className='left-box'>
<span>
{`${userInfo.name}(${userInfo.userId})`}님,
<p>반갑습니다!</p>
</span>
</div>
<div className='right-box'>
<h1>AUTH STATES</h1>
<div className='input-field'>
<h3>내 정보</h3>
<div className='userinfo-field'>
<div>{`💻 ${userInfo.position}`}</div>
<div>{`📩 ${userInfo.email}`}</div>
<div>{`📍 ${userInfo.location}`}</div>
<article>
<h3>Bio</h3>
<span>{userInfo.bio}</span>
</article>
</div>
<button className='logout-btn' onClick={logoutHandler}>
LOGOUT
</button>
</div>
</div>
</div>
);
userInfo에 로그아웃 버튼을 누르면 로그아웃 되는 기능을 구현해야한다.
그러면 현재 로그인 상태도 변경해줘야 하고 유저의 정보도 없애줘야한다.
로그아웃의 endpoint는 /logout 이므로 다음과 같이 작성해준다.
//client/userInfo
const logoutHandler = () => {
return axios
.post("http://localhost:4000/logout")
.then((res) => {
setIsLogin(false);
setUserInfo(null);
})
.catch((err) => {
console.log(err);
});
};
로그아웃 요청을 보냈으면 서버에서 로그아웃을 해줘야한다.
쿠키를 삭제해야 하므로 쿠키 삭제에 쓰이는 clearCookie를 이용한다.
//server/logout
module.exports = (req, res) => {
res
.status(205)
.clearCookie('cookieId', {
domain: 'localhost',
path: '/',
sameSite: 'strict',
secure: true,
})
.send('Logged Out Successfully');
};
클라이언트에서 따로 데이터를 담아 요청하지 않았으므로 req는 사용하지 않는다.
로그인 페이지에 도달했을때 클라이언트에 이미 쿠키가 있다면(로그인 후 쿠키가 유지되어있으면) 바로 userInfo 페이지를 보여지게 만든다.
//client/app.js
const authHandler = () => {
axios
.get('http://localhost:4000/userinfo')
.then((res) => {
setIsLogin(true);
setUserInfo(res.data);
})
.catch((err) => {
if (err.response.status === 401) {
console.log(err.response.data);
}
});
};
useEffect(() => {
authHandler();
}, []);
useEffect로 리렌더링 될때마다 실행시킨다.