어찌됐든 프로젝트를 구현했다.
4일차를 마무리하면서 주요 개념들을 확인해보자.
1. API
▶ Application Programming Interface
상호 작용을 하기위한 인터페이스 사양을 말하면서 프로그램을 작성하기 위한 일련의 sub program, protocol(통신규약)을 정의하고 있다.
위에 이미지를 보면 손님과 요리사는 서로 무엇을 원하고 전달해야하는지 모르는 상태이다. API는 손님과 요리사 사이에 역할을 하며 손님과 요리사는 API(점원)이 상호간 전달해주는 정보를 가지고 사용만 하면 된다. 이를 데이터의 개념으로 적용해보면 아래와 같다.
A. 역할
1. API는 서버와 DB 상호간의 출입문 역할을 하고 있다.
DB(데이터베이스)는 다양하고 중요한 정보들이 저장되어 있기 때문에 접근을 허락받지 않은 많은 사람들에게 노출되면 안된다. API는 그러한 통제역할을 하고 있고 권한이 하용된 인원에게만 접근을 허용한다.
2. API는 표준화되어있다.
운영체제, 사용자 등 모든 접속을 표준화해서 제공하기 때문에 접근성을 부여받는다면 누구든 쉽게 정보를 얻을 수 있다.
3. API는 프로그램, APP <=> 데이터 사이에 정보를 주고 받을 수 있게 한다.
기기와 프로그램, 스마트폰과 기계 사이에 데이터를 제공하고 전달받을 수 있는 역할을 한다.
B. 장점
위의 역할과 관련한 여러가지 장점들이 있다.
- Application 코드 작성을 표준화함
- 빠른 속도로 프로세스를 처리하게 만듬
- 프로그래머 사이에 협업을 굉장히 유연하게 만듬
- 다른 기업, 기관 등의 방대한 데이터를 활용하여 영업이익, 브랜드 인지도 등을 높일 수 있음
- 다양하고 유저의 DB 확장이 가능
2. Cookie & Session
Cookie
쿠키는 Key-Value 형식의 문자열
쿠키란 클라이언트가 어떠한 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 클라이언트의 브라우저에 설치되는 작은 기록 정보 파일을 일컫는다.
쿠키 단점
- 보안에 취약하다.
- 요청 시 쿠키의 값을 그대로 보낸다.
- 유출 및 조작 당할 위험이 존재한다.
- 쿠키에는 용량 제한이 있어 많은 정보를 담을 수 없다.
- 웹 브라우저마다 쿠키에 대한 지원 형태가 다르기 때문에 브라우저간 공유가 불가능하다.
- 쿠키의 사이즈가 커질수록 네트워크에 부하가 심해진다
Session
쿠키의 보안적인 이슈 때문에 세션은 비밀번호 등 클라이언트의 인증 정보를 쿠키가 아닌 서버 측에 저장하고 관리한다.
세션 단점
- 쿠키를 포함한 요청이 외부에 노출되더라도 세션 ID 자체는 유의미한 개인정보를 담고 있지 않는다.
그러나 해커가 이를 중간에 탈취하여 클라이언트인척 위장할 수 있다는 한계가 존재한다. (이는 서버에서 IP특정을 통해 해결 할 수 있긴 하다) - 각 사용자마다 고유한 세션 ID가 발급되기 때문에, 요청이 들어올 때마다 회원정보를 확인할 필요가 없어졌지만, 서버에서 세션 저장소를 사용하므로 요청이 많아지면 서버에 부하가 심해진다.
3. JWT
JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다.
그리고 JWT 기반 인증은 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식이다
JWT는 JSON 데이터를 Base64 URL-safe Encode 를 통해 인코딩하여 직렬화한 것이며, 토큰 내부에는 위변조 방지를 위해 개인키를 통한 전자서명도 들어있다.
따라서 사용자가 JWT 를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며 검증이 완료되면 요청한 응답을 돌려준다.
이 개념을 이해하는데 정말 오랜시간이 걸렸다 하..
JWT 구조인 Header, Payload, Signature 3개의 개념을 설명하기엔 필자의 현재 지식으로는 보는 것만으로 아직 벅차다. 차차 공부하면서 나중에 추가로 작성을 하겠다(궁금하면 구글링)
JWT 구현
- Oauth 인증
OAuth 2.0(Open Authorization 2.0, OAuth2)은 인증을 위한 개방형 표준 프로토콜이다.
이 프로토콜에서는 Third-Party 프로그램에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공있으며 현재 구글, 페이스북, 카카오, 네이버 등에서 제공하는 간편 로그인 기능도 OAuth2 프로토콜 기반의 사용자 인증 기능을 제공하고 있다
- 발급 과정
- Frontend에서 Github에 authorization code를 요청
- Github에 지정한 callback url로 redirect된 뒤 받은 authorization code를 BE에 요청
- BE에서 autorization code를 이용해 Github에 access Token을 요청
- Github에서 받은 Acess Token을 이용해 또다시 Github에 사용자 정보 요청
- Github에서 받은 사용자 정보를 이용해 JWT 토큰을 발급
- Frontend에서 JWT토큰을 Session Storage에 저장
- 이후 요청에 Access Token을 넣고 검증을 통해 응답을 받는다.
- Access Token은 계속 세션 스토리지에 저장하고 있다.
출처 [ Dev Scroll:티스토리]
JWT는 만료기간을 주지 않고, SecretKey와 algorithm을 적용하여 생성한다.
그리고 만든 JWT는 Frontend에서 새로고침을 해도 로그인이 유지되도록 하기 위해 Session Storage에 저장한다.
const jwtConfig: Config = {
secretKey: config.jwt_secret,
options: {
algorithm: config.jwt_algorithm as jwt.Algorithm,
},
};
다만, 보안 이슈가 발생한다.
JWT는 Stateless이기 때문에 한 번 만들어지면 제어가 불가능합니다 .임의로 토큰을 삭제할 수 없기 때문에 만료기간을 설정하지 않으면 탈취될 가능성이 높습니다.
해결방법은?
- HTTPS Cookie
HTTPS에서 Secure Cookie와 HTTP Only 쿠키를 사용해 자바스크립트 기반 공격을 방어할 수 있다.
- Secure Cookie : 웹브라우저와 웹서버가 HTTPS로 통신하는 경우에만 웹브라우저가 쿠키를 서버로 전송하는 옵션
- HTTP Only : 자바스크립트로 쿠키를 조회하는 것을 막는 옵션
- 사용자가 로그인을 합니다.
- DB에서 사용자를 확인합니다.
- Access Token을 sameSite, httpOnly, Secure 옵션을 준 Cookie에 담습니다.
- 새롭게 발급받은 cookie를 응답으로 받습니다.
- 데이터 요청에 Cookie를 credentials: same-origin으로 설정하고 Cookie에 담긴 Access Token과 함께 요청합니다.
- Access Token을 검증합니다.
- 검증이 완료되면 요청한 데이터를 응답합니다.
- Access Token과 Cookie는 일정 시간이 지나면 만료되고 사라집니다.
- Cookie가 담기지 않은 상태로 요청이 보내집니다.
- Cookie가 있는지 Access Token이 만료되었는지 확인하고 에러를 보냅니다.
- 응답으로 에러를 받고 로그인 페이지로 이동시킵니다.
- 재로그인을 해야 합니다.
말은 쉽지 그래서 어떻게 해보면 좋을까?
<아래의 내용과 코드는 HTTPS 보안 설정을 위해 이후에 사용해볼 것을 토대로 myday.log 님의 블로그 출처임을 밝힙니다>
- Client & Server에서 HTTPS & CORS를 고려한 Cookie 설정
Client
Client에서는 일부 API에만 credentials 옵션을 'same-origin'으로 주었고 그 외에는 'omit'으로 두었다.
const userInfoResponse = await fetch(
`${process.env.REACT_APP_API_URL as string}/api/auth/info`,
getOptions('GET', undefined, 'same-origin'),
);
const getOptions = <T>(
fetchMethod: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'UPDATE',
data: T,
credential: 'omit' | 'same-origin' | 'include' = 'omit',
isStringify = true,
contentType: string | null | undefined = 'application/json',
signal?: AbortSignal,
): RequestInit => {
const options: RequestInit = {
method: fetchMethod,
mode: 'cors',
credentials: credential,
body: (isStringify ? JSON.stringify(data) : data) as BodyInit,
signal: signal,
};
if (contentType) {
options.headers = { 'Content-Type': contentType };
}
return options;
};
Server
Server에서는 Cookie에 httpOnly와 secure 옵션을 주었고 samSite에는 'lax'로 두었다.
res.cookie(
'token',
jwtToken.token,
getCookieOption(Number(config.jwt_cookie_expire)),
);
const getCookieOption = (
maxAge: number,
sameSite: 'lax' | 'none' | 'strict' = 'lax',
): CookieOptions => {
return {
httpOnly: true,
secure: true,
sameSite: sameSite,
maxAge: maxAge,
};
};
위와 같이 설정하고 아래의 CORS 개념을 파악하기 위해서는 나중에 재차 개념을 정리할 필요가 있다.
// client request(fetch) option
{
method: fetchMethod,
mode: 'cors',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
}
}
// server response option
if (morganFormat == 'dev') {
const allowedOrigins = [`${config.react_url}`];
const options: cors.CorsOptions = {
origin: allowedOrigins,
credentials: true,
};
app.use(cors(options));
}
// res.cookie option
{
httpOnly: true,
secure: true,
sameSite: 'none',
maxAge: maxAge,
};
이를 구현하면 아래의 공격들에 대비할 수 있다고 한다.
Refresh Token
HTTPS Secure
HTTPS http Only
CORS
이번 웹 미니프로젝트를 준비하면서 최종 발표전에 HTTPS 보안과 관련된 개발블로그들과 stackflow, 구글링을 통해 앞으로 백엔드 개발자로 성장하기 위해서 보안과 관련된 이슈도 놓치지 않도록 많은 개념들과 실습을 쌓아가야겠다. 정말 쉽지 않은 도전이지만 즐겁게 하자.
오늘 멘토 개발자님에게 조언 받은 Validation Check 방법도 살펴보자