JSON 로그인 프론트엔드 + 백엔드 구현
프로젝트를 진행하면서 JWT (Json Web Token) 방식으로 로그인을 구현하기로 하였다. (JWT에 대한 설명은 생략, 이 글을 읽는 사람들은 에러를 해결하기 위해 온거겟지?)
백엔드에서 access-token
은 header
의 Authorization
에 담아서 보내주고, refresh-token
은 cookie
에 담아서 보내기로 하였다. (보안 이슈 고려 body에 넣지 않음)
프론트에서는 access-token
을 localStorage
에 저장하고, refresh-token
은 cookie
에 저장하기로 하였다. (아래 표와 같음)
프론트엔드(저장 위치) | 백엔드(response 방식) | |
access-token | localStorage | header.authorization |
refresh-token | cookie | cookie |
프로젝트 과정에서 해당 방식으로 JWT를 구현하기 위해 삽질했던 기록을 작성한다.
똑똑한 사람들을 위해 최종 코드 먼저 제공
백엔드
return res
.status(403)
.cookie('refreshToken', refreshToken, {
expires: new Date(Date.now() + 259200),
httpOnly: true,
})
.header('Authorization', newAccessToken)
.json({ message: 'Renewed expired access token' });
프론트엔드
import axios from "axios";
export const API = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
withCredentials: true,
});
처음에 설계했던 방법은 다음과 같다.
프론트엔드
import axios from "axios";
export const API = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
withCredentials: true,
});
백엔드
return res
.status(403)
.cookie("refreshToken", refreshToken, {
httpOnly: true,
})
.header("Authorization", newAccessToken)
.json({ message: "Renewed expired access token" });
request
는 간단했다. 우리는 login post를 요청하였고 headers
에서 Authorization
항목을 받아오고 싶었다.
하지만, CORS ERROR와 마주했다.
[삽질 No.1] CORS 설정
// login request code
try {
const res = await API.post("/auth/login", userInput);
// access token 저장
localStorage.setItem("accessToken", res.headers.get("Authorization"));
}
catch (error) {
// status에 따른 Error Handling
const errorResponse = error.response;
const statusCode = errorResponse.status;
console.log(statusCode);
switch (statusCode) {
case 40X:
...
break;
}
}
❓CORS란?
CORS를 이해하기 위해서는 SOP를 이해해야 합니다.
브라우저에서는 보안적인 이유로 cross-origin HTTP Request들을 제한합니다. 말 그대로, 서로 다른 origin (간단하게 url) 끼리의 HTTP Request를 제한하는 것을 의미한다.
이를 SOP (Same - Origin Policy)라고 합니다.
즉, SOP는 동일한 출처 사이에서만 리소스를 공유할 수 있다는 규칙을 지니고 있습니다.
→ CSRF(Cross Site Request Frogery, 크로스 사이트 요청 위조 공격)을 대비해 만들어짐 (공격자의 요청이 사용자의 요청인 것처럼 속이는 공격 방식)
⭐예전에는 프론트엔드 / 백엔드가 분리되지 않아서, 처리를 같은 도메인 내에서 했기 때문에 해당 정책이 효과적으로 공격들을 막아낼 수 있었다(CSRF, XSS). 하지만, 이제는 프론트엔드 / 백엔드가 분리되고, 다양한 API들을 사용하기 위해서 동일하지 않은 출처 사이에서도 리소스를 공유해야 했다.
이때, SOP를 지키면서 몇가지 예외사항을 적용하는 것이 CORS이다.
✏️출처는 Protocol + Host + Port (HTTPS://www.domain.com:3000)이다.
즉, 동일하지 않은 출처 사이에서도 HTTP Request를 가능하게끔 하는 것이 CORS이다. (매번 에러가 발생해서 나쁜 놈인줄 알았지만 사실은 좋은 놈이였다)
단, 모든 출처에서 접근할 수 있게 만들면 SOP가 없는것과도 같으니, ⭐가능한 URL들을 지정해주는 방법으로 최소한의 접근만 허용하는 것이다.
해당 프로젝트에서 어떻게 적용해야 할까?
1.CORS설정으로 프론트에서는 백엔드를, 백엔드에서는 프론트엔드를 등록한다.
- → 이때, 양쪽에서 등록해야 함을 주의하자.
// 프론트엔드 (package.json)
{
...,
"proxy": "http://localhost:3000" //백엔드 url
}
// 백엔드 (cors_config.js)
const cors = require("cors");
exports.options = cors({
origin: [
"http://localhost:5173", // 프론트 url
],
});
이렇게 했는데도, CORS Error가 발생했다.
→ The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include’
2.credentials: true
설정하기
⭐CORS는 기본적으로 보안상의 이유로 쿠키를 request로 보낼 수 없도록 설계되었다.
하지만, 다른 도메인을 가진 API 서버에 자신을 인증해야 하는 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요하므로, 해당 설정을 수정해야 한다.
// 프론트엔드
import axios from "axios";
export const API = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
headers: {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
withCredentials: true, // 추가하기
});
// 백엔드
const cors = require("cors");
exports.options = cors({
origin: [
"http://localhost:5173",
],
credentials: true, // 추가하기
});
1.header의 Authorization
에 접근 권한 설정하기
⭐ header는 기본적으로 값들이 접근가능하지 않도록 설정되어 있는 경우가 많다.
백엔드의 CORS 설정에서 exposedHeaders
값으로 노출하는 값을 변경할 수 있다.
// 백엔드
const cors = require("cors");
exports.options = cors({
origin: [
"http://localhost:5173",
],
exposedHeaders: ["Authorization"],
credentials: true,
});
최종 코드
참고로, set-Cookie에 보낸 refreshToken은 브라우저에서 자동으로 쿠키에 저장해준다.
try {
const res = await API.post("/auth/login", userInput);
// access token 저장
localStorage.setItem("accessToken", res.headers.get("Authorization"));
// refresh token 저장 : 브라우저 자동
// ... 유저 정보 저장 / redirect 등
}
catch (error) {
// status에 따른 Error Handling
const errorResponse = error.response;
const statusCode = errorResponse.status;
console.log(statusCode);
switch (statusCode) {
case 404:
alert("해당 이메일로된 아이가 없습니다");
break;
case 401:
alert("비밀번호가 틀렸습니다.");
break;
case 400:
alert("Bad request (요청 형식이 잘못됨)");
break;
}
}
[삽질 No.2] 로그아웃 기능
로그아웃 기능을 구현할때, ✏️access Token과 refresh Token을 삭제하고 싶다.
access Token은 localStorage에 있으므로, 쉽게 삭제 가능하다.
😡문제는 refresh Token이다.
쿠키에 있는 refresh token에 접근해서 삭제하고 싶었다.
암만 찾아봐도 방법이 없었다. 실제로 불가능했기 때문이다.
⭐백엔드에서 refresh Token을 쿠키에 담아서 보낼때 httpOnly:true
를 달아서 보냈는데, 해당 속성을 달아주면 프론트에서 암만 노력해도 접근할 수 없다. 일부러 막아둔 것이다.
잘 생각해보면 저장할 때도 자동으로 저장하는 이유가, 프론트에서 접근하지 못하니까 친절하게 알아서 저장해주는 것이였다.
❓그러면.. 어떻게 삭제해???
쿠키에는 2가지 종류가 있었다.
세션 쿠키 | 사용자가 사이트 탐색 시에 관련한 설정들과 선호사항을 저장하는 임시 쿠키정도라고 생각하면 된다. 브라우저를 닫는 순간 삭제된다. |
지속 쿠키 | 지속쿠키는 세션쿠키와 다르게 삭제되지 않고 더 길게 유지가 가능하다. 지속쿠키는 디스크에 저장되며, 브라우저를 닫거나 컴퓨터를 재시작해도 남아닜다. 사용자 로그인 항상 유지와 같은 곳에 사용한다 |
차이점 | 세션쿠키와 지속쿠키는 파기시점 외에 차이점이 없다. Discard라는 파라미터가 설정되어 있거나, 파기까지 남은시간인 Expires또는 Max-Age라는 파라미터가 없으면 세션쿠키이다. |
즉, 우리는 지금까지 세션 쿠키를 사용하였지만, httpOnly
속성으로 인해 직접 쿠키를 제어할 수 없으므로, 지속 쿠키를 생성하기로 하였다.
즉, refresh-token
을 삭제하는 대신 ⭐만료기간을 설정하는 방식으로 진행하였다.
백엔드분들한테 refreshToken
의 기간을 설정해달라고 요청하였다.
return res
.status(403)
.cookie('refreshToken', refreshToken, {
expires: new Date(Date.now() + 259200), // 기간 설정
httpOnly: true,
})
.header('Authorization', newAccessToken)
.json({ message: 'Renewed expired access token' });
지금까지, JWT를 구현할 때 삽질했던 기록들을 정리했습니다.
나중에 또 삽질할때 봐야겠다 :)
개인 블로그: https://velog.io/@jihostudy/posts
edited by 김지호