

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

# 使用 Bolt 通訊協定，對 Neptune 進行 openCypher 查詢
<a name="access-graph-opencypher-bolt"></a>

[Bolt](https://boltprotocol.org/) 是最初由 Neo4j 開發的陳述式導向的用戶端/伺服器通訊協定，並根據 Creative Commons 3.0 [Attribution-ShareAlike](https://creativecommons.org/licenses/by-sa/3.0/) 授權來授權。它是用戶端驅動的，這表示用戶端始終啟動訊息交換。

若要使用 Neo4j 的 Bolt 驅動程式連線至 Neptune，只需使用 `bolt` URI 方案將 URL 和連接埠號碼取代為叢集端點即可。如果您有一個正在執行的單一 Neptune 執行個體，請使用 read\_write 端點。如果有多個執行個體正在執行，則建議兩個驅動程式，一個用於寫入器，另一個用於所有僅供讀取複本。如果您只有預設的兩個端點，一個 read\_write 和一個 read\_only 驅動程式就足夠了，但如果您也有自訂端點，請考慮為每個端點建立一個驅動程式執行個體。

**注意**  
儘管 Bolt 規格指出，Bolt 可以使用 TCP 或 WebSockets 進行連線，但 Neptune 僅支援 Bolt 的 TCP 連線。

Neptune 在所有執行個體大小上允許最多 1000 個並行 Bolt 連線，但 t3.medium 和 t4g.medium 除外。在 t3.medium 和 t4g.medium 執行個體上，僅允許 512 個連線。

如需使用 Bolt 驅動程式之各種語言的 OpenCypher 查詢範例，請參閱 Neo4j [驅動程式與語言指南](https://neo4j.com/developer/language-guides/)文件。

**重要**  
Python、.NET、JavaScript 和 Golang 的 Neo4j Bolt 驅動程式最初不支援自動續約 AWS Signature v4 身分驗證字符。這表示在簽章過期後 (通常在 5 分鐘內)，驅動程式便無法進行身分驗證，而且後續請求失敗。下面的 Python、.NET、JavaScript 和 Go 範例都受到這個問題的影響。  
如需詳細資訊，請參閱 [Neo4j Python 驅動程式問題 \#834](https://github.com/neo4j/neo4j-python-driver/issues/834)、[Neo4j .NET 問題 \#664](https://github.com/neo4j/neo4j-dotnet-driver/issues/664)、[Neo4j JavaScript 驅動程式問題 \#993](https://github.com/neo4j/neo4j-javascript-driver/issues/993) 和 [Neo4j goLang 驅動程式問題 \#429](https://github.com/neo4j/neo4j-go-driver/issues/429)。  
從驅動程序 5.8.0 版開始，Go 驅動程式已發行新的預覽重新身分驗證 API (請參閱 [v5.8.0 – 重新身分驗證時所需的回饋](https://github.com/neo4j/neo4j-go-driver/discussions/482))。

## 搭配 Java 使用 Bolt 連線至 Neptune
<a name="access-graph-opencypher-bolt-java"></a>

您可以從 Maven [MVN 儲存庫](https://mvnrepository.com/artifact/org.neo4j.driver/neo4j-java-driver)下載要使用的任何版本的驅動程式，也可以將此相依性新增至您的專案：

```
<dependency>
  <groupId>org.neo4j.driver</groupId>
  <artifactId>neo4j-java-driver</artifactId>
  <version>4.3.3</version>
</dependency>
```

然後，若要在 Java 中使用這些 Bolt 驅動程式之一連線到 Neptune，請使用如下的程式碼，為叢集中的主要/寫入器執行個體建立驅動程式執行個體：

```
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

final Driver driver =
  GraphDatabase.driver("bolt://{{(your cluster endpoint URL)}}:{{(your cluster port)}}",
    AuthTokens.none(),
    Config.builder().withEncryption()
                    .withTrustStrategy(TrustStrategy.trustSystemCertificates())
                    .build());
```

如果您有一個或多個讀取器複本，則可以使用如下的程式碼，以類似的方式為它們建立驅動程式執行個體：

```
final Driver read_only_driver =              // (without connection timeout)
  GraphDatabase.driver("bolt://{{(your cluster endpoint URL)}}:{{(your cluster port)}}",
      Config.builder().withEncryption()
                      .withTrustStrategy(TrustStrategy.trustSystemCertificates())
                      .build());
```

或者，搭配逾時：

```
final Driver read_only_timeout_driver =      // (with connection timeout)
  GraphDatabase.driver("bolt://{{(your cluster endpoint URL)}}:{{(your cluster port)}}",
    Config.builder().withConnectionTimeout(30, TimeUnit.SECONDS)
                    .withEncryption()
                    .withTrustStrategy(TrustStrategy.trustSystemCertificates())
                    .build());
```

如果您具有自訂端點，則也可能值得您為每個端點建立一個驅動程式執行個體。

## 使用 Bolt 的 Python openCypher 查詢範例
<a name="access-graph-opencypher-bolt-python"></a>

以下是如何在 Python 中使用 Bolt 進行 openCypher 查詢：

```
python -m pip install neo4j
```

```
from neo4j import GraphDatabase
uri = "bolt://{{(your cluster endpoint URL)}}:{{(your cluster port)}}"
driver = GraphDatabase.driver(uri, auth=("username", "password"), encrypted=True)
```

請注意，會忽略 `auth` 參數。

## 使用 Bolt 的 .NET openCypher 查詢範例
<a name="access-graph-opencypher-bolt-dotnet"></a>

若要在. NET 中使用 Bolt 進行 OpenCypher 查詢，第一步是使用 NuHet 安裝 Neo4j 驅動程式。若要進行同步呼叫，請使用 `.Simple` 版本，如下所示：

```
Install-Package Neo4j.Driver.Simple-4.3.0
```

```
using Neo4j.Driver;

namespace hello
{
  // This example creates a node and reads a node in a Neptune
  // Cluster where IAM Authentication is not enabled.
  public class HelloWorldExample : IDisposable
  {
    private bool _disposed = false;
    private readonly IDriver _driver;
    private static string url = "bolt://(your cluster endpoint URL):{{(your cluster port)}}";
    private static string createNodeQuery = "CREATE (a:Greeting) SET a.message = 'HelloWorldExample'";
    private static string readNodeQuery = "MATCH(n:Greeting) RETURN n.message";

    ~HelloWorldExample() => Dispose(false);

    public HelloWorldExample(string uri)
    {
      _driver = GraphDatabase.Driver(uri, AuthTokens.None, o => o.WithEncryptionLevel(EncryptionLevel.Encrypted));
    }

    public void createNode()
    {
      // Open a session
      using (var session = _driver.Session())
      {
         // Run the query in a write transaction
        var greeting = session.WriteTransaction(tx =>
        {
          var result = tx.Run(createNodeQuery);
          // Consume the result
          return result.Consume();
        });

        // The output will look like this:
        //   ResultSummary{Query=`CREATE (a:Greeting) SET a.message = 'HelloWorldExample".....
        Console.WriteLine(greeting);
      }
    }

    public void retrieveNode()
    {
      // Open a session
      using (var session = _driver.Session())
      {
        // Run the query in a read transaction
        var greeting = session.ReadTransaction(tx =>
        {
          var result = tx.Run(readNodeQuery);
          // Consume the result. Read the single node
          // created in a previous step.
          return result.Single()[0].As<string>();
        });
        // The output will look like this:
        //   HelloWorldExample
        Console.WriteLine(greeting);
      }
    }

    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
      if (_disposed)
        return;
      if (disposing)
      {
        _driver?.Dispose();
      }
      _disposed = true;
    }

    public static void Main()
    {
      using (var apiCaller = new HelloWorldExample(url))
      {
        apiCaller.createNode();
        apiCaller.retrieveNode();
      }
    }
  }
}
```

## 搭配 IAM 身分驗證使用 Bolt 的 Java openCypher 查詢範例
<a name="access-graph-opencypher-bolt-java-iam-auth"></a>

下面的 Java 程式碼說明如何搭配 IAM 身分驗證使用 Bolt，在 Java 中進行 openCypher 查詢。JavaDoc 註釋會描述其用法。一旦驅動程式執行個體可用，您就可以使用它，提出多個已經過身分驗證的請求。

```
package software.amazon.neptune.bolt;

import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.http.HttpMethodName;
import com.google.gson.Gson;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.internal.security.InternalAuthToken;
import org.neo4j.driver.internal.value.StringValue;

import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static com.amazonaws.auth.internal.SignerConstants.AUTHORIZATION;
import static com.amazonaws.auth.internal.SignerConstants.HOST;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_DATE;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SECURITY_TOKEN;

/**
 * Use this class instead of `AuthTokens.basic` when working with an IAM
 * auth-enabled server. It works the same as `AuthTokens.basic` when using
 * static credentials, and avoids making requests with an expired signature
 * when using temporary credentials. Internally, it generates a new signature
 * on every invocation (this may change in a future implementation).
 *
 * Note that authentication happens only the first time for a pooled connection.
 *
 * Typical usage:
 *
 * NeptuneAuthToken authToken = NeptuneAuthToken.builder()
 *     .credentialsProvider(credentialsProvider)
 *     .region("aws region")
 *     .url("cluster endpoint url")
 *     .build();
 *
 * Driver driver = GraphDatabase.driver(
 *     authToken.getUrl(),
 *     authToken,
 *     config
 * );
 */

public class NeptuneAuthToken extends InternalAuthToken {
  private static final String SCHEME = "basic";
  private static final String REALM = "realm";
  private static final String SERVICE_NAME = "neptune-db";
  private static final String HTTP_METHOD_HDR = "HttpMethod";
  private static final String DUMMY_USERNAME = "username";
  @NonNull
  private final String region;
  @NonNull
  @Getter
  private final String url;
  @NonNull
  private final AWSCredentialsProvider credentialsProvider;
  private final Gson gson = new Gson();

  @Builder
  private NeptuneAuthToken(
      @NonNull final String region,
      @NonNull final String url,
      @NonNull final AWSCredentialsProvider credentialsProvider
  ) {
      // The superclass caches the result of toMap(), which we don't want
      super(Collections.emptyMap());
      this.region = region;
      this.url = url;
      this.credentialsProvider = credentialsProvider;
  }

  @Override
  public Map<String, Value> toMap() {
    final Map<String, Value> map = new HashMap<>();
    map.put(SCHEME_KEY, Values.value(SCHEME));
    map.put(PRINCIPAL_KEY, Values.value(DUMMY_USERNAME));
    map.put(CREDENTIALS_KEY, new StringValue(getSignedHeader()));
    map.put(REALM_KEY, Values.value(REALM));

    return map;
  }

  private String getSignedHeader() {
    final Request<Void> request = new DefaultRequest<>(SERVICE_NAME);
    request.setHttpMethod(HttpMethodName.GET);
    request.setEndpoint(URI.create(url));
    // Comment out the following line if you're using an engine version older than 1.2.0.0
    request.setResourcePath("/opencypher");

    final AWS4Signer signer = new AWS4Signer();
    signer.setRegionName(region);
    signer.setServiceName(request.getServiceName());
    signer.sign(request, credentialsProvider.getCredentials());

    return getAuthInfoJson(request);
  }

  private String getAuthInfoJson(final Request<Void> request) {
    final Map<String, Object> obj = new HashMap<>();
    obj.put(AUTHORIZATION, request.getHeaders().get(AUTHORIZATION));
    obj.put(HTTP_METHOD_HDR, request.getHttpMethod());
    obj.put(X_AMZ_DATE, request.getHeaders().get(X_AMZ_DATE));
    obj.put(HOST, request.getHeaders().get(HOST));
    obj.put(X_AMZ_SECURITY_TOKEN, request.getHeaders().get(X_AMZ_SECURITY_TOKEN));

    return gson.toJson(obj);
  }
}
```

## 搭配 IAM 身分驗證使用 Bolt 的 Python openCypher 查詢範例
<a name="access-graph-opencypher-bolt-python-iam-auth"></a>

下面的 Python 類別可讓您搭配 IAM 身分驗證使用 Bolt，在 Python 中進行 openCypher 查詢：

```
import json

from neo4j import Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
from botocore.auth import (
  SigV4Auth,
  _host_from_url,
)

SCHEME = "basic"
REALM = "realm"
SERVICE_NAME = "neptune-db"
DUMMY_USERNAME = "username"
HTTP_METHOD_HDR = "HttpMethod"
HTTP_METHOD = "GET"
AUTHORIZATION = "Authorization"
X_AMZ_DATE = "X-Amz-Date"
X_AMZ_SECURITY_TOKEN = "X-Amz-Security-Token"
HOST = "Host"


class NeptuneAuthToken(Auth):
  def __init__(
    self,
    credentials: Credentials,
    region: str,
    url: str,
    **parameters
  ):
    # Do NOT add "/opencypher" in the line below if you're using an engine version older than 1.2.0.0
    request = AWSRequest(method=HTTP_METHOD, url=url + "/opencypher")
    request.headers.add_header("Host", _host_from_url(request.url))
    sigv4 = SigV4Auth(credentials, SERVICE_NAME, region)
    sigv4.add_auth(request)

    auth_obj = {
      hdr: request.headers[hdr]
      for hdr in [AUTHORIZATION, X_AMZ_DATE, X_AMZ_SECURITY_TOKEN, HOST]
    }
    auth_obj[HTTP_METHOD_HDR] = request.method
    creds: str = json.dumps(auth_obj)
    super().__init__(SCHEME, DUMMY_USERNAME, creds, REALM, **parameters)
```

您可以使用這個類別來建立一個驅動程式，如下所示：

```
  authToken = NeptuneAuthToken(creds, REGION, URL)
  driver = GraphDatabase.driver(URL, auth=authToken, encrypted=True)
```

## 使用 IAM 身分驗證和 Bolt 的 Node.js 範例
<a name="access-graph-opencypher-bolt-nodejs-iam-auth"></a>

下面的 Node.js 程式碼使用適用於 JavaScript 的 AWS SDK 第 3 版和 ES6 語法來建立驗證請求的驅動程式：

```
import neo4j from "neo4j-driver";
import { HttpRequest }  from "@smithy/protocol-http";
import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { SignatureV4 } from "@smithy/signature-v4";
import crypto from "@aws-crypto/sha256-js";
const { Sha256 } = crypto;
import assert from "node:assert";

const region = "us-west-2";
const serviceName = "neptune-db";
const host = "(your cluster endpoint URL)";
const port = 8182;
const protocol = "bolt";
const hostPort = host + ":" + port;
const url = protocol + "://" + hostPort;
const createQuery = "CREATE (n:Greeting {message: 'Hello'}) RETURN ID(n)";
const readQuery = "MATCH(n:Greeting) WHERE ID(n) = $id RETURN n.message";

async function signedHeader() {
  const req = new HttpRequest({
    method: "GET",
    protocol: protocol,
    hostname: host,
    port: port,
    // Comment out the following line if you're using an engine version older than 1.2.0.0
    path: "/opencypher",
    headers: {
      host: hostPort
    }
  });

  const signer = new SignatureV4({
    credentials: defaultProvider(),
    region: region,
    service: serviceName,
    sha256: Sha256
  });

  return signer.sign(req, { unsignableHeaders: new Set(["x-amz-content-sha256"]) })
    .then((signedRequest) => {
      const authInfo = {
        "Authorization": signedRequest.headers["authorization"],
        "HttpMethod": signedRequest.method,
        "X-Amz-Date": signedRequest.headers["x-amz-date"],
        "Host": signedRequest.headers["host"],
        "X-Amz-Security-Token": signedRequest.headers["x-amz-security-token"]
      };
      return JSON.stringify(authInfo);
    });
}

async function createDriver() {
  let authToken = { scheme: "basic", realm: "realm", principal: "username", credentials: await signedHeader() };

  return neo4j.driver(url, authToken, {
      encrypted: "ENCRYPTION_ON",
      trust: "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES",
      maxConnectionPoolSize: 1,
      // logging: neo4j.logging.console("debug")
    }
  );
}

async function unmanagedTxn(driver) {
  const session = driver.session();
  const tx = session.beginTransaction();
  try {
    const created = await tx.run(createQuery);
    const matched = await tx.run(readQuery, { id: created.records[0].get(0) });
    const msg = matched.records[0].get("n.message");
    assert.equal(msg, "Hello");
    await tx.commit();
  } catch (err) {
    // The transaction will be rolled back, now handle the error.
    console.log(err);
  } finally {
    await session.close();
  }
}

const driver = await createDriver();
try {
  await unmanagedTxn(driver);
} catch (err) {
  console.log(err);
} finally {
  await driver.close();
}
```

## 搭配 IAM 身分驗證使用 Bolt 的 .NET openCypher 查詢範例
<a name="access-graph-opencypher-bolt-dotnet-iam-auth"></a>

若要在 .NET 中啟用 IAM 身分驗證，您需要在建立連線時簽署請求。下面的範例展示如何建立一個 `NeptuneAuthToken` 協助程式，來產生一個身分驗證權杖：

```
using Amazon.Runtime;
using Amazon.Util;
using Neo4j.Driver;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Web;

namespace Hello
{
  /*
   * Use this class instead of `AuthTokens.None` when working with an IAM-auth-enabled server.
   *
   * Note that authentication happens only the first time for a pooled connection.
   *
   * Typical usage:
   *
   * var authToken = new NeptuneAuthToken(AccessKey, SecretKey, Region).GetAuthToken(Host);
   * _driver = GraphDatabase.Driver(Url, authToken, o => o.WithEncryptionLevel(EncryptionLevel.Encrypted));
   */

  public class NeptuneAuthToken
  {
    private const string ServiceName = "neptune-db";
    private const string Scheme = "basic";
    private const string Realm = "realm";
    private const string DummyUserName = "username";
    private const string Algorithm = "AWS4-HMAC-SHA256";
    private const string AWSRequest = "aws4_request";

    private readonly string _accessKey;
    private readonly string _secretKey;
    private readonly string _region;

    private readonly string _emptyPayloadHash;

    private readonly SHA256 _sha256;


    public NeptuneAuthToken(string awsKey = null, string secretKey = null, string region = null)
    {
      var awsCredentials = awsKey == null || secretKey == null
        ? FallbackCredentialsFactory.GetCredentials().GetCredentials()
        : null;

      _accessKey = awsKey ?? awsCredentials.AccessKey;
      _secretKey = secretKey ?? awsCredentials.SecretKey;
      _region = region ?? FallbackRegionFactory.GetRegionEndpoint().SystemName; //ex: us-east-1

      _sha256 = SHA256.Create();
      _emptyPayloadHash = Hash(Array.Empty<byte>());
    }

    public IAuthToken GetAuthToken(string url)
    {
      return AuthTokens.Custom(DummyUserName, GetCredentials(url), Realm, Scheme);
    }

    /******************** AWS SIGNING FUNCTIONS *********************/
    private string Hash(byte[] bytesToHash)
    {
      return ToHexString(_sha256.ComputeHash(bytesToHash));
    }

    private static byte[] HmacSHA256(byte[] key, string data)
    {
      return new HMACSHA256(key).ComputeHash(Encoding.UTF8.GetBytes(data));
    }

    private byte[] GetSignatureKey(string dateStamp)
    {
      var kSecret = Encoding.UTF8.GetBytes($"AWS4{_secretKey}");
      var kDate = HmacSHA256(kSecret, dateStamp);
      var kRegion = HmacSHA256(kDate, _region);
      var kService = HmacSHA256(kRegion, ServiceName);
      return HmacSHA256(kService, AWSRequest);
    }

    private static string ToHexString(byte[] array)
    {
      return Convert.ToHexString(array).ToLowerInvariant();
    }

    private string GetCredentials(string url)
    {
      var request = new HttpRequestMessage
      {
        Method = HttpMethod.Get,
        RequestUri = new Uri($"https://{url}/opencypher")
      };

      var signedrequest = Sign(request);

      var headers = new Dictionary<string, object>
      {
        [HeaderKeys.AuthorizationHeader] = signedrequest.Headers.GetValues(HeaderKeys.AuthorizationHeader).FirstOrDefault(),
        ["HttpMethod"] = HttpMethod.Get.ToString(),
        [HeaderKeys.XAmzDateHeader] = signedrequest.Headers.GetValues(HeaderKeys.XAmzDateHeader).FirstOrDefault(),
        // Host should be capitalized, not like in Amazon.Util.HeaderKeys.HostHeader
        ["Host"] = signedrequest.Headers.GetValues(HeaderKeys.HostHeader).FirstOrDefault(),
      };

      return JsonSerializer.Serialize(headers);
    }

    private HttpRequestMessage Sign(HttpRequestMessage request)
    {
      var now = DateTimeOffset.UtcNow;
      var amzdate = now.ToString("yyyyMMddTHHmmssZ");
      var datestamp = now.ToString("yyyyMMdd");

      if (request.Headers.Host == null)
      {
        request.Headers.Host = $"{request.RequestUri.Host}:{request.RequestUri.Port}";
      }

      request.Headers.Add(HeaderKeys.XAmzDateHeader, amzdate);

      var canonicalQueryParams = GetCanonicalQueryParams(request);

      var canonicalRequest = new StringBuilder();
      canonicalRequest.Append(request.Method + "\n");
      canonicalRequest.Append(request.RequestUri.AbsolutePath + "\n");
      canonicalRequest.Append(canonicalQueryParams + "\n");

      var signedHeadersList = new List<string>();
      foreach (var header in request.Headers.OrderBy(a => a.Key.ToLowerInvariant()))
      {
        canonicalRequest.Append(header.Key.ToLowerInvariant());
        canonicalRequest.Append(':');
        canonicalRequest.Append(string.Join(",", header.Value.Select(s => s.Trim())));
        canonicalRequest.Append('\n');
        signedHeadersList.Add(header.Key.ToLowerInvariant());
      }
      canonicalRequest.Append('\n');

      var signedHeaders = string.Join(";", signedHeadersList);
      canonicalRequest.Append(signedHeaders + "\n");
      canonicalRequest.Append(_emptyPayloadHash);

      var credentialScope = $"{datestamp}/{_region}/{ServiceName}/{AWSRequest}";
      var stringToSign = $"{Algorithm}\n{amzdate}\n{credentialScope}\n"
        + Hash(Encoding.UTF8.GetBytes(canonicalRequest.ToString()));

      var signing_key = GetSignatureKey(datestamp);
      var signature = ToHexString(HmacSHA256(signing_key, stringToSign));

      request.Headers.TryAddWithoutValidation(HeaderKeys.AuthorizationHeader,
        $"{Algorithm} Credential={_accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}");

      return request;
    }

    private static string GetCanonicalQueryParams(HttpRequestMessage request)
    {
      var querystring = HttpUtility.ParseQueryString(request.RequestUri.Query);

      // Query params must be escaped in upper case (i.e. "%2C", not "%2c").
      var queryParams = querystring.AllKeys.OrderBy(a => a)
        .Select(key => $"{key}={Uri.EscapeDataString(querystring[key])}");
      return string.Join("&", queryParams);
    }
  }
}
```

以下是如何搭配 IAM 身分驗證使用 Bolt，在 .NET 中進行 OpenCypher 查詢。下面的範例會使用 `NeptuneAuthToken` 協助程式：

```
using Neo4j.Driver;

namespace Hello
{
  public class HelloWorldExample
  {
    private const string Host = "{{(your hostname)}}:8182";
    private const string Url = $"bolt://{Host}";
    private const string CreateNodeQuery = "CREATE (a:Greeting) SET a.message = 'HelloWorldExample'";
    private const string ReadNodeQuery = "MATCH(n:Greeting) RETURN n.message";

    private const string AccessKey = "{{(your access key)}}";
    private const string SecretKey = "{{(your secret key)}}";
    private const string Region = "{{(your AWS region)}}"; // e.g. "us-west-2"

    private readonly IDriver _driver;

    public HelloWorldExample()
    {
      var authToken = new NeptuneAuthToken(AccessKey, SecretKey, Region).GetAuthToken(Host);

      // Note that when the connection is reinitialized after max connection lifetime
      // has been reached, the signature token could have already been expired (usually 5 min)
      // You can face exceptions like:
      //   `Unexpected server exception 'Signature expired: XXXX is now earlier than YYYY (ZZZZ - 5 min.)`
      _driver = GraphDatabase.Driver(Url, authToken, o =>
                o.WithMaxConnectionLifetime(TimeSpan.FromMinutes(60)).WithEncryptionLevel(EncryptionLevel.Encrypted));
    }

    public async Task CreateNode()
    {
      // Open a session
      using (var session = _driver.AsyncSession())
      {
        // Run the query in a write transaction
        var greeting = await session.WriteTransactionAsync(async tx =>
        {
          var result = await tx.RunAsync(CreateNodeQuery);
          // Consume the result
          return await result.ConsumeAsync();
        });

        // The output will look like this:
        //   ResultSummary{Query=`CREATE (a:Greeting) SET a.message = 'HelloWorldExample".....
        Console.WriteLine(greeting.Query);
      }
    }

    public async Task RetrieveNode()
    {
      // Open a session
      using (var session = _driver.AsyncSession())
      {
        // Run the query in a read transaction
        var greeting = await session.ReadTransactionAsync(async tx =>
        {
          var result = await tx.RunAsync(ReadNodeQuery);
          var records = await result.ToListAsync();

          // Consume the result. Read the single node
          // created in a previous step.
          return records[0].Values.First().Value;
        });
        // The output will look like this:
        //   HelloWorldExample
        Console.WriteLine(greeting);
      }
    }
  }
}
```

您可以在具有下列套件的 `.NET 6` 或 `.NET 7` 上執行下面的程式碼來啟動此範例：
+ **`Neo4j`**`.Driver=4.3.0`
+ **`AWSSDK`**`.Core=3.7.102.1`

```
namespace Hello
{
  class Program
  {
    static async Task Main()
    {
      var apiCaller = new HelloWorldExample();

      await apiCaller.CreateNode();
      await apiCaller.RetrieveNode();
    }
  }
}
```

## 搭配 IAM 身分驗證使用 Bolt 的 Golang openCypher 查詢範例
<a name="access-graph-opencypher-bolt-golang-iam-auth"></a>

下列範例示範如何使用 Bolt 通訊協定搭配 IAM 身分驗證，在 Go 中進行 openCypher 查詢。它使用 AWS SDK for Go v2 for SigV4 簽署和 Neo4j Go 驅動程式 v5 搭配實作 Neo4j Go 驅動程式字符管理器界面 (`github.com/neo4j/neo4j-go-driver/v5/neo4j/auth.TokenManager`) `AuthTokenManager` 的結構，在憑證過期之前自動重新整理憑證。

首先，建立`AuthTokenManager`產生 SigV4-signed字符的 。將此儲存為 `auth_token_manager.go`：

```
// AuthTokenManager for Amazon Neptune IAM authentication via the Bolt protocol.
// Provides SigV4-signed credentials to the Neo4j driver's auth interface.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
	"github.com/neo4j/neo4j-go-driver/v5/neo4j/db"
)

