콘솔로 이동

보안 규칙 작동 방식

보안은 앱 개발 과정에서 가장 복잡한 부분일 수 있습니다. 대부분의 애플리케이션에서 개발자는 인증(사용자가 누구인지) 및 승인(사용자가 무엇을 할 수 있는지)을 처리하는 서버를 직접 빌드하여 실행해야 합니다.

Firebase 보안 규칙을 사용하면 중간(서버) 계층을 없애고 데이터에 직접 연결하는 클라이언트에게 경로 기반 권한을 지정할 수 있습니다. 이 가이드를 통해 수신 요청에 규칙을 적용하는 방법에 대해 자세히 알아보세요.

이 규칙에 대해 자세히 알아보려면 제품을 선택하세요.

Cloud Firestore

기본 구조

Cloud Firestore 및 Storage의 Firebase 보안 규칙은 다음 구조와 구문을 사용합니다.

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

다음 핵심 개념은 규칙을 작성할 때 반드시 숙지해야 합니다.

  • 요청: allow 문에서 호출되는 메소드이며, 실행하도록 지정할 수 있는 메소드입니다. 표준 메소드는 다음과 같습니다. get, list, create, update, delete. 편리한 readwrite 메소드를 통해 지정된 데이터베이스 또는 스토리지 경로에서 광범위한 읽기 및 쓰기 액세스가 가능합니다.
  • 경로: URI 경로로 표시되는 데이터베이스 또는 스토리지 위치입니다.
  • 규칙: true로 평가되면 요청을 허용하는 조건을 포함하는 allow 문입니다.

경로 일치

모든 match 문은 컬렉션이 아닌 문서를 가리켜야 합니다. match 명령문은 match /cities/SF와 같이 특정 문서를 가리킬 수도 있고, match /cities/{city}와 같이 와일드 카드를 사용해 지정한 경로의 모든 문서를 가리킬 수도 있습니다.

위의 예에서는 match 명령문에 {city} 와일드 카드 구문이 사용되었습니다. 따라서 cities 컬렉션에 포함된 /cities/SF, /cities/NYC 등의 모든 문서에 규칙이 적용됩니다. match 명령문의 allow 표현식이 평가될 때 city 변수는 SF, NYC 등의 city 문서 이름으로 확인됩니다.

하위 컬렉션 일치

Cloud Firestore의 데이터는 문서 컬렉션으로 정리되며, 각 문서는 하위 컬렉션을 통해 계층구조를 이룰 수 있습니다. 계층적 데이터에 보안 규칙이 어떻게 적용되는지 이해하는 것이 중요합니다.

cities 컬렉션의 각 문서가 landmarks 하위 컬렉션을 포함하는 경우를 예로 들어보겠습니다. 보안 규칙은 일치된 경로에만 적용되므로, cities 컬렉션에 정의된 액세스 제어는 landmarks 하위 컬렉션에 적용되지 않습니다. 따라서 하위 컬렉션의 액세스를 제어하는 명시적 규칙을 작성해야 합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

        // Explicitly define rules for the 'landmarks' subcollection
        match /landmarks/{landmark} {
          allow read, write: if <condition>;
        }
    }
  }
}

match 명령문을 중첩하면 내부 match 명령문의 경로는 항상 외부 match 명령문의 경로를 기준으로 합니다. 따라서 다음 규칙 세트는 서로 동일합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city}/landmarks/{landmark} {
      allow read, write: if <condition>;
    }
  }
}

임의적으로 깊은 계층구조까지 규칙을 적용하려면 재귀 와일드 카드 구문인 {name=**}를 사용합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

재귀 와일드 카드 구문을 사용하면 와일드 카드 변수에 일치하는 경로 세그먼트 전체가 포함되며, 깊이 중첩된 하위 컬렉션에 문서가 위치한 경우에도 마찬가지입니다. 예를 들어 위에 나열한 규칙은 /cities/SF/landmarks/coit_tower에 위치한 문서와 일치하며, 이때 document 변수의 값은 SF/landmarks/coit_tower입니다.

