From d1be3ca43aa4ff11e9efc9f138ead2cf814694bd Mon Sep 17 00:00:00 2001 From: uzma khan Date: Sat, 21 Aug 2021 21:03:31 +0100 Subject: [PATCH] [BAEL-4847] Kafka SSL with Spring Boot client --- spring-kafka/pom.xml | 16 +++++- .../com/baeldung/kafka/ssl/KafkaConsumer.java | 25 ++++++++ .../com/baeldung/kafka/ssl/KafkaProducer.java | 23 ++++++++ .../kafka/ssl/KafkaSslApplication.java | 15 +++++ .../src/main/resources/application-ssl.yml | 18 ++++++ .../ssl/KafkaSslApplicationLiveTest.java | 54 ++++++++++++++++++ .../client-certs/kafka.client.keystore.jks | Bin 0 -> 4021 bytes .../client-certs/kafka.client.truststore.jks | Bin 0 -> 978 bytes .../docker/certs/kafka.server.keystore.jks | Bin 0 -> 4021 bytes .../docker/certs/kafka.server.truststore.jks | Bin 0 -> 978 bytes .../docker/certs/kafka_keystore_credentials | 1 + .../docker/certs/kafka_sslkey_credentials | 1 + .../docker/certs/kafka_truststore_credentials | 1 + .../test/resources/docker/docker-compose.yml | 30 ++++++++++ .../{logback.xml => logback-test.xml} | 6 ++ 15 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaConsumer.java create mode 100644 spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaProducer.java create mode 100644 spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaSslApplication.java create mode 100644 spring-kafka/src/main/resources/application-ssl.yml create mode 100644 spring-kafka/src/test/java/com/baeldung/kafka/ssl/KafkaSslApplicationLiveTest.java create mode 100644 spring-kafka/src/test/resources/client-certs/kafka.client.keystore.jks create mode 100644 spring-kafka/src/test/resources/client-certs/kafka.client.truststore.jks create mode 100644 spring-kafka/src/test/resources/docker/certs/kafka.server.keystore.jks create mode 100644 spring-kafka/src/test/resources/docker/certs/kafka.server.truststore.jks create mode 100644 spring-kafka/src/test/resources/docker/certs/kafka_keystore_credentials create mode 100644 spring-kafka/src/test/resources/docker/certs/kafka_sslkey_credentials create mode 100644 spring-kafka/src/test/resources/docker/certs/kafka_truststore_credentials create mode 100644 spring-kafka/src/test/resources/docker/docker-compose.yml rename spring-kafka/src/test/resources/{logback.xml => logback-test.xml} (56%) diff --git a/spring-kafka/pom.xml b/spring-kafka/pom.xml index 2db62044b2..ed3767029e 100644 --- a/spring-kafka/pom.xml +++ b/spring-kafka/pom.xml @@ -28,6 +28,10 @@ com.fasterxml.jackson.core jackson-databind + + org.projectlombok + lombok + org.springframework.kafka spring-kafka-test @@ -41,9 +45,15 @@ test - commons-collections - commons-collections - 3.2.1 + org.testcontainers + junit-jupiter + ${testcontainers-kafka.version} + test + + + org.awaitility + awaitility + test diff --git a/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaConsumer.java b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaConsumer.java new file mode 100644 index 0000000000..77df74b6c9 --- /dev/null +++ b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaConsumer.java @@ -0,0 +1,25 @@ +package com.baeldung.kafka.ssl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class KafkaConsumer { + + public static final String TOPIC = "test-topic"; + + public final List messages = new ArrayList<>(); + + @KafkaListener(topics = TOPIC) + public void receive(ConsumerRecord consumerRecord) { + log.info("Received payload: '{}'", consumerRecord.toString()); + messages.add(consumerRecord.value()); + } + +} diff --git a/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaProducer.java b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaProducer.java new file mode 100644 index 0000000000..895d437c6b --- /dev/null +++ b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaProducer.java @@ -0,0 +1,23 @@ +package com.baeldung.kafka.ssl; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@AllArgsConstructor +@Component +public class KafkaProducer { + + private final KafkaTemplate kafkaTemplate; + + public void sendMessage(String message, String topic) { + log.info("Producing message: {}", message); + kafkaTemplate.send(topic, "key", message) + .addCallback( + result -> log.info("Message sent to topic: {}", message), + ex -> log.error("Failed to send message", ex) + ); + } +} diff --git a/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaSslApplication.java b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaSslApplication.java new file mode 100644 index 0000000000..ae6df5bee2 --- /dev/null +++ b/spring-kafka/src/main/java/com/baeldung/kafka/ssl/KafkaSslApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.kafka.ssl; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableAutoConfiguration +public class KafkaSslApplication { + + public static void main(String[] args) { + SpringApplication.run(KafkaSslApplication.class, args); + } + +} diff --git a/spring-kafka/src/main/resources/application-ssl.yml b/spring-kafka/src/main/resources/application-ssl.yml new file mode 100644 index 0000000000..8974e62a4c --- /dev/null +++ b/spring-kafka/src/main/resources/application-ssl.yml @@ -0,0 +1,18 @@ +spring: + kafka: + security: + protocol: "SSL" + bootstrap-servers: localhost:9093 + ssl: + trust-store-location: classpath:/client-certs/kafka.client.truststore.jks + trust-store-password: password + key-store-location: classpath:/client-certs/kafka.client.keystore.jks + key-store-password: password + consumer: + group-id: demo-group-id + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + producer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer \ No newline at end of file diff --git a/spring-kafka/src/test/java/com/baeldung/kafka/ssl/KafkaSslApplicationLiveTest.java b/spring-kafka/src/test/java/com/baeldung/kafka/ssl/KafkaSslApplicationLiveTest.java new file mode 100644 index 0000000000..e05298face --- /dev/null +++ b/spring-kafka/src/test/java/com/baeldung/kafka/ssl/KafkaSslApplicationLiveTest.java @@ -0,0 +1,54 @@ +package com.baeldung.kafka.ssl; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.time.Duration; +import java.util.UUID; + +import static com.baeldung.kafka.ssl.KafkaConsumer.TOPIC; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Slf4j +@ActiveProfiles("ssl") +@Testcontainers +@SpringBootTest(classes = KafkaSslApplication.class) +class KafkaSslApplicationLiveTest { + + private static final File KAFKA_COMPOSE_FILE = new File("src/test/resources/docker/docker-compose.yml"); + private static final String KAFKA_SERVICE = "kafka"; + private static final int SSL_PORT = 9093; + + @Container + public DockerComposeContainer container = + new DockerComposeContainer<>(KAFKA_COMPOSE_FILE) + .withExposedService(KAFKA_SERVICE, SSL_PORT, Wait.forListeningPort()); + + @Autowired + private KafkaProducer kafkaProducer; + + @Autowired + private KafkaConsumer kafkaConsumer; + + @Test + void givenSslIsConfigured_whenProducerSendsMessageOverSsl_thenConsumerReceivesOverSsl() { + String message = generateSampleMessage(); + kafkaProducer.sendMessage(message, TOPIC); + + await().atMost(Duration.ofMinutes(2)) + .untilAsserted(() -> assertThat(kafkaConsumer.messages).containsExactly(message)); + } + + private static String generateSampleMessage() { + return UUID.randomUUID().toString(); + } +} diff --git a/spring-kafka/src/test/resources/client-certs/kafka.client.keystore.jks b/spring-kafka/src/test/resources/client-certs/kafka.client.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..62ddfe199d37cf61b3fb2ee906b1617aab760d6a GIT binary patch literal 4021 zcmY+EXE+-UyT%h@Q%cOBC{d(L&vd(Qjex$ft>@8`Kc{-8+eN&+Au6iMwwOvaCTfjXrEk^pm&)Mg+gweeqD zABrS$|Br~s5`-jr_18A}I|jrQ|MNvj4kXA$f-a#*&?%IUnEZe3zvU1RIDXjS5LflF z;WEX`$7o-@mc52kl7JA@>tH|jQ zX&4r%7%93j+E?D12&E{k;j%rNS#-h^(*fGKMbGd{lXb0S@AVe`^K4mO-r?r0ItB`? zYJtEvTp_u4TS&o8J(|R_@4<`W{00$mrmexe)=iHv@{C}oHeO#6uN; zeBmlzEoMo>)1<%#{rkap0bk!U2p&d2S!3TRhanxH*6%g5tc`%|wlqjcz??oxs^{O=5jf6y`DL(hcLu5<%BMY5RIBwF< zgqO(&B^g(nnh5qk8^RU}d{eb6Uxpkt!7mu`;N8MtN>@IvcSOTcWHu0tf=yqncYy5)v&fAXT3 zwPO4jt7fsHABG*K+*BbYi>zZuPwLnW6dy8x=cVLLqe7;T>8l&)t18Bt(U6yVvv~`( zv8D+GBX)NKT~bd4#zU{C=Mm>z80a^{M0Aoejbx5{`F;@ ziO0YLB65`Jj1~HHSYBNj;Ka~$g_4D<-60GX44w)`)RnO~rQ++TGNh%mO4HEGTB<6s z{DWf#cDd1PdP1?b+uGOh=Yvf7r@FeGejIwk33|}1$7TTYdF9;tDxVTn(i5&W@|(NY zBa;*lANyO_$H;gONqoQvb&sqs7Amey<@nh!rd(V@D3xuQRP5l4eFmq{VT|ylGsn zJM@T{5F5Mxr5fBvw4y%WIJ%+pv>`<}BGj;SJw##%Def2YoC!}fL z2W|6{_DII-7-y(O!Uc*#)`eVY*sG|Qq_~%s;tEcXH4O5@aP`HX0m#*Btn~DQ2IoH} zzrLr#peds_ewd<-4^j@U>Zm7T%m@Yf2Nj|@x^(tEkNWtm#-e&h!}E%hy~dNeOIgJW z$N*ex+C-Y>+f!|oM*Lg%-?0g4v%6=WAFJP|=M-`; z_fU7ED!}pzHBERSIXQNfkhN0Qwx%q$EQD|trNEQTSS?fPEfuEhU?v|4lEpxD^b5{P zl%n#)+;aQh*A^X@dwdAIk%dhk5{y`VMpdQa)T<;*yTs3ZCq#cHMD|=$X9&$Zb!e_LvB*5#77?%^_^_NBQOXo9!=v?%kCGLCgL2#JDmijNAuR+Gu|E~*31u$jr|zoF6?`mJtf9N4ipA2%_VDY*Ji{w zy~iQ0?$JN=-WY3BjfZZ2&2|*Xjfv|-sxJBjyl&qq$iOVWTt^QP9+hDe=o2d6le36TH8yVBZhBXd&=UdcDn#Ts}7)cD!<*XF#zE9T0d&kvSM@$7IZ_iX|;LbTI z=_|TG8ra7ee7WIxW-5QKhYLhB$W(qQ0rnyg?<_a?fVYkaf&ccb`TStY>?7w}3)Na- zDAl@~R`SF*W5tBe%tsO-`IN*m^0X#V=MmP!`voX%LB-qzkCR<)Y$!=R2k~B)st#qt zUB@>+wjCcGb@PTiPb~2wqz`ckQ4Xln|K=4=%eIoUur@Q4^Z>&pI9bz^@lC}jut{$Z zAFp-KslaYKh#VaE*VT8NalFw{d97aw>dmB`Evc? zj(7^mDzSfeOQmIn=D=Mdj_c0@4%ODfl{Yk%AVaVPl($K79tXShsanb7;Md|q?P2e% ze#BYD9PY-&53`_&$X{(YwR%N4eMbu&8c52K2)==KHFeap0R89ORMHwTUei49^-cPj zDCto&$a9mI3diZ;veSzLPYyJGC4D!_-t=5$RYZaV4r_W-WPd70dDYw16x4ueWkEHM zSoG!`Up)GGMzVClFm+ti)5l{Hy`Ge@H~z^~Mt1b`RlmEn zjQWs9Jx2V>6>d5sXmP{nC(tgvXq9%d8lJr-aTl4rc`8ZBA<-NoF%_F7oxi+}lJbEv+eH?d_ojDG#FVkh+8 zjtwmTxd>ld>@OJI?ECSd^g}r=Mb)R(ESF80Al!kfYHka)6rlL`6Q$(XN#mp(?UDDOwmFny)=0Zx(-2?&A`jn=yc5o4-Yz{m0nm0*( zP}LATYO0V+=n@vj)S^bh@&TV2lDT0%w7qW)KiAL5d47CK&s&-Z;tujXC0C8+K4(JT+0qpxvMcRHWph(lGt478};)jE6M zOeqfU=n9M`!-jV1>z`{!@r#14L*-0fk6$YFsU>P#KXy??KZ?AO>2lThm>(&Y*%#Gr zXp~2~t(uaaK)dZ`(eJ?jRA5$I-_Vf#1fXr@RN2z}o5RAW_xjxvh)v@?a0!>BpbnuS zy+`DQ1I>FxP1T}euq+3wbL7gSN5mxA_t_RZ!WD{W&f`{v?LJKJaarAOhTXKF6ge6TL4OR4i(RFiEVG&W6rQOCH2cfjd(j5%yG9(dFwljUFGARv{LVjxY zDgRu=0qaRd4d!}ws-XK=@wXpyNW4?&+oog?A_43~FJoQer+tCc*nUkazIE~CYY>j9 z;3i!{sjz;cnuRT0@Z@ZSOHbL(;G_yJs=MOEoHju(4*D(LWdXe4pDHQH!}ohY<)EBU zGGZcuJA?!*L;w&t+A0qv`wf#LlT4xtOVWoG_=JUlz!A;N73D>qW#l6?#-}FU8M@fI KL;^w}D)(PwnwV4o literal 0 HcmV?d00001 diff --git a/spring-kafka/src/test/resources/client-certs/kafka.client.truststore.jks b/spring-kafka/src/test/resources/client-certs/kafka.client.truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..2b07327b13d9d5401a5d72aab77d98f53148639e GIT binary patch literal 978 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 + + + + + + \ No newline at end of file