Cookie
Cookie란?
HTTP 프로토콜의 무상태성을 보완해주는 도구. 원래는 보안 목적으로 만들어진 것이 아니다.
- 10일간 보지 않기
- 광고 서비스(쿠키를 기반으로 추천 광고를 띄움)
등등
HTTP 헤더에 실어서 보내는 것이 기본적인 방법임 -> 이것도 찾아보자
- 서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단
- 해당 도메인에 쿠키가 존재하면, 웹 브라우저는 도메인에게 http 요청 시 쿠키를 함께 전달
Cookie는 사용자 선호, 테마, 로그인 유지 등 장시간 보존해야 하는 정보 저장에 적합
회사가 필요로 하는 마케팅 정보를 수집하는 데에도 쓰인다.
Cookie Options
- Domain: 서버와 요청의 도메인이 일치하는 경우 쿠키 전송
- Path: 서버와 요청의 세부 경로가 일치하는 경우 쿠키 전송
- MaxAge or Expires: 쿠키의 유효기간 설정
- HttpOnly: 스크립트의 쿠키 접근 가능 여부 결정
- Secure: HTTPS 프로토콜에서만 쿠키 전송 여부 결정
- SameSite: CORS 요청의 경우 옵션 및 메서드에 따라 쿠키 전송 여부 결정
- MaxAge of Expires
PC 방에서 로그아웃을 안 한 경우
서버에서 쿠키에 MaxAge 혹은 Expires 옵션을 통해 유효 기간 지정 -> 일정 시간이 지난 후 자동 소멸
MaxAge와 Expires의 차이는?
- Expires: 쿠키의 만료일을 설정하는 데 사용한다. Expires는 HTTP 헤더의 속성으로 사용된다. 이 속성의 값은 만료 일시를 나타내며 GMT 형식으로 표시된다. 예를 들어, Expires=Tue, 07 Mar 2023 10:00:00 GMT와 같이 설정할 수 있다.
- Max-Age: 쿠키의 만료 시간(초)을 설정하는 데 사용한다. Max-Age는 HTTP 헤더의 속성으로 사용된다. 이 속성의 값은 쿠키가 만료되기까지의 초 단위의 시간을 나타낸다. 예를 들어, Max-Age=3600과 같이 설정할 수 있다.
Max-Age와 Expires의 차이점
- Expires는 쿠키의 만료 일시를 설정하기 때문에, 클라이언트와 서버 간의 시간 차이에 따라 쿠키의 만료가 예상과 다를 수 있다. 반면, Max-Age는 쿠키의 만료 시간을 초 단위로 설정하기 때문에, 시간 차이에 대한 문제가 발생하지 않는다.
따라서, Max-Age를 사용하면 쿠키의 만료 시간을 더 정확하게 제어할 수 있다. 하지만 일부 오래된 브라우저에서는 Max-Age를 지원하지 않기 때문에, 브라우저 호환성을 고려해야 한다.
예시
로그인 화면 -> db에 있는 ID와 비밀번호로 로그인 할 경우 MyPage 출력 -> 로그아웃 버튼 누르면 다시 로그인 페이지로
* '로그인 상태 유지' 버튼을 눌렀을 경우 브라우저를 껐다 켜도 로그인이 유지되어 있어야 함.
로그인 버튼을 눌렀을 때 호출되는 함수부터 살펴보자.
export default function Login({ setIsLogin, setUserInfo }) {
const [loginInfo, setLoginInfo] = useState({
userId: "",
password: "",
});
const [checkedKeepLogin, setCheckedKeepLogin] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const handleInputValue = (key) => (e) => {
setLoginInfo({ ...loginInfo, [key]: e.target.value });
};
const loginRequestHandler = () => {
/*
Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현
로그인에 필요한 유저정보가 충분히 제공되지 않았다면 에러 메시지가 뜨도록 구현
return axios
.post(login을 담당하는 endpoint)
.then((res) => {
로그인에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경
})
.catch((err) => {
로그인에 실패했다면 그에 대한 에러 핸들링을 구현
});
*/
if (!loginInfo.userId || !loginInfo.password) {
setErrorMessage("아이디와 비밀번호를 입력하세요");
// 입력되지 않은 값이 있으니 post 요청을 보낼 필요도 없이 바로 리턴
return;
}
return axios
.post("http://localhost:4000/login", { loginInfo, checkedKeepLogin })
.then((res) => {
setUserInfo(res.data);
setIsLogin(true);
//여기에서 에러 초기화
setErrorMessage("");
})
.catch((err) => {
console.log(err.response.data);
setErrorMessage("로그인에 실패했습니다.");
});
};
loginInfo
ID와 Password를 하나의 객체로 만들어 관리한다.
return (
...
<span>ID</span>
<input
type="text"
data-testid="id-input"
onChange={handleInputValue("userId")}
/>
<span>Password</span>
<input
type="password"
data-testid="password-input"
onChange={handleInputValue("password")}
/>
...
);
}
const handleInputValue = (key) => (e) => {
setLoginInfo({ ...loginInfo, [key]: e.target.value });
};
[key]
는 Computed Property Names 문법이다. 이 문법은 객체 리터럴 안에서 대괄호로 묶인 표현식을 사용하여 동적으로 프로퍼티 이름을 생성할 수 있다.
즉, 위 코드에서 handleInputValue
함수는 key
라는 매개변수를 받아, 이를 객체 loginInfo
의 프로퍼티 이름으로 사용한다.
예를 들어, handleInputValue("userId")
가 호출되면,
setLoginInfo({ ...loginInfo, ["userId"]: e.target.value })
와 같이 loginInfo
객체의 userId
프로퍼티에 e.target.value
값을 할당한다.
이렇게 함으로써, handleInputValue
함수는 userId
나 password
와 같은 다양한 프로퍼티 이름을 가진 객체를 다룰 수 있게 된다. 이는 보통 폼 입력값을 다룰 때 유용하게 사용된다.
userInfo
module.exports = {
USER_DATA: [
{
id: '0',
userId: 'hajongon',
password: '1234',
email: 'hajongon@gmail.com',
name: '하종승',
position: 'Frontend Developer',
location: 'Seoul, South Korea',
bio: '걱정할 시간에 그냥 해라.',
},
],
};
const userInfo = {
...USER_DATA.filter(
(user) => user.userId === userId && user.password === password
)[0],
};
로그인 화면에서 ID와 Password를 입력하고 로그인 버튼을 누르면 서버(엔드포인트 localhost:4000/login
)에서는 해당 ID와 비밀번호와 일치하는 data가 있는지 filtering해서 userInfo
라는 변수에 할당한다. 일치하는 것이 없다면 빈 객체가 할당될 것이다.
const cookiesOption = { // 필요한 옵션만 기재함
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
};
if (userInfo.id === undefined) {
res.status(401).send("Not Authorized");
} else if (checkedKeepLogin === true) {
// 로그인을 유지하고 싶은 경우,
// cookiesOption에 max-age 또는 expires 옵션을 추가로 설정
// max-age 옵션으로 작성하는 경우
cookiesOption.maxAge = 1000 * 60 * 30; // 단위는 ms(밀리세컨드 === 0.001초)
// 30분동안 쿠키를 유지한다.
// expires 옵션으로 작성하는 경우
cookiesOption.expires = new Date(Date.now() + 1000 * 60 * 30);
// 지금 시간 + 30분 후에 쿠키를 삭제
// res.cookie(name, value, option)
res.cookie("cookieId", userInfo.id, cookiesOption);
// 클라이언트에게 바로 응답하지 않고 서버의 /userinfo 엔드포인트로 리다이렉트
res.redirect("/userinfo");
} else {
// 로그인을 유지하고 싶지 않은 경우, max-age 또는 expires 옵션을 작성하지 않은 상태 그대로 쿠키를 설정
res.cookie("cookieId", userInfo.id, cookiesOption);
res.redirect("/userinfo")
}
/userinfo
엔드포인트
module.exports = (req, res) => {
const cookieId = req.cookies.cookieId;
const userInfo = {
...USER_DATA.filter((user) => user.id === cookieId)[0],
};
/*
* 쿠키 검증 여부에 따라 유저 정보를 전달하는 로직을 구현.
*
* 로그인 시 설정한 쿠키가 존재하는 지 확인.
* 아직 로그인을 하지 않았다면, 쿠키가 존재하지 않을 수 있다.
*/
if (!cookieId || !userInfo.id) {
res.status(401).send("Not Authorized");
} else {
// 비밀번호는 민감한 정보이기 때문에 삭제 후 응답
delete userInfo.password;
res.send(userInfo); // 이제 클라이언트에게 userInfo가 날아감
}
};
const loginRequestHandler = () => {
// ...
return axios
.post("http://localhost:4000/login", { loginInfo, checkedKeepLogin })
.then((res) => {
console.log(res);
setUserInfo(res.data);
setIsLogin(true);
//여기에서 에러 초기화
setErrorMessage("");
})
.catch((err) => {
console.log(err.response.data);
setErrorMessage("로그인에 실패했습니다.");
});
// ...
};
다시 login 버튼을 눌렀을 때 호출되는 함수로 돌아왔다. post 요청을 하고 받아온 res
를 콘솔에 찍어보자.
우리는 res.data
에서 필요한 정보를 뽑아쓰면 된다. 그럼 이제 logout 로직을 살펴보자.
export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
const logoutHandler = () => {
/*
Logout 버튼을 눌렀을 시 Login 페이지로 돌아갈 수 있도록 구현
return axios
.post(logout을 담당하는 endpoint)
.then((res) => {
로그아웃에 성공했다면 App의 상태를 변경
})
.catch((err) => {
로그아웃에 실패했다면 그에 대한 에러 핸들링을 구현
});
*/
return axios
.post("http://localhost:4001/logout")
.then((res) => {
setIsLogin(false);
setUserInfo(null);
})
.catch((err) => {
console.log(err.response.data);
});
};
isLogin
에 의해 MyPage 컴포넌트가 보일지 Login 컴포넌트가 보일지 결정되기 때문에 logout 버튼을 누르면 setIsLogin
함수를 이용해 isLogin
을 false로 바꿔준다. userInfo
는 초기화해준다.
module.exports = (req, res) => {
/*
* 로그아웃 로직을 구현
*
* cookie-parser의 clearCookie('쿠키의 키', cookieOption) 메서드로 해당 키를 가진 쿠키를 삭제할 수 있다.
* 만약 res.clearCookie('user', cookieOption) 코드가 실행된다면 `user=....` 쿠키가 삭제된다.
* 로그아웃 성공에 대한 상태 코드는 205가 되어야 한다.
*/
const cookiesOption = {
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
};
res.status(205).clearCookie("cookieId", cookiesOption).send("logout");
};
clearCookie
메서드를 쓰기 위해 cookiesOption
을 선언한다. 로그아웃 요청에 성공하면 205로 응답한다.
브라우저를 끄고 다시 접속했을 때도 로그인이 유지되도록 하기
function App() {
const [isLogin, setIsLogin] = useState(false); // 디폴트는 Login 컴포넌트
const [userInfo, setUserInfo] = useState(null);
const authHandler = () => {
/*
초기 화면 렌더링시, 서버에 유저 정보를 요청하여 Login 또는 Mypage가 렌더링되도록 구현
return axios
.get(유저의 정보를 담당하는 endpoint)
.then((res) => {
인증에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경
})
.catch((err) => {
인증에 실패했다면 그에 대한 에러 핸들링을 구현
});
*/
return axios
.get("http://localhost:4000/userinfo") // 있으면
.then((res) => {
setIsLogin(true) // MyPage 컴포넌트
setUserInfo(res.data)
})
.catch((err) => {
console.log(err.response.data)
});
};
useEffect(() => {
// 컴포넌트 생성 시 아래 함수가 실행
authHandler();
}, []);
Session
Session이란?
세션 기반 인증
서버 쪽에서 로그인 상태를 관리한다.
사용자가 웹사이트에서 아이디 및 비밀번호를 입력하고 로그인을 시도할 때,
정확한 아이디와 비밀번호를 입력했다면 서버는 인증에 성공했다고 판단한다.
그럼 인증이 필요한 상황마다 로그인을 해야 할까?
아니다. 서버가 해당 유저의 인증 여부를 알고 있다면, 유저가 매번 로그인 할 필요는 없다.
인증에 따라 리소스의 접근 권한이 달라진다.
- 서버: 사용자가 인증에 성공했는지의 여부를 알아야 한다.
- 클라이언트: 인증 성공을 증명할 수 있는 수단을 갖고 있어야 한다.
세션: 사용자가 인증에 성공한 상태.
웹사이트에서 세션을 유지하기 위한 수단으로 쿠키를 사용한다. 쿠키에는 서버에서 발급한 세션 아이디가 저장된다.
쿠키를 통해 유효한 세션 아이디를 서버에 전달하고, 세션 스토어에 해당 세션이 존재한다면 서버는 해당 요청에 대해 접근 가능하다고 판단한다. 하지만 쿠키에 세션 아이디 정보가 없는 경우, 서버는 해당 요청이 인증되지 않았음을 알려준다.
로그아웃
- 서버: 세션 정보를 삭제해야 한다.
- 클라이언트: 쿠키를 갱신하거나 삭제해야 한다.
예시
쿠키를 사용한 것과 거의 동일하다. 다른 부분은 주석을 확인하자.
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
)[0],
};
if (!userInfo.id) {
res.status(401).send("Not Authorized");
} else if (checkedKeepLogin) {
// req.session에 sessionID 프로퍼티를 추가하고 userInfo.id를 그 값으로 할당한다.
req.session.sessionId = userInfo.id;
console.log(req.session);
// 쿠키 옵션 설정하기
req.session.cookie.maxAge = 1000 * 60 * 30;
res.redirect("/userinfo");
} else {
req.session.sessionId = userInfo.id;
res.redirect("/userinfo");
}
};
const { USER_DATA } = require("../../db/data");
module.exports = (req, res) => {
// /userinfo로 리다이렉트 됐을 때도 sessionId가 필요하기 때문에 다시 정의한다.
const sessionId = req.session.sessionId;
const userInfo = {
// cookieId가 아니라 sessionId로 필터링한다.
...USER_DATA.filter((user) => user.id === sessionId)[0],
};
if (!sessionId || !userInfo.id) {
res.status(401).send("Not Authorized");
} else {
delete userInfo.password;
res.send(userInfo);
}
};
로그아웃
module.exports = (req, res) => {
// req.session.destroy 메서드로 세션을 없앤다.
req.session.destroy();
res.status(205).send("Logged Out Successfully");
};
설명 | 접속 상태 저장 경로 | 장점 | 단점 | |
Cookie | 쿠키는 그저 http의 stateless한 문제를 보완해주는 도구일 뿐 | 클라이언트 | 서버의 부담을 줄여줌 | 쿠키 자체는 인증 도구가 아니다. |
Session | 접속 상태를 서버가 가짐(stateful) 접속 상태와 권한 부여를 위해 세션 아이디를 쿠키로 전송 |
서버 | 신뢰할 수 있는 유저인지 서버에서 추가로 확인 가능. 보안이 중요한 사이트에서 많이 사용 | 하나의 서버에서만 접속 상태를 가지므로 분산에 불리 |
'부트캠프 > TIL' 카테고리의 다른 글
webpack으로 리액트 프로젝트 배포하기 (0) | 2023.03.22 |
---|---|
[백엔드 인증/보안] OAuth (0) | 2023.03.09 |
자주 사용되는 시맨틱 요소 정리 (0) | 2023.02.28 |
Day50. CMarket Redux 리덕스야 덤벼 (0) | 2023.02.26 |
Day 50. Redux야 덤벼라 (0) | 2023.02.24 |