Java-Beispiel für Absicherung eines Requests mittels STS Token

Inzwischen ist der letzte Artikel meiner kleinen Serie auf JAXenter erschienen. Dieser beschäftigt sich mit den Themen Authentisierung und Autorisierung und schließt mit einem kleinen Fazit. In diesem Artikel habe ich auch den Java-Quellcode für das Absichern eines Requests unter Verwendung von Amazon STS versprochen. Hier ist er:

 

package com.axxessio.axx2sls.integration;

import static org.junit.Assert.*;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.axxessio.axx2sls.integration.to.CredentialsTO;
import com.fasterxml.jackson.databind.ObjectMapper;

public class GetAreasWithLogin {
	private static ObjectMapper mapper = new ObjectMapper();

	@BeforeClass
	public static void setUpBeforeClass() throws Exception {
	}

	@AfterClass
	public static void tearDownAfterClass() throws Exception {
	}

	@Before
	public void setUp() throws Exception {
	}

	@After
	public void tearDown() throws Exception {
	}

	@Test
	public void test() throws Exception {
		byte[] bodyHash = null;
		byte[] requestHash = null;
		byte[] signatureKey = null;
		
		String authHeader = null;
		String body = null;
		String request = null;
		String stringToSign = null;
		
		CredentialsTO cto = null;
		Date now;
		DateFormat dfDateTime = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
		DateFormat dfDate = new SimpleDateFormat("yyyyMMdd");

		CloseableHttpClient httpclient = HttpClients.createDefault();
		CloseableHttpResponse response = null;
		HttpPost httpPost = null;
		HttpEntity entity = null;

		dfDateTime.setTimeZone(TimeZone.getTimeZone("GMT"));
		dfDate.setTimeZone(TimeZone.getTimeZone("GMT"));

		body = "{\"name\" : \"admin\",\"password\" : \"#######################\"}";
		
		httpPost = new HttpPost("https://a4sbwzghm9.execute-api.eu-central-1.amazonaws.com/prd/login/account");
		entity = new ByteArrayEntity(body.getBytes("UTF-8"));
		httpPost.setEntity(entity);

		httpPost.addHeader("Content-Type", "application/json");

		response = httpclient.execute(httpPost);
		
		cto = mapper.readValue(readResponse(response), CredentialsTO.class);

		body = "{\"atos\":[{\"id\":null, \"name\":\"axxessio\", \"folder\":null, \"number\":0, \"size\":0, \"date\":null, \"version\":null}]}";

		httpPost = new HttpPost("https://a4sbwzghm9.execute-api.eu-central-1.amazonaws.com/prd/content/area");
		entity = new ByteArrayEntity(body.getBytes("UTF-8"));
		httpPost.setEntity(entity);
        
		now = new Date(System.currentTimeMillis());
		
		MessageDigest md = MessageDigest.getInstance("SHA-256");
		
		md.update(body.getBytes("UTF-8"));

		bodyHash = md.digest();
		
		request = "POST\n" +
				  "/prd/content/area\n" +
				  "\n" +
				  "content-type:application/json; charset=utf-8\n" +
				  "host:a4sbwzghm9.execute-api.eu-central-1.amazonaws.com\n" + 
				  "x-amz-date:" + dfDateTime.format(now) + "\n" +
				  "\n" +
				  "content-type;host;x-amz-date\n" +
				  byteToHex(bodyHash);
		
		md.update(request.getBytes("UTF-8"));
		
		requestHash = md.digest();

		stringToSign = "AWS4-HMAC-SHA256\n" + dfDateTime.format(now) + "\n" + dfDate.format(now) + "/eu-central-1/execute-api/aws4_request\n" + byteToHex(requestHash);

		signatureKey = getSignatureKey(cto.getSecretAccessKey(), dfDate.format(now), "eu-central-1", "execute-api");
		
		System.out.println("Signature Key: " + byteToHex(signatureKey));
		
		authHeader = "AWS4-HMAC-SHA256 Credential=" + cto.getAccessKeyID() + 
				 	 "/" + dfDate.format(now) + 
				 	 "/eu-central-1/execute-api/aws4_request, " + 
				 	 "SignedHeaders=content-type;host;x-amz-date, Signature=" + 
				 	 byteToHex(HmacSHA256(stringToSign.getBytes("UTF-8"), signatureKey));

System.out.println("Sample data: " + byteToHex(HmacSHA256("AWS4-HMAC-SHA256\n20150830T123600Z\n20150830/us-east-1/service/aws4_request\n816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0","wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")));
		
		System.out.println("REQUEST\n" + request + "\n");
		System.out.println("STRING_TO_SIGN\n" + stringToSign + "\n");
		System.out.println("SIGNATURE_KEY\n" + byteToHex(signatureKey) + "\n");
		System.out.println("AUTH_HEADER\n" + authHeader + "\n");
		
		httpPost.addHeader("authorization", authHeader);
		httpPost.addHeader("content-type", "application/json; charset=utf-8");
		httpPost.addHeader("host", "a4sbwzghm9.execute-api.eu-central-1.amazonaws.com");
		httpPost.addHeader("x-amz-date", dfDateTime.format(now));
		//httpPost.addHeader("x-amz-security-token", gstResult.getCredentials().getSessionToken());
		httpPost.addHeader("x-amz-security-token", cto.getSessionToken());
		
		response = httpclient.execute(httpPost);
		
		readResponse(response);

		System.out.println("Request Zeitstempel [" + dfDateTime.format(now) + "]");
		
	}