재귀 와일드 카드는 빈 경로와 일치할 수 없으므로 match /cities/{city}/{document=**}cities 컬렉션이 아닌 하위 컬렉션에 속한 문서와 일치합니다. 반면, match /cities/{document=**}cities 컬렉션의 문서 및 하위 컬렉션의 문서와 모두 일치합니다.

문서 하나가 둘 이상의 match 명령문과 일치할 수도 있습니다. 요청과 일치하는 allow 표현식이 여러 개인 경우 조건 중 하나라도 true이면 액세스가 허용됩니다.

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any document in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

위의 예에서는 cities 컬렉션에 대한 읽기와 쓰기가 모두 허용되는데, 그 이유는 첫 번째 규칙은 항상 false이지만 두 번째 규칙이 항상 true이기 때문입니다.

보안 규칙 한도

보안 규칙을 사용할 때 다음 한도에 유의하시기 바랍니다.

한도 세부정보
요청당 최대 exists(), get(), getAfter() 호출 수
  • 단일 문서 요청 및 쿼리 요청의 경우 10입니다.
  • 여러 문서 읽기, 트랜잭션, 일괄 쓰기의 경우 20입니다. 이전의 한도인 10도 각 작업에 적용됩니다.

    예를 들어 세 번의 쓰기 작업으로 일괄 쓰기 요청을 만들고 보안 규칙이 두 번의 문서 액세스 호출을 사용하여 각 쓰기를 검증한다고 가정해 봅니다. 이 경우 각 쓰기는 10번의 액세스 호출 중 2번을 사용하고 일괄 쓰기 요청은 20번의 액세스 호출 중 6번을 사용합니다.

한도 중 하나라도 초과하면 권한 거부 오류가 발생합니다.

일부 문서 액세스 호출은 캐시될 수 있으며 캐시된 호출은 한도에 적용되지 않습니다.

함수 호출 최대 심도 20
재귀 또는 순환 함수 호출의 최대 개수 0 &lpar;허용되지 않음&rpar;
요청당 평가되는 최대 표현 수 1,000
규칙 세트의 최대 크기 64KB

Cloud Storage

기본 구조

Cloud Firestore 및 Storage의 Firebase 보안 규칙은 다음 구조와 구문을 사용합니다.

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

다음 핵심 개념은 규칙을 작성할 때 반드시 숙지해야 합니다.

  • 요청: allow 문에서 호출되는 메소드이며, 실행하도록 지정할 수 있는 메소드입니다. 표준 메소드는 다음과 같습니다. get, list, create, update, delete. 편리한 readwrite 메소드를 통해 지정된 데이터베이스 또는 스토리지 경로에서 광범위한 읽기 및 쓰기 액세스가 가능합니다.
  • 경로: URI 경로로 표시되는 데이터베이스 또는 스토리지 위치입니다.
  • 규칙: true로 평가되면 요청을 허용하는 조건을 포함하는 allow 문입니다.

경로 일치

스토리지 보안 규칙 match는 Cloud Storage의 파일에 액세스하는 데 사용된 파일 경로를 비교합니다. match 규칙은 정확한 경로 또는 와일드 카드 경로를 비교할 수 있으며 규칙 중첩도 가능합니다. 요청 메소드를 허용하는 match 규칙이 없거나 조건이 false로 판정되면 요청이 거부됩니다.

완전 일치

// Exact match for "images/profilePhoto.png"
match /images/profilePhoto.png {
  allow write: if <condition>;
}

// Exact match for "images/croppedProfilePhoto.png"
match /images/croppedProfilePhoto.png {
  allow write: if <other_condition>;
}

