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 클러스터에 연결하기 위한 사전 조건입니다.

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 일반 참조서명 버전 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 클러스터에 대한 연결 암호화 섹션을 참조하세요. SSL 인증서는 예제 실행 시 클래스 로더가 인증서를 찾을 수 있도록 이 Java 프로그램 파일과 동일한 디렉터리에 설치합니다.

다음은 기본 자격 증명 공급자 체인에서 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 인증을 사용하여 프록시에 연결을 참조하세요.