授权使用案例 - Amazon AppSync
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

授权使用案例

安全部分,您了解了保护 API 的不同授权模式,这一部分还介绍了精细授权机制,以便于您理解概念和流程。由于 Amazon AppSync 允许您通过使用 GraphQL 解析器映射模板对数据执行逻辑完整操作,因此,您可以组合使用用户身份、条件和数据注入,以非常灵活的方式在读取或写入时保护数据。

如果您不熟悉如何编辑 Amazon AppSync 解析器,请查看编程指南

概述

在系统中授予数据访问权限的传统方法是通过访问控制矩阵,其中行(资源)和列(用户/角色)的交叉点就是授予的权限。

Amazon AppSync 使用您自己的账户中的资源,并将身份(用户/角色)信息作为上下文对象嵌入到 GraphQL 请求和响应中,以便在解析器中使用该对象。也就是说,可以根据解析器的逻辑对读取或写入操作授予适当的权限。如果该逻辑处于资源级别,例如,仅某些指定的用户或组可以读取/写入特定的数据库行,您必须存储“授权元数据”。AmazonAppSync 不会存储任何数据,因此,您必须将该授权元数据与资源一起存储,以便可以计算权限。授权元数据通常是 DynamoDB 表中的一个属性(列),例如 owner 或用户/组的列表。例如,可能有读取者写入者属性。

从宏观角度而言,这就意味着如果您要从数据源中读取单个项目,即在解析器从数据源中读取内容之后在响应模板中执行一个条件 #if () ... #end 语句。此项检查通常会针对读取操作返回的授权元数据使用 $context.identity 中的用户或组值进行成员资格检查。如有多条记录,例如 ScanQuery 表返回的列表,您将使用类似的用户或组值将条件检查作为操作的一部分发送到数据源。

同样,在写入数据时,您将对操作(如 PutItemUpdateItem)应用条件语句,以便了解进行变更的用户或组是否拥有权限。在许多情况下,条件将使用 $context.identity 中的值与资源的授权元数据进行比较。对于请求和响应模板,您还可使用客户端的自定义标头进行验证检查。

读取数据

如上所述,执行检查的授权元数据必须随资源存储,或传递到 GraphQL 请求中(身份、标头等)。为了说明这一点,假设您有如下 DynamoDB 表:

主键是 id,要访问的数据是 Data。其他列是您可以执行的授权检查示例。OwnerString,而 PeopleCanAccessGroupsCanAccessString Sets,如 DynamoDB 解析器映射模板参考中所述。

解析器映射模板概述中的示意图展示了响应模板中不仅包含上下文对象,还包含数据源的结果。对于个别项目的 GraphQL 查询,您可以使用响应模板检查是否允许用户查看这些结果,或返回授权错误消息。这种方法有时称为”授权筛选“。对于 GraphQL 查询返回列表,更高效的方式是使用 Scan 或 Query 针对请求模板执行检查,只在满足授权条件的情况下返回数据。实施方法是:

  1. GetItem - 针对个别记录进行授权检查。使用 #if() ... #end 语句实现。

  2. Scan/Query 操作 - 授权检查是 "filter":{"expression":...} 语句。常用的检查方式是等式 (attribute = :input) 或者检查某个值是否在列表中 (contains(attribute, :input))。

在 #2 中,两条语句中的 attribute 表示表中记录的列名,例如上例中的 Owner。您可以借助 # 符号并使用 "expressionNames":{...} 设置别名,但这不是必需的。:input 可引用与数据库属性进行比较的值,该属性在 "expressionValues":{...} 中定义。您将在下文中看到这些示例。

使用案例:所有者可以读取

以上表为例,对于一个读取操作 (Owner == Nadia),如果您希望只在 GetItem 的情况下返回数据,您的模板将如下所示:

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

这里要提醒您几件事,在接下来的各节也会用到。首先,检查使用 $context.identity.username,如果使用 Amazon Cognito 用户池,这是友好的用户注册名称;如果使用 IAM,这是用户身份(包括 Amazon Cognito 联合身份)。还可以为所有者存储其他值,例如唯一的“Amazon Cognito 身份”值,这在从多个位置进行联合登录时是非常有用的,您应该查看解析器映射模板上下文参考中提供的选项。

第二,利用 $util.unauthorized() 进行响应的条件 else 检查完全是可选的,但作为最佳实践,建议您在设计 GraphQL API 时使用。

使用案例:硬编码特定的访问权限

// 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

使用案例:筛选结果列表

在上一示例中,您能够直接针对 $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)

使用案例:多人可以读取

另一种常用的授权选项是允许一组人员读取数据。在以下示例中,只有在运行 GraphQL 查询的用户属于 "filter":{"expression":...} 集的情况下 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) } } }

使用案例:组可以读取

与上一使用案例类似,可能只有一个或多个组中的人员才有权读取数据库中的某些项目。使用 "expression": "contains()" 操作具有类似效果,但在集的成员资格中,需考虑用户所在的所有组之间是逻辑或的关系。在本例中,我们构建一个 $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) } }

写入数据

将数据写入变更始终是通过请求映射模板进行控制的。对于 DynamoDB 数据源而言,关键是要使用适当的 "condition":{"expression"...}",针对表中的授权元数据执行验证。安全部分提供了一个示例,您可以使用它检查表中的 Author 字段。本节中将探索更多使用案例。

使用案例:多个所有者

使用之前的表示意图示例,假设 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) } } }

使用案例:组可以创建新记录

#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) } }

使用案例:组可以更新现有记录

#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) } }

公有记录和私有记录

您还可利用条件筛选器选择将数据标记为私有、公开,或进行其他布尔检查。还可在响应模板中进行组合,作为授权筛选的一部分。使用此检查是一种临时隐藏数据,使之不可见的好方法,而无需尝试控制组成员资格。

例如,假设您为 DynamoDB 表中的每个项目添加了一个称为 public 的属性,其值可为 yesno。以下响应模板可用于 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

以上代码还可使用逻辑或 (||) 允许有权限的人员读取记录,或允许读取公共记录:

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

通常,在执行授权检查时,您会发现标准运算符 ==!=&&|| 很有用。

实时数据

客户端进行订阅时,您可使用本文档之前介绍的方法,针对 GraphQL 订阅应用精细访问控制。您可将解析器附加到订阅字段,然后您就可以查询数据源的数据,并在请求或响应映射模板中执行条件逻辑。您也可以将其他数据返回到客户端,例如订阅的最初结果,条件是数据结构与 GraphQL 订阅中返回类型的结构相匹配。

使用案例:用户只能订阅特定对话

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() 字段,并利用以下请求模板选择消息表数据源:

{ "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。这会返回 Amazon 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

在本例中,您进行三次授权检查。第一次确保返回结果。第二次确保用户未订阅面向其他人的消息。通过检查存储为 BOOLisAuthorizedForSubscriptions DynamoDB 属性,第三次检查确保允许用户订阅任何字段。

要进行测试,您可以使用 Amazon Cognito 用户池和名为“Nadia”的用户登录到 Amazon AppSync 控制台,然后运行以下 GraphQL 订阅:

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

如果权限 表中有一条 username 的记录,其键属性为 NadiaisAuthorizedForSubscriptions 设置为 true,您将看到成功响应。如果您在以上 username 查询中尝试其他 newMessage(),将返回错误。