首页 > 解决方案 > 使用资源令牌的 CosmosDb Rest SQL 查询

问题描述

我有一个 Xamarin Forms 移动客户端,我想直接与 Cosmosdb 对话,我不想依赖整个 DocumentDb SDK,也不想承担开销。

而且由于我使用的是不受信任的客户端,因此我正在使用resource tokens身份验证。一切都是分区的。

出于测试目的,我已经使用 REST SQL 和 DocumentClient 调用复制了我正在尝试执行的操作。

Get 通过使用 REST 和资源令牌发出调用,我已成功检索到单个文档。这也适用于 DocumentClient 方法。

到目前为止,一切都很好。

当我尝试实际执行query时,使用 DocumentClient 和资源令牌效果很好。

对 REST 调用使用完全相同的查询和完全相同的资源令牌会产生Forbidden结果。

The permission mode provided in the authorization token doesn't provide sufficient permissions

我在某处(现在找不到)读到您需要一个主令牌来使用 REST 调用进行查询。

在我发布一堆代码并编写它之前,我遇到了预期的行为,还是我实际上应该能够使用 REST 调用进行查询?

提前致谢。

** 更新 #2 与 GitHub 存储库的链接**

https://github.com/nhwilly/DocumentClientVsRest.git

使用代码示例更新

using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Newtonsoft.Json;

namespace DocClientVsRestCallTest
{
    /// <summary>
    /// The purpose of this console app is to determine why I can't get a REST call
    /// to work on a read only resource token for Azure CosmosDb.  A direct comparison
    /// using identical paths and tokens should work.  I have an issue for sure, but I
    /// can't find it.  :(
    ///
    /// To run this, you need to have already created a partitioned CosmosDb collection.
    /// </summary>
    class Program
    {
        public static string dbServicePath = $"https://[YOUR-COSMOS-ACCOUNT-NAME].documents.azure.com";
        public static string databaseId = "[YOUR-DATABASE-ID]";
        public static string collectionId = "[YOUR-COLLECTION-ID]";
        public static string datetime = DateTime.UtcNow.ToString("R");
        public static string version = "2018-06-18";
        public static string resourceId = $"dbs/{databaseId}/colls/{collectionId}";
        public static string urlPath = $"{dbServicePath}/{resourceId}/docs";
        public static string partitionKey = $"TestPartition";
        public static string documentId = $"TestDocumentId";
        public static string queryString = $"select * from c where c.id='{documentId}' and c.partitionId ='{partitionKey}'";
        public static string userId = $"TestUser";
        public static string permissionToken = string.Empty;

        // the master key is supplied to create permission tokens and simulate the server only.
        public static string masterKey = $"[YOUR-MASTER-KEY]";

        static void Main(string[] args)
        {
            Debug.WriteLine("Starting...");

            // let's make sure we get a readonly token for the user/partition in question.
            permissionToken =
                Task.Run(async () => await GetPermissionToken()).GetAwaiter().GetResult();


            QueryUsingSdk();

            Task.Run(async () => await QueryUsingRest()).ConfigureAwait(false);

            Task.Run(async ()=> await CleanUp()).ConfigureAwait(false);

            Console.WriteLine("finished...");
            Console.ReadKey();
        }

        static async Task QueryUsingRest()
        {
            Uri uri = new Uri(urlPath);
            HttpClient client = new HttpClient();

            var encodedToken =
                HttpUtility.UrlEncode(permissionToken);

            string partitionAsJsonArray =
                JsonConvert.SerializeObject(new[] { partitionKey });

            client.DefaultRequestHeaders.Add("x-ms-date", datetime);
            client.DefaultRequestHeaders.Add("x-ms-documentdb-isquery", "True");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-query-enablecrosspartition", "False");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-query-iscontinuationexpected", "False");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", partitionAsJsonArray);
            client.DefaultRequestHeaders.Add("authorization", encodedToken);
            client.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
            client.DefaultRequestHeaders.Add("x-ms-version", version);
            client.DefaultRequestHeaders.Accept
                .Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var content =
                new StringContent(JsonConvert.SerializeObject(new { query = queryString }), Encoding.UTF8, "application/query+json");

            HttpResponseMessage response =
               await client.PostAsync(urlPath, content).ConfigureAwait(false);

            if (!response.IsSuccessStatusCode)
            {
                await DisplayErrorMessage(response).ConfigureAwait(false);
            }
            else
            {
                Debug.WriteLine($"Success {response.StatusCode}!");
                var jsonString =
                    await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            }

        }


        static void QueryUsingSdk()
        {

            var docClient =
                new DocumentClient(new Uri(dbServicePath), permissionToken);

            var feedOptions =
                new FeedOptions { PartitionKey = new PartitionKey(partitionKey) };

            var result =
                docClient
                    .CreateDocumentQuery(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId), queryString,
                        feedOptions)
                    .ToList().First();


