IAM 認証および AWS SDK for Java を使用した DB クラスターへの接続 - Amazon Aurora

IAM 認証および AWS SDK for Java を使用した DB クラスターへの接続

次に説明するように、AWS SDK for Java を使用して、Aurora MySQL もしくは Aurora PostgreSQL DB クラスターに接続できます。

前提条件

IAM 認証を使用して DB クラスターに接続するための前提条件は以下のとおりです。

SDK for Java 2.x の使用方法の例については、「SDK for Java 2.x を使用した Amazon RDS の例」を参照してください。

IAM 認証トークンの生成

AWS SDK for Java を使用してプログラムを作成する場合、RdsIamAuthTokenGenerator クラスを使用して署名付き認証トークンを取得できます。このクラスを使用するには、AWS 認証情報を提供する必要があります。これを行うには、DefaultAWSCredentialsProviderChain クラスのインスタンスを作成します。DefaultAWSCredentialsProviderChain は、デフォルトの認証情報プロバイダチェーンで見つける初期の AWS のアクセスキーとシークレットキーを使用します。AWS アクセスキーの詳細については、「ユーザーのアクセスキーの管理」を参照してください。

注記

DB クラスターエンドポイントの代わりに、カスタム Route 53 DNS レコードまたは Aurora カスタムエンドポイントを使用して認証トークンを生成することはできません。

RdsIamAuthTokenGenerator のインスタンスを作成した後、getAuthToken メソッドを呼び出して、署名済みトークンを取得できます。AWS リージョン、ホスト名、ポート番号、およびユーザー名を指定します。次のコード例はこれを行う方法を示しています。

package com.amazonaws.codesamples; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.rds.auth.GetIamAuthTokenRequest; import com.amazonaws.services.rds.auth.RdsIamAuthTokenGenerator; public class GenerateRDSAuthToken { public static void main(String[] args) { String region = "us-west-2"; String hostname = "rdsmysql.123456789012.us-west-2.rds.amazonaws.com"; String port = "3306"; String username = "jane_doe"; System.out.println(generateAuthToken(region, hostname, port, username)); } static String generateAuthToken(String region, String hostName, String port, String username) { RdsIamAuthTokenGenerator generator = RdsIamAuthTokenGenerator.builder() .credentials(new DefaultAWSCredentialsProviderChain()) .region(region) .build(); String authToken = generator.getAuthToken( GetIamAuthTokenRequest.builder() .hostname(hostName) .port(Integer.parseInt(port)) .userName(username) .build()); return authToken; } }

IAM 認証トークンを手動で構築する

Java では、認証トークンを生成するための最も簡単な方法は、RdsIamAuthTokenGenerator を使用することです。このクラスは、認証トークンを作成した後、AWS 署名バージョン 4 を使用してサインインします。詳細については、AWS 全般のリファレンスの「Signature Version 4 の署名プロセス」を参照してください。

ただし、次のコード例に示すように、認証トークンを手動で構築して署名できます。

