TIL 17) Cookie

Hover·2023년 5월 2일
0

TIL

목록 보기
20/27

쿠키는 어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터다.

서버가 웹 브라우저(클라이언트)에 정보를 저장하고 불러올 수 있는 수단으로
해당 도메인에 쿠키가 존재하면 웹 브라우저는 도메인에게 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 요청의 옵션 및 메서드에 따라 쿠키 전송 여부 결정
  1. Lax : GET 메서드 요청만 쿠키 전송 가능
  2. Strict : 쿠키 전송 불가
  3. None : 모든 메서드 요청에 대해 쿠키 전송 가능, 단 Secure 쿠키 옵션이 필요

    위와 같이 CSRF 공격을 받았을 경우에 효과적이다.

쿠키 활용 실습

로그인 상태 유지를 하는 기능을 만들어보려 한다.

한 파일에 서버/클라이언트가 있으며, 각각의 요구사항을 충족하면 된다.

1. CORS 설정

서버의 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를 연결해줌

2. <클라이언트> 서버로 로그인 요청 보내기

총 3개의 state가 있다.

  const [loginInfo, setLoginInfo] = useState({
    userId: "",
    password: "",
  }); // 로그인 정보
  const [checkedKeepLogin, setCheckedKeepLogin] = useState(false); // 로그인 유지 할건지
  const [errorMessage, setErrorMessage] = useState(""); // 에러 발생 시 로그인 버튼 밑에 에러메시지 표시 여부

첫 번째로 ID와 Password가 업데이트 되는 LoginInfo
두 번째로 로그인 유지 할건지에 대한 상태 checkedKeepLogin
세 번째로 에러 발생시 에러 메시지은 errorMessage 이다.

  1. 로그인 에러가 발생하는 경우는 아이디 또는 비밀번호가 입력되지 않았을 경우다.

  2. 아이디와 비밀번호가 전부 잘 입력되었으면, 서버에 로그인 데이터를 보낸다.

  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를 확인할 수 있다.

3. <서버> 로그인 요청 처리하기

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에서 쿠키 검증 후 다시 클라이언트로 보내지게 된다.

4. <클라이언트> 응답 쿠키로 React 상태 업데이트

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>
  );

5. <클라이언트> 로그아웃 버튼 클릭 시 요청 보내기

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);
      });
  };

6. <서버> 로그아웃 로직

로그아웃 요청을 보냈으면 서버에서 로그아웃을 해줘야한다.

쿠키를 삭제해야 하므로 쿠키 삭제에 쓰이는 clearCookie를 이용한다.

//server/logout
module.exports = (req, res) => {
  res
    .status(205)
    .clearCookie('cookieId', {
      domain: 'localhost',
      path: '/',
      sameSite: 'strict',
      secure: true,
    })
    .send('Logged Out Successfully');
};

클라이언트에서 따로 데이터를 담아 요청하지 않았으므로 req는 사용하지 않는다.

7. <클라이언트> 로그인 유지 로직

로그인 페이지에 도달했을때 클라이언트에 이미 쿠키가 있다면(로그인 후 쿠키가 유지되어있으면) 바로 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로 리렌더링 될때마다 실행시킨다.

profile
프론트엔드 개발자 지망생입니다

0개의 댓글