const (
	serviceName      = "neptune-db"
	emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)

// AuthTokenManager manages SigV4-signed authentication tokens for Neptune with automatic refresh.
type AuthTokenManager struct {
	region          string
	endpoint        string
	refreshInterval time.Duration
	credentials     aws.CredentialsProvider
	mutex           sync.Mutex
	cachedToken     neo4j.AuthToken
	tokenTime       time.Time
}

// NewAuthTokenManager creates a new AuthTokenManager.
//
// Parameters:
//   - region: AWS region (e.g., "us-east-1")
//   - endpoint: Neptune endpoint with port (e.g., "cluster.region.neptune.amazonaws.com:8182")
//   - profile: AWS profile name (optional, pass "" to use default)
//   - roleArn: AWS role ARN to assume (optional, pass "" to skip)
//   - refreshInterval: Token refresh interval
func NewAuthTokenManager(region, endpoint, profile, roleArn string, refreshInterval time.Duration) (*AuthTokenManager, error) {
	credentials, err := createCredentialsProvider(region, profile, roleArn)
	if err != nil {
		return nil, err
	}
	return &AuthTokenManager{
		region:          region,
		endpoint:        endpoint,
		refreshInterval: refreshInterval,
		credentials:     credentials,
	}, nil
}

// createCredentialsProvider builds an AWS credentials provider, optionally using
// a named profile and/or assuming a role.
func createCredentialsProvider(region, profile, roleArn string) (aws.CredentialsProvider, error) {
	ctx := context.Background()
	var opts []func(*config.LoadOptions) error
	opts = append(opts, config.WithRegion(region))
	if profile != "" {
		opts = append(opts, config.WithSharedConfigProfile(profile))
	}
	cfg, err := config.LoadDefaultConfig(ctx, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to load AWS config: %w", err)
	}
	var credentials aws.CredentialsProvider = cfg.Credentials
	if roleArn != "" {
		stsClient := sts.NewFromConfig(cfg)
		credentials = stscreds.NewAssumeRoleProvider(stsClient, roleArn, func(o *stscreds.AssumeRoleOptions) {
			o.RoleSessionName = "NeptuneAuthSession"
			o.Duration = 900 * time.Second
		})
	}
	return credentials, nil
}

