쿠키에 대한 개념, CORS간 쿠키요청에 대한 글은 이전 글을 참고해주면 된다.
참고
이 글은 배포환경에 아닌(배포환경은 same-site다) 개발환경에서의 cross-domain, 즉 localhost와 백엔드 서버간의 쿠키 인증, 그리고 api테스트에 대해 다룬다.
여기서 설정한 것들은 배포환경과는 별개의 로직이다.
배경과 문제상황
도입하게 된 배경
우리 서비스는 OAuth2.0을 사용해 구글, 깃허브 로그인을 진행하고, 이후에 얻어지는 서버 자체의 JWT토큰을 set-cookie를 통해 httpOnly쿠키를 set해주는 방식으로 인증을 구현했다.
들어가기에 앞서 우리 서비스의 쿠키 옵션을 살펴보자.
httpOnly | O |
secure | O |
same-site | Lax |
httpOnly JS로는 쿠키를 조작할 수 없다.
secure https일 때 만 전송된다.
same-site: lax 같은 host인 경우에 전송함
이때 비교하는 값은 브라우저 주소창의 주소와 cookie의 domain이다.
(서브 도메인도 같은 host다. api.algoitni.site ↔ algoitni.site는 same-site다.)
문제상황
배포에서는 api.algoitni.site와 algoitni.site가 같은 도메인이므로 set-cookie를 통해 받아온 쿠키가 api요청에 잘 담겨서 전송된다.
하지만, 로컬에서 개발시에는 localhost와 api.algoitni.site간의 api통신이므로 set-cookie를 통해 받아온 쿠키를 담아서 전송하지 않게된다. 심지어 httpOnly이므로, 직접넣어줄수도 없다!
이 때문에 배포하기 전까지는 api기능테스트를 실제로 해볼 수 없었고, 여러 예외상황이나, 제대로 구현 안된 부분들이 발생하기 쉬웠다. 즉, 개발의 퀄리티가 매우 떨어지는 문제가 발생했다.
해결방안
same-site를 None으로 설정하기
same-site를 none으로 만들고, secure상태를 유지한다.
localhost는 secure에서 제외이므로 가능하다.
하지만 same-site를 none으로 만든다는 것은 혹여나 보안위협이 생기기때문에 (https라면 우리 사이트 쿠키를 담아 요청을 보낼 수 있다는 것) 찝찝했다.
심지어 우리 사이트 쿠키를 다른 곳에서 사용할 필요도 없으니 lax로 최대한 유지하는게 맞다고 생각했다.
Vite Proxy Server를 활용해 우회하기
vite proxy server를 사용해서 localhost 의 쿠키를 localhost dev server로 전송하고, 이걸 서버간 통신을 통해 쿠키를 전달해주는 방식이다.
전체 로직은 아래와 같다.
- 개발자용 토큰 발급 요청 (로그인 버튼 클릭 시 dev모드 환경 변수를 통해 분기 처리)
- 토큰 응답 시, 브라우저에서 js로 직접 쿠키 설정 (이때 설정한 domain은 localhost가 된다)
- api요청 시 localhost간의 쿠키 통신이므로 same-site가 되어 cookie가 요청에 담긴다.
- 이를 proxy서버가 백엔드 서버로 전달한다.
- 응답을 브라우저에게 넘겨준다.
이렇게하면 same site를 none으로 하지 않아도 되고, 배포환경에서는 전혀 영향없이 구현해줄 수 있다. 따라서 이 방식을 채택했다.
코드레벨로 구현하기
그럼 코드레벨로 살펴보자.
Vite Proxy설정
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/codes': {
target: 'https://api.algoitni.site/',
changeOrigin: true,
},
},
},
...
});
위와 같이 설정하여 proxy서버가 요청을 중계해주도록 한다.
개발자용 토큰 발급 요청 (로그인 버튼 클릭 시 dev모드 환경 변수를 통해 분기 처리) & 토큰 응답 시, 브라우저에서 js로 직접 쿠키 설정
function LoginButtonWrapper({ handleClick, className, type, children }: LoginButtonWrapperProps) {
if (MODE === 'development') //env에서 가져옴
return (
<button type="button" onClick={handleClick} className={className}>
{children}
</button>
);
... //배포환경 코드
}
export default function LoginModal(...) {
...
const handleClick = async () => {
localStorage.setItem('code', code);
if (MODE === 'development') {
const token = await getDevCookie(); //dev쿠키를 가져온다.
document.cookie = `access_token=${token};`; //토큰을 등록한다. 이때 설정한 domain은 localhost가 된다
hide();
}
reactQueryClient.invalidateQueries({ queryKey: [QUERY_KEYS.LOAD_CODES] });
};
return (
...
<LoginButtonWrapper type="github" handleClick={handleClick} className="flex items-center p-4 text-white bg-black rounded-full">
<img src="/github.png" className="w-8 h-8" alt="github" />
<span className="text-xl font-bold px-11 basis-[90%]">Github Login</span>
</LoginButtonWrapper>
<LoginButtonWrapper type="google" handleClick={handleClick} className="flex items-center p-4 border-2 rounded-full">
<img src="/google.png" className="w-8 h-8" alt="google" />
<span className="text-xl font-bold px-11 basis-[90%]">Google Login</span>
</LoginButtonWrapper>
...
);
}
결과
이제 개발환경에서 api요청을 하면 쿠키가 요청 헤더에 담기게 된다!