JWT을 이용하여 로그인, 회원 인증과 백엔드 개발자님과 협업하는 과정에서 발생한 CORS 이슈 해결 과정을 정리했습니다.
기본적으로 Redux Saga를 이용하여 구현했습니다.
// login api
// client : axios instance
export const login = ({ email, password }) =>
client.post("/auth/login", {
email,
password,
});
// saga
import * as authApi from '../lib/auth'
function* loginSaga(action) {
yield put(startLoading(LOGIN));
try {
const { email, password } = action;
1️⃣ const response = yield call(authApi.login, { email, password });
2️⃣ const ACCESS_TOKEN = response.headers.authorization;
localStorage.setItem("access_token", ACCESS_TOKEN);
yield put({
type: LOGIN_SUCCESS,
3️⃣ payload: response.data,
});
} catch (e) {
yield put({
type: LOGIN_FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoding(LOGIN));
}
//reducer
...
case LOGIN_SUCCESS:
return {
...state,
authError: null,
auth: action.payload,
};
case LOGIN_FAILURE:
return {
...state,
authError: action.payload,
};
...
// response headers
...
authorization: eyJ0eXAiOi.....
cache-control: no-cache,private
connection: keep-alive
content-type: application/json
...
axios.defaults.withCredentials = true;
// 이 프로젝트의 경우에는 client.defaults.withCredentials = true;
// package.json
...
"proxy" : "[api 주소]"
...
Proxy를 이용하면 local에서 테스트할 때는 가능하지만, 배포 단계로 넘어가면 작동하지 않습니다. 예를 들어, github pages로 배포를 하고 api 통신을 한다면, 서버 api 주소가 [name].github.io/login으로 설정이 되어있습니다.
proxy를 사용한다면, headers의 내용이 모두 전송되지만, exposedHeaders를 설정한다면 보여주고 싶은 header만 전송이 가능합니다. proxy를 제거 후, 서버 쪽에서 headers에 노출하고 싶은 정보만 추가 설정.
//
app.use(
cors({
exposedHeaders: ["Authorization"],
})
);
클라이언트에서 따로 저장한 토큰을 header에 설정, 서버에 전송 후 유효성 검사를 통해 인증 처리합니다.
// api
export const check = () => client.get('/auth/user');
// saga
function* checkSaga() {
yield put(startLoading(CHECK));
try {
const ACCESS_TOKEN = localStorage.getItem("access_token");
1️⃣ client.defaults.headers.common["Authorization"] = `Bearer ${ACCESS_TOKEN}`;
2️⃣ const response = yield call(authApi.check);
yield put({
type: CHECK_SUCCESS,
payload: response.data,
});
} catch (e) {
yield put({
type: CHECK_FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoding(CHECK));
}
// redux
case CHECK_SUCCESS:
return {
...state,
3️⃣ user: action.payload,
};
case CHECK_FAILURE:
return {
...state,
checkError: action.payload,
};
useEffect(() => {
if (user) {
history.push("/");
try {
// user 정보 localStorage 저장
localStorage.setItem("user", JSON.stringify(user));
} catch (e) {
console.log("localStorage is not working");
}
}
}, [history, user]);
function loadUser() {
try {
const user = localStorage.getItem("user");
if (!user) return;
dispatch(check());
} catch (e) {
console.log("localStorage is not working");
}
}
loadUser();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter basename="delivery-service">
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);