// GetAuthToken returns a valid authentication token, using cached token if still valid.
func (m *AuthTokenManager) GetAuthToken(ctx context.Context) (neo4j.AuthToken, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	if time.Since(m.tokenTime) < m.refreshInterval && m.cachedToken.Tokens != nil {
		return m.cachedToken, nil
	}

	token, err := m.generateToken(ctx)
	if err != nil {
		return neo4j.AuthToken{}, err
	}

	m.cachedToken = token
	m.tokenTime = time.Now()
	return token, nil
}

// HandleSecurityException handles security exceptions by invalidating the cached
// token (if it matches the token that caused the error) and returning true so
// the driver retries with a fresh token. The comparison prevents a concurrent
// retry from unnecessarily invalidating a freshly generated token.
func (m *AuthTokenManager) HandleSecurityException(ctx context.Context, token neo4j.AuthToken, err *db.Neo4jError) (bool, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if reflect.DeepEqual(m.cachedToken.Tokens, token.Tokens) {
		m.tokenTime = time.Time{}
		m.cachedToken = neo4j.AuthToken{}
	}
	return true, nil
}

// generateToken generates a new SigV4-signed authentication token for Neptune.
func (m *AuthTokenManager) generateToken(ctx context.Context) (neo4j.AuthToken, error) {
	req, err := http.NewRequest(http.MethodGet, "https://"+m.endpoint+"/opencypher", nil)
	if err != nil {
		return neo4j.AuthToken{}, err
	}
	req.Host = m.endpoint

	creds, err := m.credentials.Retrieve(ctx)
	if err != nil {
		return neo4j.AuthToken{}, err
	}

	signer := v4.NewSigner()
	err = signer.SignHTTP(ctx, creds, req, emptyPayloadHash, serviceName, m.region, time.Now())
	if err != nil {
		return neo4j.AuthToken{}, err
	}

	authData := map[string]string{
		"Authorization": req.Header.Get("Authorization"),
		"X-Amz-Date":    req.Header.Get("X-Amz-Date"),
		"Host":          m.endpoint,
		"HttpMethod":    req.Method,
	}

	if st := req.Header.Get("X-Amz-Security-Token"); st != "" {
		authData["X-Amz-Security-Token"] = st
	}

	authJSON, err := json.Marshal(authData)
	if err != nil {
		return neo4j.AuthToken{}, err
	}

	return neo4j.BasicAuth("username", string(authJSON), ""), nil
}
```

然後使用權杖管理員來建立驅動程式，並依 ID 尋找節點。請注意，使用參數化查詢 (`$nodeId`) 而非字串插補：

```
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func findNode(ctx context.Context, driver neo4j.DriverWithContext, nodeId string) (string, error) {
	session := driver.NewSession(ctx, neo4j.SessionConfig{
		AccessMode: neo4j.AccessModeRead,
	})
	defer session.Close(ctx)

	// Use parameterized queries to prevent injection and enable query plan caching.
	result, err := session.Run(ctx,
		"MATCH (n) WHERE ID(n) = $nodeId RETURN n",
		map[string]any{"nodeId": nodeId},
	)
	if err != nil {
		return "", fmt.Errorf("error running query: %v", err)
	}

	if !result.Next(ctx) {
		if err = result.Err(); err != nil {
			return "", fmt.Errorf("error fetching result: %v", err)
		}
		return "", fmt.Errorf("node not found")
	}

	n, found := result.Record().Get("n")
	if !found {
		return "", fmt.Errorf("node not found")
	}

	return fmt.Sprintf("%+v", n), nil
}

