상황 - 오류 없는 빈 값 조회

현재 개인 사이드 프로젝트에서 Supabse를 사용해서 백엔드를 구성하고 있다. 사용자 프로필 정보를 담는 profiles 테이블을 만들었다. 이 propfiles 테이블의 데이터를 조회할 때, 본인꺼만 조회할 수 있도록 RLS 정책을 지정해 놓았다. 개발 중 웹훅 수신 로직에서 사용자 이메일 값을 기반으로 profile에서 사용자 id를 파싱 해야하는 상황이 생겼다. 이때 profiles에 접근해서 응답을 보니 항상 빈 배열 값이 나오는 문제가 생겼다.

원인 분석

  • Supabse의 profiles 테이블에 RLS(행 수준 보안)가 적용되어 있다.
  • RLS 정책이 "본인(로그인된 유저)만 자신의 profile을 select 할 수 있다."로 되어 있으면, 서버에서 웹훅(즉, 외부 시스템)이 호출할 때는 인증된 유저가 없으므로, 어떤 profile도 조회할 수 없다.

해결 방법

1. Service Role Key 사용

  • Supabse의 Service Role Key(서버 전용 시크릿 키)를 사용하면, RLS를 무시하고 모든 데이터에 접근이 가능했다.
  • 현재는 Service Role Key를 사용하지 않는 supabse 객체를 클라이언트, 서버단 2개로 나누어서 사용 중이다.
  • 이 방식은 서버에서만 사용해야 하며, 클라이언트에서는 절대 노출하면 안 된다.
import { supabaseAdmin } from '@/lib/supabase-server'; // Service Role Key 사용

const { data: userProfile } = await supabaseAdmin
  .from('profiles')
  .select('id')
  .eq('email', attr.user_email)
  .single();

2. 웹훅 전용 RLS 정책 추가

  • 만약 Service Role Key를 사용하지 않고, 일반 API Key로만 접근해야 한다면,
  • 웹훅에서 오는 요청의 IP나 특정 헤더, 혹은 별도의 인증 토큰을 기반으로 RLS 정책을 완화할 수 있다.
  • 하지만, Service Role Key를 사용하는 것이 더 쉽고 안전하다.

결론

  • 웹훅 등 서버에서 RLS를 우회해야 할 때는 Service Role Key를 사용하는 supabase client로 쿼리하기(쉽게 말해, 관리자용 supabse client)
  • 모든 곳에서 Service Role Key를 사용하는 supabase client로 하지말고, 필요한 부분에서만 사용하기

적용 예시

import { supabaseAdmin } from '@/lib/supabase-server'; // Service Role Key 사용

let userId = '';
if (attr.user_email) {
  const { data: userProfile } = await supabaseAdmin
    .from('profiles')
    .select('id')
    .eq('email', attr.user_email)
    .single();
  userId = userProfile?.id ?? '';
}
반응형

개념

CORS는 Cross-Origin Resource Sharing의 약자로, 서로 다른 출처 간의 리소스 요청을 제어하기 위한 보안 기능이다. 브라우저는 보안상의 이유로 스크립트가 다른 도메인에 있는 리소스에 접근하는 것을 기본적으로 차단한다. 이 기본적인 보안 제한을 ‘동일 출처 정책(Same-Origin Policy)’라고 한다.

CORS는 이러한 동일 출처 정책의 제한을 우회할 수 있도록 서버에서 명시적으로 설정해, 브라우저가 다른 도메인에서 리소스를 요청하는 것을 허용하게 만드는 방법이다.

  • 출처(Origin): 도메인, 프로토콜, 포트를 포함한 것을 의미한다. 예를 들어, http://example.com:3000https://example.com:3000은 서로 다른 출처다.
  • 동일 출처 정책 (Same-Origin Policy): 웹 페이지가 자신과 다른 출처의 리소스를 로드하는 것을 제한하는 브라우저의 보안 정책이다.

 

원인

클라이언트가 웹 서버에 다른 출처의 리소스에 접근하려고 할 때 발생한다. 브라우저가 서버에 요청을 보내고, 서버가 올바른 CORS 헤더를 포함하지 않으면 브라우저에서 요청이 차단된다.

  • 예를 들어, 웹 애플리케이션의 클라이언트가 http://localhost:3000에서 실행 중이고, http://api.example.com으로 API 요청을 보내려고 할 때, 출처가 다르기 때문에 브라우저는 보안 정책에 의해 요청을 차단할 수 있다.

 

동작 방식

CORS 동작 방식은 브라우저와 서버 간의 HTTP 요청 및 응답 헤더를 통해, 서로 다른 출처에서 리소스를 공유하는 과정을 제어하는 것이다.

1. Simple Request(단순 요청)