중첩 일치

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/profilePhoto.png"
  match /profilePhoto.png {
    allow write: if <condition>;
  }

  // Exact match for "images/croppedProfilePhoto.png"
  match /croppedProfilePhoto.png {
    allow write: if <other_condition>;
  }
}

와일드 카드 일치

match 규칙에 와일드 카드를 사용해 패턴을 비교할 수도 있습니다. 와일드 카드는 profilePhoto.png 같은 단일 문자열 또는 images/profilePhoto.png 같은 여러 경로 세그먼트를 나타내는 명명된 변수입니다.

와일드 카드를 만들려면 {string}과 같이 와일드 카드 이름을 중괄호로 묶습니다. {path=**}와 같이 와일드 카드 이름에 =**를 추가하면 다중 세그먼트 와일드 카드를 선언할 수 있습니다.

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/*"
  // e.g. images/profilePhoto.png is matched
  match /{imageId} {
    // This rule only matches a single path segment (*)
    // imageId is a string that contains the specific segment matched
    allow read: if <condition>;
  }

  // Exact match for "images/**"
  // e.g. images/users/user:12345/profilePhoto.png is matched
  // images/profilePhoto.png is also matched!
  match /{allImages=**} {
    // This rule matches one or more path segments (**)
    // allImages is a path that contains all segments matched
    allow read: if <other_condition>;
  }
}

여러 규칙이 파일 하나와 일치하면 모든 규칙 판정의 결과를 OR로 결합한 결과가 적용됩니다. 즉, 파일과 일치하는 규칙이 하나라도 true로 판정되면 결과는 true입니다.

위 규칙에서는 condition 또는 other_condition이 true로 판정되면 'images/profilePhoto.png' 파일을 읽을 수 있는 반면 'images/users/user:12345/profilePhoto.png' 파일은 other_condition의 결과만 따릅니다.

match 내에서 와일드 카드 변수를 참조하여 파일 이름 또는 경로에 대한 권한을 정의할 수 있습니다.

// Another way to restrict the name of a file
match /images/{imageId} {
  allow read: if imageId == "profilePhoto.png";
}

Storage 보안 규칙은 하위로 누적되지 않으며, 규칙이 지정된 경로와 요청 경로가 일치할 때만 규칙이 판정됩니다.

요청 검증

업로드, 다운로드, 메타데이터 변경 및 삭제는 Cloud Storage로 전송된 request를 사용하여 검증됩니다. request 변수는 요청이 이루어지는 파일 경로, 요청 수신 시간 및 쓰기 요청인 경우 새 resource 값을 포함합니다. HTTP 헤더 및 인증 상태도 포함됩니다.

또한 request 객체는 request.auth 객체에 사용자의 고유 ID 및 Firebase 인증 페이로드를 포함합니다. 자세한 내용은 문서의 사용자 기반 보안 섹션을 참조하세요.

다음은 request 객체의 전체 속성 목록입니다.

속성 유형 설명
auth 맵<문자열, 문자열> 사용자가 로그인한 경우 uid(사용자의 고유 ID) 및 token(Firebase 인증 JWT 소유권의 맵)을 제공합니다. 그렇지 않은 경우에는 null입니다.
params 맵<문자열, 문자열> 요청의 쿼리 매개변수를 포함하는 맵입니다.
path 경로 요청이 이루어지는 경로를 나타내는 path입니다.
resource 맵<문자열, 문자열> 새 리소스 값이며 write 요청에만 있습니다.
time 타임스탬프 요청을 검증하는 서버 시간을 나타내는 타임스탬프입니다.

리소스 검증

규칙을 판정할 때 업로드, 다운로드, 수정 또는 삭제되는 파일의 메타데이터를 검증할 수도 있습니다. 이를 통해 특정 콘텐츠 유형의 파일만 업로드를 허용하거나 특정 크기보다 큰 파일만 삭제를 허용하는 등의 복잡하고 강력한 규칙을 만들 수 있습니다.

Cloud Storage에 대한 Firebase 보안 규칙은 resource 객체에 파일 메타데이터를 제공하며 이 객체는 Cloud Storage 객체에 노출된 메타데이터의 키-값 쌍을 포함합니다. read 또는 write 요청에서 이러한 속성을 검사하여 데이터 무결성을 확인할 수 있습니다.

업로드, 메타데이터 업데이트, 삭제 등의 write 요청에서는 현재 요청 경로에 있는 파일의 메타데이터를 포함하는 resource 객체뿐 아니라 쓰기가 허용된 경우 쓸 파일 메타데이터 중 일부를 포함하는 request.resource 객체도 사용할 수 있습니다. 이러한 두 값을 사용하여 데이터 무결성을 확인하거나 파일 형식, 크기 등의 애플리케이션 제한사항을 적용할 수 있습니다.

다음은 resource 객체의 전체 속성 목록입니다.

속성 유형 설명
name 문자열 객체의 전체 이름입니다.
bucket 문자열 객체가 속한 버킷의 이름입니다.
generation 정수 이 객체의 GCS 객체 세대입니다.
metageneration 정수 이 객체의 GCS 객체 메타세대입니다.
size 정수 객체 크기(바이트)입니다.
timeCreated 타임스탬프 객체 생성 시간을 나타내는 타임스탬프입니다.
updated 타임스탬프 객체의 마지막 업데이트 시간을 나타내는 타임스탬프입니다.
md5Hash 문자열 객체의 MD5 해시입니다.
crc32c 문자열 객체의 crc32c 해시입니다.
etag 문자열 이 객체에 연결된 etag입니다.
contentDisposition 문자열 이 객체에 연결된 콘텐츠 처리입니다.
contentEncoding 문자열 이 객체에 연결된 콘텐츠 인코딩입니다.
contentLanguage 문자열 이 객체에 연결된 콘텐츠 언어입니다.
contentType 문자열 이 객체에 연결된 콘텐츠 유형입니다.
metadata 맵<문자열, 문자열> 개발자가 추가로 지정한 맞춤 메타데이터의 키-값 쌍입니다.

request.resource는 이러한 속성을 모두 포함하지만 generation, metageneration, etag, timeCreated, updated는 예외입니다.

전체 예

다음은 모든 규칙을 조합하여 만든 이미지 스토리지 솔루션용 규칙의 전체 예입니다.

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // Cascade read to any image type at any path
     match /{allImages=**} {
       allow read;
     }

     // Allow write files to the path "images/*", subject to the constraints:
     // 1) File is less than 5MB
     // 2) Content type is an image
     // 3) Uploaded content type matches existing content type
     // 4) File name (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow write: if request.resource.size < 5 * 1024 * 1024
                    && request.resource.contentType.matches('image/.*')
                    && request.resource.contentType == resource.contentType
                    && imageId.size() < 32
     }
   }
 }
}

실시간 데이터베이스

기본 구조

실시간 데이터베이스에서 Firebase 보안 규칙은 JSON 문서에 포함된 자바스크립트와 유사한 표현식으로 구성됩니다.

여기에서는 다음 구문을 사용합니다.

{
  "rules": {
    "<<path>>": {
    // Allow the request if the condition for each method is true.
      ".read": <<condition>>,
      ".write": <<condition>>,
      ".validate": <<condition>>
    }
  }
}

규칙에는 세 가지 기본 요소가 있습니다.

  • 경로: 데이터베이스 위치입니다. 데이터베이스의 JSON 구조를 반영합니다.
  • 요청: 규칙에서 액세스를 허용하기 위해 사용하는 메소드입니다. readwrite 규칙은 광범위한 읽기 및 쓰기 액세스를 허용하지만 validate 규칙은 수신되는 데이터 또는 기존 데이터를 기반으로 액세스를 허용하는 2차 검증으로 작용합니다.
  • 조건: true로 평가되면 요청을 허용하는 조건입니다.

규칙이 경로에 적용되는 방법

실시간 데이터베이스에서 규칙은 원자적으로 적용됩니다. 즉, 더 높은 수준의 상위 노드에 있는 규칙이 더 세부적인 하위 노드에 있는 규칙에 우선하며 하위 노드의 규칙은 상위 경로에 액세스를 허용할 수 없습니다. 상위 경로 중 하나에 이미 액세스가 허용된 경우 데이터베이스 구조의 하위 경로에 대한 액세스를 세분화하거나 취소할 수 없습니다.

예를 들어 다음 규칙을 살펴보세요.

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          // ignored, since read was allowed already
          ".read": false
        }
     }
  }
}

이 보안 구조는 /foo/에 포함된 하위 baz의 값이 true일 때 /bar/를 읽을 수 있도록 허용합니다. 하위 경로에서 액세스를 취소할 수 없으므로 /foo/bar/ 아래의 ".read": false 규칙은 효과가 없습니다.

이 방식은 다소 직관적이지 않을 수 있으나 규칙 언어의 강력한 기능으로서 매우 복잡한 액세스 권한을 최소한의 노력만으로 구현할 수 있게 해 줍니다. 이는 특히 사용자 기반 보안에 유용합니다.

하지만 .validate 규칙은 하위로 전파되지 않습니다. 즉, 계층구조의 모든 수준에서 모든 검증 규칙이 충족되어야 쓰기가 허용됩니다.

또한 규칙이 상위 경로에 다시 적용되지 않기 때문에 액세스를 허용한 상위 위치 또는 요청한 위치에 규칙이 없는 경우 읽기 또는 쓰기 작업이 실패합니다. 영향을 받는 하위 경로에 모두 액세스할 수 있다고 해도 상위 위치의 읽기 작업은 모두 실패합니다. 예를 들어 아래 구조를 살펴보세요.

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

규칙을 유기적으로 판단한다는 점을 이해하지 못한 경우 /records/ 경로를 가져올 때 rec2가 아닌 rec1만 반환된다고 생각할 수 있습니다. 그러나 실제로는 오류가 발생합니다.

자바스크립트
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

/records/의 읽기 작업은 원자적이며 /records/ 아래의 모든 데이터에 대해 권한을 부여하는 읽기 규칙이 없으므로 PERMISSION_DENIED 오류가 발생합니다. Firebase 콘솔의 보안 시뮬레이터에서 이 규칙을 판정하면 읽기 작업이 거부되었음을 확인할 수 있습니다.

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

작업이 거부된 이유는 /records/ 경로에 대한 액세스를 허용하는 읽기 규칙이 없기 때문입니다. rec1에 대한 규칙은 요청된 경로에 없었으므로 판정되지 않았습니다. rec1을 가져오려면 다음과 같이 직접 액세스해야 합니다.

자바스크립트
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

위치 변수

실시간 데이터베이스 규칙은 경로 세그먼트를 일치시키는 $location 변수를 지원합니다. 경로 세그먼트 앞에 $ 프리픽스를 사용하여 경로에 따라 임의의 하위 노드에 규칙을 일치시킵니다.

  {
    "rules": {
      "rooms": {
        // This rule applies to any child of /rooms/, the key for each room id
        // is stored inside $room_id variable for reference
        "$room_id": {
          "topic": {
            // The room's topic can be changed if the room id has "public" in it
            ".write": "$room_id.contains('public')"
          }
        }
      }
    }
  }

$variable을 상수 경로 이름과 같이 사용할 수도 있습니다.

  {
    "rules": {
      "widget": {
        // a widget can have a title or color attribute
        "title": { ".validate": true },
        "color": { ".validate": true },

        // but no other child paths are allowed
        // in this case, $other means any key excluding "title" and "color"
        "$other": { ".validate": false }
      }
    }
  }