func main() {
	region := "(your AWS region)"                  // e.g. "us-east-1"
	endpoint := "(your Neptune endpoint):8182"     // e.g. "cluster.xxx.us-east-1.neptune.amazonaws.com:8182"

	ctx := context.Background()

	// Pass a profile name for local development, or a role ARN for cross-account access
	authManager, err := NewAuthTokenManager(region, endpoint, "", "", 4*time.Minute) // Refresh before the 5-minute SigV4 signature expiry
	if err != nil {
		log.Fatalf("auth manager error: %v", err)
	}

	// bolt+s:// enables TLS with full certificate verification.
	// If you're developing on macOS, use bolt+ssc:// instead. Go on macOS uses the
	// system TLS verifier, which requires Certificate Transparency compliance that
	// Neptune endpoints don't support. In production (typically Linux), bolt+s://
	// works with system CA certificates.
	driver, err := neo4j.NewDriverWithContext("bolt+s://"+endpoint, authManager)
	if err != nil {
		log.Fatalf("driver error: %v", err)
	}
	defer driver.Close(ctx)

	if err = driver.VerifyConnectivity(ctx); err != nil {
		log.Fatalf("connectivity error: %v", err)
	}

	// Neptune assigns UUID-format node IDs if a user does not supply their own node IDs
	res, err := findNode(ctx, driver, "72c2e8c1-7d5f-5f30-10ca-9d2bb8c4afbc")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res)
}
```

**注意**  
此範例中使用的`auth.TokenManager`界面 (`github.com/neo4j/neo4j-go-driver/v5/neo4j/auth`) 已在 Neo4j Go 驅動程式 v**5.14.0** 中正式推出。此界面會啟用自動登入資料重新整理，這是 Neptune IAM 身分驗證的必要功能，因為 SigV4 簽章僅在短時間內有效，而且必須在驅動程式建立新的連線時重新產生。

此範例已使用下列 Go 模組進行驗證：

```
require (
    github.com/aws/aws-sdk-go-v2         v1.30.3
    github.com/aws/aws-sdk-go-v2/config   v1.27.27
    github.com/aws/aws-sdk-go-v2/credentials v1.17.27
    github.com/aws/aws-sdk-go-v2/service/sts v1.30.3
    github.com/neo4j/neo4j-go-driver/v5   v5.22.0
  )