package com.amazonaws.codesamples; import com.amazonaws.SdkClientException; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.auth.SigningAlgorithm; import com.amazonaws.util.BinaryUtils; import org.apache.commons.lang3.StringUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.SortedMap; import java.util.TreeMap; import static com.amazonaws.auth.internal.SignerConstants.AWS4_TERMINATOR; import static com.amazonaws.util.StringUtils.UTF8; public class CreateRDSAuthTokenManually { public static String httpMethod = "GET"; public static String action = "connect"; public static String canonicalURIParameter = "/"; public static SortedMap<String, String> canonicalQueryParameters = new TreeMap(); public static String payload = StringUtils.EMPTY; public static String signedHeader = "host"; public static String algorithm = "AWS4-HMAC-SHA256"; public static String serviceName = "rds-db"; public static String requestWithoutSignature; public static void main(String[] args) throws Exception { String region = "us-west-2"; String instanceName = "rdsmysql.123456789012.us-west-2.rds.amazonaws.com"; String port = "3306"; String username = "jane_doe"; Date now = new Date(); String date = new SimpleDateFormat("yyyyMMdd").format(now); String dateTimeStamp = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'").format(now); DefaultAWSCredentialsProviderChain creds = new DefaultAWSCredentialsProviderChain(); String awsAccessKey = creds.getCredentials().getAWSAccessKeyId(); String awsSecretKey = creds.getCredentials().getAWSSecretKey(); String expiryMinutes = "900"; System.out.println("Step 1: Create a canonical request:"); String canonicalString = createCanonicalString(username, awsAccessKey, date, dateTimeStamp, region, expiryMinutes, instanceName, port); System.out.println(canonicalString); System.out.println(); System.out.println("Step 2: Create a string to sign:"); String stringToSign = createStringToSign(dateTimeStamp, canonicalString, awsAccessKey, date, region); System.out.println(stringToSign); System.out.println(); System.out.println("Step 3: Calculate the signature:"); String signature = BinaryUtils.toHex(calculateSignature(stringToSign, newSigningKey(awsSecretKey, date, region, serviceName))); System.out.println(signature); System.out.println(); System.out.println("Step 4: Add the signing info to the request"); System.out.println(appendSignature(signature)); System.out.println(); } //Step 1: Create a canonical request date should be in format YYYYMMDD and dateTime should be in format YYYYMMDDTHHMMSSZ public static String createCanonicalString(String user, String accessKey, String date, String dateTime, String region, String expiryPeriod, String hostName, String port) throws Exception { canonicalQueryParameters.put("Action", action); canonicalQueryParameters.put("DBUser", user); canonicalQueryParameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); canonicalQueryParameters.put("X-Amz-Credential", accessKey + "%2F" + date + "%2F" + region + "%2F" + serviceName + "%2Faws4_request"); canonicalQueryParameters.put("X-Amz-Date", dateTime); canonicalQueryParameters.put("X-Amz-Expires", expiryPeriod); canonicalQueryParameters.put("X-Amz-SignedHeaders", signedHeader); String canonicalQueryString = ""; while(!canonicalQueryParameters.isEmpty()) { String currentQueryParameter = canonicalQueryParameters.firstKey(); String currentQueryParameterValue = canonicalQueryParameters.remove(currentQueryParameter); canonicalQueryString = canonicalQueryString + currentQueryParameter + "=" + currentQueryParameterValue; if (!currentQueryParameter.equals("X-Amz-SignedHeaders")) { canonicalQueryString += "&"; } } String canonicalHeaders = "host:" + hostName + ":" + port + '\n'; requestWithoutSignature = hostName + ":" + port + "/?" + canonicalQueryString; String hashedPayload = BinaryUtils.toHex(hash(payload)); return httpMethod + '\n' + canonicalURIParameter + '\n' + canonicalQueryString + '\n' + canonicalHeaders + '\n' + signedHeader + '\n' + hashedPayload; } //Step 2: Create a string to sign using sig v4 public static String createStringToSign(String dateTime, String canonicalRequest, String accessKey, String date, String region) throws Exception { String credentialScope = date + "/" + region + "/" + serviceName + "/aws4_request"; return algorithm + '\n' + dateTime + '\n' + credentialScope + '\n' + BinaryUtils.toHex(hash(canonicalRequest)); } //Step 3: Calculate signature /** * Step 3 of the &AWS; Signature version 4 calculation. It involves deriving * the signing key and computing the signature. Refer to * http://docs.aws.amazon * .com/general/latest/gr/sigv4-calculate-signature.html */ public static byte[] calculateSignature(String stringToSign, byte[] signingKey) { return sign(stringToSign.getBytes(Charset.forName("UTF-8")), signingKey, SigningAlgorithm.HmacSHA256); } public static byte[] sign(byte[] data, byte[] key, SigningAlgorithm algorithm) throws SdkClientException { try { Mac mac = algorithm.getMac(); mac.init(new SecretKeySpec(key, algorithm.toString())); return mac.doFinal(data); } catch (Exception e) { throw new SdkClientException( "Unable to calculate a request signature: " + e.getMessage(), e); } } public static byte[] newSigningKey(String secretKey, String dateStamp, String regionName, String serviceName) { byte[] kSecret = ("AWS4" + secretKey).getBytes(Charset.forName("UTF-8")); byte[] kDate = sign(dateStamp, kSecret, SigningAlgorithm.HmacSHA256); byte[] kRegion = sign(regionName, kDate, SigningAlgorithm.HmacSHA256); byte[] kService = sign(serviceName, kRegion, SigningAlgorithm.HmacSHA256); return sign(AWS4_TERMINATOR, kService, SigningAlgorithm.HmacSHA256); } public static byte[] sign(String stringData, byte[] key, SigningAlgorithm algorithm) throws SdkClientException { try { byte[] data = stringData.getBytes(UTF8); return sign(data, key, algorithm); } catch (Exception e) { throw new SdkClientException( "Unable to calculate a request signature: " + e.getMessage(), e); } } //Step 4: append the signature public static String appendSignature(String signature) { return requestWithoutSignature + "&X-Amz-Signature=" + signature; } public static byte[] hash(String s) throws Exception { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(s.getBytes(UTF8)); return md.digest(); } catch (Exception e) { throw new SdkClientException( "Unable to compute hash while signing request: " + e.getMessage(), e); } } }

DB クラスターへの接続

次のコード例では、認証トークンを生成し、それを使用して Aurora MySQL を実行しているクラスターに接続する方法を示しています。

このコードサンプルを実行するには、AWS SDK for Java サイトにある AWS が必要です。また、以下が必要になります。

  • MySQL Connector/J。 このコードの例は mysql-connector-java-5.1.33-bin.jar でテストされています。

  • AWS リージョンに固有の、Amazon Aurora の中間証明書。(詳細については、SSL/TLS を使用した DB クラスターへの接続の暗号化 を参照してください)。クラスローダーは、実行時にこの Java コード例と同じディレクトリで証明書を探し、クラスローダーがその証明書を見つけられるようにします。

  • 必要に応じて以下の可変の値を変更します。

    • RDS_INSTANCE_HOSTNAME - アクセス先の DB クラスターのホスト名。

    • RDS_INSTANCE_PORT - PostgreSQL DB クラスターへの接続に使用されるポート番号。

    • REGION_NAME - DB クラスターが実行中の AWS リージョン

    • DB_USER - アクセス先のデータベースアカウント。

    • SSL_CERTIFICATE - AWS リージョンに固有の、Amazon Aurora の SSL 証明書。

      AWS リージョンの証明書をダウンロードするには、SSL/TLS を使用した DB クラスターへの接続の暗号化 を参照してください。この Java プログラムファイルと同じディレクトリに SSL 証明書を配置し、クラスローダーが実行時にその証明書を見つけられるようにします。

このコード例では、デフォルトの認証情報プロバイダチェーンから AWS 認証情報を取得します。

注記

セキュリティ上のベストプラクティスとして、ここに示されているプロンプト以外の DEFAULT_KEY_STORE_PASSWORD のパスワードを指定してください。

package com.amazonaws.samples; import com.amazonaws.services.rds.auth.RdsIamAuthTokenGenerator; import com.amazonaws.services.rds.auth.GetIamAuthTokenRequest; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.auth.AWSStaticCredentialsProvider; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.security.KeyStore; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; import java.net.URL; public class IAMDatabaseAuthenticationTester { //&AWS; Credentials of the IAM user with policy enabling IAM Database Authenticated access to the db by the db user. private static final DefaultAWSCredentialsProviderChain creds = new DefaultAWSCredentialsProviderChain(); private static final String AWS_ACCESS_KEY = creds.getCredentials().getAWSAccessKeyId(); private static final String AWS_SECRET_KEY = creds.getCredentials().getAWSSecretKey(); //Configuration parameters for the generation of the IAM Database Authentication token private static final String RDS_INSTANCE_HOSTNAME = "rdsmysql.123456789012.us-west-2.rds.amazonaws.com"; private static final int RDS_INSTANCE_PORT = 3306; private static final String REGION_NAME = "us-west-2"; private static final String DB_USER = "jane_doe"; private static final String JDBC_URL = "jdbc:mysql://" + RDS_INSTANCE_HOSTNAME + ":" + RDS_INSTANCE_PORT; private static final String SSL_CERTIFICATE = "rds-ca-2019-us-west-2.pem"; private static final String KEY_STORE_TYPE = "JKS"; private static final String KEY_STORE_PROVIDER = "SUN"; private static final String KEY_STORE_FILE_PREFIX = "sys-connect-via-ssl-test-cacerts"; private static final String KEY_STORE_FILE_SUFFIX = ".jks"; private static final String DEFAULT_KEY_STORE_PASSWORD = "changeit"; public static void main(String[] args) throws Exception { //get the connection Connection connection = getDBConnectionUsingIam(); //verify the connection is successful Statement stmt= connection.createStatement(); ResultSet rs=stmt.executeQuery("SELECT 'Success!' FROM DUAL;"); while (rs.next()) { String id = rs.getString(1); System.out.println(id); //Should print "Success!" } //close the connection stmt.close(); connection.close(); clearSslProperties(); } /** * This method returns a connection to the db instance authenticated using IAM Database Authentication * @return * @throws Exception */ private static Connection getDBConnectionUsingIam() throws Exception { setSslProperties(); return DriverManager.getConnection(JDBC_URL, setMySqlConnectionProperties()); } /** * This method sets the mysql connection properties which includes the IAM Database Authentication token * as the password. It also specifies that SSL verification is required. * @return */ private static Properties setMySqlConnectionProperties() { Properties mysqlConnectionProperties = new Properties(); mysqlConnectionProperties.setProperty("verifyServerCertificate","true"); mysqlConnectionProperties.setProperty("useSSL", "true"); mysqlConnectionProperties.setProperty("user",DB_USER); mysqlConnectionProperties.setProperty("password",generateAuthToken()); return mysqlConnectionProperties; } /** * This method generates the IAM Auth Token. * An example IAM Auth Token would look like follows: * btusi123.cmz7kenwo2ye.rds.cn-north-1.amazonaws.com.cn:3306/?Action=connect&DBUser=iamtestuser&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20171003T010726Z&X-Amz-SignedHeaders=host&X-Amz-Expires=899&X-Amz-Credential=AKIAPFXHGVDI5RNFO4AQ%2F20171003%2Fcn-north-1%2Frds-db%2Faws4_request&X-Amz-Signature=f9f45ef96c1f770cdad11a53e33ffa4c3730bc03fdee820cfdf1322eed15483b * @return */ private static String generateAuthToken() { BasicAWSCredentials awsCredentials = new BasicAWSCredentials(AWS_ACCESS_KEY, AWS_SECRET_KEY); RdsIamAuthTokenGenerator generator = RdsIamAuthTokenGenerator.builder() .credentials(new AWSStaticCredentialsProvider(awsCredentials)).region(REGION_NAME).build(); return generator.getAuthToken(GetIamAuthTokenRequest.builder() .hostname(RDS_INSTANCE_HOSTNAME).port(RDS_INSTANCE_PORT).userName(DB_USER).build()); } /** * This method sets the SSL properties which specify the key store file, its type and password: * @throws Exception */ private static void setSslProperties() throws Exception { System.setProperty("javax.net.ssl.trustStore", createKeyStoreFile()); System.setProperty("javax.net.ssl.trustStoreType", KEY_STORE_TYPE); System.setProperty("javax.net.ssl.trustStorePassword", DEFAULT_KEY_STORE_PASSWORD); } /** * This method returns the path of the Key Store File needed for the SSL verification during the IAM Database Authentication to * the db instance. * @return * @throws Exception */ private static String createKeyStoreFile() throws Exception { return createKeyStoreFile(createCertificate()).getPath(); } /** * This method generates the SSL certificate * @return * @throws Exception */ private static X509Certificate createCertificate() throws Exception { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); URL url = new File(SSL_CERTIFICATE).toURI().toURL(); if (url == null) { throw new Exception(); } try (InputStream certInputStream = url.openStream()) { return (X509Certificate) certFactory.generateCertificate(certInputStream); } } /** * This method creates the Key Store File * @param rootX509Certificate - the SSL certificate to be stored in the KeyStore * @return * @throws Exception */ private static File createKeyStoreFile(X509Certificate rootX509Certificate) throws Exception { File keyStoreFile = File.createTempFile(KEY_STORE_FILE_PREFIX, KEY_STORE_FILE_SUFFIX); try (FileOutputStream fos = new FileOutputStream(keyStoreFile.getPath())) { KeyStore ks = KeyStore.getInstance(KEY_STORE_TYPE, KEY_STORE_PROVIDER); ks.load(null); ks.setCertificateEntry("rootCaCertificate", rootX509Certificate); ks.store(fos, DEFAULT_KEY_STORE_PASSWORD.toCharArray()); } return keyStoreFile; } /** * This method clears the SSL properties. * @throws Exception */ private static void clearSslProperties() throws Exception { System.clearProperty("javax.net.ssl.trustStore"); System.clearProperty("javax.net.ssl.trustStoreType"); System.clearProperty("javax.net.ssl.trustStorePassword"); } }

プロキシ経由で DB クラスターに接続する場合は、「IAM 認証を使用したプロキシへの接続」を参照してください。