Nube de primavera: AWS SNS

AWS SNS es un sistema de mensajería para editores/suscriptores y una opción popular para muchos desarrolladores. En este tutorial, crearemos una aplicación Spring Cloud compatible con mensajes de correo electrónico y SMS.

Introducción

Enviar notificaciones a los usuarios es una tarea bastante común, ya sea a través de correo electrónico, mensajes SMS o incluso a través de solicitudes HTTP/HTTPS POST.

El Servicio de notificación simple (SNS) es un sistema de mensajería editor/suscriptor proporcionado por Servicios web de Amazon (AWS). Es una opción popular para muchos desarrolladores y muy confiable.

En este artículo, crearemos una aplicación Spring Cloud con soporte de mensajería (SMS y correo electrónico) con la ayuda de AWS SNS.

¿Por qué elegir AWS SNS?

El servicio de notificación simple de AWS permite a un editor (normalmente un microservicio) enviar (publicar) notificaciones sobre determinados temas a los receptores (suscriptores) a través de varios medios: SMS, correo electrónico, HTTP, AWS Lambda y AWS SQS.

Estos receptores se suscriben deliberadamente a un tema del que desean recibir notificaciones:

Spring Cloud AWS SNS - Soporte de mensajería

Estas son algunas de las razones por las que AWS SNS es extremadamente popular:

  • Permite la publicación en puntos de enlace HTTP y otros servicios de AWS
  • Admite más de 205 países para envío de SMS y correo electrónico. Este nivel de disponibilidad es especialmente importante si sus usuarios van a ser de origen global.
  • Garantiza la entrega de mensajes. siempre que la dirección de correo electrónico/SMS sea válida.
  • AWS proporciona un SDK para Java rico en funciones y bien escrito para SNS con excelente documentación.
  • A través de los módulos fácilmente integrados de Spring, la molestia de integrar el SDK de AWS para Java se hace extremadamente simple.
  • Si ya está utilizando otros servicios de AWS para el almacenamiento o la implementación, entonces es obvio permanecer en el mismo ecosistema y utilizar SNS.

Casos de uso de Spring Boot para AWS SNS

Hay muchas áreas en las que puede usar notificaciones por SMS, correo electrónico o HTTP/S en una aplicación Spring Web:

  • Notificar a todos los microservicios de un evento de toda la aplicación.
  • Notificar a los administradores/desarrolladores sobre errores críticos o servicios caídos.
  • Verificación del número de teléfono a través de OTP (contraseña de un solo uso) durante el registro de usuario o el restablecimiento de contraseña.
  • Notificar a los usuarios de un evento que está directamente asociado con el usuario (por ejemplo: se acepta una solicitud).
  • Aumente la participación del usuario, ya que las notificaciones por correo electrónico y SMS pueden hacer que el usuario regrese a su aplicación.

Cuenta de AWS

Al igual que con cualquier servicio de AWS, necesitamos obtener la Identificación de la clave de acceso y la Clave secreta de nuestra cuenta de AWS. Inicie sesión en su Consola de AWS y visite la página "Mis credenciales de seguridad" que aparece en el menú desplegable de su cuenta:

My Security Credentials

Expanda la pestaña "Claves de acceso (ID de clave de acceso y clave de acceso secreta)" y haga clic en "Crear nueva clave de acceso":

Create New Access Key

¡Descargue su archivo de credenciales y guárdelo en un lugar seguro! Nadie debería tener acceso a este archivo, ya que también tendrán autorización completa para usar su cuenta de AWS:

Download credentials

Debe elegir una Región de AWS para utilizarla como ubicación de procesamiento de sus solicitudes de servicio SNS. Tenga en cuenta que el precio de sus SMS puede diferir según la región elegida y que no todas las regiones admiten mensajes SMS.

Asegúrese de elegir una ubicación compatible con SMS de aquí.

