Tworząc funkcję AWS Lambda, AWS oferuje SDK, który pozwala nam wywołać ją bezpośrednio z kodu Java. Innym popularnym podejściem jest integracja funkcji Lambda z Amazon API Gateway. Taka konfiguracja zamienia funkcję Lambda w HTTP API, umożliwiając dostęp do niej za pośrednictwem standardowych żądań HTTP.
Jak wspomniał Piotr, opisując jak wywołać AWS API gateway z Javy, chcemy wysłać żądanie HTTP. Musimy również zapewnić uwierzytelnienie, podpisując każde z tych żądań za pomocą AWS Signature Version 4.
Okazuje się, że ta sama zasada dotyczy również adresów URL funkcji Lambda wprowadzonych przez Amazon 6 kwietnia 2022 roku. Brak podpisu w takim żądaniu zazwyczaj skutkuje błędem 403 Forbidden. Zacznijmy jednak od początku.
Czym dokładnie jest adres URL funkcji Lambda?
Funkcja Lambda URL jest wygodną alternatywą dla Amazon API Gateway. Pozwala nam stworzyć prosty punkt końcowy HTTP. Zapewnia to szybki sposób na wywołanie naszej funkcji Lambda poprzez wysłanie odpowiedniego żądania na dedykowany adres URL.

