

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 用於保護請求和回應安全的存取控制使用案例
<a name="security-authorization-use-cases"></a>

您在[安全](security-authz.md#aws-appsync-security)章節了解保護 API 的各種授權模式，其中也簡介精細分級授權機制，以了解各項概念與流程。Since AWS AppSync 可讓您透過使用 GraphQL 解析程式[映射範本](resolver-mapping-template-reference-overview.md#aws-appsync-resolver-mapping-template-reference-overview)對資料執行邏輯完整操作，您可以使用使用者身分、條件和資料注入的組合，以非常靈活的方式保護讀取或寫入時的資料。

如果您不熟悉 editing AWS AppSync Resolvers，請檢閱[程式設計指南](resolver-mapping-template-reference-programming-guide.md#aws-appsync-resolver-mapping-template-reference-programming-guide)。

## 概觀
<a name="overview"></a>

授予系統中資料的存取權傳統上是透過[存取控制矩陣](https://en.wikipedia.org/wiki/Access_Control_Matrix)完成，其中資料列 （資源） 和資料欄 （使用者/角色） 的交集是授予的許可。

AWS AppSync 會在您自己的帳戶中使用資源，並將身分 （使用者/角色） 資訊執行緒至 GraphQL 請求和回應做為[內容物件](resolver-context-reference.md#aws-appsync-resolver-mapping-template-context-reference)，您可以在解析程式中使用。這表示您可依據解析程式邏輯，將權限適當地授與寫入或讀取操作。如果此邏輯位於資源層級，例如只有特定具名使用者或群組可以讀取/寫入特定資料庫資料列，則必須儲存該「授權中繼資料」。 AWS AppSync 不會儲存任何資料，因此您必須將此授權中繼資料與資源一起存放，才能計算許可。授權中繼資料通常是 DynamoDB 表之中的屬性 (欄)，例如**擁有者**或使用者/群組清單。例如其中可能有**讀取器**及**寫入器**屬性。

大致而言，這表示若您由資料來源讀取個別項目，就會在解析程式由資料來源讀取之後，於回應範本執行條件式 `#if () ... #end` 陳述式。檢查通常會使用 `$context.identity` 之中的使用者或群組值，依據讀取操作傳回的授權中繼資料進行成員檢查。對於多筆記錄，例如由資料表 `Scan` 或 `Query` 傳回的清單，您將使用類似的使用者或群組值，將條件檢查做為操作的一部分傳送至資料來源。

同樣地，寫入資料時，您會將條件式陳述式套用至動作 (例如 `PutItem` 或 `UpdateItem`)，了解進行變動的使用者或群組是否具有許可。條件式將再次多次使用 `$context.identity` 之中的值，與該項資源的授權中繼資料進行比較。對於要求及回應範本，您也可使用用戶端的自訂標頭執行驗證檢查。

## 讀取資料
<a name="reading-data"></a>

如前所述，執行檢查的授權中繼資料必須與資源儲存，或傳送至 GraphQL 要求 (身分、標頭等等)。為了示範，假設您有下列的 DynamoDB 表格：

![\[DynamoDB table with ID, Data, PeopleCanAccess, GroupsCanAccess, and Owner columns.\]](http://docs.aws.amazon.com/zh_tw/appsync/latest/devguide/images/auth.png)


主索引鍵為 `id`，要存取的資料為 `Data`。其他資料欄是您可以執行以進行授權的檢查範例。 `Owner`會是`String`一段時間`PeopleCanAccess`，並且`GroupsCanAccess`會`String Sets`如 [ DynamoDB 的解析程式映射範本參考](resolver-mapping-template-reference-dynamodb.md#aws-appsync-resolver-mapping-template-reference-dynamodb)中所述。

在[解析程式映射範本概觀](resolver-mapping-template-reference-overview.md#aws-appsync-resolver-mapping-template-reference-overview)之中的圖會顯示回應範本不僅包含內容物件，也包含資料來源結果。對個別項目的 GraphQL 查詢而言，您可使用回應範本檢查使用者是否允許檢視這些結果，否則將傳回授權錯誤訊息。這有時稱為「授權篩選」。對使用掃描或查詢的 GraphQL 查詢傳回清單而言，效能較高的作法是在要求範本執行檢查，並僅於滿足授權條件時傳回資料。實作之後將：

1. GetItem - 個別記錄的授權檢查。使用 `#if() ... #end` 陳述式完成。

1. 掃描/查詢操作 - 授權檢查為 `"filter":{"expression":...}` 陳述式。一般檢查為對等 (`attribute = :input`) 或檢查值是否在清單之中 (`contains(attribute, :input)`)。

在 \$12 之中，兩種陳述式的 `attribute` 都代表表格之中記錄的欄名稱，例如以上範例中的 `Owner`。您可利用 `#` 簽署和使用 `"expressionNames":{...}` 為此設定別名，但並非必要。`:input` 是您要與資料庫屬性比較值的參考，該屬性將於 `"expressionValues":{...}` 定義。您可參考以下各項範例。

### 使用案例：擁有者可以讀取
<a name="use-case-owner-can-read"></a>

使用以上表格，若您只想傳回資料，了解 `Owner == Nadia` 是否用於個別讀取操作 (`GetItem`)，您的範本可能會像這樣：

```
#if($context.result["Owner"] == $context.identity.username)
    $utils.toJson($context.result)
#else
    $utils.unauthorized()
#end
```

這裡提出兩點注意事項，在剩餘章節中會用到。首先，如果使用 Amazon Cognito 使用者集區，檢查會使用`$context.identity.username`易記的使用者註冊名稱，如果使用 IAM，檢查將是使用者身分 （包括 Amazon Cognito 聯合身分）。還有其他值要存放給擁有者，例如唯一的「Amazon Cognito 身分」值，在聯合來自多個位置的登入時很有用，您應該檢閱[解析程式映射範本內容參考](resolver-context-reference.md#aws-appsync-resolver-mapping-template-context-reference)中可用的選項。

第二，以 `$util.unauthorized()` 進行的條件式否則檢查回應為完全可選用的選項，但建議做為設計 GraphQL API 時的最佳實務。

### 使用案例：硬式編碼特定存取
<a name="use-case-hardcode-specific-access"></a>

```
// This checks if the user is part of the Admin group and makes the call
#foreach($group in $context.identity.claims.get("cognito:groups"))
    #if($group == "Admin")
        #set($inCognitoGroup = true)
    #end
#end
#if($inCognitoGroup)
{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
    },
    "attributeValues" : {
        "owner" : $util.dynamodb.toDynamoDBJson($context.identity.username)
        #foreach( $entry in $context.arguments.entrySet() )
            ,"${entry.key}" : $util.dynamodb.toDynamoDBJson($entry.value)
        #end
    }
}
#else
    $utils.unauthorized()
#end
```

### 使用案例：篩選結果清單
<a name="use-case-filtering-a-list-of-results"></a>

在前一項範例之中，您可在 `$context.result` 傳回單一項目時對其執行檢查，不過像是掃描等部分操作，將在 `$context.result.items` 傳回多個項目；您在此需要執行驗證篩選，並僅傳回使用者允許檢視的結果。假設 `Owner` 欄位在記錄中設定了 Amazon Cognito IdentityID，則您可以使用下列回應映射範本來篩選，只顯示使用者擁有的記錄：

```
#set($myResults = [])
#foreach($item in $context.result.items)
    ##For userpools use $context.identity.username instead
    #if($item.Owner == $context.identity.cognitoIdentityId)
        #set($added = $myResults.add($item))
    #end
#end
$utils.toJson($myResults)
```

### 使用案例：多個人員可以讀取
<a name="use-case-multiple-people-can-read"></a>

另一種熱門的授權選擇，就是允許一群人讀取資料。在以下範例中，`"filter":{"expression":...}` 僅傳回表格掃描的值 (若使用者執行的 GraphQL 查詢列於 `PeopleCanAccess` 集之中)。

```
{
    "version" : "2017-02-28",
    "operation" : "Scan",
    "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end,
    "nextToken": #if(${context.arguments.nextToken})  $util.toJson($context.arguments.nextToken) #else null #end,
    "filter":{
        "expression": "contains(#peopleCanAccess, :value)",
        "expressionNames": {
                "#peopleCanAccess": "peopleCanAccess"
        },
        "expressionValues": {
                ":value": $util.dynamodb.toDynamoDBJson($context.identity.username)
        }
    }
}
```

### 使用案例：群組可以讀取
<a name="use-case-group-can-read"></a>

類似於上一項使用案例，可能只有單一或更多群組的人，有權讀取資料庫之中的特定項目。`"expression": "contains()"` 操作的使用方式類似，不過這是所有群組的邏輯 OR，使用者可能是其中的一部分，需要在設定成員資格時加以考量。在此案例中，我們針對使用者所屬的各個群組建立以下 `$expression` 陳述式，然後將其傳送至篩選器：

```
#set($expression = "")
#set($expressionValues = {})
#foreach($group in $context.identity.claims.get("cognito:groups"))
    #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" )
    #set( $val = {})
    #set( $test = $val.put("S", $group))
    #set( $values = $expressionValues.put(":var$foreach.count", $val))
    #if ( $foreach.hasNext )
    #set( $expression = "${expression} OR" )
    #end
#end
{
    "version" : "2017-02-28",
    "operation" : "Scan",
    "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end,
    "nextToken": #if(${context.arguments.nextToken})  $util.toJson($context.arguments.nextToken) #else null #end,
    "filter":{
        "expression": "$expression",
        "expressionValues": $utils.toJson($expressionValues)
    }
}
```

## 寫入資料
<a name="writing-data"></a>

在變動寫入資料一定是在要求映射範本進行控制。就 DynamoDB 資料來源而言，關鍵在於使用適當的 `"condition":{"expression"...}"`，依據該表格之中的授權中繼資料執行驗證。在[安全性](security-authz.md#aws-appsync-security)中，我們提供範例讓您用來檢查資料表中的 `Author` 欄位。本節使用案例將探索更多使用案例。

### 使用案例：多個擁有者
<a name="use-case-multiple-owners"></a>

使用之前的範例表格圖示，假設 `PeopleCanAccess` 清單

```
{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
    },
    "update" : {
        "expression" : "SET meta = :meta",
        "expressionValues": {
            ":meta" : $util.dynamodb.toDynamoDBJson($ctx.args.meta)
        }
    },
    "condition" : {
        "expression"       : "contains(Owner,:expectedOwner)",
        "expressionValues" : {
            ":expectedOwner" : $util.dynamodb.toDynamoDBJson($context.identity.username)
        }
    }
}
```

### 使用案例：群組可以建立新的記錄
<a name="use-case-group-can-create-new-record"></a>

```
#set($expression = "")
#set($expressionValues = {})
#foreach($group in $context.identity.claims.get("cognito:groups"))
    #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" )
    #set( $val = {})
    #set( $test = $val.put("S", $group))
    #set( $values = $expressionValues.put(":var$foreach.count", $val))
    #if ( $foreach.hasNext )
    #set( $expression = "${expression} OR" )
    #end
#end
{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key" : {
        ## If your table's hash key is not named 'id', update it here. **
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
        ## If your table has a sort key, add it as an item here. **
    },
    "attributeValues" : {
        ## Add an item for each field you would like to store to Amazon DynamoDB. **
        "title" : $util.dynamodb.toDynamoDBJson($ctx.args.title),
        "content": $util.dynamodb.toDynamoDBJson($ctx.args.content),
        "owner": $util.dynamodb.toDynamoDBJson($context.identity.username)
    },
    "condition" : {
        "expression": $util.toJson("attribute_not_exists(id) AND $expression"),
        "expressionValues": $utils.toJson($expressionValues)
    }
}
```

### 使用案例： 群組可以更新現有記錄
<a name="use-case-group-can-update-existing-record"></a>

```
#set($expression = "")
#set($expressionValues = {})
#foreach($group in $context.identity.claims.get("cognito:groups"))
    #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" )
    #set( $val = {})
    #set( $test = $val.put("S", $group))
    #set( $values = $expressionValues.put(":var$foreach.count", $val))
    #if ( $foreach.hasNext )
    #set( $expression = "${expression} OR" )
    #end
#end
{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
    },
    "update":{
                "expression" : "SET title = :title, content = :content",
        "expressionValues": {
            ":title" : $util.dynamodb.toDynamoDBJson($ctx.args.title),
            ":content" : $util.dynamodb.toDynamoDBJson($ctx.args.content)
        }
    },
    "condition" : {
        "expression": $util.toJson($expression),
        "expressionValues": $utils.toJson($expressionValues)
    }
}
```

## 公有和私有記錄
<a name="public-and-private-records"></a>

您也可以使用條件式篩選器，選擇將資料標示為公開、私人或其他布林檢查。並於之後合併為回應範本之中授權篩選的一部分。此項檢查適合用於暫時隱藏資料，或由檢視之中移除資料，不必嘗試控制群組成員資格。

例如，假設您在名為 `public` 的 DynamoDB 表格，以 `yes` 或 `no` 值在各個項目新增屬性。下列回應範本可於 `GetItem` 呼叫時使用；只有在使用者位於具有權限的群組之中，且該資料標示為公開的情況下，資料才會顯示：

```
#set($permissions = $context.result.GroupsCanAccess)
#set($claimPermissions = $context.identity.claims.get("cognito:groups"))

#foreach($per in $permissions)
    #foreach($cgroups in $claimPermissions)
        #if($cgroups == $per)
            #set($hasPermission = true)
        #end
    #end
#end

#if($hasPermission && $context.result.public == 'yes')
    $utils.toJson($context.result)
#else
    $utils.unauthorized()
#end
```

以上程式碼也可使用邏輯 OR (`||`)，在使用者擁有權限或記錄為公開的情況下能夠讀取記錄：

```
#if($hasPermission || $context.result.public == 'yes')
    $utils.toJson($context.result)
#else
    $utils.unauthorized()
#end
```

一般而言，您在執行授權檢查時，運算子 `==`、`!=`、`&&` 及 `||` 都相當實用。

## 即時資料
<a name="security-real-time-data"></a>

您可在用戶端訂閱時，使用本文之前介紹的相同技巧，將精細分級的存取控制套用至 GraphQL 訂閱。您將解析程式附加至訂閱欄位，在此由資料來源查詢資料，並於要求或回應映射範本時執行條件式邏輯。只要資料結構符合 GraphQL 訂閱之中傳回的類型，您也可以傳回其他資料給用戶端，例如訂閱的初始結果。

### 使用案例：使用者只能訂閱特定對話
<a name="use-case-user-can-subscribe-to-specific-conversations-only"></a>

這是常見的即時資料使用案例，其中 GraphQL 訂閱正在建構訊息或私人交談應用程式。建立具有多位使用者的交談應用程式時，可能在兩位或多位使用者之間產生對話。這些使用者可能會聚集成為私人或公開的「聊天室」。在這種情況下，您可能只想授權使用者訂閱其有權存取的對話 (一對一或群組之中)。以下範例示範單一使用者向他人傳送私人訊息的簡單使用案例。設定有兩個 Amazon DynamoDB 資料表：
+ 訊息表格：(主索引鍵) `toUser`、(排序索引鍵) `id` 
+ 權限表格：(主索引鍵) `username` 

訊息表格儲存透過 GraphQL 變動傳送的實際訊息。權限表格會由 GraphQL 訂閱檢查，在用戶端連線時進行授權。以下範例假設您使用下列 GraphQL 結構描述：

```
input CreateUserPermissionsInput {
    user: String!
    isAuthorizedForSubscriptions: Boolean
}

type Message {
    id: ID
    toUser: String
    fromUser: String
    content: String
}

type MessageConnection {
    items: [Message]
    nextToken: String
}

type Mutation {
    sendMessage(toUser: String!, content: String!): Message
    createUserPermissions(input: CreateUserPermissionsInput!): UserPermissions
    updateUserPermissions(input: UpdateUserPermissionInput!): UserPermissions
}

type Query {
    getMyMessages(first: Int, after: String): MessageConnection
    getUserPermissions(user: String!): UserPermissions
}

type Subscription {
    newMessage(toUser: String!): Message
        @aws_subscribe(mutations: ["sendMessage"])
}

input UpdateUserPermissionInput {
    user: String!
    isAuthorizedForSubscriptions: Boolean
}

type UserPermissions {
    user: String
    isAuthorizedForSubscriptions: Boolean
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}
```

下面`createUserPermissions()`未涵蓋一些標準操作，例如 ，以說明訂閱解析程式，但 是 DynamoDB 解析程式的標準實作。我們將重點放在訂閱授權流程與解析程式。使用下列要求範本，讓使用者將訊息傳送給其他使用者，將解析程式連接至 `sendMessage()` 欄位，然後選取 **Messages (訊息)** 資料表資料來源：

```
{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key" : {
        "toUser" : $util.dynamodb.toDynamoDBJson($ctx.args.toUser),
        "id" : $util.dynamodb.toDynamoDBJson($util.autoId())
    },
    "attributeValues" : {
        "fromUser" : $util.dynamodb.toDynamoDBJson($context.identity.username),
        "content" : $util.dynamodb.toDynamoDBJson($ctx.args.content),
    }
}
```

在此範例中，我們使用 `$context.identity.username`. 這會傳回 AWS Identity and Access Management 或 Amazon Cognito 使用者的使用者資訊。回應範本為簡單的 `$util.toJson($ctx.result)` 傳遞。儲存後回到結構描述頁面。然後連接解析程式以用於 `newMessage()` 訂閱，使用**許可**資料表做為資料來源，以及下列要求映射範本：

```
{
    "version": "2018-05-29",
    "operation": "GetItem",
    "key": {
        "username": $util.dynamodb.toDynamoDBJson($ctx.identity.username),
    },
}
```

之後請使用下列回應映射範本，以**許可**資料表的資料執行授權檢查：

```
#if(! ${context.result})
    $utils.unauthorized()
#elseif(${context.identity.username} != ${context.arguments.toUser})
    $utils.unauthorized()
#elseif(! ${context.result.isAuthorizedForSubscriptions})
    $utils.unauthorized()
#else
##User is authorized, but we return null to continue
    null
#end
```

在此情況下，您會進行三項授權檢查。第一項檢查確保結果已返回。第二項檢查確保使用者並未訂閱其他人的訊息。第三個會檢查`isAuthorizedForSubscriptions`存放為 的 DynamoDB 屬性，以確保使用者能夠訂閱任何欄位`BOOL`。

若要測試物件，您可以使用 Amazon Cognito 使用者集區和名為「Nadia」的使用者登入 AWS AppSync 主控台，然後執行下列 GraphQL 訂閱：

```
subscription AuthorizedSubscription {
    newMessage(toUser: "Nadia") {
        id
        toUser
        fromUser
        content
    }
}
```

如果在**許可**資料表之中，存在 `Nadia` 的 `username` 索引鍵屬性記錄，且 `isAuthorizedForSubscriptions` 設定為 `true`，您就可以獲得成功回應。如果您在以上的 `username` 查詢嘗試不同 `newMessage()`，就會傳回錯誤。