En aras de la brevedad, hemos utilizado la cuenta raíz para generar el ‘Id. de clave de AWS’ y la ‘Clave secreta’, pero se desaconseja esta práctica y AWS recomienda utilizar [Roles de usuario de IAM](https:/ /docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html) en su lugar.

Proyecto de arranque de primavera {#proyecto de arranque de primavera}

Como siempre, para un proyecto Spring Boot con arranque rápido, usaremos Spring Initializr:

Spring Initializr

Alternativamente, podemos usar la CLI de arranque de primavera:

1
$ spring init --dependencies=web sns-demo

Dependencias

Con la herramienta de compilación de su elección, agregue las dependencias requeridas:

Gradle

1
2
3
4
5
6
7
8
dependencies {
    implementation platform('software.amazon.awssdk:bom:2.5.29') // BOM for AWS SDK For Java
    implementation 'software.amazon.awssdk:sns' // We only need to get SNS SDK in our case
    implementation 'software.amazon.awssdk:ses' // Needed for sending emails with attachment
    implementation 'com.sun.mail:javax.mail' // Needed for sending emails with attachment
    compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-messaging', version: '2.2.1.RELEASE'
    compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-autoconfigure', version: '2.2.1.RELEASE'
}

Maven

1
2
3
4
5
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-aws-messaging</artifactId>
    <version>{version}</version>
</dependency>

Envío de correos electrónicos mediante SNS

Crear un tema de SNS

Un tema de SNS es un punto de acceso que agrupa diferentes puntos finales entre un editor (nuestro proyecto Spring Boot) y suscriptores. Un editor publica un mensaje en un tema y ese mensaje se enviará a todos los suscriptores de ese tema.

Primero, definamos un método auxiliar que nos permita obtener un cliente SNS:

1
2
3
4
5
6
7
8
private SnsClient getSnsClient() throws URISyntaxException {
    return SnsClient.builder()
            .credentialsProvider(getAwsCredentials(
                    "Access Key ID",
                    "Secret Key"))
            .region(Region.US_EAST_1) // Set your selected region
            .build();
}

Este método utiliza otro método auxiliar, getAWSCredentials():

1
2
3
4
5
private AwsCredentialsProvider getAwsCredentials(String accessKeyID, String secretAccessKey {
    AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKeyID, secretAccessKey);
    AwsCredentialsProvider awsCredentialsProvider = () -> awsBasicCredentials;
    return awsCredentialsProvider;
}

Realmente, puede configurar el cliente cuando lo usa, pero los métodos auxiliares son un poco más elegantes. Con eso fuera del camino, hagamos un punto final para la creación de temas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RequestMapping("/createTopic")
private String createTopic(@RequestParam("topic_name") String topicName) throws URISyntaxException {

    // Topic name cannot contain spaces
    final CreateTopicRequest topicCreateRequest = CreateTopicRequest.builder().name(topicName).build();

    // Helper method makes the code more readable
    SnsClient snsClient = getSnsClient();

    final CreateTopicResponse topicCreateResponse = snsClient.createTopic(topicCreateRequest);

    if (topicCreateResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Topic creation successful");
        System.out.println("Topic ARN: " + topicCreateResponse.topicArn());
        System.out.println("Topics: " + snsClient.listTopics());
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, topicCreateResponse.sdkHttpResponse().statusText().get()
        );
    }

    snsClient.close();

    return "Topic ARN: " + topicCreateResponse.topicArn();
}

Nota: Si su sistema está detrás de un proxy, entonces necesita configurar su SnsClient con un cliente HTTP personalizado configurado para trabajar con su proxy:

1
2
3
4
5
6
7
SnsClient snsClient = SnsClient.builder()
        .credentialsProvider(getAwsCredentials(
                "Access Key ID",
                "Secret Key"))
        .httpClient(getProxyHTTPClient("http://host:port"))
        .region(Region.US_EAST_1) // Set your selected region
        .build();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private SdkHttpClient getProxyHTTPClient(String proxy) throws URISyntaxException {
    URI proxyURI = new URI(proxy);
    // This HTTP Client supports custom proxy
    final SdkHttpClient sdkHttpClient = ApacheHttpClient.builder()
            .proxyConfiguration(ProxyConfiguration.builder()
                    .endpoint(proxyURI)
                    .build())
            .build();

    return sdkHttpClient;
}