특정 조건을 만족할 때 preflight 요청을 보내지 않고 바로 요청이 이루어진다. 특정 조건은 아래와 같다.

  • HTTP 메서드는 GET, POST, 또는 HEAD 여야 한다.
  • Content-Type 헤더가 다음과 같은 POST 요청
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 헤더는 브라우저가 자동으로 추가하는 안전한 헤더여야 하며, 여기에는 다음과 같은 제한이 있다.
    • Accept, Accept-Language, Content-Language, Content-Type (application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나).

단순 요청에서는 브라우저가 요청을 보낼 때 Origin 헤더를 추가한다. 서버는 응답 시 해당 요청을 허용할지 여부를 결정하고, 다음과 같은 CORS 관련 헤더를 응답에 포함한다.

  • Access-Control-Allow-Origin: 요청을 허용할 출처를 지정( * 또는 특정 도메인).
  • Access-Control-Expose-Headers (선택적): 클라이언트에서 접근 가능한 응답 헤더 목록을 지정.

브라우저는 서버의 응답에 Access-Control-Allow-Origin 헤더가 포함되어 있는지 확인하고, 클라이언트가 해당 출처의 리소스에 접근하는 것을 허용할지 결정한다.

2. Preflight Request(사전 요청)

브라우저가 보안 확인을 위해 실제 요청을 보내기 전에 서버에 미리 허락을 구하는 요청이다. 주로 민감한 요청에 대해 사용되며, 다음과 같은 경우에 발생한다.

  • 요청 메서드가 GET, POST, HEAD가 아닌 다른 메서드인 경우 (PUT, DELETE, OPTIONS 등)
  • 요청 헤더가 안전하지 않은 커스텀 헤더를 포함하는 경우 (예: Authorization, Content-Type이 JSON 등)
  • 요청에 Credentials (쿠키, 인증 토큰 등)이 포함된 경우

프리플라이트 요청은 OPTIONS 메서드를 사용해 서버에 보내진다. 이 요청은 브라우저가 실제 요청을 보내도 안전한지 확인하기 위해 서버에게 허락을 구하는 과정이다. 프리플라이트 요청에서는 다음과 같은 헤더가 사용된다.

Request Header(브라우저 → 서버)

  • Origin: 요청을 보낸 출처.
  • Access-Control-Request-Method: 실제 요청에서 사용하려는 HTTP 메서드.
  • Access-Control-Request-Headers (선택적): 실제 요청에서 사용될 추가 헤더 목록.

서버는 이러한 프리플라이트 요청에 대해 다음과 같은 응답을 보내야 한다.

Response Header(서버 → 브라우저)

  • Access-Control-Allow-Origin: 허용된 출처(* 또는 특정 도메인)
  • Access-Control-Allow-Methods: 클라이언트가 사용할 수 있는 HTTP 메서드 목록 (GET, POST, PUT 등).
  • Access-Control-Allow-Headers: 허용된 헤더 목록 (예: Authorization, Content-Type).
  • Access-Control-Max-Age (선택적): 클라이언트가 프리플라이트 응답을 캐시할 시간 (초 단위).
  • Access-Control-Allow-Credentials: 클라이언트가 쿠키를 전송할 수 있도록 허용

서버가 프리플라이트 요청에 대해 성공적으로 응답하면, 브라우저는 실제 요청을 서버로 전송하게 된다!



CORS 요청 예시

브라우저가 CORS 요청을 보내고, 서버가 응답하는 예시다.

1. Simple Request 예시

  • 클라이언트가 GET 요청을 보내는 경우
    • 여기서 브라우저는 응답에 포함된 Access-Control-Allow-Origin 헤더를 확인하여 클라이언트에서 이 데이터를 사용할 수 있게 한다.
// 요청(client -> Server)
GET /api/data HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000

// 응답(Server -> Client)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json

{
  "message": "CORS 요청 성공"
}

2. Preflight Request 예시

// Preflight 요청(client -> Server)
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