	private String readResponse (CloseableHttpResponse response) throws Exception {
		HttpEntity entity = null;
    	StringBuffer result = new StringBuffer();
		
		try {
		    System.out.println(response.getStatusLine());
		    entity = response.getEntity();
		    // do something useful with the response body
		    
		    BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

	    	String line = "";
	    	while ((line = rd.readLine()) != null) {
	    		result.append(line);
	    	}		    
		    EntityUtils.consume(entity);
		    
		    System.out.println(result);
		} finally {
		    response.close();
		}
		
		return result.toString();
	}
	
	private byte[] HmacSHA256(String data, String key) throws Exception {
	    return HmacSHA256(data.getBytes("UTF8"), key.getBytes("UTF8"));
	}
	
	private byte[] HmacSHA256(byte[] data, String key) throws Exception {
	    return HmacSHA256(data, key.getBytes("UTF8"));
	}
	
	private byte[] HmacSHA256(String data, byte[] key) throws Exception {
	    return HmacSHA256(data.getBytes("UTF8"), key);
	}
	
	private byte[] HmacSHA256(byte[] data, byte[] key) throws Exception {
	    String algorithm="HmacSHA256";
	    Mac mac = Mac.getInstance(algorithm);
	    mac.init(new SecretKeySpec(key, algorithm));
	    return mac.doFinal(data);
	}

	/**
	 * Wurde erfolgreich validiert mit folgenden Testdaten
	 * key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
	 * dateStamp = "20120215"
	 * regionName = "us-east-1"
	 * serviceName = "iam"
	 * @param key
	 * @param dateStamp
	 * @param regionName
	 * @param serviceName
	 * @return
	 * das erwartete Ergebis ist
	 * kSecret  = "41575334774a616c725855746e46454d492f4b374d44454e472b62507852666943594558414d504c454b4559"
	 * kDate    = "969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d"
	 * kRegion  = "69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c"
	 * kService = "f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa"
	 * kSigning = "f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d"
	 * @throws Exception
	 */
	private byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
	    byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
	    byte[] kDate = HmacSHA256(dateStamp, kSecret);
	    byte[] kRegion = HmacSHA256(regionName, kDate);
	    byte[] kService = HmacSHA256(serviceName, kRegion);
	    byte[] kSigning = HmacSHA256("aws4_request", kService);

	    System.out.println("Secret:" + byteToHex(kSecret));
	    System.out.println("Date: " + byteToHex(kDate));
	    System.out.println("Region: " + byteToHex(kRegion));
	    System.out.println("Service: " + byteToHex(kService));
	    System.out.println("Signing: " + byteToHex(kSigning));
	    