O bien, podría usar el proxy del sistema:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private SdkHttpClient getProxyHTTPClient() throws URISyntaxException {
    // This HTTP Client supports system proxy
    final SdkHttpClient sdkHttpClient = ApacheHttpClient.builder()
            .proxyConfiguration(ProxyConfiguration.builder()
                    .useSystemPropertyValues(true)
                    .build())
            .build();

    return sdkHttpClient;
}

Finalmente, hagamos una solicitud curl para probar si la creación de nuestro tema funciona:

1
2
$ curl http://localhost:8080/createTopic?topic_name=Stack-Abuse-Demo
Topic ARN: arn:aws:sns:us-east-1:123456789:Stack-Abuse-Demo

También puedes confirmar si el tema fue creado o no desde tu consola de AWS:

AWS SNS Topics

Guarde el ARN del tema (nombre de recurso de Amazon) en algún lugar (por ejemplo, en una base de datos junto con los registros de usuario), ya que lo necesitaremos más adelante.

Suscripción a un tema

Con la configuración del tema fuera del camino, hagamos un punto final para la suscripción. Ya que estamos haciendo correo electrónico, configuraremos el protocolo para "correo electrónico". Tenga en cuenta que, en términos de AWS, un "suscriptor" se denomina "punto final", por lo que utilizaremos nuestra dirección de correo electrónico para la propiedad punto final:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/addSubscribers")
private String addSubscriberToTopic(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
            .topicArn(arn)
            .protocol("email")
            .endpoint("[correo electrónico protegido]")
            .build();

    SubscribeResponse subscribeResponse = snsClient.subscribe(subscribeRequest);

    if (subscribeResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Subscriber creation successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, subscribeResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();

    return "Subscription ARN request is pending. To confirm the subscription, check your email.";
}

Enviemos otra solicitud de curl:

1
2
$ curl http://localhost:8080/addSubscribers?arn=arn:aws:sns:us-east-1:123456789:Stack-Abuse-Demo
Subscription ARN request is pending. To confirm the subscription, check your email.

Nota: El suscriptor debe confirmar la suscripción visitando su dirección de correo electrónico y haciendo clic en el correo electrónico de confirmación enviado por AWS:

AWS SNS Topic Subscription

Envío de correos electrónicos

Ahora puede publicar correos electrónicos en su tema, y ​​todos los destinatarios que hayan confirmado su suscripción deberían recibir el mensaje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RequestMapping("/sendEmail")
private String sendEmail(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
                                              .topicArn(arn)
                                              .protocol("email")
                                              .endpoint("[correo electrónico protegido]")
                                              .build();

    final String msg = "This Stack Abuse Demo email works!";

    final PublishRequest publishRequest = PublishRequest.builder()
                                          .topicArn(arn)
                                          .subject("Stack Abuse Demo email")
                                          .message(msg)
                                          .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get());
    }

    snsClient.close();
    return "Email sent to subscribers. Message-ID: " + publishResponse.messageId();
}

Enviemos otra solicitud de curl:

1
2
$ curl http://localhost:8080/sendEmail?arn=arn:aws:sns:us-east-1:650924441247:Stack-Abuse-Demo
Email sent to subscribers. Message-ID: abdcted-8bf8-asd54-841b-5e0be960984c

Y revisando nuestro correo electrónico, somos recibidos con:

AWS SNS Notification Email

Manejo de archivos adjuntos de correo electrónico

AWS SNS admite tamaños de mensaje de solo hasta 256 Kb y no admite archivos adjuntos. La función principal de SNS es enviar mensajes de notificación, no archivos adjuntos.