```

## Neptune 中的 Bolt 連線行為
<a name="access-graph-opencypher-bolt-connections"></a>

以下是一些要謹記的 Neptune Bolt 連線相關事項：
+ 因為 Bolt 連線是在 TCP 層建立的，所以您無法像使用 HTTP 端點一樣，在它們前面使用 [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html)。
+ Neptune 用於 Bolt 連線的連接埠是您資料庫叢集的連接埠。
+ Neptune 伺服器會根據傳遞給它的 Bolt 前導碼，選取最高的 Bolt 版本 (1、2、3 或 4.0)。
+ 用戶端可以在任何時間點開啟的 Neptune 伺服器連線數目上限為 1,000。
+ 如果用戶端在查詢之後沒有關閉連線，則該連線可以用來執行下一個查詢。
+ 不過，如果連線閒置 20 分鐘，伺服器會自動將其關閉。
+ 如果未啟用 IAM 身分驗證，您可以使用 `AuthTokens.none()`，而不是提供虛擬使用者名稱和密碼。例如，在 Java 中：

  ```
  GraphDatabase.driver("bolt://{{(your cluster endpoint URL)}}:{{(your cluster port)}}", AuthTokens.none(),
      Config.builder().withEncryption().withTrustStrategy(TrustStrategy.trustSystemCertificates()).build());
  ```
+ 啟用 IAM 身分驗證時，如果 Bolt 連線由於其他一些原因而尚未關閉，則 Bolt 連線一律會在建立 10 天之後的幾分鐘內中斷連線。
+ 如果用戶端傳送查詢以透過連線執行，而未取用先前查詢的結果，則會捨棄新查詢。若要改為捨棄先前的結果，用戶端必須透過連線傳送重設訊息。
+ 在給定連線上一次只能建立一個交易。
+ 如果在交易期間發生例外狀況，Neptune 伺服器會復原交易並關閉連線。在此情況下，驅動程式會為下一個查詢建立一個新連線。
+ 請注意，工作階段不是執行緒安全的工作階段。多個平行操作必須使用多個個別的工作階段。