	    return kSigning;
	}

	public static String uriEncode(CharSequence input, boolean encodeSlash) {
		StringBuilder result = new StringBuilder();
		for (int i = 0; i < input.length(); i++) { char ch = input.charAt(i); if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') { result.append(ch); } else if (ch == '/') { result.append(encodeSlash ? "%2F" : ch); } else { result.append(charToHex(ch)); } } return result.toString(); } public static String byteToHex(byte b) { // Returns hex String representation of byte b char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; char[] array = { hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f] };
		return new String(array);
	}

	public static String byteToHex(byte[] b) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < b.length; i++) { sb.append(byteToHex(b[i])); } return sb.toString(); } public static String charToHex(char c) { // Returns hex String representation of char c byte hi = (byte) (c >>> 8);
		byte lo = (byte) (c & 0xff);
		return byteToHex(hi) + byteToHex(lo);
	}
}

Javascript-Beispiel für Absicherung eines Requests mittels STS Token

Wie in meinem Artikel angekündigt hier also ein Codebeispiel für die Absicherung eines REST Requests zum Anlegen einer Area mittels eines STS Token:

function newArea() {
  var areaName;
  var areaNameLbl;
  var areaNameTxt;
  var body;
  var bodyHash;

  var date;
  var header;
  var request;
  var requestHash;
  var signature;
  var signatureKey;
  var shaObj;
  var stringToSign;
  
  var now = new Date(); 

  var day = now.getDate() < 10 ? '0' + now.getDate() : now.getDate();
  var mon = now.getMonth() < 9 ? '0' + (now.getMonth()+1) : (now.getMonth()+1);
  var year = now.getFullYear();
  var hour = now.getUTCHours() < 10 ? '0' + now.getUTCHours() : now.getUTCHours();
  var min = now.getMinutes() < 10 ? '0' + now.getMinutes() : now.getMinutes();
  var sec = now.getSeconds() < 10 ? '0' + now.getSeconds() : now.getSeconds();
  
  var date = year+mon+day
  var datetime = date + "T" + hour + min + sec + "Z";

  formObject  = document.forms['new-area-form'];
  
  areaNameTxt = formObject.elements['new-area-area-name-text'];

  areaNameLbl = document.getElementById('new-area-area-name-label');

  areaName    = areaNameTxt.value;
  
  if (!areaName || areaName.length < 6) {
    areaNameLbl.innerHTML = 'Der Areaname muss mindestens 6 Zeichen lang sein';
    areaNameLbl.style.color = '#FF0000';
    areaNameTxt.focus();
    
    return;
  }

  sessionStorage.setItem ('areaName', areaName);
  
  console.log('add new area [' + areaName + ']');

  // der Body ist der Content den es abzusichern gilt
  body = '{"atos":[{"id":null, "name":"' + areaName + '", "folder":null, "number":0, "size":0, "date":null, "version":null}]}';
  
  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.update(body);
  bodyHash = shaObj.getHash("HEX");

  request = 'POST\n' +
        '/prd/content/area\n' +
        '\n' +
        'content-type:application/json; charset=utf-8\n' +
        'host:a4sbwzghm9.execute-api.eu-central-1.amazonaws.com\n' +
        'x-amz-date:' + datetime + '\n' +
        '\n' +
        'content-type;host;x-amz-date\n' +
        bodyHash;
  
  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.update(request);
  requestHash = shaObj.getHash("HEX");

  stringToSign = 'AWS4-HMAC-SHA256\n' + datetime + '\n' + date + '/eu-central-1/execute-api/aws4_request\n' + requestHash;

  signatureKey = getSignatureKey(secretAccessKey, date, "eu-central-1", "execute-api");

  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey(signatureKey, "HEX");
  shaObj.update(stringToSign);
  signature = shaObj.getHMAC("HEX");
  
  header = 'AWS4-HMAC-SHA256 Credential=' + accessKeyID + '/' + date + '/eu-central-1/execute-api/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=' + signature;

  $.ajax({
    async: false,
    data: body,
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    error: function(xhr, status, error) {
      alert(status);
    },
    headers: {
      'authorization':header,
      'content-type':'application/json; charset=utf-8',
      'x-amz-date':datetime,
      'x-amz-security-token':sessionToken
    },  
    success: function(data, status, xhr){
      if (xhr.status == 200) {
        $.mobile.changePage('#new-area-dialog', 'pop', true, true);
      }
    },
    type: 'POST',
    url: srvURL + '/area'
  });
}