Si necesita enviar archivos adjuntos con su correo electrónico, deberá usar Servicio de correo electrónico simple de AWS (SES), junto con su SendRawEmail para lograr esta funcionalidad. Construiremos los correos electrónicos con la biblioteca javax.mail.

If you're unfamiliar with it, feel free to check out Cómo enviar correos electrónicos en Java.

Primero, configuremos SesClient, al igual que configuramos SnsClient y agregamos una dirección de correo electrónico:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
SesClient sesClient = SesClient.builder()
        .credentialsProvider(getAwsCredentials(
                "Access Key ID",
                "Secret Key"))
        .region(Region.US_EAST_1) //Set your selected region
        .build();

VerifyEmailAddressRequest verifyEmailAddressRequest = VerifyEmailAddressRequest.builder()
        .emailAddress("[correo electrónico protegido]").build();
sesClient.verifyEmailAddress(verifyEmailAddressRequest);

Las direcciones de correo electrónico que agregue aquí recibirán un mensaje de confirmación y los propietarios de la dirección de correo electrónico deberán confirmar la suscripción.

Y luego, construyamos un objeto de correo electrónico y usemos SendRawEmail de AWS para enviarlos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@RequestMapping("/sendEmailWithAttachment")
private String sendEmailWithAttachment(@RequestParam("arn") String arn) throws URISyntaxException, MessagingException, IOException {

    String subject = "Stack Abuse AWS SES Demo";

    String attachment = "{PATH_TO_ATTACHMENT}";

    String body = "<html>"
                    + "<body>"
                        + "<h1>Hello!</h1>"
                        + "<p>Please check your email for an attachment."
                    + "</body>"
                + "</html>";

    Session session = Session.getDefaultInstance(new Properties(), null);
    MimeMessage message = new MimeMessage(session);

    // Setting subject, sender and recipient
    message.setSubject(subject, "UTF-8");
    message.setFrom(new InternetAddress("[correo electrónico protegido]")); // AWS Account Email
    message.setRecipients(Message.RecipientType.TO,
            InternetAddress.parse("[correo electrónico protegido]")); // Recipient email

    MimeMultipart msg_body = new MimeMultipart("alternative");
    MimeBodyPart wrap = new MimeBodyPart();

    MimeBodyPart htmlPart = new MimeBodyPart();
    htmlPart.setContent(body, "text/html; charset=UTF-8");
    msg_body.addBodyPart(htmlPart);
    wrap.setContent(msg_body);

    MimeMultipart msg = new MimeMultipart("mixed");

    message.setContent(msg);
    msg.addBodyPart(wrap);

    MimeBodyPart att = new MimeBodyPart();
    DataSource fds = new FileDataSource(attachment);
    att.setDataHandler(new DataHandler(fds));
    att.setFileName(fds.getName());
    msg.addBodyPart(att);

    // Build SesClient
    SesClient sesClient = SesClient.builder()
            .credentialsProvider(getAwsCredentials(
                    "Access Key ID",
                    "Secret Key"))
            .region(Region.US_EAST_1) // Set your preferred region
            .build();

    // Send the email
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    message.writeTo(outputStream);

    RawMessage rawMessage = RawMessage.builder().data(SdkBytes.fromByteArray(outputStream.toByteArray())).build();

    SendRawEmailRequest rawEmailRequest = SendRawEmailRequest.builder().rawMessage(rawMessage).build();

    // The .sendRawEmail method is the one that actually sends the email
    SendRawEmailResponse sendRawEmailResponse = sesClient.sendRawEmail(rawEmailRequest);

    if (sendRawEmailResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, sendRawEmailResponse.sdkHttpResponse().statusText().get()
        );
    }

    return "Email sent to subscribers. Message-ID: " + sendRawEmailResponse.messageId();
}

Y finalmente, enviemos una solicitud para probar si esto funciona:

1
2
$ curl http://localhost:8080/sendEmailWithAttachment?arn=arn:aws:sns:Stack-Abuse-Demo
Email sent to subscribers. Message-ID: 0100016fa375071f-4824-2b69e1050efa-000000