https://<generated-url-id>.lambda-url.<region>.on.aws
URL id jest zawsze generowany podczas tworzenia i nie można go zmienić ręcznie - po utworzeniu nigdy się nie zmienia. Część region zależy od regionu, w którym skonfigurowana jest funkcja Lambda.
Konfiguracja wymaga od nas wybrania jednego z dostępnych typ autoryzacji :
- NONE - wyłącza potrzebę podpisywania żądań. Powoduje to również, że adres URL funkcji jest publiczny, co oznacza, że każdy nieuwierzytelniony klient może wywołać funkcję
- AWS_IAM - nadawca żądania musi mieć lambda: InvokeFunctionUrl w swojej polityce opartej na tożsamości lub to samo uprawnienie przyznane mu w polityce opartej na zasobach wywoływanej funkcji.
W naszym przypadku jesteśmy oczywiście zainteresowani parametrem AWS_IAM ustawienie typu auth. Po utworzeniu, adres URL funkcji Lambda jest gotowy do użycia.
Wywołanie adresu URL funkcji Lambda z kodu Java
Niedawno musiałem stworzyć system do podpisywania żądań HTTP wysyłanych z aplikacji Java Spring Boot do istniejącego adresu URL funkcji Lambda. Ponieważ używaliśmy już RestTemplate dla żądań z aplikacji, zdecydowaliśmy się użyć go również w tym przypadku. Pozwoliło nam to zainstalować przechwytywacz, który obsługiwałby wymagania dotyczące podpisywania.
Ostatnią rzeczą, która pozostała, było wybranie sposobu na podpisanie żądania wewnątrz interceptora. Po poszukiwaniach natknąłem się na stronę referencyjną API opisującą metodę AwsV4HttpSigner class. Jest to część AWS SDK v2 dla Javy i wydawała się idealnie pasować do moich potrzeb. Po podjęciu decyzji zacząłem implementację, którą opiszę w poniższych krokach.
Zbieranie niezbędnych danych
Przed przystąpieniem do implementacji musimy zebrać wszystkie dane do logiki podpisywania:
- AWS_LAMBDA_FUNCTION_URL (adres URL funkcji lambda, którą chcemy wywołać za pomocą ŻĄDANIA HTTP requests)
- AWS_REGION (region wdrożonej funkcji lambda)
- AWS_ACCESS_KEY (klucz dostępu do tożsamości IAM)
- AWS_SECRET_ACCESS_KEY (klucz tajny tożsamości IAM)
Dołącz AWS SDK do podpisywania
Wybraliśmy AWS SDK v2 ponieważ AWS SDK v1 osiągnie fazę końca wsparcia w grudniu 2025 roku. Do celów podpisania potrzebujemy następujących zależności:
// maven
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
<version>2.26.27</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>http-auth-aws</artifactId>
<version>2.26.27</version>
</dependency>
// gradle
implementation("software.amazon.awssdk:auth:2.26.27")
implementation("software.amazon.awssdk:http-auth-aws:2.26.27")
Implementacja logiki podpisywania
1. Utwórz przechwytywacz żądań HTTP
Ponieważ używamy RestTemplate do wywoływania adresu URL funkcji Lambda, utworzymy klasę przechwytującą, która implementuje interfejs ClientHttpRequestInterceptor. Interfejs ten zawiera funkcję intercept() którą możemy nadpisać i dostosować do naszych potrzeb.
@Component
public class AwsSigningInterceptor implements ClientHttpRequestInterceptor {
private final AwsConfiguration awsConfiguration;
public AwsSigningInterceptor(AwsConfiguration awsConfiguration) {
this.awsConfiguration = awsConfiguration;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
return execution.execute(request, body);
}
}
The AwsConfiguration jest wstrzykiwana przez konstruktor i jest klasą właściwości konfiguracji, która zawiera dane wspomniane w punkcie 1. W naszym przykładzie dane te są pobierane z pliku application.propertiesale metoda pobierania może się różnić w zależności od konfiguracji środowiska.
2. Przygotowanie wniosku do podpisania
Nadpisanie funkcji intercept() pozwala nam pracować z przechwyconymi żądaniami. Użyjemy ich do zbudowania obiektu SdkHttpRequest, który jest obiektem AWS SDK odpowiednikiem interfejsu HttpRequest.
SdkHttpRequest sdkHttpRequest = SdkHttpRequest.builder()
.uri(request.getURI())
.method(SdkHttpMethod.fromValue(request.getMethod().name()))
.putHeader("Content-Type", "application/json")
.build();
Tworzy to obiekt, który zawiera podobne dane żądania (URI i metoda HTTP), ale jest specjalnie zaprojektowany do procesu podpisywania. Należy zauważyć, że nagłówek Content-Type jest ustawiony na application/json, co jest niezbędne do prawidłowej komunikacji z funkcją Lambda, ponieważ odpowiada ona w formacie JSON.
3. Podpisanie przechwyconego żądania
W tym kroku nadszedł czas na przedstawienie naszego głównego bohatera - AwsV4HttpSigner instancję klasy. Odbywa się to po prostu za pomocą dostarczonej metody fabrycznej AwsV4HttpSigner.create() która zwraca jej domyślną implementację:
AwsV4HttpSigner awsSigner = AwsV4HttpSigner.create();
Będziemy używać funkcji sign() która obsługuje wszystkie szczegóły automatycznie (jak wyjaśniono w dokumentacji AWS). Najpierw jednak musimy dostarczyć jej kilka niezbędnych danych.
Obiekt AwsCredentialsIdentity zawierający klucze accessKey i secretAccessKey:
AwsCredentialsIdentity credentialsIdentity = AwsCredentialsIdentity.create(
awsConfiguration.getAccessKey(),
awsConfiguration.getSecretKey()
);
Nazwa usługiktóry jest prostym ciągiem znaków wskazującym, że podpisujemy żądania do funkcji Lambda, oraz region w którym ta funkcja jest wdrożona:
String serviceName = "lambda";
String region = awsConfiguration.getRegion();
Łącząc to z SdkHttpRequest zbudowanym wcześniej i przechwyconą treścią żądania, jesteśmy wreszcie gotowi do podpisania żądania:
SdkHttpRequest signedRequest = awsSigner.sign(r -> r.identity(credentialsIdentity)
.request(sdkHttpRequest)
.payload(ContentStreamProvider.fromByteArray(body))
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName)
.putProperty(AwsV4HttpSigner.REGION_NAME, region))
.request();
The sign() method requires a Consumer<SignRequest.Builder> argument, which we provide as a lambda expression. The method returns a SignedRequest object, from which we retrieve the signed SdkHttpRequest by using the request() method.
4. Wykonanie podpisanego żądania HTTP
Ostatnią rzeczą do zrobienia jest wykonanie podpisanego żądania. Wcześniej musimy przekonwertować je z powrotem na reprezentację HttpRequest Springa. W tym przykładzie po prostu utworzymy instancję jako anonimową klasę i zaimplementujemy potrzebne metody:
private HttpRequest convertToHttpRequest(SdkHttpRequest request) {
return new HttpRequest() {
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(request.method().name());
}
@Override
public URI getURI() {
return request.getUri();
}
@Override
public HttpHeaders getHeaders() {
var headers = new HttpHeaders();
request.headers().forEach(headers::addAll);
return headers;
}
};
}
Na koniec przekazujemy przekonwertowany HttpRequest i przechwyconą treść żądania do execute metody ClientHttpRequestExecution która jest argumentem nadpisanej funkcji intercept() method:
HttpRequest signedHttpRequest = convertToHttpRequest(signedRequest);
return execution.execute(signedHttpRequest, body);
Po połączeniu powyższych fragmentów kodu, pełna metoda przechwytywania żądań i podpisywania ich wygląda następująco:
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
SdkHttpRequest sdkHttpRequest = SdkHttpRequest.builder()
.uri(request.getURI())
.method(SdkHttpMethod.fromValue(request.getMethod().name()))
.putHeader("Content-Type", "application/json")
.build();
AwsV4HttpSigner awsSigner = AwsV4HttpSigner.create();
AwsCredentialsIdentity credentialsIdentity = AwsCredentialsIdentity.create(
awsConfiguration.getAccessKey(),
awsConfiguration.getSecretKey()
);
String serviceName = "lambda";
String region = awsConfiguration.getRegion();
SdkHttpRequest signedRequest = awsSigner.sign(r -> r.identity(credentialsIdentity)
.request(sdkHttpRequest)
.payload(ContentStreamProvider.fromByteArray(body))
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName)
.putProperty(AwsV4HttpSigner.REGION_NAME, region))
.request();
HttpRequest signedHttpRequest = convertToHttpRequest(signedRequest);
return execution.execute(signedHttpRequest, body);
}
Konfiguracja przechwyconej fasoli RestTemplate
Mając gotową klasę przechwytującą, możemy użyć jej do skonfigurowania szablonu RestTemplate który będzie używany do wysyłania żądań do adresu URL funkcji Lambda. Tworzymy klasę konfiguracyjną i wstrzykujemy przechwytywacz jako argument klasy RestTemplate bean:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate awsSignedRestTemplate(AwsSigningInterceptor awsSigningInterceptor) {
return new RestTemplateBuilder().interceptors(awsSigningInterceptor).build();
}
}
Wyślij podpisane żądanie do adresu URL funkcji Lambda
Teraz skonfigurowany RestTemplate bean może wysłać podpisane żądanie do naszego adresu URL funkcji Lambda:
ResponseEntity<String> lambdaResponse = awsSignedRestTemplate.exchange(
awsConfiguration.getLambdaFunctionUrl(),
HttpMethod.GET,
null,
String.class
);
System.out.println(lambdaResponse.getStatusCode());
System.out.println(lambdaResponse.getBody());
Funkcja użyta jako przykład zwraca domyślną odpowiedź 200 OK z ciągiem "Hello from Lambda!" jako treścią - udane żądanie skutkuje następującymi komunikatami wypisanymi w konsoli:
200 OK
"Hello from Lambda!"
Dzięki takiemu podejściu i odpowiedniej konfiguracji możemy zaimplementować logikę wysyłania żądań, aby kierować adres URL funkcji Lambda bez obawy o otrzymanie odpowiedzi 403 Forbidden.
Wnioski
Adres URL funkcji Lambda jest dobrą alternatywą dla Amazon API Gateway, gdy potrzebujesz szybkiego sposobu na wywołanie funkcji Lambda za pomocą żądań HTTP. Chociaż podpisywanie żądań może wymagać dodatkowego kodu standardowego w porównaniu do wywoływania funkcji bezpośrednio za pomocą AWS SDK, podejście to pozwala dostosować zachowanie funkcji Lambda w oparciu o metodę HTTP przychodzącego żądania.
Kod Java przedstawiony w tym artykule (oraz dodatkowo jego wersja oparta na AWS SDK v1) jest dostępny na GitHub.