Now Loading ...
-
Cookie
쿠키 개요
쿠키는 서버가 클라이언트에게 특정 정보를 저장하도록 전달하는 수단으로, 서버가 HTTP 응답에 쿠키를 포함하면 이후 클라이언트는 서버와의 요청마다 해당 쿠키를 함께 전송한다.
이 용어는 원래 유닉스·네트워크 시스템에서 프로세스 간 통신 시 신원 식별이나 권한 확인을 위해 주고받던 작은 데이터 조각인 매직 쿠키(Magic Cookie) 개념에서 비롯되었다. 이러한 아이디어를 웹에 적용하면서, HTTP 통신에서도 클라이언트와 서버가 교환하는 작은 데이터 조각이라는 공통된 특성을 반영해 쿠키라 불리게 되었다.
실제 사용 예로는 로그인 상태 유지(세션 관리), 장바구니와 같은 사용자 활동 기록, 언어·테마 설정과 같은 개인화, 그리고 광고나 분석을 위한 사용자 행동 추적 등이 있다.
브라우저 저장소
브라우저가 인증 등에 사용하는 정보들을 저장하기 위한 방법론에는 쿠키, 로컬 스토리지, 세션 스토리지, 웹 스토리지 등이 있다.
각 저장소에 대한 간략한 설명은 아래와 같다.
쿠키
쿠키란 서버가 클라이언트에게 보내는 작은 데이터 파일로, 서버가 쿠키를 브라우저에게 적용을 시키면 그 이후 서버와 클라이언트에 모든 요청과 응답에는 쿠키가 포함되어 전송된다. 이러한 특징 때문에 저장할 수 있는 용량이 작으며 보안의 취약하다는 단점이 있다.
로컬 스토리지
브라우저 자체에 영구적으로 데이터를 저장하고, 브라우저가 종료되더라도 데이터가 유지가 된다.
로컬 스토리지 세팅 예시
세션 스토리지
탭 윈도우 단위로 스토리지가 생성이 되며, 탭 윈도우를 닫을 때 데이터가 삭제되는 특징을 가진다. 페이지 새로고침으론 데이터가 삭제되지 않는다.
세션 스토리지 세팅 예시
로컬이나 세션 스토리지의 경우, CrossSite 공격에 취약하기 때문에 민감한 정보를 저장하면 안된다.(SOP 정책이 있는 이유인 공격에 취약)
또한 JS를 통해 데이터에 접근하며 쿠키와 다르게 항상 붙여져서 서버에 전송되지 않는다.
쿠키의 구성
response.set_cookie(
key="access_token",
value=token.access_token,
httponly=True,
secure=True, # dev use only https
samesite="None",
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
)
위와 같은 예시로 쿠키를 서버가 설정하여 브라우저가 특정 쿠키를 저장하도록 설정할 수 있다.
이때 쿠키에 적용될 수 있는 옵션들은 아래와 같은 의미를 갖는다.
참고링크
1. Key & value
쿠키에 저장되는 정보는 기본적으로 key와 value의 쌍으로 저장되며, 각 쿠키는 브라우저와 통신하는 서버별로 따로 저장된다.
만약 여러 개의 key, value를 저장하고 싶다면 각 key, value별로 옵션을 설정하여 set_cookie를 실행해야한다.
2. Domain
쿠키를 보낼 호스트를 정의하는 옵션이다.
쿠키는 쿠키가 생성된 도메인 별로 종속되어서 저장된다. 예를 들어 example.com에서 생성된 쿠키는 www.example.com에선 사용될 순 있지만, 반대로 www.example.com에서 생성된 쿠키는 example.com에선 사용될 수 없다.
이런 식으로 쿠키가 저장될 도메인을 설정할 옵션은 자신과 관련된 서브 도메인까지만 가능하다.
예를 들어 우리의 주소 dev.medai.im 도메인에서 쿠키를 설정할 땐 해당 옵션을 사용하여 medai.im 도메인에 쿠키를 저장하여 다른 서브 도메인들에서 해당 쿠키를 사용하게 만들 순 있지만, 다르게 ai.im 같은 전혀 다른 도메인에 쿠키를 저장할 순 없다.
3. Expires, max_age : 쿠키의 만료시간과 관련된 옵션
이 두가지 옵션은 쿠키가 유지되는 시간을 정의하는 옵션이다.
MaxAge : 쿠키가 유지되는 시간을 정의하며, 만약 Expires와 동시에 설정된다면 우선되는 옵션이다. 현재 시간에 해당 옵션의 시간이 추가되어 만료시간이 계산된다.
Expires : HTTP Date형식으로 쿠키가 만료되는 날짜와 시간을 정의한다.
만약 2개의 옵션을 모두 설정하지 않으면, 쿠키는 브라우저가 종료되는 시점까지 유지되는데 이를 Session Cookie라고 한다.
4. HttpOnly, Secure : 쿠키의 보안과 관련된 옵션
HttpOnly : 해당 옵션을 활성화할 시, 쿠키를 JavaScript를 통해서 저장된 곳을 접근해 확인하는 것이 불가능해진다. 이 옵션이 true가 되었다면 개발자도구를 통해서 직접 확인하거나, 서버와 클라이언트가 통신 중 헤더에 붙은 쿠키를 직접 확인하는 방법으로만 체크할 수 있게 된다.
Secure : HTTP는 기본적으로 평문으로 통신하기에 쿠키 역시도 확인이 가능하다. 하지만 Secure 옵션이 존재하는 순간 http에 쿠키를 붙여서 보낼 수 없게되며, 오로지 https를 통해서 전송할 시에만 쿠키를 붙일 수 있게 된다.
5. Partitioned
쿠키는 기본적으로 요청을 받는 서버별로 브라우저가 쿠키를 저장한다. 이와 같은 특징으로 인해 만약 광고 tracker.com 이라는 사이트가 특정 js를 광고용으로 여러 사이트 a.com, b.com에 넣어두면, 브라우저가 a.com, b.com을 방문했을 때 같은 tracker.com으로 요청이 들어감으로써 쿠키를 통해 브라우저를 고유하게 식별해서 사용자 추적이 가능해진다.
이러한 사용자 추적을 막기위한 기능이 Partitioned로 쿠키를 저장한 버킷을 tracker.com으로만 두는 것이 아닌 탑레벨 사이트 + 쿠키 도메인으로 a.com, b.com도 쿠키 저장소를 구분하는 기준에 추가하여, 탑레벨 사이트만으로 쿠키가 공유되지 않도록 한다.
6. Path
해당 쿠키를 클라이언트-서버가 통신할 때 전부 붙여서 보내는 게 아닌,
Path=/target-path로 설정하면 www.example.com/other-path 같은 url에선 쿠키가 전송되지 않고, www.example.com/target-path 및 해당 url의 하위 경로로 요청을 보낼때만 브라우저가 쿠키를 포함시킨다.
7. SameSite
Strict, Lax, None 3가지 옵션을 가질 수 있으며 각 옵션의 의미는 아래와 같다.
Strict
브라우저가 동일한 사이트 요청에만 쿠키를 전송한다. 예를 들어 www.a-example.com에서 백엔드인 www.be-example.com으로 요청을 보내 백엔드에서 쿠키를 설정해 보낸다면, 2개의 사이트의 도메인이 다르기에 Strict 옵션을 사용한 쿠키라면 저장이 되지 않는다.
만약 a-example.com에서 api.a-example.com 백엔드로 보낸다면 2개의 사이트의 도메인이 같기에 Strict 옵션을 통과하고 저장된다.
이때 동일한 사이트로 구분되는 기준은 Public Domain과 앞의 접미사까지 동일해야 동일한 사이트로 취급된다. public domain 리스트
ex) .com, .github.io 등 Public Domain에 대해 그 다음 항목인 a.com a.github.io | b.com b.github.io 는 다른 사이트이다.
Lax
Lax 옵션은 SameSite가 아니더라도, 특정 접근에 관해서는 쿠키를 붙이는 것을 허용하는 옵션이다. 여기서 특정 접근은 아래와 같다.
사용자가 직접 사이트에서 클릭을 해서 이동하는 경우
해당하는 내용은 특정 이미지 등의 리소스 요청이 아닌 전체 사이트가 변경되는 GET 요청(특정 사이트에서 동일한 로그인 정보를 공유하는 다른 사이트로 갈 때 등)에는 Lax 옵션을 통해 허용해준다.
None
오로지 Secure 옵션이 true 인 것과 병행될 때만 사용 가능한 옵션으로, 백엔드와 요청을 보낸 사이트가 samesite가 아니더라도 언제나 쿠키를 보낼 수 있는 옵션이다. 해당 옵션이 활성화되지 않는다면, 서버와 클라이언트의 도메인 주소가 다르다면 쿠키를 사용할 수 없다.
개발환경에서 localhost:3000에서 arna.medai.im으로 요청을 보낸다면 SameSite가 None이 아닌 경우 Cookie를 설정할 수 없다.
-
JWT
JWT 개요
JWT란 토큰의 일종으로, http 통신을 수행할 때 해당 토큰을 요청이나 응답의 일부로 넣어줌으로써 서버와 클라이언트가 상태유지를 수행할 수 있게 해주는 개념이다.
JWT는 header, payload, verify signature로 구성되어 JWT 토큰을 인코딩하고, 반대로 JWT 토큰을 디코딩하면 해당 값들을 획득할 수 있다.
이와 관련된 예시는 JWT 예시 사이트를 접속해 확인해보자
JWT 토큰 예시
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
이렇게 구성된 토큰은 Based64Decode 등의 방법론을 통해서 encode, decode 가능하다. 즉 별도의 키가 필요한 해독방식은 아니다.
토큰을 살펴보면 .으로 구분된 3개의 영역이 있는데 이를 각각 디코딩을 수행하면 아래와 같은 정보들이 생성된다.
디코딩된 정보 예시
1. header
{
"alg": "HS256",
"typ": "JWT"
}
토큰의 타입(typ)는 보통 JWT로 고정되어있고, alg는 알고리즘의 약자로, 3번 서명값을 만드는데 사용될 알고리즘의 약자가 적혀있다.
2. payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Json 형식으로 서비스가 사용자에게 해당 토큰을 통해 공개하기를 원하는 내용, 사용자 닉네임, 서비스 상의 레벨, 관리자 여부 등의 정보를 저장할 수 있는 부분이다. 이렇게 토큰에 담긴 사용자에 대한 정보를 Claim이라고 한다. 이렇게 사용자에 대한 정보를 애초에 포함한 정보가 보내지기에, 서버가 DB 등을 뒤질 필요성도 적어진다.
3. verify signature
a-string-secret-at-least-256-bits-long
해당 값에는 header + payload + 서버에 존재하는 비밀값을 기반으로 헤더에 적힌 암호화알고리즘을 통해 생성된 secret string 서명값이 기록되어있다. 별도의 키 값 없이 payload나 header가 해독되더라도, 그 값을 멋대로 수정해버리면 서버에 존재하는 비밀키 값과 조합되서 알고리즘을 실행시켰을 때 verify signature값이 달라져버리기에 JWT가 인증의 수단으로 사용될 수 있다.
JWT의 특징
위와 같은 JWT의 구성방식을 보면 알 수 있듯이, JWT의 기록되는 상태 정보는 시간에 따라 달라지는 것이 아닌 해당 인증이 유효할 때 항상 동일한 정보를 기반으로 사용자를 구분한다. 이런 식으로 시간에 따라 바뀌지 않는 상태값을 갖는 것을 stateless라고 불리며, 반대로 세션은 stateful이라고 불린다.
이러한 특징으로 인해
인증에 관련된 사용자의 상태정보가 시간에 따라 바뀌지 않는다.
서버가 사용자의 정보를 별도로 기록할 필요가 없기 때문에, 비용적인 측면에서 장점이 존재한다.
토큰이 탈취되면 서버는 이를 구분할 방법이 없기에, 탈취에 대한 대처가 취약하다.
토큰 탈취의 위험을 막기 위해 보통 refresh Token과 access Token을 따로 구현한 후 위에서 이야기한 인증에 관련된 기능은 유효기간이 짧은 access Token으로 refresh Token은 access Token을 재발급해줄 수 있는 역할과 이를 서버가 기억함으로써 서버가 로그인을 관리할 수 있도록 한다.
실제 구현(파이썬 프로젝트와 관련된 예시)
.env 등 공개되지 않는 환경설정 파일에서 아래와 같이 JWT에 사용될 서버가 저장하는 비밀키를 저장한다.
JWT_SECRET_KEY=~
파이썬 코드 내부에서 사용될 알고리즘과 Access Token 만료시간, refresh Token 만료시간을 저장한다.
SECRET_KEY = JWT_SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 4 * 60 # AccessToken 만료 시간
REFRESH_TOKEN_EXPIRE_DAYS = 14 # RefreshToken 만료 시간
로그인이 완료된 후 payload에 입력될 정보들을 서버에서 입력해주어 Access Token을 생성해준다.
new_access_token = create_access_token(
data={"sub": username}, expires_delta=access_token_expires
)
def create_access_token(data: dict, expires_delta: Union[timedelta , None] = None):
to_encode = data.copy()
print("to_encode", to_encode)
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
현재 구현된 코드에선 사용자의 username과 access token이 만료되는 시간을 인증 payload에 넣은 모습을 볼 수 있다. AlGORITHM은 위에서 상수로 고정되어있으며, SECRET KEY는 .env파일을 통해 가져와서 jwt 코드를 인코딩해 Access Token을 사용하는 모습을 볼 수 있다.
또한 refresh Token은 서버에 저장해서 비교하는 용도이기 때문에, JWT만을 쓰지 않고 랜덤한 난수 등을 활용해 키를 만들 수도 있으면, 탈취되었다면 서버 DB에서 제거하는 등으로 대처할 수 있다.
다만 현재 구현되는 시스템은 병원의 내부망에서 사용될 예정이기에, 보안상 access Token의 탈취 위험이 적기에 현재는 별도의 refresh token을 사용하지 않게 변경되었다.
async def get_current_doctor(request: Request, db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token = request.cookies.get("access_token")
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
try:
# this decode will check expire token, if expired, raise exception
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(payload)
username: str = payload.get("sub")
print("get_current_doctor: ", username)
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError as e:
print("JWTError", e)
raise credentials_exception
doctor = get_doctor_by_username(username=token_data.username, db = db)[0]
if doctor is None:
raise credentials_exception
return doctor
마지막으로 위처럼 각 http 요청이 들어올 때마다, Middleware로 JWT를 디코딩하고 만약 서명에 문제가 발생시 JWTError를 발생시키는 코드를 통해 JWT를 통한 인증을 구현할 수 있다.
-
상태관리 개요
개요
서버와 클라이언트의 상태관리를 위해서 웹 서버는 인증(Authentication)과 보안(Security)을 고려해야한다. 인증을 통해 사용자가 누구인지, 해당 사용자가 어떠한 권한을 가지고 있는지를 증명할 수 있고, 보안을 통해 이러한 사용자를 인증하는 요소를 다른 사람이 탈취하고나, 사용자의 개인정보를 열람할 수 없도록 보호할 수 있다. 이러한 요소를 구현하는 방법론에 대하여 정리해보자.
Authentication(인증)
왜 인증이 필요한가?
HTTP라는 프로토콜은 무상태(stateless)라는 특징을 갖고 있다.
이 말은 곧, 서버와 클라이언트가 HTTP로 통신할 때 요청 하나하나는 독립적이며, 이전 요청과의 연관성을 HTTP 자체만으로는 알 수 없다는 뜻이다.
예를 들어, 사용자가 어떤 사이트에서 장바구니에 물건을 담았다고 해보자. 그 뒤 다시 장바구니 페이지를 열었을 때, 이전에 담은 상품이 반영되어 있어야 한다. 하지만 HTTP 프로토콜만으로는 “이 사용자가 방금 전에 장바구니에 물건을 담았다”는 사실을 알 길이 없다. 서버는 새로운 요청이 들어올 때마다 단순히 그 순간의 요청 데이터만 보고 응답할 뿐, 누가 어떤 상태를 유지하고 있는지는 구분할 수 없다.
따라서 상태를 유지하기 위해서는 서버가 별도의 장치(세션 저장소, 데이터베이스, 쿠키, 토큰 등)를 활용해 요청과 요청을 연결할 수 있는 수단을 마련해야 한다. 여기서 등장하는 개념이 바로 인증(Authentication)이다.
인증이란 무엇인가?
인증은 말 그대로 서버와 클라이언트가 서로를 식별하는 절차를 의미한다.
즉, 클라이언트가 “나는 누구다”라고 주장할 때, 서버가 그것이 진짜인지 확인할 수 있어야 한다.
사용자가 로그인하면 서버는 그 사용자를 구분할 수 있는 식별자(세션ID, 토큰 등)를 발급한다.
이후 클라이언트는 새로운 요청을 보낼 때마다 이 식별자를 함께 전달한다.
서버는 이를 확인함으로써 “아, 이 요청은 앞에서 로그인한 그 사용자구나” 하고 신원을 이어서 파악할 수 있다.
이 과정을 통해 서버는 단순히 독립된 요청을 처리하는 것을 넘어, 연속된 사용자 경험(로그인 유지, 장바구니 상태 반영, 권한 확인 등)을 제공할 수 있게 된다.
인증된 상태를 유지하는 수단
보통 인증은 로그인, MFA, OTP, 하드웨어 토큰 등 강력하지만 복잡한 알고리즘을 사용하기에 무거운 1차적인 인증과, 아래의 나열한 한번 인증된 상태를 유지하여 HTTP 통신에 대한 인증된 상태를 유지하는 인증이 있다. 해당 포스트는 상태 유지와 관련된 인증에 대해서 소개한다.
Cookie
쿠키란 서버가 브라우저에 사용자에 대한 정보를 넣을 수 있는 수단으로, 응답을 보낼 때 쿠키를 설정하면 브라우저는 이를 저장하고 앞으로 해당 서버와 통신을 수행할 때 항상 쿠키를 같이 붙여서 보낸다. 서버와 클라이언트의 상태관리를 브라우저가 서버별로 저장하는 것이다.
Token TODO
토큰이란 문자열로 서버가 사용자를 인증하기 위해 특정 문자열을 통해 사용자를 인증하는 방식을 말한다. 브라우저가 존재하지 않는 Android나 IOS 등은 쿠키를 사용할 수 없기에 이러한 토큰을 통해 인증을 수행한다.
Session TODO
세션은 서버가 세션 DB라는 별도의 데이터베이스를 생성하여, 브라우저에 쿠키 등을 통해 세션 ID를 저장하게 해 접속을 수행하게 하는 방법으로 서버가 각 사용자에 대한 모든 인증 정보를 세션 DB에 저장하기에 모든 리퀘스트가 들어올 때마다 탐색하는 자원이 필요하지만 유저의 접속을 종료시키는 등의 추가적인 기능을 개발할 수 있게 해준다.
JWT
세션의 탐색 비용 및 추가적인 DB의 필요성이라는 단점을 해결하기 위한 방법론으로, 유저의 ID를 기반으로 서버가 Sign을 수행하여 JWT라는 토큰을 생성하여 유저에게 넘겨주고, 유저가 JWT를 기반으로 인증을 수행하는 방법이다. 서버가 해야할 일을 토큰의 유효성만 판단하면 되기에 비용이 값싸지만 유저의 계정 접속 종료등 유저를 관리하기 위한 구분을 수행할 수 없기에 추가적인 기능의 개발은 힘들다. (Redis 등의 세션 DB를 위한 값싼 DB를 사용)
Security(보안)
CORS
브라우저는 Origin이 다른 출처에 대한 접근을 원칙적으로 차단한다. 이러한 정책으로 인해 실제 서버를 구성할 때 오류가 나는 경우가 많은데, 이러한 CORS Error의 CORS에 대해서 자세하게 알아보자.
HTTPS
SSL 인증서
웹에 대하여
-
CORS
Cross Origin Resource Sharing(CORS)
우리가 API요청을 보내다보면 자주 CORS Error라는 표현을 들어볼 수 있다. 각종 통신을 수행할 때 오류를 쉽게 걸리게 하는 머리가 아픈 녀석이다. 그렇다면 브라우저는 왜 이런 정책을 사용하고 우린 어떻게 이런 Error를 해결할 수 있을까?
SOP 정책
SOP란?
사실 실제 오류가 걸리게하는 정책은 CORS라 불리는 것이 아닌 SOP(Same Origin Policy)이다. 브라우저는 기본적으로 Origin이 다른 곳에 요청을 보낼 때 해당 요청을 차단하는 정책을 기본값으로 가지고 있다. 그렇다면 여기서 Origin이 정확히 무엇일까?
http://front.com:80/pages/10?search=good 이런 URL이 있다고 가정해자
프로토콜은 http
host주소는 front.com
포트 번호는 80
이 3가지 요소를 합쳐져서 Origin이라고 불린다.
기본적으로 브라우저는 우리가 이런 Origin이 다른 곳에 요청을 보낼 때 요청을 차단을 수행하지만, 실제 우리가 사용하는 환경은 다른 출처에 요청을 보내야하는 경우가 존재하기에, 이러한 정책을 통과시켜주는 방법론이 CORS이다. 즉 브라우저는 너가 다른 Origin으로 요청을 보내고 싶으면 CORS를 통해 허용을 하라고 표현해주는 것이다.
왜 SOP가 필요할까?
아래와 같은 시나리오를 생각해보자.
무수한 CORS 오류를 겪은 철수는 이 에러에 진절머리가나 SOP 정책을 없애리도록, 자신이 만든 사이트에
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
처럼 허용을 해버렸다.
영희는 철수에 사이트를 가입하고, 플래시 게임을 하려 evil.com에 접속을 한다. 그런데 evil.com 관리자는 철수의 동료라 SOP를 무시하는 코드를 실행시켰다는 것을 알아 아래와 같은 코드를 넣어두었다.
<script>
fetch('http://철수.com').then(/**/) // 이후 공격자에게 전송
</script>
영희의 브라우저는 요청을 받았을 때, Source와 fetch의 요청되는 사이트의 Origin이 다르다는 사실을 확인했지만, 철수의 사이트는 어떤 사이트가 출처든 허용되는 설정을 해버렸다.
공격자는 영희가 evil.com에 접속했을 때 철수의 사이트에 화면을 그대로 볼 수 있게 되버렸고, 영희의 해당 사이트의 개인정보가 탈취되었다…
즉 SOP 정책을 통해서 Origin이 다른 사이트의 fetch를 브라우저가 자체적으로 차단함으로써, 해당 사이트의 정보를 다른 사이트가 탈취할 수 없도록 만드는 것이 해당 정책이 있는 이유이다. 즉 사용자가 악성 사이트에 들어가더라도, 다른 사이트의 정보, 토큰, 세션 ID 등이 탈취되지 않도록 보호하는 정책이다.
CORS
CORS란?
현대 웹에서는 특정 사이트가 다른 사이트의 데이터를 활용해야 하는 경우가 많다. 예를 들어, 프론트엔드에서 자체 백엔드 서버로 요청을 보내거나, 공공 데이터 포털처럼 외부에서 제공하는 API에 접근하는 경우가 있다. 하지만 이러한 요청은 대체로 서로 다른 Origin을 가지므로, 브라우저의 SOP 정책에 의해 기본적으로 차단된다.
이를 해결하기 위해 등장한 것이 CORS(Cross-Origin Resource Sharing) 정책이다.
예를 들어, 프론트엔드에서 fetch 명령으로 어떤 백엔드 서버에 데이터를 요청한다고 하자. 이때 프론트엔드의 URL과 백엔드 서버의 URL이 서로 다른 Origin을 가지면, 브라우저는 SOP 규칙에 따라 해당 요청을 차단한다.
그러나 만약 백엔드 서버가 CORS 설정을 통해 프론트엔드 서버의 Origin을 허용한다면, 브라우저는 이 요청을 정상적으로 실행할 수 있다. 그 결과, 사용자는 브라우저를 통해 해당 백엔드 서버의 데이터에 접근할 수 있게 된다.
즉, CORS라는 것은 미리 특정 사이트에 대한 사용자가 접근할 수 있는 페이지나, 정보를 다른 사이트가 봐도 된다고 허용을 해주는 것이다. 또한 이런 허용은 브라우저에서 설정하는 것이 아닌 특정 사이트(즉 백엔드 서버나, API 서버 등등)에서 허용을 해주는 것이다.
CORS의 세가지 시나리오
JavaScript, Browser, API Server의 통신은 위와 같은 그림으로 나타낼 수 있다. 여기서 빨간 부분이 CORS의 시나리오에 해당하는 부분이다. 각 동작에 따라 CORS는 크게 3가지의 시나리오로 나누어서 판단을 수행한다.
Simple Request
Simple Request는 Get과 Post 같은 요청을 보낼 때 수행되며, 요청을 보내는 걸 별도의 인증 없이 바로 수행할 수 있으며, 만약 인증이 수행되지 않더라도 응답에 대한 데이터를 JavaScript가 못받는 것 뿐이다. 해당 요청은 Access-Control-Allow-Origin에 Origin이 포함만 되어있다면, 별도의 요청없이 바로 수행된다.
Preflight Request
HTTPS의 특별한 메소드인 Option 메소드를 활용하며 예비 요청을 사전에 보낸다. 이 예비 요청의 역할은 Origin과 Access-Control-Allow-Origin을 비교해주어 Origin이 CORS에 등록되어있으면 200을 보내는 역할이다. 그 후 해당 요청을 통과하면 본 요청을 보내주는 역할이다. 대부분의 요청은 Preflight Request로 검증을 수행한다.
CORS가 생기기 이전 SOP만 가능하다는 가정하에 만들어진 서버들이 CORS 처리 매커니즘이 있는지 확인해주기 위해 Preflight Request를 통해 브라우저가 사전에 확인을 해줌으로써, 해당 사항을 처리할 수 있는 서버만 해당 동작이 수행될 수 있도록 만든 것이다.
Credentitaled Request
Cookie나 Session에 대한 정보가 담긴 HTTPS 요청이 들어올 때 Credentitaled Request가 수행된다. 기본적인 매커니즘은 Origin을 검사하는 Preflight Request와 동일하지만, 해당 Request를 수행할 때 Access-Control-Allow-Origin이 Wild Card : * 로 설정되어있을 때 브라우저가 자동으로 차단하며, Access-Control-Allow-Credentials가 true일 때만 해당 요청을 수락하는 추가적인 보안 사항이 존재한다.
CORS와 개발환경
CORS는 기본적으로 Origin이 사전에 허용되어있거나, 동일해야지만 브라우저가 fetch를 허용해준다. 하지만 여기서 문제가 발생한다. 개발환경인 경우 모든 개발자의 컴퓨터를 어딘가에 배포한 것이 아닌 각자 컴퓨터에서 프론트엔드를 실행하기에 Origin을 하나로 고정할 수 없다는 문제가 발생한다.
또한 리다이렉트 등의 문제가 발생하면, 기존의 허용해두었던 Origin이 변경되어 무수한 CORS 에러를 만나게 되었다.
근본적인 문제는 각 개발환경은 ip가 모두 다른 것이다. 그렇기에 FE 개발환경과 백엔드 서버를 CORS 오류가 없이 연결하기 위해서는, localhost:3000과 같은 특정 도메인을 프론트엔드 개발환경에서 만드는 브라우저가 인식하는 Origin을 모든 개발환경의 개발자들이 통일할 필요성이 있고, 통일한 Origin을 백엔드에서 allow origin으로 허용해주어야 SOP 정책을 통과할 수 있다.
프론트엔드와 백엔드를 연결하는 과정에서 인증과 CORS 정책이 겹쳐 해결한 방식을 아래에 포스트에 정리하였다.
```
-
Web의 개요
개요
프론트 엔드 뷰어와, 백엔드 서버를 결합하는 작업을 수행하면서 CORS 오류로 인한 통신후 Body를 못보는 문제, 분명히 통신과 쿠키는 생성되었는데 쿠키를 읽어드리지 못하는 문제 등, 통신 자체는 성공했는데 웹의 자체적인 보안 및 시스템으로 인해 버그가 자주 발생하였다. 웹에 대한 지식이 없이 이런 버그를 고치는 것은 시행착오가 많을 것 같아, cs-note 섹션에 해당 정보들을 정리하려 한다.
웹에 대하여
초창기 웹은 단순히 URI(Uniform Resource Identifier) 을 통해 클라이언트에 리소스를 보내주고, HTML로 규정된 문서 규칙을 통해 문서끼리, 다른 문서를 쉽게 링크를 통해 가져올 수 있는 구조였다. 하지만 웹서버가 발전하며, 웹 서버는 기존의 서버에서, 클라이언트로 HTML 문서를 보내주는 것을 넘어서, 동적으로 움직이고 디자인이 가능한 문서의 송수신, 자원의 송수신을 넘어선 로직의 실행, 클라이언트와 서버 간의 상태의 저장 등 더욱 다양한 역할을 수행해주게 발전되었다.
해당 문서는 이러한 웹에 발전에 따라, 웹에서 구동되는 제품이 구현되기 위한 백엔드의 구성 요소와 도움이 되는 방법론 등을 정리하는 문서이다.
어떻게 통신할 것인가?
TODO Resource의 송수신
초창기 웹의 주요 역할은 서버에 저장된 리소스(HTML 문서, 이미지·동영상·오디오 같은 미디어 파일, 데이터 파일 등)를 URI를 통해 탐색하고, 이를 클라이언트로 전달하는 것이었다. 이후 웹은 단순한 파일 전송과 링크 연결을 넘어, HTML로 문서를 표현하고, CSS로 시각적 디자인을 더하며, JavaScript로 동적 기능을 구현하는 방향으로 발전하였다. 최근에는 단순한 정적 파일 전송을 넘어, XML·JSON과 같은 데이터 포맷을 송수신하여 브라우저가 직접 HTML을 생성하거나 동적 데이터 처리를 수행한다. 이러한 내용을 정리해보자.
정적 리소스
HTML (문서 구조)
하이퍼링크(URI 기반 파일 연결)
이미지·미디어 파일 전송
문서의 표현력 강화 & 동적 요소
CSS (스타일·디자인)
JavaScript (동적 상호작용)
현대 웹 (데이터 송수신 중심)
XML, JSON (데이터 교환 포맷)
AJAX (비동기 데이터 요청)
TODO 표준화된 데이터 송수신 방식
웹이 단순한 문서 전송을 넘어 다양한 데이터 교환을 필요로 하게 되면서, 서버와 클라이언트 간의 데이터 송수신을 표준화하는 프로토콜이 등장하였다. 이러한 프로토콜은 웹 서비스가 확장될수록 일관성 있는 데이터 접근과 통신 효율성을 보장하는 핵심 역할을 한다.
다룰 내용 예시 : RestAPI, GraphQL, gRPC
클라이언트와 서버의 상태관리
웹이 발전하면서 로그인, 장바구니 같은 기능을 제공하기 위해 서버와 클라이언트는 서로의 상태를 유지할 필요가 생겼다. 이를 위해 상태 관리와 인증 방식이 사용되며, 만약 인증 정보가 유출되면 다른 사용자가 이를 도용해 사칭할 수 있다. 따라서 안전한 인증과 보안 기능이 필수적이다.
인증(Authentication) 대표 기술
세션(Session) + 쿠키(Cookie)
토큰 기반 인증(JWT, OAuth2)
다중 인증(MFA, OTP)
보안(Security) 대표 기술
HTTPS/TLS(데이터 암호화 전송)
CSRF/XSS 방어 기법
세션 하이재킹 방지(만료시간, HttpOnly, Secure 옵션 등)
어떻게 배포환경을 파악할 것인가?
어떻게 WAS를 관리할 것인가?
데이터베이스 최적화
##
참고자료
용어설명
URI
URI(Uniform Resource Identifier)란 인터넷에 있는 자원을 어디에 있는지 자원 자체를 식별하는 방법이다. 우리가 어떠한 자원을 식별할 때는 그 자원이 어디에 있는가? 혹은 그 자원을 뭐라고 하는가? 2가지 방법을 통해 식별을 할 수 있다. 이들이 각각 URL(Uniform Resource Locator)와 URN(Uniform Resource Name)이다.
예시 URI : https://example.com:8080/articles/index.html?search=chatgpt#intro
Type
Context
Schema
https://
Host
example.com
Port
8080
Path
/articles/index.html
Query
?search=chatgpt
Fragment
#intro
우리는 Host + Port + Path를 통해 어떠한 자원이 어느 서버의 어느 위치에 있는지를 알아낼 수 있으며, 이런게 URL이다. 이런 자원이 만약 어디에서 접근하든 고유한 이름으로 구분 가능하면 URN이다. URI는 이 모든 개념을 포함하며, Fragment처럼 자원의 위치만이 아닌 해당 자원 내부를 가르키는 특정 지점에 대한 정보까지도 URI는 포함할 수 있다.
참고링크
웹 개발자가 봐야할 하나의 지도 강의
-
-
-
프로그래밍의 기본 요소 설명
개요
출처: 쉬운코드 영상
객체와 클래스
객체와 클래스
객체란?
=> 상태가 있고 행동을 하는 실체
클래스란?
=> 객체의 관점에서의 클래스는, 객체가 어떠한 속성이 있고 어떠한 행동을 하는지를 기술한 설계도이다.
// example1
class Car {
private String name;
private double speed;
private Size size;
...
public void start() { ... }
public void stop() { ... }
...
}
Car myCar = new Car("니로");
Car yourCar = new Car("소나타");
Car ourCar = new Car("스포티지");
Class Car는 Car라는 객체들이 어떠한 상태(name, speed, size)를 가질 수 있고, 어떠한 행동을 할 수 있는지를 설명한다.
또한 이런 Class로 myCar, yourCar, outCar 각 객체는 선언되며, 속성은 모두 동일하지만 각 객체가 가질 수 있는 상태는 모두 다를 수 있다!
// example2
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int get() {
return count;
}
}
Counter appleCounter = new Counter();
Counter orangeCounter = new Counter();
카운터는 어떤 것을 세는 것을 정의한 클래스
내부적으로 count(개수라는 상태), 카운트를 증가시키는 행동, 숫자를 센 값을 가져오는 행동을 기술
instantiate란?
new라는 키워드처럼 객체화시켜서 만든 객체(object)를 instance라고 말한다.
// example3
class Switch {
private int state = 0;
public void on() {
this.state = 1;
}
public void off() {
this.state = 0;
}
public boolean isOn() {
return this.state == 1;
}
}
Switch tvSwitch = new Switch();
내가 원하는 속성, 행동을 구체적으로 기술하고 이를 실체화한 것이 객체이다.
이러한 클래스와 객체의 개념은 현실세계를 효율적으로 프로그래밍으로 옮길 수 있게 된다.
코드의 재사용성과 확장성
클래스를 만들면 반복해서 객체를 생성할 수 있음 -> 생산성과 유지보수 용이
데이터와 행동을 함께 묶음
클래스는 속성(데이터)과 기능(메서드)를 하나로 묶음으로써 큰 시스템을 설계할 떄 클래스 단위로 나누면 역할 분담이 쉽다.
객체 지향 프로그래밍(OOP)의 기반
OOP는 캡슐화, 상속, 다형성을 통해 유연하고 강력한 프로그램을 만듦
클래스와 객체가 없다면 이러한 구조적 설계가 불가능
변수와 값
변수란 값을 담을 수 있는 이름이 있는 그릇!
변수는 어떠한 객체가 있는 주소나, 실제 값이 담긴다. ex) 1, “안녕”, Object(“곡괭이”)
변수는 값이 바뀔 수 있다.
클린코드를 위해서는 변수를 꼭꼭꼭 모르는 사람이 봐도 이해하기 쉽게 적자!
함수
아래에 2개 중 무엇이 함수인가?
class Add:
def add(a, b):
return a + b
vs
def add(a, b):
return a + b
함수란?
독립적으로 존재하며 임무(task)를 수행하는 코드들의 집합
함수 이름으로 호출한다.
매개변수를 받을 수도 않을 수도 있다.
결과 값을 리턴할 수도 안할 수도 있다
재사용이 가능하다
매서드란?
객체 혹은 클래스에 종속되어 임무를 수행하는 코드들의 집합
클래스나 객체의 상태 정보에 접근 가능
매소드는 객체의 상태에 영향을 받기에, 같은 클래스로 선언된 메소드더라도, 객체 혹은 클래스에 종속되어 임무를 수행한다!
그래서 질문에 대한 대답은, 객체의 상태에 영향을 받는 클래스의 add는 메소드, 그냥 add는 함수이다!
변수와 객체는 메모리에 어떻게 저장되는가?
어플리케이션은 어떻게 실행되는가?
어플리케이션은 일반 사용자가 사용할 기능을 제공하는 컴퓨터가 실행할 수 있는 명령어들의 집합
메모리는 실행된 애플리케이션이 상주하는 곳 => 어플리케이션이 메모리에 있어야 실행이 가능하다.
public class Main {
public static void main(String[] args){
int a = 7;
int b = 3;
int c = a + b;
}
}
a = 7이라는 명령을 cpu에서 실행하고, 이를 메모리에 7이란 값을 올리고, 그 곳에 a라는 이름을 붙임
b = 3이라는 명령을 cpu에서 실행하고, 이를 메모리에 3이란 값을 올리고, 그 곳에 a라는 이름을 붙임
…
이런 식의 명령과 저장이 계속되는 것.
runtime => application이 메모리에 올려져서 실행되고 있는 순간을 말한다.
사실, 이런 변수와 변수의 연산과정만이 아닌, 함수도 메모리에 저장된다.
메모리 구조
애플리케이션에 할당되는 메모리는 stack 메모리와 heap 메모리 등의 여러 영역으로 나눠진다.
| 메모리 영역 | 역할 | 주의 점|
| -------- | --------- | --------- |
| Stack | 함수나 메서드의 지역 변수와 매개 변수가 저장되는 곳, 함수나 메서도가 호출될 때도 스택 프레임이 쌓인다. | 스택 프레임은 개발자가 이를 신경 쓸 필요는 보통 없지만, 함수를 재귀적으로 많이 호출하여 스택 메모리 이상을 저장하게 되면 문제가 발생한다.|
| Heap | 객체가 저장되는 공간 | |

// example
public class Main {
public static void main(String[] args) { // 매개변수
Counter c = new Counter(); // 객체, 지역 변수
}
}
public class Counter {
private int state = 0; //상태를 나타내는 instance 변수
public void increment() {
state++;
}
public int get() {
return state;
}
}
3가지 변수가 존재!
객체가 클래스로 부터 생성될 때 생성자가 실행되는 스택프레임이 먼저 생성되고, 이것이 힙에 객체를 생성한 후, 스택프레임이 사라진다. 이때 생성자 스택 프레임에서 this라는 보이지 않는 변수가 힙 주소를 저장하였다가 반환한다.
그리고 해당 stack의 지역변수로 객체가 선언되었으므로, 스택 프레임에 방금 생성된 객체의 heap 주소가 저장된다.
매서드 역시도 stackframe을 통해 생성되며, 매서드는 객체에 종속되어있는데 이 정보가 this를 통해 어떤 객체를 가리키도록 생성된다.
호출된 함수나 매서드는 파라매터로 해당 객체의 주소를 전달받아 상태를 변경시키면, 해당 함수나 매서드가 종료되어 스택메모리에서 사라지더라도 변화된 정보는 힙 메모리에서 유지된다.
쓰레기 객체(garbage object)
public class Main {
public static void main(String[] args) {
Counter c = make();
}
public static Counter make() {
Counter c = new Counter(); // => 새로운 객체를 heap에 생성 그러나 이를 전달해주지 않고
return new Counter(); // => 새로운 객체를 heap에 생성해 전달
}
}
public class Counter {
private int state = 0;
public void increment() {
state++;
}
public int get() {
return state;
}
}
위의 예시처럼 접근할 수 없는 객체가 생겨버리면, 쓸모없는 객체가 heap 메모리를 차지하게 된다. 이런 객체를 쓰레기 객체라고 한다.
이런 것을 삭제해주기 위해 2가지 방법이 있다.
개발자가 직접 해당 객체를 지정해 메모리를 해제
gc(garbage collector)를 지원하는 언어에서 이러한 객체를 자동으로 삭제
파이썬은 모든 것이 객체이기 때문에
def wow(num):
print(num)
a = 1
wow(a)
파이썬은 모든 것을 객체로 저장하기 위해 스택에 올릴 때 글로벌 프레임을 사용한다.
Touch background to close