            Debug.WriteLine($"SDK result: {result}");
        }


        /// <summary>
        /// This method simulates what would happen on the server during an authenticated
        /// request.  The token (and other permission info) would be returned to the client.
        /// </summary>
        /// <returns></returns>
        static async Task<string> GetPermissionToken()
        {
            string token = string.Empty;
            try
            {
                var docClient =
                    new DocumentClient(new Uri(dbServicePath), masterKey);

                var userUri =
                        UriFactory.CreateUserUri(databaseId, userId);

                // delete the user if it exists...
                try
                {
                    await docClient.DeleteUserAsync(userUri).ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Delete user error: {e.Message}");

                }

                // create the user
                var dbUri =
                    UriFactory.CreateDatabaseUri(databaseId);

                await docClient.CreateUserAsync(dbUri, new User { Id = userId }).ConfigureAwait(false);

                // create the permission
                var link =
                    await docClient
                        .ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId))
                        .ConfigureAwait(false);

                var resourceLink =
                    link.Resource.SelfLink;

                var permission =
                    new Permission
                    {
                        Id = partitionKey,
                        PermissionMode = PermissionMode.Read,
                        ResourceLink = resourceLink,
                        ResourcePartitionKey = new PartitionKey(partitionKey)
                    };


                await docClient.CreatePermissionAsync(userUri, permission).ConfigureAwait(false);

                // now create a document that should be returned when we do the query
                var doc = new { id = documentId, partitionId = partitionKey, message = "Sample document for testing" };
                try
                {
                    await docClient.DeleteDocumentAsync(UriFactory.CreateDocumentUri(databaseId, collectionId,
                        documentId), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) }).ConfigureAwait(false);


                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Test document not found to delete - this is normal.");
                }

                try
                {
                    var document = await docClient
                        .CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId), doc)
                        .ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Create document message: {e.Message}");
                }

                // now read the permission back as it would happen on the server
                var result = await docClient.ReadPermissionFeedAsync(userUri).ConfigureAwait(false);
                if (result.Count > 0)
                {
                    token = result.First(c => c.Id == partitionKey).Token;
                }


            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Create and get permission failed: {ex.Message}");
            }

            if (string.IsNullOrEmpty(token))
            {
                Debug.WriteLine("Did not find token");
            }
            return token;
        }

        static async Task CleanUp()
        {
            var docClient =
                new DocumentClient(new Uri(dbServicePath), masterKey);

            var doc = new { id = documentId, partitionId = partitionKey, message = "Sample document for testing" };
            try
            {
                await docClient.DeleteDocumentAsync(UriFactory.CreateDocumentUri(databaseId, collectionId,
                    documentId), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) }).ConfigureAwait(false);


            }
            catch (Exception e)
            {
                Debug.WriteLine($"Delete document message: {e.Message}");
            }

        }

        static async Task DisplayErrorMessage(HttpResponseMessage response)
        {
            var messageDefinition =
                new
                {
                    code = "",
                    message = ""
                };

            var jsonString =
                await response.Content.ReadAsStringAsync().ConfigureAwait(false);

            var message =
                JsonConvert.DeserializeAnonymousType(jsonString, messageDefinition);

            Debug.WriteLine($"Failed with {response.StatusCode} : {message.message}");

        }

    }
}

标签: azurexamarin.formsazure-cosmosdb

解决方案


我遇到了预期的行为,还是应该能够使用 REST 调用进行查询?

是的,您可以使用资源令牌在 rest 调用中查询文档。请参考我的示例 rest java 代码,如下所示:

import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONObject;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

public class QueryDocumentsRest {

    private static final String account = "***";

    private static final String query = "select * from c";

    public static void main(String args[]) throws Exception {

        String urlString = "https://" + account + ".documents.azure.com/dbs/db/colls/coll/docs";
        HttpURLConnection connection = (HttpURLConnection) (new URL(urlString)).openConnection();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("query",query);
        JSONArray jsonArray = new JSONArray();
        jsonObject.put("parameters",jsonArray);
        byte[] data = (jsonObject.toString()).getBytes("UTF-8");

        connection.setRequestMethod("POST");
        connection.setRequestProperty("x-ms-version", "2017-02-22");
        connection.setRequestProperty("x-ms-documentdb-isquery", "true");
        //connection.setRequestProperty("x-ms-documentdb-query-enablecrosspartition", "true");

        connection.setRequestProperty("Content-Type", "application/query+json");
        System.out.println(data.length);
        connection.setRequestProperty("Content-Length", String.valueOf(data.length));

        SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
        fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
        String date = fmt.format(Calendar.getInstance().getTime()) + " GMT";
        String auth = getAuthenticationString();

        connection.setRequestProperty("x-ms-date", date);
        connection.setRequestProperty("Authorization", auth);

        connection.setDoOutput(true);
        OutputStream os = connection.getOutputStream();

        os.write(data);
        os.flush();
        os.close();

        System.out.println("Response message : " + connection.getResponseMessage());
        System.out.println("Response code : " + connection.getResponseCode());
        System.out.println(connection.getHeaderField("x-ms-request-charge"));


        BufferedReader br = null;
        if (connection.getResponseCode() != 200) {
            br = new BufferedReader(new InputStreamReader((connection.getErrorStream())));
        } else {
            br = new BufferedReader(new InputStreamReader((connection.getInputStream())));
        }
        System.out.println("Response body : " + br.readLine());
    }


    private static String getAuthenticationString() throws Exception {
        String auth = "type=resource&ver=1&sig=***";
        auth = URLEncoder.encode(auth);
        System.out.println("authString:" + auth);
        return auth;
    }

}

我在测试期间设置了权限模式All


推荐阅读