function getSignatureKey(secretAccessKey, date, region, service) {
  var kDate;
  var kRegion;
  var kService;
  var kSigning;
  var shaObj;
  
  
  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey("AWS4" + secretAccessKey, "TEXT");
  shaObj.update(date);
  kDate = shaObj.getHMAC("HEX")
  
  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey(kDate, "HEX");
  shaObj.update(region);
  kRegion = shaObj.getHMAC("HEX")

  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey(kRegion, "HEX");
  shaObj.update(service);
  kService = shaObj.getHMAC("HEX")

  shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey(kService, "HEX");
  shaObj.update("aws4_request");
  kSigning = shaObj.getHMAC("HEX")

  return kSigning;
}

Importclient für DynamoDB

Der erste Teil meiner Artikelserie zu AWS Lambda ist nun im Jaxenter erschienen unter Spring-Boot vs. AWS Lambda. In diesem Artikel habe ich auch einen Client für den Import von Jason basierten Datensätzen für die Area sowie die Pictures angesprochen. Im Grund möchte ich in meine DynamoDB die folgenden Datensätze importieren:

Areas:

[
{"id":"1", "name":"Architektur", "folder":"0100000000000000000", "number":50, "size":6422, "date":"2016-01-01T12:00:00.000Z", "version":1},
{"id":"2", "name":"Audio", "folder":"0200000000000000000", "number":17, "size":1655, "date":"2016-01-01T12:00:00.000Z", "version":1},
{"id":"3", "name":"Auto", "folder":"0300000000000000000", "number":70, "size":8459, "date":"2016-01-01T12:00:00.000Z", "version":1},
usw.

Pictures:

[
{"id":"1", "areaId":"1", "name":"5878385598646124113", "size":112, "date":"2012-10-11T12:00:00.000Z", "version":1},
{"id":"2", "areaId":"1", "name":"7655519180858176879", "size":109, "date":"2012-10-11T12:00:00.000Z", "version":1},
{"id":"3", "areaId":"1", "name":"8086031961797504572", "size":98, "date":"2012-10-11T12:00:00.000Z", "version":1},

Der zugehörige Code sieht dann wie folgt aus:

package com.axxessio.axx2sls.content;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;

import org.junit.Test;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class LoadDB {

    @Test
    public void test() throws JsonParseException, IOException {
    Table table;
    JsonParser parser;
    JsonNode rootNode;

    
    AmazonDynamoDBClient client = new AmazonDynamoDBClient().withEndpoint("https://dynamodb.eu-central-1.amazonaws.com");
    DynamoDB dynamoDB = new DynamoDB(client);

    table = dynamoDB.getTable("area");

    parser = new JsonFactory().createParser(new File("./src/main/resources/areas.json"));
    
    rootNode = new ObjectMapper().readTree(parser);

    loadTable (rootNode, table);
    
    parser.close();    

    table = dynamoDB.getTable("picture");

    parser = new JsonFactory().createParser(new File("./src/main/resources/pictures.json"));
    
    rootNode = new ObjectMapper().readTree(parser);

    loadTable (rootNode, table);
    
    parser.close();
  }

  private void loadTable (JsonNode rootNode, Table table) {
    Iterator iter = rootNode.iterator();

    ObjectNode currentNode;

    while (iter.hasNext()) {
      currentNode = (ObjectNode) iter.next();

      String id = currentNode.path("id").asText();

      try {
        table.putItem(Item.fromJSON(currentNode.toString()).withPrimaryKey("id", id));
        System.out.println("PutItem succeeded [" + id + "] into table [" + table.getTableName() + "]");

      } catch (Exception e) {
        System.err.println("Unable to add data set [" + id + "] into table [" + table.getTableName() + "]");
        System.err.println(e.getMessage());
        break;
      }
    }
  }
}

Von Spring Boot nach AWS Lambda.

In diesem Eintrag möchte ich die Migration eines bestehenden Microservice nach AWS Lambda beschreiben. Ich beginne also mit einer kurzen Wiederholung meines Beispiels aus der Artikelserie „Resilient Microservices“ aus dem Javamagazin.

axx2cld-microservice-setup

Bei dieser Demo handelt es sich um vier Microservices:

  • Main: Zeigt die Landingpage an und bietet die Möglichkeit weiter zu verzweigen zu Content, Login oder Registration
  • Content: Zeigt den eigentlichen Inhalt an. Abhängig vom Login und der damit verbundenen Rolle hat der Benutzer mehr oder weniger Möglichkeiten.
  • Login: Hier kann sich der Benutzer einloggen.
  • Registration: Hier kann sich der Nutzer registrieren und anschließend einloggen.

Bei dieser Demo habe ich eine besondere Form der Verfügbarkeit eingeführt, indem ich Daten je Microservice gezielt redundant gehalten habe. Bei Ausfall eines Microservice wie z.B. der Registrierung, funktioniert das Login weiterhin, da es sich die notwendigen Daten des Nutzers (Account, Passwort) zuvor per Polling von der Registrierung gelesen und lokal abgespeichert hat.

Die Implementierung erfolgte in jQuery Mobile und Spring Boot. Für die Persistenz habe ich Apache Derby genutzt. Hier habe ich zum einen die Registrierungsdaten (persistent) abgelegt sowie die Logindaten (in memory) zu Redundanzzwecken kopiert. Des Weiteren habe ich die Metadaten für den Content in einer NoSQL DB abgelegt, hier also Mongo DB. Hier nun der Migrationspfad bottom up.

Persistenz

Amazon bietet diverse Lösungen zum Speichern von Daten an. In meinem Fall benötige ich eine relationale Datenbank sowie eine NoSQL DB.

Bei den relationalen Datenbanken hat man die Wahl zwischen Amazon „Aurora“ sowie Amazon „Relational Database Service“ (RDS). „Aurora“ ist mehr ein Service als nur eine Datenbank. Sie ist MySQL-kompatibel, skaliert gut, ist performant und sehr gut abgesichert. Das wirklich ideale ist, dass man sich bis zu einem gewissen Grade nicht drum kümmern muss, dafür fallen Bereitstellungsgebühren an.

Das ist bei Amazon RDS anders. Hier kann man im Grunde relativ fix eine Datenbank (PostgreSQL, MySQL, MariaDB, Oracle und Microsoft SQL Server.) in der Cloud einrichten, ist für deren Sizing und Betrieb selbst verantwortlich. Je nach Größe ist diese dann auch kostenlos. Für meine Migration hier werde ich eine MySQL DB nutzen. Somit besteht die Option, diese später nach Aurora zu portieren.

Eine erste Änderung gegenüber der vorigen Lösung gibt es jedoch schon. Ich werde keine zwei Instanzen getrennt hochziehen. Amazon bietet mir relativ einfach die Möglichkeit an, die DB hochverfügbar auszulegen.

Für das Speichern der Daten je Microservice verwende ich daher jeweils ein eigenes Schema, um die Daten so weiterhin sauber zu trennen und den Bounded Context aufrecht zu erhalten.

schemasetup

 

Das Speichern der Metadaten des Content Microservice erfolgt in der Amazon eigenen NoSQL Datenbank „DynamoDB“.

Logik

Als nächstes muss nun die eigentliche Logik implementiert werden. Hier finden die größten Änderungen statt und ich werde wohl nur wenig Code übernehmen können. Die einzelnen Methoden des CRUD Interface je Microservice werde ich als „Lambdas“ implementieren. Allerdings geht dies nur in Zusammenspiel mit dem „API Gateway“. Während in den Lambdas die eigentliche Funktionalität liegt werden diese über das „API Gateway“ mittels REST Protokoll angesprochen.

Hier gibt es dann auch schon die erste Herausforderung. Alle Beispiele, die ich bisher gefunden habe, sprechen eine Lambdafunktion über eine URL an. Unter REST ist es aber nun mal so, dass auf eine URL die vier http Methoden GET, PUT, POST und DELTE entsprechende den CRUD Anforderungen unserer Microservices, abgebildet werden müssen. Hierzu bietet das „API Gateway“ vier Möglichkeiten auf die jeweilige URL konfigurativ einzuwirken. Das genaue Vorgehen beschreibe ich später, entscheidend ist jedoch, dass ich ein CRUD Interface auf eine Lambda Methode abbilden möchte und nicht auf vier.

Was die zu verwendenden Frameworks betrifft gilt es ja nun entsprechenden Ersatz für Spring zu finden. Viele Dinge werden bereits durch das Amazon SDK sowie Runtime für Lambda erledigt. Somit muss man sich z.B. nicht mehr um das Mapping von JSON auf ein Pojo kümmern. Auch Security wird durch Amazon abgedeckt, hierzu gleich mehr. Somit muss man sich eigentlich nur noch um die eigentliche Geschäftslogik sowie Persistenz kümmern. Aus diesem Grund werde ich auf Hibernate sowie Apache Commons zurückgreifen. Ziel ist es, die eigentlichen Lambdas so klein wie möglich zu  halten und hierzu ggf. auch etwas mehr Code zu riskieren.

Präsentation

Bei der Präsentation wird es wiederum angenehm. Ich denke, dass ich große Teile der Anwendung einfach übernehmen kann. Statt diese aber auf einem dedizierten WebServer  in der Cloud zu hosten wird diese über Amazon „Simple Storage Service“ (kurz Amazon S3) ausgeliefert. Die dazugehörige URL wird in Amazon „Route 53“ eingerichtet. Der eigentliche Content, in meinem Fall also eine größere Anzahl von Bildern, wird über das Amazon eigene CDN „CloudFront“ bereitgestellt.

Bei der eigentlichen WebAnwendung handelt es sich um einen mobilen HTML5 Client, der mittels REST mit dem Backend des jeweils dazugehörigen Microservices spricht. Hierbei tritt ein angenehmer Nebeneffekt auf. Da die REST-Schnittstelle via „API Gateway“ zur Verfügung gestellt wird tritt nun das Problem mit der Verletzung der Same-Origin-Policy nicht mehr auf. Bei der bisherigen Lösung liefen die Microservices jeweils unter einem eigenen Port. Dies musste ich in Spring Boot berücksichtigen und entsprechende Ausnahmen konfigurieren.

Authentisierung und Autorisierung

In der bestehenden Microservice Lösung basierte dieser Aspekt auf einem selbst implementierten OAuth2 Microservice. Hierzu hatte ich im Login Microservice eine Tokengenerierung, -verwaltung sowie –validierung implementiert. Aber auch hier hat Amazon eine Lösung parat und diese lautet IAM (Identity and Access Management). Allerdings bietet Amazon hier keine komplette Out-of-the-Box Lösung an. IAM dient mehr der Verwaltung von technischen Usern. Das kann man auch daran erkennen, dass man je IAM-Account nur maximal 5.000 Benutzer anlegen kann, für die meisten WebSites zu wenig. Amazon bietet aber innerhalb von IAM einen weiteren Service an, AWS Security Token Service (AWS STS). Dieser stellt Token mit einem begrenzten Gültigkeitszeitraum aus. Diese Token können dann genutzt werden, um auf die eigentliche Lambda Funktionen zugreifen zu können. Lediglich die Benutzerverwaltung muss dann noch selbst geschrieben werden.

Zielarchitektur

Das folgende Diagramm zeigt nun die angestrebte Zielarchitektur unter Verwendung aller AWS Dienste:

zielarchitektur

Los geht’s

Hallo alle miteinander,

ich starte diesen Blog, da ich das Thema Serverless für Java für mich enteckt habe. Schnell habe ich mich in AWS Lambda eingearbeitet, jedoch dabei festgestellt, dass es gerade für Java wenig bis gar keine brauchbaren Ende zu Ende Beispiele für eine Serverless Anwendung gibt. Daher werde ich an dieser Stelle immer wieder einmal beschreiben, wie man insbesondere eine Microservice Architektur auf AWS Lambda hochhebt.

Ausgangsbasis hierfür ist meine Artikelserie Resilient Microservices im Javamagazin vom Oktober letzten Jahres.

In diesem Blog werde ich schrittweise beschreiben, wie ich das ursprüngliche Design und die Implementierung unter Spring-Boot nach AWS Lambda durchgeführt habe.