Nota: Si no puede encontrar el correo electrónico, asegúrese de revisar su carpeta de correo no deseado:

Correo electrónico de notificación de AWS SNS con archivo adjunto

Envío de mensajes SMS

Algunos prefieren enviar mensajes SMS en lugar de correos electrónicos, principalmente porque es más probable que se vean los mensajes SMS. Hay dos tipos de mensajes SMS:

  1. Promocional: como su nombre lo indica, estos tipos de mensajes se utilizan solo con fines promocionales. Estos mensajes se entregan entre las 9 a. m. y las 9 p. m. y solo deben contener material promocional.
  2. Transaccional: estos mensajes se utilizan para notificaciones críticas y de alto valor. Por ejemplo, para OTP y verificación de números de teléfono. Este tipo de mensajes no se pueden utilizar con fines promocionales ya que viola las normas establecidas para los mensajes transaccionales.

Enviar SMS a un único número de teléfono

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/sendSMS")
private String sendSMS(@RequestParam("phone") String phone) throws URISyntaxException {
    SnsClient snsClient = getSnsClient();

    final PublishRequest publishRequest = PublishRequest.builder()
            .phoneNumber(phone)
            .message("This is Stack Abuse SMS Demo")
            .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Message publishing to phone successful");
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();
    return "SMS sent to " + phone + ". Message-ID: " + publishResponse.messageId();
}

Vamos a probarlo con una solicitud curl:

1
2
$ curl http://localhost:8080/sendSMS?phone=%2B9112345789
SMS sent to +919538816148. Message-ID: 82cd26aa-947c-a978-703d0841fa7b

Enviar SMS a granel

El envío masivo de SMS no se realiza simplemente repitiendo el enfoque anterior. Esta vez, crearemos un tema SNS y en lugar de correo electrónico, usaremos el protocolo sms. Cuando deseemos enviar un mensaje de forma masiva, todos los teléfonos suscritos recibirán la notificación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RequestMapping("/sendBulkSMS")
private String sendBulkSMS(@RequestParam("arn") String arn) throws URISyntaxException {

    SnsClient snsClient = getSnsClient();

    String[] phoneNumbers = new String[]{"+917760041698", "917760041698", "7760041698" };

    for (String phoneNumber: phoneNumbers) {
        final SubscribeRequest subscribeRequest = SubscribeRequest.builder()
                                                  .topicArn(arn)
                                                  .protocol("sms")
                                                  .endpoint(phoneNumber)
                                                  .build();

        SubscribeResponse subscribeResponse = snsClient.subscribe(subscribeRequest);
        if (subscribeResponse.sdkHttpResponse().isSuccessful()) {
            System.out.println(phoneNumber + " subscribed to topic "+arn);
        }
    }

    final PublishRequest publishRequest = PublishRequest.builder()
                                          .topicArn(arn)
                                          .message("This is Stack Abuse SMS Demo")
                                          .build();

    PublishResponse publishResponse = snsClient.publish(publishRequest);

    if (publishResponse.sdkHttpResponse().isSuccessful()) {
        System.out.println("Bulk Message sending successful");
        System.out.println(publishResponse.messageId());
    } else {
        throw new ResponseStatusException(
            HttpStatus.INTERNAL_SERVER_ERROR, publishResponse.sdkHttpResponse().statusText().get()
        );
    }
    snsClient.close();
    return "Done";
}

Conclusión

Spring Cloud AWS hace que sea extremadamente fácil incorporar los servicios de AWS en un proyecto de Spring Boot.

AWS SNS es un servicio de publicador/suscriptor confiable y simple, utilizado por muchos desarrolladores en todo el mundo para enviar notificaciones simples a otros puntos de enlace HTTP, correos electrónicos, teléfonos y otros servicios de AWS.

Hemos creado una aplicación Spring Boot simple que genera un tema SNS, puede agregarle suscriptores y enviarles mensajes por correo electrónico y SMS.

El código fuente está disponible en GitHub.