// Preflight 응답(Server -> Client)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
  • Access-Control-Allow-Origin: 해당 요청을 허용할 출처를 명시 (http://localhost:3000).
  • Access-Control-Allow-Methods: 허용 가능한 메서드(GET, POST, PUT, DELETE).
  • Access-Control-Allow-Headers: Authorization 헤더가 허용됨을 명시.
  • Access-Control-Max-Age: 86400초 동안 프리플라이트 응답을 캐시함.

Preflight 요청에 대한 성공적인 응답을 받은 후, 클라이언트는 실제 요청을 보낸다.

// 실제 요청(client -> Server)
DELETE /api/data HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Authorization: Bearer token123

// 실제 응답(Server -> Client)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json

{
  "message": "리소스가 삭제되었습니다"
}

 

CORS 관련 주요 HTTP 헤더 요약

  1. Access-Control-Allow-Origin: 허용할 출처를 지정 (은 모든 출처를 허용).
  2. Access-Control-Allow-Methods: 클라이언트가 사용할 수 있는 HTTP 메서드를 지정.
  3. Access-Control-Allow-Headers: 클라이언트가 사용할 수 있는 추가 헤더를 지정.
  4. Access-Control-Allow-Credentials: 클라이언트가 쿠키나 인증 정보를 서버로 전송할 수 있도록 허용.
  5. Access-Control-Max-Age: 프리플라이트 요청의 응답을 캐시할 수 있는 시간을 지정 (초 단위).
  6. Access-Control-Expose-Headers: 클라이언트에서 접근 가능한 응답 헤더를 지정.

 

해결 방법

1.서버 측에서 CORS 설정

  • 서버가 올바른 CORS 헤더를 포함하도록 설정하는 것이 가장 일반적인 해결 방법이다.
  • 예를 들어, Node.js/Express 서버에서는 다음과 같이 설정할 수 있다.
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors()); // 모든 출처에 대해 CORS 허용
// 또는 특정 출처만 허용하고 싶다면:
// app.use(cors({ origin: 'http://localhost:3000' }));

app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS 설정 성공' });
});

app.listen(5000, () => {
  console.log('Server is running on port 5000');
});

2.프록시 서버 사용

  • 클라이언트와 서버 간에 프록시 서버를 두어 요청을 중계함으로써 동일 출처 정책을 회피하는 방법이다.
  • 개발 환겨에서 주로 사용되며, 예를 들어 React에서는 package.json 에 proxy 설정을 추가해 사용할 수 있다.
{
  "proxy": "http://api.example.com"
}

3.브라우저 확장 프로그램 사용(개발 환경)

  • Chrome 등 브라우저에서 제공하는 CORS 해제 확장 프로그램을 사용해 에러를 일시적으로 해결할 수 있다.
  • 하지만, 이 방법은 개발 환경에서만 사용하는 것이 좋다. 보안상 이유로 운영 환경에서는 권장되지 않는다!

4.서버 설정 변경

  • Apache 또는 Nginx 같은 웹 서버를 사용하는 경우, 서버 설정 파일에서 CORS 헤더를 추가해야 한다.
  • Nginx의 경우
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

Access-Control-Allow-Credentials 사용 시 주의

  • 클라이언트가 쿠키 또는 인증 정보를 서버로 보내기 위해서는 Access-Control-Allow-Credentials: true를 설정하고, Access-Control-Allow-Origin* 대신 특정 출처를 명시해야 한다.


정리

  • CORS는 브라우저 보안 정책으로 인해 발생하는 문제로, 클라이언트가 다른 출처의 리소스에 접근하려고 할 때 발생한다.
  • 서버에서 CORS 헤더를 적절히 설정함으로써 이를 해결할 수 있다.
  • 프록시 서버 사용이나 브라우저 확장 프로그램 같은 해결책이 있지만, 이는 개발 환경에서 주로 사용되며, 운영 환경에서는 서버에서 CORS 정책을 설정하는 것이 가장 바람직한 방법이다.
반응형

https://www.acmicpc.net/problem/9375

 

import sys

input = sys.stdin.readline

N = int(input().rstrip())
for _ in range(N):
    M = int(input().rstrip())
    clothes = {}

    for _ in range(M):
        (item, cloth_type) = list(input().rstrip().split())
        clothes[cloth_type] = clothes.get(cloth_type, 0) + 1

    result = 1
    for count in clothes.values():
        result *= (count + 1) #해당 종류의 의상을 선택하지 않는 경우 포함

    print(result - 1) # 아무 옷도 안 입는 경우 제외

 

의상 타입별 고르지 않는 경우도 카운트하는게 중요하다.

반응형

https://www.acmicpc.net/problem/11478

 

 

1번째 풀이 - O(N^2)

import sys

input = sys.stdin.readline

S = input().rstrip()

for i in range(len(S)):
    if S[i] in result:
        result[S[i]] += 1
    else:
        result[S[i]] = 1

    if i == len(S) - 1:
        break

    current_string = S[i]

    for j in range(i + 1, len(S)):
        current_string = current_string + S[j]
        if current_string in result:
            result[current_string] += 1
        else:
            result[current_string] = 1

print(len(result))

 

시간복잡도 O(n^2)이 걸리는 풀이로 작성했다. 해당 코드를 python의 set 자료형을 활용하면 간단하게 작성할 수 있다. 

 


python set() 활용 풀이 - O(N^2)

import sys

input = sys.stdin.readline

S = input().rstrip()

result = set()

for i in range(len(S)):
    for j in range(i + 1, len(S) + 1):
        result.add(S[i:j])
print(len(result))

 

set(집합) 자료형은 집합의 중복을 제거하고 순서를 보장하지 않는 리터럴 객체를 생성한다.

반응형

+ Recent posts