<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title><![CDATA[Şuayb's Blog]]></title>
    <link>https://suaybsimsek.com</link>
    <description><![CDATA[Explore the latest articles, tutorials, and insights.]]></description>
    <docs>https://www.rssboard.org/rss-specification</docs>
    <generator><![CDATA[Şuayb's Blog RSS Generator]]></generator>
    <ttl>60</ttl>
    <pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate>
    <lastBuildDate>Tue, 04 Aug 2026 00:00:00 GMT</lastBuildDate>
    <atom:updated>2026-08-04T00:00:00.000Z</atom:updated>
    <language>en-US</language>
    <copyright><![CDATA[© 2026 Şuayb Şimşek. All rights reserved.]]></copyright>
    <image>
      <url>https://suaybsimsek.com/images/logo.webp</url>
      <title><![CDATA[Şuayb's Blog]]></title>
      <link>https://suaybsimsek.com</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://suaybsimsek.com/rss.xml" />
    <item>
      <title><![CDATA[Spring Boot gRPC]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-grpc</link>
      <description><![CDATA[Learn how to build a secured gRPC API with Spring Boot using protobuf contracts, JWT authentication, validation, JPA, and in-process testing.]]></description>
      <pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate>
      <atom:updated>2026-08-04T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-grpc</guid>
      <content:encoded><![CDATA[<p>Spring Boot's gRPC support lets you expose protobuf-defined services without writing the server bootstrap plumbing yourself. This walkthrough follows the Java and Kotlin implementations in the <a href="https://github.com/susimsek/spring-grpc-samples">spring-grpc-samples</a> repository.</p>
<p>The repository's Todo service uses Spring gRPC, Spring Security, Protovalidate, JPA, and Liquibase, with localized error handling and integration tests.</p>
<hr>
<h2>🌟 Why Use Spring Boot gRPC?</h2>
<p>Spring Boot lets you expose protobuf-defined RPC methods as Spring-managed services.</p>
<ul>
<li>Contract-first API design: protobuf files define RPC methods, message shapes, and validation rules.</li>
<li>Spring-managed services: Spring Boot registers <code>BindableService</code> beans with the gRPC server.</li>
<li>gRPC security: authentication and authorization run through the server pipeline.</li>
<li>Centralized validation: request constraints live in the protobuf contract and are checked before service logic.</li>
<li>Consistent errors: invalid requests and authentication failures become gRPC status responses.</li>
<li>Deployment support: the repository includes tests, native-image metadata, Docker, Helm, and Terraform assets.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<ul>
<li>☕ Java Development Kit (JDK) 25</li>
<li>📦 Maven Wrapper with <code>./mvnw</code></li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse, VS Code)</li>
<li>🛢️ Basic familiarity with JPA and Spring Security</li>
<li>🧪 Optional: <code>grpcurl</code> and <code>jq</code> for manual verification</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the dependencies used by the sample for gRPC, JPA, Liquibase, JWT security, caching, MapStruct, and Protovalidate.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;properties>
  &#x3C;java.version>25&#x3C;/java.version>
  &#x3C;org.mapstruct.version>1.6.3&#x3C;/org.mapstruct.version>
  &#x3C;protovalidate.version>0.14.0&#x3C;/protovalidate.version>
&#x3C;/properties>

&#x3C;dependencies>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-grpc-server&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-cache&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-data-jpa&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-liquibase&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.hibernate.orm&#x3C;/groupId>
    &#x3C;artifactId>hibernate-jcache&#x3C;/artifactId>
    &#x3C;version>${hibernate.version}&#x3C;/version>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>com.github.ben-manes.caffeine&#x3C;/groupId>
    &#x3C;artifactId>caffeine&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>com.github.ben-manes.caffeine&#x3C;/groupId>
    &#x3C;artifactId>jcache&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-oauth2-resource-server&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>build.buf&#x3C;/groupId>
    &#x3C;artifactId>protovalidate&#x3C;/artifactId>
    &#x3C;version>${protovalidate.version}&#x3C;/version>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.projectlombok&#x3C;/groupId>
    &#x3C;artifactId>lombok&#x3C;/artifactId>
    &#x3C;optional>true&#x3C;/optional>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.mapstruct&#x3C;/groupId>
    &#x3C;artifactId>mapstruct&#x3C;/artifactId>
    &#x3C;version>${org.mapstruct.version}&#x3C;/version>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-grpc-server-test&#x3C;/artifactId>
    &#x3C;scope>test&#x3C;/scope>
  &#x3C;/dependency>
&#x3C;/dependencies>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-grpc-server'
  implementation 'org.springframework.boot:spring-boot-starter-cache'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-liquibase'
  implementation 'org.hibernate.orm:hibernate-jcache'
  implementation 'com.github.ben-manes.caffeine:caffeine'
  implementation 'com.github.ben-manes.caffeine:jcache'
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
  implementation 'build.buf:protovalidate:0.14.0'
  implementation 'org.mapstruct:mapstruct:1.6.3'
  compileOnly 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-grpc-server-test'
}
</code></pre>
<p>Gradle Kotlin DSL:</p>
<pre><code class="language-kotlin">dependencies {
    implementation("org.springframework.boot:spring-boot-starter-grpc-server")
    implementation("org.springframework.boot:spring-boot-starter-cache")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-liquibase")
    implementation("org.hibernate.orm:hibernate-jcache")
    implementation("com.github.ben-manes.caffeine:caffeine")
    implementation("com.github.ben-manes.caffeine:jcache")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
    implementation("build.buf:protovalidate:0.14.0")
    implementation("org.mapstruct:mapstruct:1.6.3")
    compileOnly("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-grpc-server-test")
}
</code></pre>
<p>The <code>dev</code> profile uses <code>H2</code>, while <code>prod</code> uses <code>PostgreSQL</code>; each database driver is scoped to its profile.</p>
<p>Together, these dependencies let the service persist Todos, run Liquibase migrations, map entities with MapStruct, validate protobuf messages, cache Hibernate data, and protect business RPCs with JWTs.</p>
<hr>
<h2>🛠️ Step 2: Configuration Files</h2>
<p>Application configuration connects the gRPC listener to the active profile, persistence layer, migrations, and localized messages. The files below show those settings along with the schema and seed data.</p>
<ul>
<li>
<p><code>src/main/resources/config/application.yml</code>
Holds the application name, active profile handling, JPA defaults, Hibernate cache settings, Liquibase changelog path, message bundle path, and gRPC server port.</p>
</li>
<li>
<p><code>src/main/resources/config/application-dev.yml</code>
Defines the local H2-based development profile, debug logging, and the <code>faker</code> Liquibase context.</p>
</li>
<li>
<p><code>src/main/resources/config/application-prod.yml</code>
Switches the sample to PostgreSQL-oriented production defaults with tighter JWT expiration and larger cache sizes.</p>
</li>
<li>
<p><code>src/main/resources/logback-spring.xml</code>
Trims noisy framework logs so gRPC, Liquibase, Hibernate, and security output stay readable during local development.</p>
</li>
<li>
<p><code>src/main/resources/db/changelog/db.changelog-master.xml</code>
The Liquibase master file that declares DBMS-specific <code>now</code> properties and includes the Todo and user-related schema files.</p>
</li>
<li>
<p><code>src/main/resources/db/changelog/changes/001-create-todos.xml</code>
Creates the Todo sequence, <code>todos</code> table, supporting indexes, and Faker-context seed load.</p>
</li>
<li>
<p><code>src/main/resources/db/changelog/changes/002-create-users.xml</code>
Creates <code>users</code>, <code>authorities</code>, and <code>user_authorities</code>, then loads the bootstrap security data.</p>
</li>
<li>
<p><code>src/main/resources/db/data/todos.csv</code>
Seed Todo rows used when the <code>faker</code> Liquibase context is enabled.</p>
</li>
<li>
<p><code>src/main/resources/db/data/users.csv</code>
Initial application users with bcrypt-hashed passwords.</p>
</li>
<li>
<p><code>src/main/resources/db/data/authorities.csv</code>
Seed role definitions.</p>
</li>
<li>
<p><code>src/main/resources/db/data/user-authorities.csv</code>
User-to-role mappings for the bootstrap security model.</p>
</li>
<li>
<p><code>src/main/resources/i18n/messages.properties</code>
Default English validation, authentication, and Todo error messages returned through the gRPC advice layer.</p>
</li>
<li>
<p><code>src/main/resources/i18n/messages_tr.properties</code>
Turkish translations for the same validation and security message codes.</p>
</li>
<li>
<p><code>src/main/java/io/github/susimsek/springgrpcsamples/config/i18n/GrpcLocaleServerInterceptor.java</code>
Resolves <code>accept-language</code> from gRPC metadata and pushes the selected locale into <code>LocaleContextHolder</code> before validation and exception translation run.</p>
</li>
</ul>
<pre><code class="language-yaml">spring:
  application:
    name: spring-grpc-samples
  profiles:
    active: '@spring.profiles.active@'
  docker:
    compose:
      lifecycle-management: start-only
      file: src/main/docker/services.yml
  main:
    allow-bean-definition-overriding: true
  data:
    jpa:
      repositories:
        bootstrap-mode: deferred
  jpa:
    open-in-view: false
    properties:
      hibernate.jdbc.time_zone: Europe/Istanbul
      hibernate.cache.region.factory_class: jcache
      hibernate.javax.cache.provider: com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
      hibernate.cache.use_second_level_cache: true
      hibernate.cache.use_query_cache: false
      hibernate.generate_statistics: false
      hibernate.jdbc.batch_size: 50
      hibernate.order_inserts: true
      hibernate.order_updates: true
      hibernate.jdbc.fetch_size: 50
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
        implicit-strategy: org.springframework.boot.hibernate.SpringImplicitNamingStrategy
  messages:
    basename: i18n/messages
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml
  grpc:
    server:
      port: 9090
</code></pre>
<p>The gRPC listener uses <code>spring.grpc.server.port</code>. JPA, Liquibase, and message-source settings configure the application around it.</p>
<p><code>application-dev.yml</code></p>
<pre><code class="language-yaml">spring:
  devtools:
    restart:
      enabled: true
  docker:
    compose:
      enabled: false
  datasource:
    url: jdbc:h2:mem:grpcsamples;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
  jpa:
    show-sql: true
    properties:
      hibernate.format_sql: true
  liquibase:
    contexts: dev, faker

app:
  cache:
    caffeine:
      ttl: PT1H
      initial-capacity: 50
      maximum-size: 100
  security:
    jwt:
      secret: CDyi/yxfK/EMHtj20eOFoLc0t08WVHotnfcpgpnjdt4=
      issuer: https://spring-grpc-samples.local
      expires-in: PT1H

logging:
  level:
    root: INFO
    org.hibernate.SQL: DEBUG
    io.github.susimsek.springgrpcsamples: DEBUG
</code></pre>
<p><code>application-prod.yml</code></p>
<pre><code class="language-yaml">spring:
  devtools:
    restart:
      enabled: false
  docker:
    compose:
      profiles:
        active: prod
  datasource:
    url: jdbc:postgresql://localhost:5432/grpcsamples
    username: ${SPRING_DATASOURCE_USERNAME}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
  jpa:
    show-sql: false
    properties:
      hibernate.format_sql: false
  liquibase:
    contexts: prod

app:
  cache:
    caffeine:
      ttl: PT1H
      initial-capacity: 500
      maximum-size: 1000
  security:
    jwt:
      secret: ${SECURITY_JWT_SECRET}
      issuer: https://spring-grpc-samples.local
      expires-in: PT30M

logging:
  level:
    root: INFO
    org.hibernate.SQL: INFO
    io.github.susimsek.springgrpcsamples: INFO
</code></pre>
<p><code>logback-spring.xml</code></p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;configuration>
    &#x3C;include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    &#x3C;include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    &#x3C;!-- Noisy frameworks trimmed down to WARN to keep dev console readable -->
    &#x3C;logger name="com.zaxxer" level="WARN"/>
    &#x3C;logger name="org.apache" level="WARN"/>
    &#x3C;logger name="org.hibernate" level="WARN"/>
    &#x3C;logger name="org.springframework" level="WARN"/>
    &#x3C;logger name="org.springframework.boot.docker" level="WARN"/>
    &#x3C;logger name="org.springframework.cache" level="WARN"/>
    &#x3C;logger name="org.springframework.grpc" level="WARN"/>
    &#x3C;logger name="org.springframework.security" level="WARN"/>
    &#x3C;logger name="org.springframework.security.oauth2" level="WARN"/>
    &#x3C;logger name="liquibase" level="WARN"/>
    &#x3C;logger name="LiquibaseSchemaResolver" level="INFO"/>
    &#x3C;logger name="io.grpc" level="WARN"/>

    &#x3C;springProperty name="log.level" source="logging.level.root" defaultValue="INFO"/>
    &#x3C;root level="${log.level}">
        &#x3C;appender-ref ref="CONSOLE"/>
    &#x3C;/root>
&#x3C;/configuration>
</code></pre>
<p><code>messages.properties</code></p>
<pre><code class="language-properties">grpc.validation.failed=One or more validation errors occurred.
grpc.validation.unknown=invalid value
grpc.validation.constraints.notBlank=This field cannot be blank.
grpc.validation.constraints.notEmpty=This field cannot be empty.
grpc.validation.constraints.int32.gte=Value must be greater than or equal to {0}.
grpc.validation.constraints.int32.gte_lte=Value must be between 1 and 100.
grpc.validation.constraints.int32.lte=Value must be less than or equal to {0}.
grpc.validation.constraints.int64.gt=Value must be greater than {0}.
grpc.validation.constraints.string.max_len=Value length must be at most {0} characters.
grpc.validation.constraints.string.min_len=Value length must be at least {0} characters.
grpc.auth.invalidCredentials=invalid username or password
grpc.auth.invalidToken=Invalid or expired token.
grpc.auth.unauthenticated=Authentication failed.
grpc.auth.accessDenied=Access denied.
grpc.todo.notFound=todo not found with id: {0}
grpc.internal=An unexpected error occurred. Please try again later.
</code></pre>
<p><code>messages_tr.properties</code></p>
<pre><code class="language-properties">grpc.validation.failed=Bir veya daha fazla doğrulama hatası oluştu.
grpc.validation.unknown=geçersiz değer
grpc.validation.constraints.notBlank=Bu alan boş bırakılamaz.
grpc.validation.constraints.notEmpty=Bu alan boş olamaz.
grpc.validation.constraints.int32.gte=Değer {0} veya daha büyük olmalıdır.
grpc.validation.constraints.int32.gte_lte=Değer 1 ile 100 arasında olmalıdır.
grpc.validation.constraints.int32.lte=Değer {0} veya daha küçük olmalıdır.
grpc.validation.constraints.int64.gt=Değer {0}''dan büyük olmalıdır.
grpc.validation.constraints.string.max_len=Uzunluk en fazla {0} karakter olmalıdır.
grpc.validation.constraints.string.min_len=Uzunluk en az {0} karakter olmalıdır.
grpc.auth.invalidCredentials=kullanıcı adı veya parola geçersiz
grpc.auth.invalidToken=Token geçersiz veya süresi dolmuş.
grpc.auth.unauthenticated=Authentication failed.
grpc.auth.accessDenied=Access denied.
grpc.todo.notFound=id''si {0} olan todo bulunamadı
grpc.internal=Beklenmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin.
</code></pre>
<p><code>GrpcLocaleServerInterceptor.java</code></p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.i18n;

import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@GlobalServerInterceptor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GrpcLocaleServerInterceptor implements ServerInterceptor {

    private static final String ACCEPT_LANGUAGE_HEADER = "accept-language";
    private static final List&#x3C;Locale> SUPPORTED_LOCALES =
            List.of(Locale.ENGLISH, Locale.forLanguageTag("tr"));
    private static final Metadata.Key&#x3C;String> ACCEPT_LANGUAGE =
            Metadata.Key.of(ACCEPT_LANGUAGE_HEADER, Metadata.ASCII_STRING_MARSHALLER);

    @Override
    public &#x3C;ReqT, RespT> ServerCall.Listener&#x3C;ReqT> interceptCall(
            ServerCall&#x3C;ReqT, RespT> call, Metadata headers, ServerCallHandler&#x3C;ReqT, RespT> next) {
        Locale locale = resolveLocale(headers);
        ServerCall.Listener&#x3C;ReqT> listener =
                callWithLocale(locale, () -> next.startCall(call, headers));
        return new LocaleAwareServerCallListener&#x3C;>(listener, locale);
    }

    private static Locale resolveLocale(Metadata headers) {
        String language = headers.get(ACCEPT_LANGUAGE);
        if (!StringUtils.hasText(language)) {
            return Locale.ENGLISH;
        }
        try {
            Locale locale = Locale.lookup(Locale.LanguageRange.parse(language), SUPPORTED_LOCALES);
            return locale != null ? locale : Locale.ENGLISH;
        } catch (IllegalArgumentException ex) {
            return Locale.ENGLISH;
        }
    }

    private static &#x3C;T> T callWithLocale(Locale locale, Supplier&#x3C;T> action) {
        LocaleContext previous = LocaleContextHolder.getLocaleContext();
        try {
            LocaleContextHolder.setLocaleContext(new SimpleLocaleContext(locale));
            return action.get();
        } finally {
            LocaleContextHolder.setLocaleContext(previous);
        }
    }

    private static final class LocaleAwareServerCallListener&#x3C;ReqT>
            extends ForwardingServerCallListener.SimpleForwardingServerCallListener&#x3C;ReqT> {

        private final Locale locale;

        private LocaleAwareServerCallListener(ServerCall.Listener&#x3C;ReqT> delegate, Locale locale) {
            super(delegate);
            this.locale = locale;
        }

        @Override
        public void onMessage(ReqT message) {
            runWithLocale(() -> super.onMessage(message));
        }

        @Override
        public void onHalfClose() {
            runWithLocale(super::onHalfClose);
        }

        @Override
        public void onCancel() {
            runWithLocale(super::onCancel);
        }

        @Override
        public void onComplete() {
            runWithLocale(super::onComplete);
        }

        @Override
        public void onReady() {
            runWithLocale(super::onReady);
        }

        private void runWithLocale(Runnable action) {
            callWithLocale(locale, () -> {
                action.run();
                return null;
            });
        }
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.i18n

import io.grpc.*
import org.springframework.context.i18n.LocaleContext
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.context.i18n.SimpleLocaleContext
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.grpc.server.GlobalServerInterceptor
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import java.util.Locale

@Component
@GlobalServerInterceptor
@Order(Ordered.HIGHEST_PRECEDENCE)
class GrpcLocaleServerInterceptor : ServerInterceptor {

    override fun &#x3C;ReqT : Any?, RespT : Any?> interceptCall(
        call: ServerCall&#x3C;ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler&#x3C;ReqT, RespT>
    ): ServerCall.Listener&#x3C;ReqT> {
        val locale = resolveLocale(headers)
        val listener = callWithLocale(locale) { next.startCall(call, headers) }
        return LocaleAwareServerCallListener(listener, locale)
    }

    private fun resolveLocale(headers: Metadata): Locale {
        val language = headers.get(ACCEPT_LANGUAGE)
        if (!StringUtils.hasText(language)) return Locale.ENGLISH
        return try {
            Locale.lookup(Locale.LanguageRange.parse(language), SUPPORTED_LOCALES) ?: Locale.ENGLISH
        } catch (_: IllegalArgumentException) {
            Locale.ENGLISH
        }
    }

    private fun &#x3C;T> callWithLocale(locale: Locale, action: () -> T): T {
        val previous: LocaleContext = LocaleContextHolder.getLocaleContext()
        return try {
            LocaleContextHolder.setLocaleContext(SimpleLocaleContext(locale))
            action()
        } finally {
            LocaleContextHolder.setLocaleContext(previous)
        }
    }

    private inner class LocaleAwareServerCallListener&#x3C;ReqT>(
        delegate: ServerCall.Listener&#x3C;ReqT>,
        private val locale: Locale
    ) : ForwardingServerCallListener.SimpleForwardingServerCallListener&#x3C;ReqT>(delegate) {

        override fun onMessage(message: ReqT) = runWithLocale { super.onMessage(message) }
        override fun onHalfClose() = runWithLocale { super.onHalfClose() }
        override fun onCancel() = runWithLocale { super.onCancel() }
        override fun onComplete() = runWithLocale { super.onComplete() }
        override fun onReady() = runWithLocale { super.onReady() }

        private fun runWithLocale(action: () -> Unit) {
            callWithLocale(locale) { action() }
        }
    }

    companion object {
        private const val ACCEPT_LANGUAGE_HEADER = "accept-language"
        private val SUPPORTED_LOCALES = listOf(Locale.ENGLISH, Locale.forLanguageTag("tr"))
        private val ACCEPT_LANGUAGE: Metadata.Key&#x3C;String> =
            Metadata.Key.of(ACCEPT_LANGUAGE_HEADER, Metadata.ASCII_STRING_MARSHALLER)
    }
}
</code></pre>
<p><code>db.changelog-master.xml</code></p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    &#x3C;property name="now" value="now()" dbms="h2"/>
    &#x3C;property name="now" value="current_timestamp" dbms="postgresql"/>
    &#x3C;include file="db/changelog/changes/001-create-todos.xml"/>
    &#x3C;include file="db/changelog/changes/002-create-users.xml"/>
&#x3C;/databaseChangeLog>
</code></pre>
<p><code>001-create-todos.xml</code></p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
    &#x3C;changeSet id="001-create-todo-sequence" author="susimsek">
        &#x3C;createSequence
            sequenceName="todo_seq"
            startValue="1"
            incrementBy="1"
            minValue="1"
            maxValue="9999999999999999"
            cacheSize="100"
            cycle="false"/>
    &#x3C;/changeSet>
    &#x3C;changeSet id="002-create-todos-table" author="susimsek">
        &#x3C;createTable tableName="todos">
            &#x3C;column name="id" type="bigint" defaultValueSequenceNext="todo_seq">
                &#x3C;constraints nullable="false" primaryKey="true" primaryKeyName="pk_todos"/>
            &#x3C;/column>
            &#x3C;column name="title" type="varchar(255)">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="completed" type="boolean" defaultValueBoolean="false">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="created_by" type="varchar(100)">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="created_at" type="timestamp" defaultValueComputed="${now}">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="last_modified_by" type="varchar(100)"/>
            &#x3C;column name="updated_at" type="timestamp" defaultValueComputed="${now}">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
        &#x3C;/createTable>
    &#x3C;/changeSet>
    &#x3C;changeSet id="003-create-todo-indexes" author="susimsek">
        &#x3C;createIndex indexName="idx_todos_completed" tableName="todos">
            &#x3C;column name="completed"/>
        &#x3C;/createIndex>
        &#x3C;createIndex indexName="idx_todos_title" tableName="todos">
            &#x3C;column name="title"/>
        &#x3C;/createIndex>
    &#x3C;/changeSet>
    &#x3C;changeSet id="004-seed-todos" author="susimsek" context="faker">
        &#x3C;loadData
            encoding="UTF-8"
            file="db/data/todos.csv"
            separator=";"
            tableName="todos"
            usePreparedStatements="true">
            &#x3C;column header="title" type="string"/>
            &#x3C;column header="completed" type="boolean"/>
            &#x3C;column header="created_by" type="string"/>
            &#x3C;column header="last_modified_by" type="string"/>
        &#x3C;/loadData>
    &#x3C;/changeSet>
&#x3C;/databaseChangeLog>
</code></pre>
<p><code>002-create-users.xml</code></p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
    &#x3C;changeSet id="001-create-user-sequence" author="susimsek">
        &#x3C;createSequence
            sequenceName="user_seq"
            startValue="1"
            incrementBy="1"
            minValue="1"
            maxValue="9999999999999999"
            cacheSize="100"
            cycle="false"/>
    &#x3C;/changeSet>
    &#x3C;changeSet id="002-create-users-table" author="susimsek">
        &#x3C;createTable tableName="users">
            &#x3C;column name="id" type="bigint" defaultValueSequenceNext="user_seq">
                &#x3C;constraints nullable="false" primaryKey="true" primaryKeyName="pk_users"/>
            &#x3C;/column>
            &#x3C;column name="username" type="varchar(100)">
                &#x3C;constraints nullable="false" unique="true" uniqueConstraintName="uk_users_username"/>
            &#x3C;/column>
            &#x3C;column name="password" type="varchar(255)">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="enabled" type="boolean" defaultValueBoolean="true">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="created_by" type="varchar(100)">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="created_at" type="timestamp" defaultValueComputed="${now}">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="last_modified_by" type="varchar(100)"/>
            &#x3C;column name="updated_at" type="timestamp" defaultValueComputed="${now}">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
        &#x3C;/createTable>
    &#x3C;/changeSet>
    &#x3C;changeSet id="003-create-user-indexes" author="susimsek">
        &#x3C;createIndex indexName="idx_users_username" tableName="users" unique="true">
            &#x3C;column name="username"/>
        &#x3C;/createIndex>
    &#x3C;/changeSet>
    &#x3C;changeSet id="004-create-authorities-table" author="susimsek">
        &#x3C;createSequence
            sequenceName="authority_seq"
            startValue="1"
            incrementBy="1"
            minValue="1"
            maxValue="9999999999999999"
            cacheSize="100"
            cycle="false"/>
        &#x3C;createTable tableName="authorities">
            &#x3C;column name="id" type="bigint" defaultValueSequenceNext="authority_seq">
                &#x3C;constraints nullable="false" primaryKey="true" primaryKeyName="pk_authorities"/>
            &#x3C;/column>
            &#x3C;column name="name" type="varchar(50)">
                &#x3C;constraints nullable="false" unique="true" uniqueConstraintName="uk_authorities_name"/>
            &#x3C;/column>
        &#x3C;/createTable>
    &#x3C;/changeSet>
    &#x3C;changeSet id="005-create-user-authorities-table" author="susimsek">
        &#x3C;createTable tableName="user_authorities">
            &#x3C;column name="user_id" type="bigint">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
            &#x3C;column name="authority_id" type="bigint">
                &#x3C;constraints nullable="false"/>
            &#x3C;/column>
        &#x3C;/createTable>
        &#x3C;addPrimaryKey
            tableName="user_authorities"
            columnNames="user_id, authority_id"
            constraintName="pk_user_authorities"/>
        &#x3C;addForeignKeyConstraint
            baseTableName="user_authorities"
            baseColumnNames="user_id"
            constraintName="fk_user_authorities_user"
            referencedTableName="users"
            referencedColumnNames="id"/>
        &#x3C;addForeignKeyConstraint
            baseTableName="user_authorities"
            baseColumnNames="authority_id"
            constraintName="fk_user_authorities_authority"
            referencedTableName="authorities"
            referencedColumnNames="id"/>
        &#x3C;createIndex indexName="idx_user_authorities_authority" tableName="user_authorities">
            &#x3C;column name="authority_id"/>
        &#x3C;/createIndex>
    &#x3C;/changeSet>
    &#x3C;changeSet id="006-seed-authorities" author="susimsek">
        &#x3C;loadData
            encoding="UTF-8"
            file="db/data/authorities.csv"
            separator=";"
            tableName="authorities"
            usePreparedStatements="true">
            &#x3C;column header="id" type="numeric"/>
            &#x3C;column header="name" type="string"/>
        &#x3C;/loadData>
    &#x3C;/changeSet>
    &#x3C;changeSet id="007-seed-users" author="susimsek">
        &#x3C;loadData
            encoding="UTF-8"
            file="db/data/users.csv"
            separator=";"
            tableName="users"
            usePreparedStatements="true">
            &#x3C;column header="username" type="string"/>
            &#x3C;column header="password" type="string"/>
            &#x3C;column header="enabled" type="boolean"/>
            &#x3C;column header="created_by" type="string"/>
            &#x3C;column header="last_modified_by" type="string"/>
        &#x3C;/loadData>
    &#x3C;/changeSet>
    &#x3C;changeSet id="008-seed-user-authorities" author="susimsek">
        &#x3C;loadData
            encoding="UTF-8"
            file="db/data/user-authorities.csv"
            separator=";"
            tableName="user_authorities"
            usePreparedStatements="true">
            &#x3C;column header="user_id" type="numeric"/>
            &#x3C;column header="authority_id" type="numeric"/>
        &#x3C;/loadData>
    &#x3C;/changeSet>
&#x3C;/databaseChangeLog>
</code></pre>
<p><code>todos.csv</code></p>
<pre><code class="language-csv">title;completed;created_by;last_modified_by
Refactor gRPC service;false;system;system
Review pageable todo list;true;system;system
Validate mapper records;false;system;system
Prepare Liquibase changelog;true;system;system
Check auditing fields;false;system;system
Update repository tests;true;system;system
Document todo contract;false;system;system
Exercise patch endpoint;true;system;system
Verify delete behavior;false;system;system
Run Maven coverage;true;system;system
</code></pre>
<p><code>users.csv</code></p>
<pre><code class="language-csv">username;password;enabled;created_by;last_modified_by
admin;$2a$10$dsEILtFz.VyZL6vXnj1Kg.GFczEdAJ9XlBI234j4ExulU3xub4zTe;true;system;system
user;$2a$10$X4IqH828fc4B.V4t7PtB7.n1eKxlSvCRB6BmntaXnxDCBtj23g6L6;true;system;system
</code></pre>
<p><code>authorities.csv</code></p>
<pre><code class="language-csv">id;name
1;ROLE_ADMIN
2;ROLE_USER
</code></pre>
<p><code>user-authorities.csv</code></p>
<pre><code class="language-csv">user_id;authority_id
1;1
1;2
2;2
</code></pre>
<p>Native-image metadata lives under <code>src/main/resources/META-INF/native-image</code>. <code>NativeRuntimeHints</code> registers application-specific hints; the dependency metadata covers Protovalidate, Hibernate JCache, and Liquibase resource scanning:</p>
<ul>
<li><code>src/main/resources/META-INF/native-image/build.buf/protovalidate/reflect-config.json</code>
Registers Buf validation model types for reflection in GraalVM builds.</li>
<li><code>src/main/resources/META-INF/native-image/org.hibernate.orm/hibernate-jcache/reflect-config.json</code>
Enables reflective construction of Hibernate’s JCache region factory.</li>
<li><code>src/main/resources/META-INF/native-image/org.liquibase/liquibase-core/resource-config.json</code>
Keeps Liquibase CSV seed resources reachable in native mode.</li>
</ul>
<p><code>reflect-config.json</code></p>
<pre><code class="language-json">[
  {
    "name": "org.hibernate.cache.jcache.internal.JCacheRegionFactory",
    "allPublicConstructors": true
  }
]
</code></pre>
<p><code>resource-config.json</code></p>
<pre><code class="language-json">{
  "resources": {
    "includes": [
      {
        "pattern": "\\Qdb/data/\\E.*"
      }
    ]
  }
}
</code></pre>
<hr>
<h2>🛠️ Step 3: gRPC Bootstrap and Contracts</h2>
<p>The application class starts Spring Boot. The protobuf contracts that follow define the public RPC methods and message shapes.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/SpringGrpcSamplesApplication.java</code>
Starts the Spring application; component scanning discovers the gRPC service beans.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples;

import io.github.susimsek.springgrpcsamples.config.aot.NativeRuntimeHints;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ImportRuntimeHints;

@SpringBootApplication
@ConfigurationPropertiesScan
@ImportRuntimeHints(NativeRuntimeHints.class)
public class SpringGrpcSamplesApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringGrpcSamplesApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples

import io.github.susimsek.springgrpcsamples.config.aot.NativeRuntimeHints
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ImportRuntimeHints

@SpringBootApplication
@ConfigurationPropertiesScan
@ImportRuntimeHints(NativeRuntimeHints::class)
class SpringGrpcSamplesApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;SpringGrpcSamplesApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 4: JPA Integration</h2>
<p>The persistence layer brings together application and database settings, shared audit fields, protobuf contracts, and JPA entities.</p>
<ul>
<li><code>ApplicationProperties</code>, <code>DatabaseConfig</code>, <code>AuditableEntity</code>, and <code>AuthorityEntity</code>
These classes hold JWT/cache settings, configure JPA auditing, and model shared audit fields and user authorities.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config;

import java.time.Duration;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@Setter
@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {

    private Cache cache = new Cache();
    private Security security = new Security();

    @Getter
    @Setter
    public static class Cache {
        private Caffeine caffeine = new Caffeine();
    }

    @Getter
    @Setter
    public static class Caffeine {
        private Duration ttl = Duration.ofMinutes(10);
        private int initialCapacity = 100;
        private long maximumSize = 1_000;
    }

    @Getter
    @Setter
    public static class Security {
        private Jwt jwt = new Jwt();
    }

    @Getter
    @Setter
    public static class Jwt {
        private String issuer;
        private String secret;
        private Duration expiresIn;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config

import org.springframework.boot.context.properties.ConfigurationProperties
import java.time.Duration

@ConfigurationProperties(prefix = "app")
class ApplicationProperties {
    var cache: Cache = Cache()
    var security: Security = Security()

    class Cache {
        var caffeine: Caffeine = Caffeine()
    }

    class Caffeine {
        var ttl: Duration = Duration.ofMinutes(10)
        var initialCapacity: Int = 100
        var maximumSize: Long = 1_000
    }

    class Security {
        var jwt: Jwt = Jwt()
    }

    class Jwt {
        var issuer: String? = null
        var secret: String? = null
        var expiresIn: Duration? = null
    }
}
</code></pre>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config;

import io.github.susimsek.springgrpcsamples.security.SecurityAuditorAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration(proxyBeanMethods = false)
@EnableJpaAuditing
public class DatabaseConfig {

    @Bean
    AuditorAware&#x3C;String> auditorAware() {
        return new SecurityAuditorAware();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config

import io.github.susimsek.springgrpcsamples.security.SecurityAuditorAware
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.domain.AuditorAware
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@Configuration(proxyBeanMethods = false)
@EnableJpaAuditing
class DatabaseConfig {
    @Bean
    fun auditorAware(): AuditorAware&#x3C;String> = SecurityAuditorAware()
}
</code></pre>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.domain;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false, length = 100)
    private String createdBy;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedBy
    @Column(name = "last_modified_by", length = 100)
    private @Nullable String lastModifiedBy;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.domain

import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AuditableEntity {
    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false, length = 100)
    lateinit var createdBy: String

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    lateinit var createdAt: Instant

    @LastModifiedBy
    @Column(name = "last_modified_by", length = 100)
    var lastModifiedBy: String? = null

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    lateinit var updatedAt: Instant
}
</code></pre>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "authorities")
public class AuthorityEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "authority_seq")
    @SequenceGenerator(name = "authority_seq", sequenceName = "authority_seq", allocationSize = 1)
    private Long id;

    @Column(name = "name", nullable = false, unique = true, length = 50)
    private String name;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.domain

import jakarta.persistence.*
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "authorities")
class AuthorityEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "authority_seq")
    @SequenceGenerator(name = "authority_seq", sequenceName = "authority_seq", allocationSize = 1)
    var id: Long? = null,

    @Column(name = "name", nullable = false, unique = true, length = 50)
    var name: String? = null
)
</code></pre>
<ul>
<li><code>src/main/proto/auth.proto</code>
Defines the public <code>Login</code> RPC and validates username/password shape.</li>
</ul>
<pre><code class="language-proto">syntax = "proto3";

import "buf/validate/validate.proto";

option java_multiple_files = true;
option java_package = "io.github.susimsek.springgrpcsamples.proto";
option java_outer_classname = "AuthProto";

service AuthService {
  rpc Login(LoginRequest) returns (Token) {}
}

message LoginRequest {
  string username = 1 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string = {
      min_len: 3
      max_len: 100
    }
  ];

  string password = 2 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string = {
      min_len: 3
      max_len: 100
    }
  ];

  option (buf.validate.message).cel = {
    id: "grpc.validation.constraints.notBlank"
    message: "This field cannot be blank."
    expression: "this.username.matches('.*\\\\S.*') &#x26;&#x26; this.password.matches('.*\\\\S.*')"
  };
}

message Token {
  string access_token = 1;
  string token_type = 2;
  int64 expires_in = 3;
}
</code></pre>
<ul>
<li><code>src/main/proto/todo.proto</code>
Defines the full CRUD surface and embeds validation rules directly into request messages.</li>
</ul>
<pre><code class="language-proto">syntax = "proto3";

import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";
import "util/pagination.proto";

option java_multiple_files = true;
option java_package = "io.github.susimsek.springgrpcsamples.proto";
option java_outer_classname = "TodoProto";

service TodoService {
  rpc CreateTodo(CreateTodoRequest) returns (Todo) {}
  rpc GetTodo(GetTodoRequest) returns (Todo) {}
  rpc ListTodos(ListTodosRequest) returns (TodoList) {}
  rpc UpdateTodo(UpdateTodoRequest) returns (Todo) {}
  rpc PatchTodo(PatchTodoRequest) returns (Todo) {}
  rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {}
}

message CreateTodoRequest {
  string title = 1 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string = {
      min_len: 3
      max_len: 255
    }
  ];

  option (buf.validate.message).cel = {
    id: "grpc.validation.constraints.notBlank"
    message: "This field cannot be blank."
    expression: "this.title.matches('.*\\\\S.*')"
  };
}

message GetTodoRequest {
  int64 id = 1 [
    (buf.validate.field).int64 = {
      gt: 0
    }
  ];
}

message ListTodosRequest {
  util.PageRequest page_request = 1;
}

message TodoList {
  repeated Todo items = 1;
  int32 page = 2;
  int32 size = 3;
  int64 total_elements = 4;
  int32 total_pages = 5;
  bool first = 6;
  bool last = 7;
}

message UpdateTodoRequest {
  int64 id = 1 [
    (buf.validate.field).int64 = {
      gt: 0
    }
  ];

  string title = 2 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).string = {
      min_len: 3
      max_len: 255
    }
  ];

  bool completed = 3;

  option (buf.validate.message).cel = {
    id: "grpc.validation.constraints.notBlank"
    message: "This field cannot be blank."
    expression: "this.title.matches('.*\\\\S.*')"
  };
}

message PatchTodoRequest {
  int64 id = 1 [
    (buf.validate.field).int64 = {
      gt: 0
    }
  ];

  optional string title = 2 [
    (buf.validate.field).string = {
      min_len: 3
      max_len: 255
    }
  ];

  optional bool completed = 3;

  option (buf.validate.message).cel = {
    id: "grpc.validation.constraints.notEmpty"
    message: "This field cannot be empty."
    expression: "has(this.title) || has(this.completed)"
  };

  option (buf.validate.message).cel = {
    id: "grpc.validation.constraints.notBlank"
    message: "This field cannot be blank."
    expression: "!has(this.title) || this.title.matches('.*\\\\S.*')"
  };
}

message DeleteTodoRequest {
  int64 id = 1 [
    (buf.validate.field).int64 = {
      gt: 0
    }
  ];
}

message DeleteTodoResponse {
  int64 id = 1;
  bool deleted = 2;
}

message Todo {
  int64 id = 1;
  string title = 2;
  bool completed = 3;
  google.protobuf.Timestamp created_at = 4;
  google.protobuf.Timestamp updated_at = 5;
  string created_by = 6;
  string last_modified_by = 7;
}
</code></pre>
<p>The protobuf contracts keep validation rules, pagination, and API boundaries close to the RPC definitions, leaving less request-shape logic in the service layer.</p>
<ul>
<li><code>TodoEntity</code>, <code>UserEntity</code>, and <code>UserRepository</code>
JPA entities and repositories model Todo persistence, cached user lookups, and authority loading.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "todos")
public class TodoEntity extends AuditableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "todo_seq")
    @SequenceGenerator(name = "todo_seq", sequenceName = "todo_seq", allocationSize = 1)
    private Long id;

    @Column(name = "title", nullable = false, length = 255)
    private String title;

    @Column(name = "completed", nullable = false)
    private boolean completed;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.domain

import jakarta.persistence.*
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "todos")
class TodoEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "todo_seq")
    @SequenceGenerator(name = "todo_seq", sequenceName = "todo_seq", allocationSize = 1)
    var id: Long? = null,

    @Column(name = "title", nullable = false, length = 255)
    var title: String? = null,

    @Column(name = "completed", nullable = false)
    var completed: Boolean = false
) : AuditableEntity()
</code></pre>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.domain;

import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "users")
@NamedEntityGraph(
        name = "User.withAuthorities",
        attributeNodes = @NamedAttributeNode("authorities"))
public class UserEntity extends AuditableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    @SequenceGenerator(name = "user_seq", sequenceName = "user_seq", allocationSize = 1)
    private Long id;

    @Column(name = "username", nullable = false, length = 100)
    private String username;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "enabled", nullable = false)
    private boolean enabled;

    @ManyToMany
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @JoinTable(
            name = "user_authorities",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "authority_id"))
    private Set&#x3C;AuthorityEntity> authorities = new HashSet&#x3C;>();
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.domain

import jakarta.persistence.*
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "users")
@NamedEntityGraph(
    name = "User.withAuthorities",
    attributeNodes = [NamedAttributeNode("authorities")]
)
class UserEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    @SequenceGenerator(name = "user_seq", sequenceName = "user_seq", allocationSize = 1)
    var id: Long? = null,

    @Column(name = "username", nullable = false, length = 100)
    var username: String? = null,

    @Column(name = "password", nullable = false)
    var password: String? = null,

    @Column(name = "enabled", nullable = false)
    var enabled: Boolean = false,

    @ManyToMany
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @JoinTable(
        name = "user_authorities",
        joinColumns = [JoinColumn(name = "user_id")],
        inverseJoinColumns = [JoinColumn(name = "authority_id")]
    )
    var authorities: MutableSet&#x3C;AuthorityEntity> = linkedSetOf()
) : AuditableEntity()
</code></pre>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.repository;

import io.github.susimsek.springgrpcsamples.domain.UserEntity;
import java.util.Optional;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository&#x3C;UserEntity, Long> {

    String USER_BY_USERNAME_CACHE = "usersByUsername";

    @EntityGraph(value = "User.withAuthorities")
    @Cacheable(cacheNames = USER_BY_USERNAME_CACHE, key = "#username", unless = "#result == null")
    Optional&#x3C;UserEntity> findByUsername(String username);
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.repository

import io.github.susimsek.springgrpcsamples.domain.UserEntity
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import java.util.Optional

interface UserRepository : JpaRepository&#x3C;UserEntity, Long> {

    companion object {
        const val USER_BY_USERNAME_CACHE: String = "usersByUsername"
    }

    @EntityGraph(value = "User.withAuthorities")
    @Cacheable(cacheNames = [USER_BY_USERNAME_CACHE], key = "#username", unless = "#result == null")
    fun findByUsername(username: String): Optional&#x3C;UserEntity>
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Authentication &#x26; Protected Endpoints</h2>
<p>The Todo handlers depend on a repository and mapper; authentication also needs a <code>UserDetailsService</code> that loads JPA users.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/repository/TodoRepository.java</code>
Uses Spring Data JPA directly; no custom implementation is needed for basic CRUD and paging.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.repository;

import io.github.susimsek.springgrpcsamples.domain.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository&#x3C;TodoEntity, Long> {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.repository

import io.github.susimsek.springgrpcsamples.domain.TodoEntity
import org.springframework.data.jpa.repository.JpaRepository

interface TodoRepository : JpaRepository&#x3C;TodoEntity, Long>
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/mapper/TodoMapper.java</code>
Converts protobuf requests into JPA entities and maps entities back into protobuf responses.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.mapper;

import io.github.susimsek.springgrpcsamples.domain.TodoEntity;
import io.github.susimsek.springgrpcsamples.proto.CreateTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.PatchTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.Todo;
import io.github.susimsek.springgrpcsamples.proto.UpdateTodoRequest;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.NullValuePropertyMappingStrategy;

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        uses = ProtobufMapper.class,
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface TodoMapper {

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "completed", constant = "false")
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    TodoEntity toEntity(CreateTodoRequest request);

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    void updateEntity(UpdateTodoRequest request, @MappingTarget TodoEntity entity);

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    void patchEntity(PatchTodoRequest request, @MappingTarget TodoEntity entity);

    @ProtobufMapping
    @Mapping(target = "mergeCreatedAt", ignore = true)
    @Mapping(target = "mergeUpdatedAt", ignore = true)
    @Mapping(target = "createdByBytes", ignore = true)
    @Mapping(target = "lastModifiedByBytes", ignore = true)
    @Mapping(target = "titleBytes", ignore = true)
    Todo toProto(TodoEntity todo);
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.mapper

import io.github.susimsek.springgrpcsamples.domain.TodoEntity
import io.github.susimsek.springgrpcsamples.proto.CreateTodoRequest
import io.github.susimsek.springgrpcsamples.proto.PatchTodoRequest
import io.github.susimsek.springgrpcsamples.proto.Todo
import io.github.susimsek.springgrpcsamples.proto.UpdateTodoRequest
import org.mapstruct.BeanMapping
import org.mapstruct.Mapper
import org.mapstruct.Mapping
import org.mapstruct.MappingConstants
import org.mapstruct.MappingTarget
import org.mapstruct.NullValueCheckStrategy
import org.mapstruct.NullValuePropertyMappingStrategy

@Mapper(
    componentModel = MappingConstants.ComponentModel.SPRING,
    uses = [ProtobufMapper::class],
    nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
interface TodoMapper {

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "completed", constant = "false")
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    fun toEntity(request: CreateTodoRequest): TodoEntity

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    fun updateEntity(request: UpdateTodoRequest, @MappingTarget entity: TodoEntity)

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdBy", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "lastModifiedBy", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    fun patchEntity(request: PatchTodoRequest, @MappingTarget entity: TodoEntity)

    @ProtobufMapping
    @Mapping(target = "mergeCreatedAt", ignore = true)
    @Mapping(target = "mergeUpdatedAt", ignore = true)
    @Mapping(target = "createdByBytes", ignore = true)
    @Mapping(target = "lastModifiedByBytes", ignore = true)
    @Mapping(target = "titleBytes", ignore = true)
    fun toProto(todo: TodoEntity): Todo
}
</code></pre>
<p>Two small helpers keep protobuf-specific conversion details out of <code>TodoMapper</code>.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/mapper/ProtobufMapper.java</code>
Converts <code>Instant</code> values into protobuf <code>Timestamp</code> instances.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.mapper;

import com.google.protobuf.Timestamp;
import java.time.Instant;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProtobufMapper {

    public static Timestamp toTimestamp(Instant instant) {
        return Timestamp.newBuilder()
                .setSeconds(instant.getEpochSecond())
                .setNanos(instant.getNano())
                .build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.mapper

import com.google.protobuf.Timestamp
import java.time.Instant

object ProtobufMapper {
    @JvmStatic
    fun toTimestamp(instant: Instant): Timestamp =
        Timestamp.newBuilder()
            .setSeconds(instant.epochSecond)
            .setNanos(instant.nano)
            .build()
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/mapper/ProtobufMapping.java</code>
Ignores protobuf builder internals that should not be mapped by MapStruct.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.mapper;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.mapstruct.Mapping;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "allFields", ignore = true)
@Mapping(target = "clearField", ignore = true)
@Mapping(target = "clearOneof", ignore = true)
@Mapping(target = "mergeFrom", ignore = true)
@Mapping(target = "mergeUnknownFields", ignore = true)
@Mapping(target = "unknownFields", ignore = true)
public @interface ProtobufMapping {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.mapper

import org.mapstruct.Mapping

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
@Mapping(target = "allFields", ignore = true)
@Mapping(target = "clearField", ignore = true)
@Mapping(target = "clearOneof", ignore = true)
@Mapping(target = "mergeFrom", ignore = true)
@Mapping(target = "mergeUnknownFields", ignore = true)
@Mapping(target = "unknownFields", ignore = true)
annotation class ProtobufMapping
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/security/DomainUserDetailsService.java</code>
Loads a persisted user and adapts it to Spring Security’s <code>UserDetails</code>.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.security;

import io.github.susimsek.springgrpcsamples.domain.UserEntity;
import io.github.susimsek.springgrpcsamples.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class DomainUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String username) {
        return userRepository
                .findByUsername(username)
                .map(
                        user ->
                                User.withUsername(user.getUsername())
                                        .password(user.getPassword())
                                        .authorities(authorities(user))
                                        .disabled(!user.isEnabled())
                                        .build())
                .orElseThrow(() -> new UsernameNotFoundException("user not found"));
    }

    private static String[] authorities(UserEntity user) {
        return user.getAuthorities().stream()
                .map(authority -> authority.getName())
                .toArray(String[]::new);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.security

import io.github.susimsek.springgrpcsamples.domain.UserEntity
import io.github.susimsek.springgrpcsamples.repository.UserRepository
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class DomainUserDetailsService(
    private val userRepository: UserRepository
) : UserDetailsService {

    @Transactional(readOnly = true)
    override fun loadUserByUsername(username: String): UserDetails =
        userRepository.findByUsername(username)
            .map { user ->
                User.withUsername(user.username)
                    .password(user.password)
                    .authorities(*authorities(user))
                    .disabled(!user.enabled)
                    .build()
            }
            .orElseThrow { UsernameNotFoundException("user not found") }

    private fun authorities(user: UserEntity): Array&#x3C;String> =
        user.authorities.map { it.name }.toTypedArray()
}
</code></pre>
<hr>
<h2>🛠️ Step 6: Security, Validation, and Error Handling</h2>
<p>Both gRPC services are Spring-managed classes that extend generated service base classes.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/service/AuthGrpcService.java</code>
Authenticates credentials and returns a JWT-based token response.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.service;

import io.github.susimsek.springgrpcsamples.exception.InvalidCredentialsException;
import io.github.susimsek.springgrpcsamples.proto.AuthServiceGrpc;
import io.github.susimsek.springgrpcsamples.proto.LoginRequest;
import io.github.susimsek.springgrpcsamples.proto.Token;
import io.github.susimsek.springgrpcsamples.security.JwtService;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthGrpcService extends AuthServiceGrpc.AuthServiceImplBase {

    private static final String BEARER_TOKEN_TYPE = "Bearer";

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    @Override
    public void login(LoginRequest request, StreamObserver&#x3C;Token> responseObserver) {
        Authentication authentication = authenticate(request);
        responseObserver.onNext(
                Token.newBuilder()
                        .setAccessToken(jwtService.generateToken(authentication))
                        .setTokenType(BEARER_TOKEN_TYPE)
                        .setExpiresIn(jwtService.getExpiresInSeconds())
                        .build());
        responseObserver.onCompleted();
    }

    private Authentication authenticate(LoginRequest request) {
        try {
            return authenticationManager.authenticate(
                    UsernamePasswordAuthenticationToken.unauthenticated(
                            request.getUsername(), request.getPassword()));
        } catch (AuthenticationException ex) {
            throw new InvalidCredentialsException();
        }
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.service

import io.github.susimsek.springgrpcsamples.exception.InvalidCredentialsException
import io.github.susimsek.springgrpcsamples.proto.AuthServiceGrpc
import io.github.susimsek.springgrpcsamples.proto.LoginRequest
import io.github.susimsek.springgrpcsamples.proto.Token
import io.github.susimsek.springgrpcsamples.security.JwtService
import io.grpc.stub.StreamObserver
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.stereotype.Service

@Service
class AuthGrpcService(
    private val authenticationManager: AuthenticationManager,
    private val jwtService: JwtService
) : AuthServiceGrpc.AuthServiceImplBase() {

    override fun login(request: LoginRequest, responseObserver: StreamObserver&#x3C;Token>) {
        val authentication = authenticate(request)
        responseObserver.onNext(
            Token.newBuilder()
                .setAccessToken(jwtService.generateToken(authentication))
                .setTokenType(BEARER_TOKEN_TYPE)
                .setExpiresIn(jwtService.expiresInSeconds)
                .build()
        )
        responseObserver.onCompleted()
    }

    private fun authenticate(request: LoginRequest): Authentication =
        try {
            authenticationManager.authenticate(
                UsernamePasswordAuthenticationToken.unauthenticated(request.username, request.password)
            )
        } catch (ex: AuthenticationException) {
            throw InvalidCredentialsException()
        }

    companion object {
        private const val BEARER_TOKEN_TYPE = "Bearer"
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/service/TodoGrpcService.java</code>
Implements the CRUD flow with JPA repository access and mapper-based protobuf conversion.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.service;

import io.github.susimsek.springgrpcsamples.domain.TodoEntity;
import io.github.susimsek.springgrpcsamples.exception.TodoNotFoundException;
import io.github.susimsek.springgrpcsamples.mapper.TodoMapper;
import io.github.susimsek.springgrpcsamples.proto.CreateTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.DeleteTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.DeleteTodoResponse;
import io.github.susimsek.springgrpcsamples.proto.GetTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.ListTodosRequest;
import io.github.susimsek.springgrpcsamples.proto.PatchTodoRequest;
import io.github.susimsek.springgrpcsamples.proto.Todo;
import io.github.susimsek.springgrpcsamples.proto.TodoList;
import io.github.susimsek.springgrpcsamples.proto.TodoServiceGrpc;
import io.github.susimsek.springgrpcsamples.proto.UpdateTodoRequest;
import io.github.susimsek.springgrpcsamples.repository.TodoRepository;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class TodoGrpcService extends TodoServiceGrpc.TodoServiceImplBase {

    private static final int DEFAULT_PAGE_SIZE = 20;

    private final TodoRepository todoRepository;
    private final TodoMapper todoMapper;

    @Override
    public void createTodo(CreateTodoRequest request, StreamObserver&#x3C;Todo> responseObserver) {
        TodoEntity created = todoRepository.save(todoMapper.toEntity(request));
        responseObserver.onNext(todoMapper.toProto(created));
        responseObserver.onCompleted();
    }

    @Override
    public void getTodo(GetTodoRequest request, StreamObserver&#x3C;Todo> responseObserver) {
        TodoEntity todo = findTodo(request.getId());

        responseObserver.onNext(todoMapper.toProto(todo));
        responseObserver.onCompleted();
    }

    @Override
    public void listTodos(ListTodosRequest request, StreamObserver&#x3C;TodoList> responseObserver) {
        var pageRequest = request.getPageRequest();
        int pageSize = pageRequest.getSize() == 0 ? DEFAULT_PAGE_SIZE : pageRequest.getSize();

        Page&#x3C;TodoEntity> page =
                todoRepository.findAll(PageRequest.of(pageRequest.getPage(), pageSize));
        TodoList.Builder response =
                TodoList.newBuilder()
                        .setPage(page.getNumber())
                        .setSize(page.getSize())
                        .setTotalElements(page.getTotalElements())
                        .setTotalPages(page.getTotalPages())
                        .setFirst(page.isFirst())
                        .setLast(page.isLast());
        page.stream().map(todoMapper::toProto).forEach(response::addItems);
        responseObserver.onNext(response.build());
        responseObserver.onCompleted();
    }

    @Override
    public void updateTodo(UpdateTodoRequest request, StreamObserver&#x3C;Todo> responseObserver) {
        TodoEntity updated = findTodo(request.getId());

        todoMapper.updateEntity(request, updated);
        updated = todoRepository.save(updated);
        responseObserver.onNext(todoMapper.toProto(updated));
        responseObserver.onCompleted();
    }

    @Override
    public void patchTodo(PatchTodoRequest request, StreamObserver&#x3C;Todo> responseObserver) {
        TodoEntity existing = findTodo(request.getId());
        todoMapper.patchEntity(request, existing);
        TodoEntity patched = todoRepository.save(existing);

        responseObserver.onNext(todoMapper.toProto(patched));
        responseObserver.onCompleted();
    }

    @Override
    public void deleteTodo(
            DeleteTodoRequest request, StreamObserver&#x3C;DeleteTodoResponse> responseObserver) {
        TodoEntity existing = findTodo(request.getId());
        todoRepository.delete(existing);

        responseObserver.onNext(
                DeleteTodoResponse.newBuilder().setId(request.getId()).setDeleted(true).build());
        responseObserver.onCompleted();
    }

    private TodoEntity findTodo(Long id) {
        return todoRepository.findById(id).orElseThrow(() -> new TodoNotFoundException(id));
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.service

import io.github.susimsek.springgrpcsamples.domain.TodoEntity
import io.github.susimsek.springgrpcsamples.exception.TodoNotFoundException
import io.github.susimsek.springgrpcsamples.mapper.TodoMapper
import io.github.susimsek.springgrpcsamples.proto.*
import io.github.susimsek.springgrpcsamples.repository.TodoRepository
import io.grpc.stub.StreamObserver
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service

@Service
class TodoGrpcService(
    private val todoRepository: TodoRepository,
    private val todoMapper: TodoMapper
) : TodoServiceGrpc.TodoServiceImplBase() {

    override fun createTodo(request: CreateTodoRequest, responseObserver: StreamObserver&#x3C;Todo>) {
        val created = todoRepository.save(todoMapper.toEntity(request))
        responseObserver.onNext(todoMapper.toProto(created))
        responseObserver.onCompleted()
    }

    override fun getTodo(request: GetTodoRequest, responseObserver: StreamObserver&#x3C;Todo>) {
        val todo = findTodo(request.id)
        responseObserver.onNext(todoMapper.toProto(todo))
        responseObserver.onCompleted()
    }

    override fun listTodos(request: ListTodosRequest, responseObserver: StreamObserver&#x3C;TodoList>) {
        val pageRequest = request.pageRequest
        val pageSize = if (pageRequest.size == 0) DEFAULT_PAGE_SIZE else pageRequest.size
        val page = todoRepository.findAll(PageRequest.of(pageRequest.page, pageSize))
        val response = TodoList.newBuilder()
            .setPage(page.number)
            .setSize(page.size)
            .setTotalElements(page.totalElements)
            .setTotalPages(page.totalPages)
            .setFirst(page.isFirst)
            .setLast(page.isLast())
        page.map(todoMapper::toProto).forEach(response::addItems)
        responseObserver.onNext(response.build())
        responseObserver.onCompleted()
    }

    override fun updateTodo(request: UpdateTodoRequest, responseObserver: StreamObserver&#x3C;Todo>) {
        var updated = findTodo(request.id)
        todoMapper.updateEntity(request, updated)
        updated = todoRepository.save(updated)
        responseObserver.onNext(todoMapper.toProto(updated))
        responseObserver.onCompleted()
    }

    override fun patchTodo(request: PatchTodoRequest, responseObserver: StreamObserver&#x3C;Todo>) {
        val existing = findTodo(request.id)
        todoMapper.patchEntity(request, existing)
        val patched = todoRepository.save(existing)
        responseObserver.onNext(todoMapper.toProto(patched))
        responseObserver.onCompleted()
    }

    override fun deleteTodo(request: DeleteTodoRequest, responseObserver: StreamObserver&#x3C;DeleteTodoResponse>) {
        val existing = findTodo(request.id)
        todoRepository.delete(existing)
        responseObserver.onNext(DeleteTodoResponse.newBuilder().setId(request.id).setDeleted(true).build())
        responseObserver.onCompleted()
    }

    private fun findTodo(id: Long): TodoEntity =
        todoRepository.findById(id).orElseThrow { TodoNotFoundException(id) }

    companion object {
        private const val DEFAULT_PAGE_SIZE = 20
    }
}
</code></pre>
<p>Each handler receives a protobuf request and completes its response through <code>StreamObserver</code>. Todo handlers delegate persistence and conversion to repositories and mappers; login delegates credential checks to Spring Security.</p>
<hr>
<p>Security configuration defines password encoding, the authentication manager, and gRPC authorization rules.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/security/SecurityConfig.java</code>
Wires Spring Security into the gRPC server pipeline and protects <code>TodoService/*</code> with admin authority.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.security;

import io.github.susimsek.springgrpcsamples.security.AuthoritiesConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authenticationProvider =
                new DaoAuthenticationProvider(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    @GlobalServerInterceptor
    @Order(Ordered.HIGHEST_PRECEDENCE + 5)
    AuthenticationProcessInterceptor grpcSecurityFilterChain(GrpcSecurity grpc) throws Exception {
        return grpc.authorizeRequests(
                        requests ->
                                requests.methods("AuthService/Login", "grpc.*/*")
                                        .permitAll()
                                        .methods("TodoService/*")
                                        .hasAuthority(AuthoritiesConstants.ADMIN)
                                        .allRequests()
                                        .authenticated())
                .oauth2ResourceServer(
                        resourceServer -> resourceServer.jwt(Customizer.withDefaults()))
                .build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.security

import io.github.susimsek.springgrpcsamples.security.AuthoritiesConstants
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.grpc.server.GlobalServerInterceptor
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor
import org.springframework.grpc.server.security.GrpcSecurity
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder

@Configuration(proxyBeanMethods = false)
class SecurityConfig {

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    @Bean
    fun authenticationManager(
        userDetailsService: UserDetailsService,
        passwordEncoder: PasswordEncoder
    ): AuthenticationManager {
        val provider = DaoAuthenticationProvider(userDetailsService)
        provider.setPasswordEncoder(passwordEncoder)
        return ProviderManager(provider)
    }

    @Bean
    @GlobalServerInterceptor
    @Order(Ordered.HIGHEST_PRECEDENCE + 5)
    @Throws(Exception::class)
    fun grpcSecurityFilterChain(grpc: GrpcSecurity): AuthenticationProcessInterceptor =
        grpc.authorizeRequests { requests ->
            requests.methods("AuthService/Login", "grpc.*/*")
                .permitAll()
                .methods("TodoService/*")
                .hasAuthority(AuthoritiesConstants.ADMIN)
                .allRequests()
                .authenticated()
        }.oauth2ResourceServer { resourceServer ->
            resourceServer.jwt(withDefaults())
        }.build()
}
</code></pre>
<p>The access boundary is explicit: login and infrastructure calls are public, while business RPCs require a bearer token with the expected authority.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/security/JwtService.java</code>
Generates JWT tokens from authenticated Spring Security principals.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.security;

import io.github.susimsek.springgrpcsamples.config.ApplicationProperties;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

@Service
public class JwtService {

    private final JwtEncoder jwtEncoder;
    private final String issuer;
    private final Duration expiresIn;

    public JwtService(JwtEncoder jwtEncoder, ApplicationProperties applicationProperties) {
        this.jwtEncoder = jwtEncoder;
        ApplicationProperties.Jwt jwtProperties = applicationProperties.getSecurity().getJwt();
        this.issuer = jwtProperties.getIssuer();
        this.expiresIn = jwtProperties.getExpiresIn();
    }

    public String generateToken(Authentication authentication) {
        Instant issuedAt = Instant.now();
        JwtClaimsSet claims =
                JwtClaimsSet.builder()
                        .issuer(issuer)
                        .issuedAt(issuedAt)
                        .expiresAt(issuedAt.plus(expiresIn))
                        .subject(authentication.getName())
                        .claim(SecurityUtils.AUTHORITIES_CLAIM, resolveRoles(authentication))
                        .build();
        JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build();
        return jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
    }

    public long getExpiresInSeconds() {
        return expiresIn.toSeconds();
    }

    private static List&#x3C;String> resolveRoles(Authentication authentication) {
        return authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .toList();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.security

import io.github.susimsek.springgrpcsamples.config.ApplicationProperties
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.jose.jws.MacAlgorithm
import org.springframework.security.oauth2.jwt.JwsHeader
import org.springframework.security.oauth2.jwt.JwtClaimsSet
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.JwtEncoderParameters
import org.springframework.stereotype.Service
import java.time.Duration
import java.time.Instant

@Service
class JwtService(
    private val jwtEncoder: JwtEncoder,
    applicationProperties: ApplicationProperties
) {
    private val issuer: String? = applicationProperties.security.jwt.issuer
    private val expiresIn: Duration = applicationProperties.security.jwt.expiresIn ?: Duration.ofHours(1)

    fun generateToken(authentication: Authentication): String {
        val issuedAt = Instant.now()
        val claims = JwtClaimsSet.builder()
            .issuer(issuer)
            .issuedAt(issuedAt)
            .expiresAt(issuedAt.plus(expiresIn))
            .subject(authentication.name)
            .claim(SecurityUtils.AUTHORITIES_CLAIM, resolveRoles(authentication))
            .build()
        val header = JwsHeader.with(MacAlgorithm.HS256).build()
        return jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).tokenValue
    }

    val expiresInSeconds: Long
        get() = expiresIn.seconds

    private fun resolveRoles(authentication: Authentication): List&#x3C;String> =
        authentication.authorities.map(GrantedAuthority::getAuthority)
}
</code></pre>
<hr>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/security/AuthoritiesConstants.java</code>
Defines role names used across bootstrap data, authorization rules, and JWT claims.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.security;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/** Constants for Spring Security authorities. */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {

    public static final String ADMIN = "ROLE_ADMIN";

    public static final String USER = "ROLE_USER";

    public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.security

object AuthoritiesConstants {
    const val ADMIN = "ROLE_ADMIN"
    const val USER = "ROLE_USER"
    const val ANONYMOUS = "ROLE_ANONYMOUS"
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/security/SecurityUtils.java</code>
Extracts the current username from <code>UserDetails</code>, JWT subjects, or plain string principals.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.security;

import java.util.Optional;
import lombok.experimental.UtilityClass;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;

@UtilityClass
public class SecurityUtils {

    public static final String AUTHORITIES_CLAIM = "auth";

    public Optional&#x3C;String> getCurrentUserLogin() {
        String principal = extractPrincipal(SecurityContextHolder.getContext().getAuthentication());
        return Optional.ofNullable(principal);
    }

    private String extractPrincipal(Authentication authentication) {
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails userDetails) {
            return userDetails.getUsername();
        }
        if (principal instanceof Jwt jwt) {
            return jwt.getSubject();
        }
        if (principal instanceof String username) {
            return username;
        }
        return null;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.security

import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.jwt.Jwt

object SecurityUtils {
    const val AUTHORITIES_CLAIM = "auth"

    fun getCurrentUserLogin(): String? =
        extractPrincipal(SecurityContextHolder.getContext().authentication)

    private fun extractPrincipal(authentication: Authentication?): String? {
        if (authentication == null || authentication is AnonymousAuthenticationToken) {
            return null
        }
        val principal = authentication.principal
        return when (principal) {
            is UserDetails -> principal.username
            is Jwt -> principal.subject
            is String -> principal
            else -> null
        }
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/security/SecurityAuditorAware.java</code>
Bridges Spring Security into JPA auditing so <code>createdBy</code> and <code>lastModifiedBy</code> fields are populated automatically.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.security;

import java.util.Optional;
import org.springframework.data.domain.AuditorAware;

public class SecurityAuditorAware implements AuditorAware&#x3C;String> {

    private static final String DEFAULT_AUDITOR = "system";

    @Override
    public Optional&#x3C;String> getCurrentAuditor() {
        return Optional.of(SecurityUtils.getCurrentUserLogin().orElse(DEFAULT_AUDITOR));
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.security

import org.springframework.data.domain.AuditorAware
import java.util.Optional

class SecurityAuditorAware : AuditorAware&#x3C;String> {
    override fun getCurrentAuditor(): Optional&#x3C;String> =
        Optional.of(SecurityUtils.getCurrentUserLogin() ?: DEFAULT_AUDITOR)

    companion object {
        private const val DEFAULT_AUDITOR = "system"
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/security/SecurityJwtConfig.java</code>
Creates the HMAC key, JWT encoder/decoder, and a <code>JwtAuthenticationConverter</code> compatible with the custom <code>auth</code> claim.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.security;

import io.github.susimsek.springgrpcsamples.config.ApplicationProperties;
import io.github.susimsek.springgrpcsamples.security.SecurityUtils;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

@Configuration(proxyBeanMethods = false)
public class SecurityJwtConfig {

    private static final String HMAC_SHA_256 = "HmacSHA256";

    @Bean
    SecretKey jwtSecretKey(ApplicationProperties applicationProperties) {
        String secret = applicationProperties.getSecurity().getJwt().getSecret();
        return new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA_256);
    }

    @Bean
    JwtEncoder jwtEncoder(SecretKey jwtSecretKey) {
        return NimbusJwtEncoder.withSecretKey(jwtSecretKey).algorithm(MacAlgorithm.HS256).build();
    }

    @Bean
    JwtDecoder jwtDecoder(SecretKey jwtSecretKey) {
        return NimbusJwtDecoder.withSecretKey(jwtSecretKey)
                .macAlgorithm(MacAlgorithm.HS256)
                .build();
    }

    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
                new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityUtils.AUTHORITIES_CLAIM);
        grantedAuthoritiesConverter.setAuthorityPrefix("");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.security

import io.github.susimsek.springgrpcsamples.config.ApplicationProperties
import io.github.susimsek.springgrpcsamples.security.SecurityUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.jose.jws.MacAlgorithm
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import java.nio.charset.StandardCharsets
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

@Configuration(proxyBeanMethods = false)
class SecurityJwtConfig {

    @Bean
    fun jwtSecretKey(applicationProperties: ApplicationProperties): SecretKey {
        val secret = applicationProperties.security.jwt.secret
        return SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), HMAC_SHA_256)
    }

    @Bean
    fun jwtEncoder(jwtSecretKey: SecretKey): JwtEncoder =
        NimbusJwtEncoder.withSecretKey(jwtSecretKey).algorithm(MacAlgorithm.HS256).build()

    @Bean
    fun jwtDecoder(jwtSecretKey: SecretKey): JwtDecoder =
        NimbusJwtDecoder.withSecretKey(jwtSecretKey).macAlgorithm(MacAlgorithm.HS256).build()

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        grantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityUtils.AUTHORITIES_CLAIM)
        grantedAuthoritiesConverter.setAuthorityPrefix("")

        return JwtAuthenticationConverter().apply {
            setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
        }
    }

    companion object {
        private const val HMAC_SHA_256 = "HmacSHA256"
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/cache/CacheConfig.java</code>
Configures Spring Cache and optional Hibernate second-level caching backed by Caffeine.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.cache;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration;
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider;
import io.github.susimsek.springgrpcsamples.config.ApplicationProperties;
import io.github.susimsek.springgrpcsamples.domain.AuthorityEntity;
import io.github.susimsek.springgrpcsamples.domain.TodoEntity;
import io.github.susimsek.springgrpcsamples.domain.UserEntity;
import io.github.susimsek.springgrpcsamples.repository.UserRepository;
import java.util.OptionalLong;
import javax.cache.CacheManager;
import javax.cache.Caching;
import lombok.RequiredArgsConstructor;
import org.hibernate.cache.jcache.ConfigSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer;
import org.springframework.boot.hibernate.autoconfigure.HibernatePropertiesCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {

    private final ApplicationProperties applicationProperties;

    @Bean
    public org.springframework.cache.CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(buildCaffeineConfig(cacheProperties()));
        return cacheManager;
    }

    private ApplicationProperties.Caffeine cacheProperties() {
        return applicationProperties.getCache().getCaffeine();
    }

    private Caffeine&#x3C;Object, Object> buildCaffeineConfig(ApplicationProperties.Caffeine config) {
        return Caffeine.newBuilder()
                .expireAfterWrite(config.getTtl())
                .initialCapacity(config.getInitialCapacity())
                .maximumSize(config.getMaximumSize())
                .recordStats();
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnProperty(
            name = "spring.jpa.properties.hibernate.cache.use_second_level_cache",
            havingValue = "true")
    @RequiredArgsConstructor
    static class HibernateSecondLevelCacheConfiguration {

        private final ApplicationProperties applicationProperties;

        @Bean
        CacheManager jcacheManager(JCacheManagerCustomizer customizer) {
            CacheManager manager =
                    Caching.getCachingProvider(CaffeineCachingProvider.class.getName())
                            .getCacheManager();
            customizer.customize(manager);
            return manager;
        }

        @Bean
        HibernatePropertiesCustomizer hibernatePropertiesCustomizer(CacheManager jcacheManager) {
            return properties -> properties.put(ConfigSettings.CACHE_MANAGER, jcacheManager);
        }

        @Bean
        JCacheManagerCustomizer cacheManagerCustomizer() {
            return cacheManager -> {
                createCache(cacheManager, "default-update-timestamps-region");
                createCache(cacheManager, "default-query-results-region");
                createCache(cacheManager, AuthorityEntity.class.getName());
                createCache(cacheManager, TodoEntity.class.getName());
                createCache(cacheManager, UserEntity.class.getName());
                createCache(cacheManager, UserEntity.class.getName() + ".authorities");
                createCache(cacheManager, UserRepository.USER_BY_USERNAME_CACHE);
            };
        }

        private void createCache(CacheManager cacheManager, String cacheName) {
            javax.cache.Cache&#x3C;Object, Object> cache = cacheManager.getCache(cacheName);
            if (cache != null) {
                cache.clear();
                return;
            }
            ApplicationProperties.Caffeine config = applicationProperties.getCache().getCaffeine();
            CaffeineConfiguration&#x3C;Object, Object> caffeineConfig = new CaffeineConfiguration&#x3C;>();
            caffeineConfig.setMaximumSize(OptionalLong.of(config.getMaximumSize()));
            caffeineConfig.setExpireAfterWrite(OptionalLong.of(config.getTtl().toNanos()));
            caffeineConfig.setStatisticsEnabled(true);
            cacheManager.createCache(cacheName, caffeineConfig);
        }
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.cache

import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
import io.github.susimsek.springgrpcsamples.config.ApplicationProperties
import io.github.susimsek.springgrpcsamples.domain.AuthorityEntity
import io.github.susimsek.springgrpcsamples.domain.TodoEntity
import io.github.susimsek.springgrpcsamples.domain.UserEntity
import io.github.susimsek.springgrpcsamples.repository.UserRepository
import org.hibernate.cache.jcache.ConfigSettings
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer
import org.springframework.boot.hibernate.autoconfigure.HibernatePropertiesCustomizer
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.caffeine.CaffeineCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.OptionalLong
import javax.cache.CacheManager
import javax.cache.Caching

@Configuration(proxyBeanMethods = false)
@EnableCaching
class CacheConfig(
    private val applicationProperties: ApplicationProperties
) {

    @Bean
    fun cacheManager(): org.springframework.cache.CacheManager =
        CaffeineCacheManager().apply {
            setCaffeine(buildCaffeineConfig(cacheProperties()))
        }

    private fun cacheProperties(): ApplicationProperties.Caffeine =
        applicationProperties.cache.caffeine

    private fun buildCaffeineConfig(config: ApplicationProperties.Caffeine): Caffeine&#x3C;Any, Any> =
        Caffeine.newBuilder()
            .expireAfterWrite(config.ttl)
            .initialCapacity(config.initialCapacity)
            .maximumSize(config.maximumSize)
            .recordStats()

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnProperty(
        name = ["spring.jpa.properties.hibernate.cache.use_second_level_cache"],
        havingValue = "true"
    )
    class HibernateSecondLevelCacheConfiguration(
        private val applicationProperties: ApplicationProperties
    ) {

        @Bean
        fun jcacheManager(customizer: JCacheManagerCustomizer): CacheManager =
            Caching.getCachingProvider(CaffeineCachingProvider::class.java.name)
                .cacheManager
                .also(customizer::customize)

        @Bean
        fun hibernatePropertiesCustomizer(jcacheManager: CacheManager): HibernatePropertiesCustomizer =
            HibernatePropertiesCustomizer { properties ->
                properties[ConfigSettings.CACHE_MANAGER] = jcacheManager
            }

        @Bean
        fun cacheManagerCustomizer(): JCacheManagerCustomizer =
            JCacheManagerCustomizer { cacheManager ->
                createCache(cacheManager, "default-update-timestamps-region")
                createCache(cacheManager, "default-query-results-region")
                createCache(cacheManager, AuthorityEntity::class.java.name)
                createCache(cacheManager, TodoEntity::class.java.name)
                createCache(cacheManager, UserEntity::class.java.name)
                createCache(cacheManager, UserEntity::class.java.name + ".authorities")
                createCache(cacheManager, UserRepository.USER_BY_USERNAME_CACHE)
            }

        private fun createCache(cacheManager: CacheManager, cacheName: String) {
            val cache = cacheManager.getCache(cacheName)
            if (cache != null) {
                cache.clear()
                return
            }
            val config = applicationProperties.cache.caffeine
            val caffeineConfig = CaffeineConfiguration&#x3C;Any, Any>()
            caffeineConfig.setMaximumSize(OptionalLong.of(config.maximumSize))
            caffeineConfig.setExpireAfterWrite(OptionalLong.of(config.ttl.toNanos()))
            caffeineConfig.isStatisticsEnabled = true
            cacheManager.createCache(cacheName, caffeineConfig)
        }
    }
}
</code></pre>
<p>The sample does not validate requests manually in every service method. Instead, it uses a global server interceptor to run Protovalidate against application protobuf messages, then translates failures through a centralized gRPC advice.</p>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/validation/GrpcValidationConfig.java</code>
Registers the shared Protovalidate <code>Validator</code> bean used by the interceptor.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.validation;

import build.buf.protovalidate.Validator;
import build.buf.protovalidate.ValidatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class GrpcValidationConfig {

    @Bean
    public Validator grpcValidator() {
        return ValidatorFactory.newBuilder().build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.validation

import build.buf.protovalidate.Validator
import build.buf.protovalidate.ValidatorFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class GrpcValidationConfig {
    @Bean
    fun grpcValidator(): Validator = ValidatorFactory.newBuilder().build()
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/validation/GrpcValidationServerInterceptor.java</code>
Applies protobuf validation before service logic runs.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.validation;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Validator;
import build.buf.protovalidate.exceptions.ValidationException;
import com.google.protobuf.Message;
import io.github.susimsek.springgrpcsamples.exception.GrpcValidationException;
import io.github.susimsek.springgrpcsamples.exception.GrpcViolation;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.stereotype.Component;

@Component
@GlobalServerInterceptor
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
@RequiredArgsConstructor
public class GrpcValidationServerInterceptor implements ServerInterceptor {

    private static final String APPLICATION_PROTO_PACKAGE =
            "io.github.susimsek.springgrpcsamples.proto";

    private final Validator validator;

    @Override
    public &#x3C;ReqT, RespT> ServerCall.Listener&#x3C;ReqT> interceptCall(
            ServerCall&#x3C;ReqT, RespT> call, Metadata headers, ServerCallHandler&#x3C;ReqT, RespT> next) {
        ServerCall.Listener&#x3C;ReqT> listener = next.startCall(call, headers);
        return new ValidationServerCallListener&#x3C;>(listener, validator);
    }

    private static final class ValidationServerCallListener&#x3C;ReqT>
            extends ForwardingServerCallListener.SimpleForwardingServerCallListener&#x3C;ReqT> {

        private final Validator validator;

        private ValidationServerCallListener(
                ServerCall.Listener&#x3C;ReqT> delegate, Validator validator) {
            super(delegate);
            this.validator = validator;
        }

        @Override
        public void onMessage(ReqT message) {
            if (message instanceof Message protoMessage
                    &#x26;&#x26; APPLICATION_PROTO_PACKAGE.equals(protoMessage.getClass().getPackageName())) {
                validate(protoMessage);
            }
            super.onMessage(message);
        }

        private void validate(Message message) {
            try {
                ValidationResult result = validator.validate(message);
                if (!result.isSuccess()) {
                    throw new GrpcValidationException(
                            result.getViolations().stream()
                                    .map(v -> GrpcViolation.from(v))
                                    .toList());
                }
            } catch (ValidationException ex) {
                throw new IllegalStateException("Failed to validate gRPC request", ex);
            }
        }
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.validation

import build.buf.protovalidate.ValidationResult
import build.buf.protovalidate.Validator
import build.buf.protovalidate.exceptions.ValidationException
import com.google.protobuf.Message
import io.github.susimsek.springgrpcsamples.exception.GrpcValidationException
import io.github.susimsek.springgrpcsamples.exception.GrpcViolation
import io.grpc.*
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.grpc.server.GlobalServerInterceptor
import org.springframework.stereotype.Component

@Component
@GlobalServerInterceptor
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
class GrpcValidationServerInterceptor(
    private val validator: Validator
) : ServerInterceptor {

    override fun &#x3C;ReqT : Any?, RespT : Any?> interceptCall(
        call: ServerCall&#x3C;ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler&#x3C;ReqT, RespT>
    ): ServerCall.Listener&#x3C;ReqT> {
        val listener = next.startCall(call, headers)
        return ValidationServerCallListener(listener, validator)
    }

    private class ValidationServerCallListener&#x3C;ReqT>(
        delegate: ServerCall.Listener&#x3C;ReqT>,
        private val validator: Validator
    ) : ForwardingServerCallListener.SimpleForwardingServerCallListener&#x3C;ReqT>(delegate) {

        override fun onMessage(message: ReqT) {
            if (message is Message &#x26;&#x26; message.javaClass.packageName == APPLICATION_PROTO_PACKAGE) {
                validate(message)
            }
            super.onMessage(message)
        }

        private fun validate(message: Message) {
            try {
                val result: ValidationResult = validator.validate(message)
                if (!result.isSuccess) {
                    throw GrpcValidationException(result.violations.map { GrpcViolation.from(it) })
                }
            } catch (ex: ValidationException) {
                throw IllegalStateException("Failed to validate gRPC request", ex)
            }
        }
    }

    companion object {
        private const val APPLICATION_PROTO_PACKAGE = "io.github.susimsek.springgrpcsamples.proto"
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/GlobalGrpcExceptionHandler.java</code>
Converts validation, authentication, authorization, and unexpected failures into structured gRPC status responses.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import build.buf.validate.FieldPathElement;
import build.buf.validate.Violation;
import com.google.protobuf.Any;
import com.google.rpc.BadRequest;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.protobuf.StatusProto;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.grpc.server.advice.GrpcAdvice;
import org.springframework.grpc.server.advice.GrpcExceptionHandler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.util.StringUtils;

@GrpcAdvice
@RequiredArgsConstructor
@Slf4j
public class GlobalGrpcExceptionHandler {

    private final MessageSource messageSource;

    @GrpcExceptionHandler(GrpcApiException.class)
    public StatusException handleGrpcApiException(GrpcApiException exception) {
        return Status.fromCode(exception.getStatusCode())
                .withDescription(resolveMessage(exception))
                .asException();
    }

    @GrpcExceptionHandler(GrpcValidationException.class)
    public StatusException handleGrpcValidationException(GrpcValidationException exception) {
        BadRequest.Builder badRequest = BadRequest.newBuilder();
        exception.getViolations().stream()
                .map(this::toFieldViolation)
                .forEach(badRequest::addFieldViolations);

        com.google.rpc.Status status =
                com.google.rpc.Status.newBuilder()
                        .setCode(com.google.rpc.Code.INVALID_ARGUMENT_VALUE)
                        .setMessage(
                                resolveMessage(
                                        "grpc.validation.failed",
                                        "One or more validation errors occurred."))
                        .addDetails(Any.pack(badRequest.build()))
                        .build();

        return StatusProto.toStatusException(status);
    }

    @GrpcExceptionHandler(InvalidBearerTokenException.class)
    public StatusException handleInvalidBearerTokenException(
            InvalidBearerTokenException exception) {
        return Status.UNAUTHENTICATED
                .withDescription(
                        resolveMessage("grpc.auth.invalidToken", "Invalid or expired token."))
                .asException();
    }

    @GrpcExceptionHandler(AuthenticationException.class)
    public StatusException handleAuthenticationException(AuthenticationException exception) {
        return Status.UNAUTHENTICATED
                .withDescription(
                        resolveMessage("grpc.auth.unauthenticated", "Authentication failed."))
                .asException();
    }

    @GrpcExceptionHandler(AccessDeniedException.class)
    public StatusException handleAccessDeniedException(AccessDeniedException exception) {
        return Status.PERMISSION_DENIED
                .withDescription(resolveMessage("grpc.auth.accessDenied", "Access denied."))
                .asException();
    }

    @GrpcExceptionHandler(Exception.class)
    public StatusException handleException(Exception exception) {
        log.error("Unhandled gRPC exception", exception);
        return Status.INTERNAL
                .withDescription(
                        resolveMessage(
                                "grpc.internal",
                                "An unexpected error occurred. Please try again later."))
                .asException();
    }

    private String resolveMessage(GrpcApiException exception) {
        return resolveMessage(
                exception.getMessageCode(),
                exception.getMessage(),
                exception.getMessageArguments());
    }

    private String resolveMessage(
            String messageCode, String defaultMessage, Object... messageArguments) {
        Locale locale = LocaleContextHolder.getLocale();
        return messageSource.getMessage(messageCode, messageArguments, defaultMessage, locale);
    }

    private BadRequest.FieldViolation toFieldViolation(GrpcViolation validationViolation) {
        Violation violation = validationViolation.violation();
        return BadRequest.FieldViolation.newBuilder()
                .setField(resolveField(violation))
                .setDescription(resolveValidationMessage(validationViolation))
                .build();
    }

    private String resolveValidationMessage(GrpcViolation validationViolation) {
        Violation violation = validationViolation.violation();
        String defaultMessage = violation.hasMessage() ? violation.getMessage() : "invalid value";
        return resolveMessage(
                validationViolation.messageCode(),
                defaultMessage,
                validationViolation.messageArguments());
    }

    private static String resolveField(Violation violation) {
        if (!violation.hasField() || violation.getField().getElementsCount() == 0) {
            return resolveMessageLevelField();
        }
        return violation.getField().getElementsList().stream()
                .map(GlobalGrpcExceptionHandler::resolveFieldElement)
                .filter(StringUtils::hasText)
                .reduce((left, right) -> left + "." + right)
                .orElseGet(GlobalGrpcExceptionHandler::resolveMessageLevelField);
    }

    private static String resolveFieldElement(FieldPathElement element) {
        if (element.hasFieldName()) {
            return element.getFieldName();
        }
        if (element.hasFieldNumber()) {
            return String.valueOf(element.getFieldNumber());
        }
        return "";
    }

    private static String resolveMessageLevelField() {
        return "request";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

import build.buf.validate.FieldPathElement
import build.buf.validate.Violation
import com.google.protobuf.Any
import com.google.rpc.BadRequest
import io.grpc.Status
import io.grpc.StatusException
import io.grpc.protobuf.StatusProto
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.grpc.server.advice.GrpcAdvice
import org.springframework.grpc.server.advice.GrpcExceptionHandler
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.AuthenticationException
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException
import org.springframework.util.StringUtils

@GrpcAdvice
class GlobalGrpcExceptionHandler(
    private val messageSource: MessageSource
) {
    private val log = LoggerFactory.getLogger(javaClass)

    @GrpcExceptionHandler(GrpcApiException::class)
    fun handleGrpcApiException(exception: GrpcApiException): StatusException =
        Status.fromCode(exception.statusCode).withDescription(resolveMessage(exception)).asException()

    @GrpcExceptionHandler(GrpcValidationException::class)
    fun handleGrpcValidationException(exception: GrpcValidationException): StatusException {
        val badRequest = BadRequest.newBuilder()
        exception.violations.map(::toFieldViolation).forEach(badRequest::addFieldViolations)
        val status = com.google.rpc.Status.newBuilder()
            .setCode(com.google.rpc.Code.INVALID_ARGUMENT_VALUE)
            .setMessage(resolveMessage("grpc.validation.failed", "One or more validation errors occurred."))
            .addDetails(Any.pack(badRequest.build()))
            .build()
        return StatusProto.toStatusException(status)
    }

    @GrpcExceptionHandler(InvalidBearerTokenException::class)
    fun handleInvalidBearerTokenException(exception: InvalidBearerTokenException): StatusException =
        Status.UNAUTHENTICATED.withDescription(resolveMessage("grpc.auth.invalidToken", "Invalid or expired token.")).asException()

    @GrpcExceptionHandler(AuthenticationException::class)
    fun handleAuthenticationException(exception: AuthenticationException): StatusException =
        Status.UNAUTHENTICATED.withDescription(resolveMessage("grpc.auth.unauthenticated", "Authentication failed.")).asException()

    @GrpcExceptionHandler(AccessDeniedException::class)
    fun handleAccessDeniedException(exception: AccessDeniedException): StatusException =
        Status.PERMISSION_DENIED.withDescription(resolveMessage("grpc.auth.accessDenied", "Access denied.")).asException()

    @GrpcExceptionHandler(Exception::class)
    fun handleException(exception: Exception): StatusException {
        log.error("Unhandled gRPC exception", exception)
        return Status.INTERNAL.withDescription(resolveMessage("grpc.internal", "An unexpected error occurred. Please try again later.")).asException()
    }

    private fun resolveMessage(exception: GrpcApiException): String =
        resolveMessage(exception.messageCode, exception.message ?: "", *exception.messageArguments)

    private fun resolveMessage(messageCode: String, defaultMessage: String, vararg args: Any?): String =
        messageSource.getMessage(messageCode, args, defaultMessage, LocaleContextHolder.getLocale())

    private fun toFieldViolation(validationViolation: GrpcViolation): BadRequest.FieldViolation =
        BadRequest.FieldViolation.newBuilder()
            .setField(resolveField(validationViolation.violation()))
            .setDescription(resolveValidationMessage(validationViolation))
            .build()

    private fun resolveValidationMessage(validationViolation: GrpcViolation): String {
        val violation = validationViolation.violation()
        val defaultMessage = if (violation.hasMessage()) violation.message else "invalid value"
        return resolveMessage(validationViolation.messageCode(), defaultMessage, *validationViolation.messageArguments())
    }

    private fun resolveField(violation: Violation): String {
        if (!violation.hasField() || violation.field.elementsCount == 0) return "request"
        return violation.field.elementsList.map(::resolveFieldElement).filter(StringUtils::hasText).reduceOrNull { left, right -> "$left.$right" } ?: "request"
    }

    private fun resolveFieldElement(element: FieldPathElement): String =
        when {
            element.hasFieldName() -> element.fieldName
            element.hasFieldNumber() -> element.fieldNumber.toString()
            else -> ""
        }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/GrpcApiException.java</code>
Defines the base contract for application-level gRPC exceptions with a status code and message metadata.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import io.grpc.Status;
import lombok.Getter;

@Getter
public abstract class GrpcApiException extends RuntimeException {

    private final Status.Code statusCode;
    private final String messageCode;
    private final Object[] messageArguments;

    protected GrpcApiException(
            Status.Code statusCode,
            String messageCode,
            String defaultMessage,
            Object... messageArguments) {
        super(defaultMessage);
        this.statusCode = statusCode;
        this.messageCode = messageCode;
        this.messageArguments = messageArguments;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

import io.grpc.Status

abstract class GrpcApiException(
    val statusCode: Status.Code,
    val messageCode: String,
    defaultMessage: String,
    vararg val messageArguments: Any?
) : RuntimeException(defaultMessage)
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/GrpcValidationException.java</code>
Wraps all Protovalidate violations into a single runtime exception handled by the global advice.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import java.util.List;
import lombok.Getter;

@Getter
public class GrpcValidationException extends RuntimeException {

    private final List&#x3C;GrpcViolation> violations;

    public GrpcValidationException(List&#x3C;GrpcViolation> violations) {
        super("gRPC request validation failed");
        this.violations = List.copyOf(violations);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

class GrpcValidationException(
    val violations: List&#x3C;GrpcViolation>
) : RuntimeException("gRPC request validation failed")
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/GrpcViolation.java</code>
Normalizes Protovalidate violations into i18n-friendly message codes and arguments.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import build.buf.validate.Violation;
import java.util.Arrays;

public record GrpcViolation(Violation violation, String messageCode, Object[] messageArguments) {

    private static final String CUSTOM_RULE_ID_PREFIX = "grpc.";
    private static final String STANDARD_RULE_MESSAGE_CODE_PREFIX = "grpc.validation.constraints.";
    private static final String UNKNOWN_MESSAGE_CODE = "grpc.validation.unknown";

    public static GrpcViolation from(build.buf.protovalidate.Violation violation) {
        Violation proto = violation.toProto();
        Object[] args =
                isStandardRule(proto)
                        ? new Object[] {violation.getRuleValue().getValue()}
                        : new Object[0];
        return new GrpcViolation(proto, resolveMessageCode(proto), args);
    }

    private static boolean isStandardRule(Violation violation) {
        return violation.hasRuleId() &#x26;&#x26; !violation.getRuleId().startsWith(CUSTOM_RULE_ID_PREFIX);
    }

    private static String resolveMessageCode(Violation violation) {
        if (!violation.hasRuleId()) {
            return UNKNOWN_MESSAGE_CODE;
        }
        String ruleId = violation.getRuleId();
        boolean standardRule = !ruleId.startsWith(CUSTOM_RULE_ID_PREFIX);
        return standardRule ? STANDARD_RULE_MESSAGE_CODE_PREFIX + ruleId : ruleId;
    }

    @Override
    public Object[] messageArguments() {
        return messageArguments;
    }

    @Override
    public boolean equals(Object object) {
        if (object == this) {
            return true;
        }
        return object
                        instanceof
                        GrpcViolation(Violation violation1, String code, Object[] arguments)
                &#x26;&#x26; violation.equals(violation1)
                &#x26;&#x26; messageCode.equals(code)
                &#x26;&#x26; Arrays.equals(messageArguments, arguments);
    }

    @Override
    public int hashCode() {
        int result = violation.hashCode();
        result = 31 * result + messageCode.hashCode();
        result = 31 * result + Arrays.hashCode(messageArguments);
        return result;
    }

    @Override
    public String toString() {
        return "GrpcViolation[violation="
                + violation
                + ", messageCode="
                + messageCode
                + ", messageArguments="
                + Arrays.toString(messageArguments)
                + "]";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

import build.buf.validate.Violation

data class GrpcViolation(
    val violation: Violation,
    val messageCode: String,
    private val args: Array&#x3C;out Any?>
) {
    fun messageArguments(): Array&#x3C;out Any?> = args

    companion object {
        private const val CUSTOM_RULE_ID_PREFIX = "grpc."
        private const val STANDARD_RULE_MESSAGE_CODE_PREFIX = "grpc.validation.constraints."
        private const val UNKNOWN_MESSAGE_CODE = "grpc.validation.unknown"

        fun from(violation: build.buf.protovalidate.Violation): GrpcViolation {
            val proto = violation.toProto()
            val arguments =
                if (isStandardRule(proto)) arrayOf(violation.ruleValue.value) else emptyArray()
            return GrpcViolation(proto, resolveMessageCode(proto), arguments)
        }

        private fun isStandardRule(violation: Violation): Boolean =
            violation.hasRuleId() &#x26;&#x26; !violation.ruleId.startsWith(CUSTOM_RULE_ID_PREFIX)

        private fun resolveMessageCode(violation: Violation): String {
            if (!violation.hasRuleId()) {
                return UNKNOWN_MESSAGE_CODE
            }
            val ruleId = violation.ruleId
            val standardRule = !ruleId.startsWith(CUSTOM_RULE_ID_PREFIX)
            return if (standardRule) STANDARD_RULE_MESSAGE_CODE_PREFIX + ruleId else ruleId
        }
    }
}
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/InvalidCredentialsException.java</code>
Represents failed login attempts as a gRPC <code>UNAUTHENTICATED</code> application exception.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import io.grpc.Status;

public class InvalidCredentialsException extends GrpcApiException {

    public InvalidCredentialsException() {
        super(
                Status.Code.UNAUTHENTICATED,
                "grpc.auth.invalidCredentials",
                "invalid username or password");
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

import io.grpc.Status

class InvalidCredentialsException : GrpcApiException(
    Status.Code.UNAUTHENTICATED,
    "grpc.auth.invalidCredentials",
    "invalid username or password"
)
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/exception/TodoNotFoundException.java</code>
Exposes missing records as a gRPC <code>NOT_FOUND</code> failure with the missing Todo ID.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.exception;

import io.grpc.Status;

public class TodoNotFoundException extends GrpcApiException {

    public TodoNotFoundException(Long id) {
        super(
                Status.Code.NOT_FOUND,
                "grpc.todo.notFound",
                "todo not found with id: " + id,
                id.toString());
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.exception

import io.grpc.Status

class TodoNotFoundException(id: Long) : GrpcApiException(
    Status.Code.NOT_FOUND,
    "grpc.todo.notFound",
    "todo not found with id: $id",
    id.toString()
)
</code></pre>
<ul>
<li><code>src/main/java/io/github/susimsek/springgrpcsamples/config/aot/NativeRuntimeHints.java</code>
Registers i18n resources, protobuf request classes, and exception handler reflection hints for native builds.</li>
</ul>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springgrpcsamples.config.aot;

import io.github.susimsek.springgrpcsamples.exception.GlobalGrpcExceptionHandler;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;

public class NativeRuntimeHints implements RuntimeHintsRegistrar {

    private static final String APPLICATION_PROTO_REQUEST_PATTERN =
            "classpath*:io/github/susimsek/springgrpcsamples/proto/*Request.class";

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.resources().registerPattern("i18n/**");
        hints.reflection()
                .registerTypes(
                        findApplicationProtoRequestTypes(classLoader).stream()
                                .flatMap(
                                        typeName ->
                                                Stream.of(
                                                        TypeReference.of(typeName),
                                                        TypeReference.of(typeName + "$Builder")))
                                .toList(),
                        builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
        hints.reflection()
                .registerType(
                        GlobalGrpcExceptionHandler.class, MemberCategory.INVOKE_PUBLIC_METHODS);
    }

    private static List&#x3C;String> findApplicationProtoRequestTypes(ClassLoader classLoader) {
        PathMatchingResourcePatternResolver resolver =
                new PathMatchingResourcePatternResolver(classLoader);
        SimpleMetadataReaderFactory metadataReaderFactory =
                new SimpleMetadataReaderFactory(classLoader);
        try {
            return Stream.of(resolver.getResources(APPLICATION_PROTO_REQUEST_PATTERN))
                    .filter(Resource::isReadable)
                    .map(resource -> className(resource, metadataReaderFactory))
                    .sorted()
                    .toList();
        } catch (IOException ex) {
            throw new IllegalStateException("Failed to scan application proto request types", ex);
        }
    }

    private static String className(
            Resource resource, SimpleMetadataReaderFactory metadataReaderFactory) {
        try {
            MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
            return metadataReader.getClassMetadata().getClassName();
        } catch (IOException ex) {
            throw new IllegalStateException("Failed to read proto request metadata", ex);
        }
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springgrpcsamples.config.aot

import io.github.susimsek.springgrpcsamples.exception.GlobalGrpcExceptionHandler
import org.springframework.aot.hint.MemberCategory
import org.springframework.aot.hint.RuntimeHints
import org.springframework.aot.hint.RuntimeHintsRegistrar
import org.springframework.aot.hint.TypeReference
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.core.type.classreading.MetadataReader
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory
import java.io.IOException

class NativeRuntimeHints : RuntimeHintsRegistrar {

    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader) {
        hints.resources().registerPattern("i18n/**")
        hints.reflection().registerTypes(
            findApplicationProtoRequestTypes(classLoader).flatMap { typeName ->
                listOf(TypeReference.of(typeName), TypeReference.of("${typeName}\$Builder"))
            }
        ) { builder ->
            builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)
        }
        hints.reflection().registerType(
            GlobalGrpcExceptionHandler::class.java,
            MemberCategory.INVOKE_PUBLIC_METHODS
        )
    }

    private fun findApplicationProtoRequestTypes(classLoader: ClassLoader): List&#x3C;String> {
        val resolver = PathMatchingResourcePatternResolver(classLoader)
        val metadataReaderFactory = SimpleMetadataReaderFactory(classLoader)
        try {
            return resolver.getResources(APPLICATION_PROTO_REQUEST_PATTERN)
                .asSequence()
                .filter(Resource::isReadable)
                .map { resource -> className(resource, metadataReaderFactory) }
                .sorted()
                .toList()
        } catch (ex: IOException) {
            throw IllegalStateException("Failed to scan application proto request types", ex)
        }
    }

    private fun className(
        resource: Resource,
        metadataReaderFactory: SimpleMetadataReaderFactory
    ): String =
        try {
            val metadataReader: MetadataReader = metadataReaderFactory.getMetadataReader(resource)
            metadataReader.classMetadata.className
        } catch (ex: IOException) {
            throw IllegalStateException("Failed to read proto request metadata", ex)
        }

    companion object {
        private const val APPLICATION_PROTO_REQUEST_PATTERN =
            "classpath*:io/github/susimsek/springgrpcsamples/proto/*Request.class"
    }
}
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<p>Start the server with the Maven Wrapper:</p>
<pre><code class="language-bash">./mvnw spring-boot:run

grpcurl -plaintext localhost:9090 list

TOKEN=$(grpcurl -plaintext \
  -d '{"username":"admin","password":"admin"}' \
  localhost:9090 \
  AuthService/Login | jq -r '.access_token')

grpcurl -plaintext \
  -rpc-header "authorization: Bearer ${TOKEN}" \
  -d '{"pageRequest":{"page":0,"size":5}}' \
  localhost:9090 \
  TodoService/ListTodos
</code></pre>
<p>Expected behavior:</p>
<ul>
<li><code>grpcurl ... list</code> should show <code>AuthService</code>, <code>TodoService</code>, <code>grpc.health.v1.Health</code>, and reflection support.</li>
<li><code>AuthService/Login</code> should return <code>access_token</code>, <code>token_type</code>, and <code>expires_in</code>.</li>
<li><code>TodoService/ListTodos</code> should work only with a valid bearer token that has the expected authority.</li>
<li>Invalid payloads should return <code>InvalidArgument</code> with structured field violations.</li>
</ul>
<p>The repository also uses in-process gRPC transport for integration tests. Its README shows <code>@AutoConfigureTestGrpcTransport</code> and explicit stub registration with <code>@ImportGrpcClients</code>.</p>
<hr>
<h2>🧪 Test gRPC Endpoints</h2>
<p>Use <code>grpcurl</code> to exercise both authorization paths: the seeded <code>admin</code> account should complete Todo operations, while <code>user</code> should receive an authorization error.</p>
<h3>Admin Flow</h3>
<p>Login as <code>admin</code> and capture the JWT token:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -d '{"username":"admin","password":"admin"}' \
  localhost:9090 \
  AuthService/Login
</code></pre>
<p>Expected response:</p>
<pre><code class="language-json">{
  "accessToken": "&#x3C;jwt-token>",
  "tokenType": "Bearer",
  "expiresIn": "3600"
}
</code></pre>
<p>Use that token to list Todos:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -d '{"pageRequest":{"page":0,"size":5}}' \
  localhost:9090 \
  TodoService/ListTodos
</code></pre>
<p>Create a Todo:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -d '{"title":"Write article examples"}' \
  localhost:9090 \
  TodoService/CreateTodo
</code></pre>
<p>Patch an existing Todo:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -d '{"id":1,"completed":true}' \
  localhost:9090 \
  TodoService/PatchTodo
</code></pre>
<p>Delete a Todo:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -d '{"id":1}' \
  localhost:9090 \
  TodoService/DeleteTodo
</code></pre>
<h3>User Flow</h3>
<p>Login as the lower-privileged <code>user</code> account:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -d '{"username":"user","password":"user"}' \
  localhost:9090 \
  AuthService/Login
</code></pre>
<p>Then try to call an admin-protected RPC:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -d '{"pageRequest":{"page":0,"size":5}}' \
  localhost:9090 \
  TodoService/ListTodos
</code></pre>
<p>Expected behavior:</p>
<ul>
<li><code>admin</code> can call every <code>TodoService/*</code> RPC successfully.</li>
<li><code>user</code> receives <code>PermissionDenied</code> because the service requires <code>ROLE_ADMIN</code>.</li>
<li>Invalid credentials return <code>Unauthenticated</code>.</li>
</ul>
<h3>Validation and i18n Check</h3>
<p>Send an invalid payload to verify Protovalidate and localized error handling:</p>
<pre><code class="language-bash">grpcurl -plaintext \
  -rpc-header "authorization: Bearer &#x3C;jwt-token>" \
  -rpc-header "accept-language: tr" \
  -d '{"title":""}' \
  localhost:9090 \
  TodoService/CreateTodo
</code></pre>
<p>Expected behavior:</p>
<ul>
<li>The RPC fails with <code>InvalidArgument</code>.</li>
<li>The response details contain structured field violations.</li>
<li>Validation messages are resolved from <code>messages_tr.properties</code> when <code>accept-language: tr</code> is present.</li>
</ul>
<hr>
<h2>🏁 Conclusion</h2>
<p>The example combines protobuf contracts, JPA persistence, Spring Security, interceptor-based validation, and centralized gRPC error mapping. Before production, move the sample JWT defaults and database settings into secure environment-specific configuration, then add regression tests for the RPC contracts your clients depend on.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[gRPC]]></category>
      <category><![CDATA[Security]]></category>
      <category><![CDATA[Validation]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-grpc-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Captain Tsubasa 2: World Fighters]]></title>
      <link>https://suaybsimsek.com/posts/captain-tsubasa-2-world-fighters</link>
      <description><![CDATA[A detailed breakdown of Captain Tsubasa 2: World Fighters from Bandai Namco’s official page and community tracking, covering platforms, gameplay scale, known details, and open questions before launch.]]></description>
      <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
      <atom:updated>2026-02-28T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/captain-tsubasa-2-world-fighters</guid>
      <content:encoded><![CDATA[<p>A new major Captain Tsubasa game is on the way, and the first official reveal already points to a much bigger scope than many fans expected. <code>Captain Tsubasa 2: World Fighters</code> is being presented as a high-energy arcade soccer experience with a strong anime identity and a large playable roster.</p>
<p>In this article, I separate confirmed details from early assumptions, using the official Bandai Namco page as the primary source and the community wiki as a secondary tracking reference.</p>
<hr>
<h2>🌟 Why This Announcement Matters</h2>
<p>What makes this announcement notable is not only the sequel label, but the scale implied by the official messaging: broader platform reach, a larger cast, and a stronger emphasis on cinematic match presentation.</p>
<ul>
<li>Official page already lists a <code>2026</code> release window</li>
<li>Platform coverage includes console + PC (<code>Steam</code>)</li>
<li>The reveal messaging emphasizes scale: teams, characters, and cinematic cutscenes</li>
<li>Gameplay wording suggests a deeper on-field action system rather than a simple roster refresh</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>This breakdown is written for readers who already know the <code>Captain Tsubasa</code> anime/manga universe and want a clean, decision-friendly snapshot of what is confirmed today.</p>
<ul>
<li>Familiarity with arcade football / anime sports game design</li>
<li>Interest in launch-platform planning (<code>PS5</code>, <code>Xbox Series X|S</code>, <code>Switch</code>, <code>PC</code>)</li>
<li>Expectation that early announcement pages may still change before release</li>
</ul>
<hr>
<h2>🛠️ Official Facts</h2>
<p>Start with the official Bandai Namco page. At this stage, it is the most reliable source for release-window, platform, and publisher-facing metadata.</p>
<p>Source links:</p>
<ul>
<li><a href="https://www.bandainamcoent.com/games/captain-tsubasa-2-world-fighters">Bandai Namco - CAPTAIN TSUBASA 2: WORLD FIGHTERS</a></li>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa_2:_World_Fighters">Captain Tsubasa Fandom Wiki page</a></li>
<li><a href="https://youtu.be/oXf50MRXfM8">Announcement Trailer (YouTube)</a></li>
</ul>
<p>Confirmed on the official page:</p>
<ul>
<li>Title: <code>CAPTAIN TSUBASA 2: WORLD FIGHTERS</code></li>
<li>Release window: <code>2026</code></li>
<li>Developer listed: <code>Tamsoft Corporation</code></li>
<li>Genre listing: <code>Sport (Arcade)</code></li>
<li>Feature tag shown: <code>Single-player</code></li>
<li>Platforms shown: <code>PlayStation 5</code>, <code>Xbox Series X|S</code>, <code>Steam</code>, <code>Nintendo Switch</code></li>
</ul>
<p>The page also includes wishlist/store entry points, media assets, and a news card tied to the reveal announcement dated <code>February 5, 2026</code>.</p>
<hr>
<h2>🛠️ Gameplay Pitch</h2>
<p>Even without a full gameplay breakdown, the wording on the official page is enough to outline the intended player experience.</p>
<p>The official overview highlights:</p>
<ul>
<li><code>22 national teams</code></li>
<li><code>110+ playable characters</code></li>
<li>A “super action soccer” style with constant superhuman plays</li>
</ul>
<p>The feature section adds more detail:</p>
<ul>
<li><code>150+</code> cutscenes/move highlights on the pitch</li>
<li>New and improved on-field action for passing, dribbling, tackling, and blocking</li>
<li>A new goalkeeper-versus-shooter interaction system for super shots and blocks</li>
</ul>
<p>Taken together, these points suggest a cinematic arcade formula that leans into anime spectacle while adding more structured shot-versus-save moments.</p>
<hr>
<h2>🛠️ Community References</h2>
<p>The Fandom page is useful as a fast-moving reference hub, but it should still be treated as a secondary source until the official site and storefront pages publish fuller details.</p>
<p>As of <code>February 26, 2026</code>, the Fandom page is helpful for tracking updates, but several areas still appear to be in-progress:</p>
<ul>
<li>Story section placeholders are still being filled</li>
<li>Team listings exist, but coverage may be incomplete</li>
<li>Gallery/media and metadata sections can update quickly after trailers/news drops</li>
</ul>
<p>That is normal for a newly announced title. Use the wiki to monitor changes, but verify release-critical details against official sources before making purchase assumptions.</p>
<hr>
<h2>🧪 Key Unknowns</h2>
<p>Reveal pages usually lock in the marketing pitch first, while the most purchase-relevant details arrive later. These are the key items to watch next.</p>
<ul>
<li>Exact release date (official page currently shows <code>2026</code> only)</li>
<li>Full mode list (single-player is listed, but online/local modes need explicit confirmation)</li>
<li>Pricing and edition breakdowns by platform/store</li>
<li>Performance targets on <code>Switch</code> vs <code>PS5/Xbox Series X|S/PC</code></li>
<li>Monetization details (the official page already shows ESRB interactive notice for <code>In-Game Purchases</code>)</li>
<li>Region-specific availability and language support</li>
</ul>
<p>The official page also lists ESRB-related descriptors such as <code>Mild Violence</code> and <code>Mild Language</code>, which helps set early expectations for the U.S. rating context.</p>
<hr>
<h2>▶️ Tracking Updates</h2>
<p>If you want to stay updated without chasing every rumor, this order keeps the signal-to-noise ratio high:</p>
<ol>
<li>Official Bandai Namco game page for platform and release metadata changes</li>
<li>Official trailers/media on the game page and store pages</li>
<li>Community wiki/Fandom page for aggregated roster/team/story references</li>
<li>Store pages (<code>Steam</code>, console storefronts) for pricing, edition, and system requirement updates</li>
</ol>
<p>If you plan to buy at launch, wishlist the game on your preferred platform and re-check the official page after each trailer or news drop.</p>
<hr>
<h2>🎬 Media</h2>
<p>This media section is split into two parts so you can quickly check the official trailer first, then browse the visual gallery snapshots.</p>
<h3>Trailer</h3>

  <a href="https://youtu.be/oXf50MRXfM8" target="_blank" rel="noopener noreferrer">
    <img src="/images/posts/captain-tsubasa-2-world-fighters/announcement-trailer.webp" alt="Captain Tsubasa 2 World Fighters Announcement Trailer thumbnail" width="1200" height="675">
  </a>
  
    Announcement Trailer
    <br>
    <a class="" href="https://youtu.be/oXf50MRXfM8" target="_blank" rel="noopener noreferrer">Watch on YouTube</a>
  

<h3>Gallery</h3>
<p>This gallery gives you a quick visual read on the game’s art direction, character presentation, and match effects while the official rollout is still in its early phase.</p>
<p>Source gallery: <a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa_2:_World_Fighters">Captain Tsubasa Fandom Wiki page</a></p>
<div class="post-media-grid">
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-cover.webp" alt="Captain Tsubasa 2 World Fighters cover art" width="1200" height="630">
    Cover art
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-flying-drive-shot.webp" alt="Tsubasa Flying Drive Shot screenshot" width="1200" height="630">
    Flying Drive Shot scene
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-wakabayashi.webp" alt="Wakabayashi Genzo artwork" width="1200" height="630">
    Wakabayashi Genzo character art
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-jito-hiroshi.webp" alt="Jito Hiroshi artwork" width="1200" height="630">
    Jito Hiroshi character art
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-hayabusa-volley.webp" alt="Hayabusa Volley screenshot" width="1200" height="630">
    Hayabusa Volley scene
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-neo-tiger-shot.webp" alt="Neo Tiger Shot screenshot" width="1200" height="630">
    Neo Tiger Shot scene
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-tsubasa-ozora.webp" alt="Tsubasa Ozora artwork" width="1200" height="630">
    Tsubasa Ozora character art
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-misugi-jun.webp" alt="Misugi Jun artwork" width="1200" height="630">
    Misugi Jun character art
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-heel-lift-vs-santana.webp" alt="Heel Lift versus Santana screenshot" width="1200" height="630">
    Heel Lift confrontation scene
  
  
    <img src="/images/posts/captain-tsubasa-2-world-fighters/gallery-drive-shot.webp" alt="Drive Shot screenshot" width="1200" height="630">
    Drive Shot scene
  
</div>
<hr>
<h2>🏁 Conclusion</h2>
<p>Based on the current reveal materials, <code>Captain Tsubasa 2: World Fighters</code> looks like a significantly bigger anime arcade football release with broad platform coverage, a large roster, and a more cinematic gameplay pitch. The practical next step is to track the official Bandai Namco page for the exact release date and full mode breakdown before choosing a launch platform.</p>]]></content:encoded>
      <category><![CDATA[Captain Tsubasa]]></category>
      <category><![CDATA[Anime Game]]></category>
      <category><![CDATA[Sports Game]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/captain-tsubasa-2-world-fighters-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Captain Tsubasa: Rise of New Champions]]></title>
      <link>https://suaybsimsek.com/posts/captain-tsubasa-rise-of-new-champions</link>
      <description><![CDATA[Before World Fighters arrives, Rise of New Champions remains the clearest snapshot of Captain Tsubasa’s modern console identity: fast, theatrical, and built around pure anime football spectacle.]]></description>
      <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
      <atom:updated>2026-02-27T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/captain-tsubasa-rise-of-new-champions</guid>
      <content:encoded><![CDATA[<p>Before <code>Captain Tsubasa 2: World Fighters</code> arrives, <code>Captain Tsubasa: Rise of New Champions</code> remains the modern reference point for what a big-budget Captain Tsubasa console game looks like. It is still the clearest example of how Bandai Namco and Tamsoft translated the series into fast, spectacle-first arcade football.</p>
<p>This article looks at the game from a current editorial angle: what was officially confirmed at launch, what the edition and DLC structure tells us, and why the game still matters if you are revisiting the series today.</p>
<hr>
<h2>🌟 Why This Game Still Matters</h2>
<p><code>Rise of New Champions</code> was positioned as the first new Captain Tsubasa console game in roughly a decade, and that context still matters. It set the template that later discussions around the franchise continue to reference: anime-faithful presentation, exaggerated super plays, and a clear preference for arcade speed over simulation.</p>
<ul>
<li>Official release date on the Bandai Namco Europe page is <code>August 28, 2020</code></li>
<li>The official page lists <code>TAMSOFT</code> as developer and <code>Arcade</code> as the genre label</li>
<li>Platform listing on the official page covers <code>PlayStation 4</code>, <code>Nintendo Switch</code>, and <code>Steam</code></li>
<li>The current official page still reflects a sizable edition and DLC footprint, which makes it a useful baseline when comparing later Captain Tsubasa releases</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>This breakdown is written for readers who want a practical picture of <code>Rise of New Champions</code> today, not just a launch-day recap.</p>
<ul>
<li>Basic familiarity with the <code>Captain Tsubasa</code> anime/manga universe</li>
<li>Interest in anime-styled arcade football rather than realistic football simulation</li>
<li>Willingness to check edition/store details carefully, because this game’s content rollout extended beyond the base launch package</li>
</ul>
<hr>
<h2>🛠️ Official Facts</h2>
<p>Start with the Bandai Namco Europe page. For release date, platform support, and official product framing, it remains the cleanest primary source.</p>
<p>Source links:</p>
<ul>
<li><a href="https://en.bandainamcoent.eu/captain-tsubasa/captain-tsubasa-rise-of-new-champions">Bandai Namco Europe - CAPTAIN TSUBASA: RISE OF NEW CHAMPIONS</a></li>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa:_Rise_of_New_Champions">Captain Tsubasa Fandom Wiki page</a></li>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa:_Rise_of_New_Champions/Gallery#Art">Fandom Art gallery</a></li>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa:_Rise_of_New_Champions/Gallery#Japan">Fandom Japan gallery reference</a></li>
<li><a href="https://youtu.be/WHBQO6tH4Dk">Official trailer embed on Bandai page (YouTube)</a></li>
</ul>
<p>Confirmed on the official page:</p>
<ul>
<li>Title: <code>CAPTAIN TSUBASA: RISE OF NEW CHAMPIONS</code></li>
<li>Release date: <code>28/08/2020</code></li>
<li>Developer: <code>TAMSOFT</code></li>
<li>Genre: <code>Arcade</code></li>
<li>Platforms shown: <code>PlayStation 4</code>, <code>Nintendo Switch</code>, <code>Steam</code></li>
</ul>
<p>The official description also frames the game as the series’ return to consoles after roughly ten years, with an explicit promise of anime spectacle and up-to-date presentation.</p>
<hr>
<h2>🛠️ Gameplay Pitch</h2>
<p>The official key-features section makes the game’s direction very clear: this is not a grounded football sim. It is an anime football action game built around visual impact and fast exchanges.</p>
<p>The official page emphasizes:</p>
<ul>
<li>Toon shading combined with more realistic effects</li>
<li>Detailed presentation for signature character styles</li>
<li>High-speed “arcade football action”</li>
<li>Simple controls designed to make special moves and highlight plays accessible</li>
</ul>
<p>That framing is important because it explains both the game’s appeal and its limits. If you want structured, sim-like football, this is the wrong entry point. If you want exaggerated shots, tackles, rivalries, and momentum swings that feel close to the anime, this is exactly the lane the game targets.</p>
<hr>
<h2>🛠️ Editions and DLC</h2>
<p>One of the most practical things about the current official page is that it still exposes how much post-launch content was attached to the game.</p>
<p>The page shows multiple editions and related content references, including:</p>
<ul>
<li><code>Champions Edition</code></li>
<li><code>Collector's Edition</code></li>
<li><code>Deluxe Edition</code></li>
<li><code>Legends Edition</code></li>
<li><code>New Hero Edition</code></li>
<li><code>Character Pass: 9 DLC players</code></li>
<li><code>Season Pass Bonus: New Champions Uniform Set</code></li>
<li><code>Deluxe Edition Bonus: V Jump Collaboration Uniform Set</code></li>
</ul>
<p>From a player’s perspective, this matters more than marketing nostalgia. If you are buying or replaying the game now, edition structure directly affects how much roster-related and cosmetic content you actually receive.</p>
<p>The official page also still surfaces later DLC/news references such as <code>EPISODE: RISING STARS</code>, which shows that the game’s support window expanded well beyond the base launch moment.</p>
<hr>
<h2>🛠️ Community References</h2>
<p>The Fandom page is still useful, especially when you want a single place for roster references, story structure summaries, and archived promotional art. That includes the <code>Art</code> and <code>Japan</code> gallery sections, which are useful visual references when official media blocks rotate over time.</p>
<p>Use it for:</p>
<ul>
<li>Aggregated roster/team references</li>
<li>Story-mode and character reference browsing</li>
<li>Archived gallery snapshots and regional cover/art references</li>
</ul>
<p>Use official sources first for:</p>
<ul>
<li>Release date and platform confirmation</li>
<li>Edition/store metadata</li>
<li>Current DLC/storefront navigation</li>
</ul>
<p>The right split is simple: official pages for buying facts, community pages for context and archival browsing.</p>
<hr>
<h2>▶️ What to Verify Today</h2>
<p>Because this is a 2020 release with layered editions and DLC, the key questions today are not the same as they were at launch.</p>
<p>Check these points first:</p>
<ol>
<li>Which edition is actually available on your preferred platform today</li>
<li>Whether the <code>Character Pass</code> or later DLC content is bundled or separate</li>
<li>Whether you want the game mainly for arcade match play, series presentation, or collection value</li>
<li>Whether the platform you are considering (<code>PS4</code>, <code>Switch</code>, or <code>Steam</code>) is the one you actually want to play long-term</li>
</ol>
<p>If your goal is to understand the modern Captain Tsubasa console formula before <code>World Fighters</code>, this game still does that job well. If your goal is simply to own the most complete package, edition details matter enough that you should not treat storefront listings as interchangeable.</p>
<hr>
<h2>🎬 Media</h2>
<p>This media section starts with the official trailer and then moves into a quick visual gallery built from stable official assets. For additional archived artwork and the Japan cover reference, the Fandom gallery links above remain useful companion sources.</p>
<h3>Trailer</h3>

  <a href="https://youtu.be/WHBQO6tH4Dk" target="_blank" rel="noopener noreferrer">
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/announcement-trailer.webp" alt="Captain Tsubasa Rise of New Champions trailer thumbnail" width="1200" height="675">
  </a>
  
    Official Trailer
    <br>
    <a class="" href="https://youtu.be/WHBQO6tH4Dk" target="_blank" rel="noopener noreferrer">Watch on YouTube</a>
  

<h3>Gallery</h3>
<p>This gallery gives you a fast read on the game’s visual identity: anime-faithful key art, match presentation frames, and edition packaging that defined the launch period.</p>
<p>Additional reference links:</p>
<ul>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa:_Rise_of_New_Champions/Gallery#Art">Fandom Art gallery</a></li>
<li><a href="https://captaintsubasa.fandom.com/wiki/Captain_Tsubasa:_Rise_of_New_Champions/Gallery#Japan">Fandom Japan cover reference</a></li>
</ul>
<div class="post-media-grid">
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-drive-shot.webp" alt="Tsubasa&#x27;s Drive Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Tsubasa's Drive Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-key-art.webp" alt="Tsubasa&#x27;s Neo Drive Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Tsubasa's Neo Drive Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-breakthrough.webp" alt="Hyuga&#x27;s Straight Line Dribble from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Hyuga's Straight Line Dribble
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-key-feature-1.webp" alt="Hyuga&#x27;s Neo Tiger Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Hyuga's Neo Tiger Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-key-feature-2.webp" alt="Hyuga and Tsubasa&#x27;s Neo Drive Tiger Twin Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Hyuga and Tsubasa's Neo Drive Tiger Twin Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-champions-edition.webp" alt="Schneider&#x27;s Fire Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Schneider's Fire Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-new-hero-edition.webp" alt="Wakabayashi&#x27;s save from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Wakabayashi's save
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-packshot.webp" alt="Misugi&#x27;s Slice Shot from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Misugi's Slice Shot
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-sky-rocket-volley.webp" alt="Misugi&#x27;s Crystal Overhead from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Misugi's Crystal Overhead
  
  
    <img src="/images/posts/captain-tsubasa-rise-of-new-champions/gallery-carlos-turn.webp" alt="Carlos Turn from Captain Tsubasa Rise of New Champions" width="1200" height="630">
    Carlos Turn
  
</div>
<hr>
<h2>🏁 Conclusion</h2>
<p><code>Captain Tsubasa: Rise of New Champions</code> is still the clearest modern template for the franchise’s console direction before <code>World Fighters</code>: aggressive arcade pacing, anime-faithful spectacle, and a product structure shaped heavily by editions and DLC. If you are revisiting the series now, the smartest move is to verify edition contents first, then decide whether you want this game as a playable entry point, a collection piece, or a baseline for comparing the next Captain Tsubasa release.</p>]]></content:encoded>
      <category><![CDATA[Captain Tsubasa]]></category>
      <category><![CDATA[Anime Game]]></category>
      <category><![CDATA[Sports Game]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/captain-tsubasa-rise-of-new-champions-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Configuration Properties]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-configuration-properties</link>
      <description><![CDATA[Learn how to use @ConfigurationProperties for type-safe configuration, validate settings with @Validated, and manage environment-specific values with profile-specific application-{profile}.yml files.]]></description>
      <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
      <atom:updated>2026-02-17T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-configuration-properties</guid>
      <content:encoded><![CDATA[<p>If you’ve ever ended up with a dozen <code>@Value</code> annotations and no idea which settings are required, <code>@ConfigurationProperties</code> is the Spring Boot way to make configuration type-safe, structured, and validatable.</p>
<p>In this post, you’ll learn:</p>
<ul>
<li>How to model configuration as a class (Java/Kotlin)</li>
<li>How to validate configuration at startup</li>
<li>How profile-specific config files (<code>application-{profile}.yml</code>) override defaults</li>
</ul>
<hr>
<h2>🌟 Why <code>@ConfigurationProperties</code>?</h2>
<p>In this section, we clarify Why <code>@ConfigurationProperties</code>? and summarize the key points you will apply in implementation.</p>
<ul>
<li>✅ Type-safe access to config (IDE autocomplete, refactoring-safe)</li>
<li>✅ Group and document related settings under a single prefix</li>
<li>✅ Validate misconfigurations early (fail fast on startup)</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>Java 17+</li>
<li>Spring Boot 3.x</li>
<li>Maven or Gradle</li>
</ul>
<hr>
<h2>🛠️ Step 1: Define configuration in <code>application.yml</code></h2>
<p>Create a namespace (prefix) for your app config, for example <code>app</code>:</p>
<pre><code class="language-yaml">app:
  security:
    enabled: true
    token-expiry-seconds: 3600
  allowed-origins:
    - 'https://example.com'
    - 'http://localhost:3000'
</code></pre>
<hr>
<h2>🛠️ Step 2: Create a <code>@ConfigurationProperties</code> class</h2>
<h3>Java</h3>
<pre><code class="language-java">import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app")
public class AppProperties {

  private Security security = new Security();
  private List&#x3C;String> allowedOrigins = List.of();

  public Security getSecurity() {
    return security;
  }

  public void setSecurity(Security security) {
    this.security = security;
  }

  public List&#x3C;String> getAllowedOrigins() {
    return allowedOrigins;
  }

  public void setAllowedOrigins(List&#x3C;String> allowedOrigins) {
    this.allowedOrigins = allowedOrigins;
  }

  public static class Security {
    private boolean enabled = true;
    private int tokenExpirySeconds = 3600;

    public boolean isEnabled() {
      return enabled;
    }

    public void setEnabled(boolean enabled) {
      this.enabled = enabled;
    }

    public int getTokenExpirySeconds() {
      return tokenExpirySeconds;
    }

    public void setTokenExpirySeconds(int tokenExpirySeconds) {
      this.tokenExpirySeconds = tokenExpirySeconds;
    }
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "app")
data class AppProperties(
  val security: Security = Security(),
  val allowedOrigins: List&#x3C;String> = emptyList(),
) {
  data class Security(
    val enabled: Boolean = true,
    val tokenExpirySeconds: Int = 3600,
  )
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Enable configuration properties</h2>
<p>You have two common options:</p>
<ol>
<li>Scan for <code>@ConfigurationProperties</code> types:</li>
</ol>
<pre><code class="language-java">import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {}
</code></pre>
<ol start="2">
<li>Register specific classes explicitly:</li>
</ol>
<pre><code class="language-java">import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class DemoApplication {}
</code></pre>
<hr>
<h2>🧪 Step 4: Validate properties at startup (fail fast)</h2>
<p>Add validation constraints and annotate the class with <code>@Validated</code>.</p>
<h3>Java</h3>
<pre><code class="language-java">import java.util.List;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

  @Valid
  private Security security = new Security();

  @NotEmpty
  private List&#x3C;String> allowedOrigins = List.of();

  public Security getSecurity() { return security; }
  public void setSecurity(Security security) { this.security = security; }

  public List&#x3C;String> getAllowedOrigins() { return allowedOrigins; }
  public void setAllowedOrigins(List&#x3C;String> allowedOrigins) { this.allowedOrigins = allowedOrigins; }

  public static class Security {
    private boolean enabled = true;

    @Min(60)
    private int tokenExpirySeconds = 3600;

    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }

    public int getTokenExpirySeconds() { return tokenExpirySeconds; }
    public void setTokenExpirySeconds(int tokenExpirySeconds) { this.tokenExpirySeconds = tokenExpirySeconds; }
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">import jakarta.validation.Valid
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotEmpty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated

@ConfigurationProperties(prefix = "app")
@Validated
data class AppProperties(
  @field:Valid
  val security: Security = Security(),

  @field:NotEmpty
  val allowedOrigins: List&#x3C;String> = emptyList(),
) {
  data class Security(
    val enabled: Boolean = true,

    @field:Min(60)
    val tokenExpirySeconds: Int = 3600,
  )
}
</code></pre>
<p>Note: you need a Bean Validation implementation on the classpath (e.g., <code>spring-boot-starter-validation</code>) for these constraints to be enforced.</p>
<hr>
<h2>🛠️ Step 5: Use profile-specific files (<code>application-{profile}.yml</code>)</h2>
<p>Spring Boot loads <code>application.yml</code> and then applies profile-specific overrides.</p>
<p>Example:</p>
<ul>
<li><code>application.yml</code> (defaults)</li>
<li><code>application-prod.yml</code> (production overrides)</li>
</ul>
<pre><code class="language-yaml"># application-prod.yml
app:
  security:
    enabled: true
  allowed-origins:
    - 'https://mycompany.com'
</code></pre>
<p>When multiple profiles are active, profile files are applied with a “last wins” strategy (the last active profile can override earlier ones).</p>
<hr>
<h2>🌟 Tips</h2>
<p>In this section, we clarify Tips and summarize the key points you will apply in implementation.</p>
<ul>
<li>Prefer <code>@ConfigurationProperties</code> over scattered <code>@Value</code> for anything non-trivial.</li>
<li>Keep a single <code>AppProperties</code> root per bounded domain (<code>security</code>, <code>mail</code>, <code>storage</code>, etc.).</li>
<li>Add validation constraints for anything that must not be empty or must be within a safe range.</li>
</ul>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a type-safe Spring Boot configuration layer with startup validation and profile-based overrides. As a next step, externalize environment-specific secrets and add configuration tests to catch regressions early.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Configuration]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-configuration-properties-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot GraphQL JWE Authentication]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-graphql-jwe-authentication</link>
      <description><![CDATA[Learn how to secure your Spring Boot GraphQL APIs with stateless encrypted JWTs (JWE) while persisting user identities and roles in a JPA-backed database.]]></description>
      <pubDate>Sat, 17 May 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-05-18T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-graphql-jwe-authentication</guid>
      <content:encoded><![CDATA[<p>Spring Boot GraphQL JWE Authentication combines the flexibility of GraphQL with stateless encrypted JWTs (JWE) and a JPA-backed user store to deliver a secure, scalable API.</p>
<hr>
<h2>🌟 Why Use GraphQL + JWE Authentication?</h2>
<p>In this section, we clarify Why Use GraphQL + JWE Authentication? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Stateless Security: Tokens are self-contained and require no server-side storage.</li>
<li>GraphQL Flexibility: Secure any query or mutation uniformly.</li>
<li>Data Precision: Fetch exactly what clients request.</li>
<li>Integrity: Signed tokens ensure tamper evidence.</li>
<li>Confidentiality: Encrypted JWTs hide sensitive claims.</li>
<li>Standards-based: Leverage JOSE, Spring Security, and GraphQL.</li>
<li>Scalable: Scale horizontally without session replication or sticky sessions.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 17 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse)</li>
<li>🛢️ PostgreSQL (or H2 for dev)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include these in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependencies>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-websocket&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.security&#x3C;/groupId>
    &#x3C;artifactId>spring-security-oauth2-resource-server&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-data-jpa&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-graphql&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>com.graphql-java&#x3C;/groupId>
    &#x3C;artifactId>graphql-java-extended-scalars&#x3C;/artifactId>
    &#x3C;version>22.0&#x3C;/version>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.liquibase&#x3C;/groupId>
    &#x3C;artifactId>liquibase-core&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.projectlombok&#x3C;/groupId>
    &#x3C;artifactId>lombok&#x3C;/artifactId>
    &#x3C;optional>true&#x3C;/optional>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>com.h2database&#x3C;/groupId>
    &#x3C;artifactId>h2&#x3C;/artifactId>
    &#x3C;scope>runtime&#x3C;/scope>
  &#x3C;/dependency>
&#x3C;/dependencies>

&#x3C;build>
&#x3C;plugins>
  &#x3C;plugin>
    &#x3C;groupId>org.hibernate.orm.tooling&#x3C;/groupId>
    &#x3C;artifactId>hibernate-enhance-maven-plugin&#x3C;/artifactId>
    &#x3C;version>${hibernate.version}&#x3C;/version>
    &#x3C;executions>
      &#x3C;execution>
        &#x3C;id>enhance&#x3C;/id>
        &#x3C;goals>
          &#x3C;goal>enhance&#x3C;/goal>
        &#x3C;/goals>
        &#x3C;configuration>
          &#x3C;enableLazyInitialization>true&#x3C;/enableLazyInitialization>
          &#x3C;enableDirtyTracking>true&#x3C;/enableDirtyTracking>
          &#x3C;enableAssociationManagement>true&#x3C;/enableAssociationManagement>
        &#x3C;/configuration>
      &#x3C;/execution>
    &#x3C;/executions>
  &#x3C;/plugin>
  &#x3C;plugin>
    &#x3C;groupId>org.graalvm.buildtools&#x3C;/groupId>
    &#x3C;artifactId>native-maven-plugin&#x3C;/artifactId>
  &#x3C;/plugin>
&#x3C;/plugins>
&#x3C;/build>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-websocket'
  implementation 'org.springframework.security:spring-security-oauth2-resource-server'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-graphql'
  implementation 'com.graphql-java:graphql-java-extended-scalars:22.0'
  implementation 'org.liquibase:liquibase-core'
  compileOnly 'org.projectlombok:lombok'
  runtimeOnly 'com.h2database:h2'
}

plugins {
  id 'org.hibernate.orm' version '6.6.13.Final'
  id 'org.graalvm.buildtools.native' version '0.10.6'
}

hibernate {
  enhancement {
    enableAssociationManagement = true
  }
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Configuration Files</h2>
<p>In this section, we define all of the application- and database-level configuration files required to wire up our Spring Boot app with H2/PostgreSQL, JPA, Liquibase changelogs, initial data loads, JWE key properties and GraphQL schema definitions, and GraalVM native-image reflection config.</p>
<ul>
<li>
<p><code>application.yml</code>
Holds Spring datasource, H2 console, JPA/Hibernate settings, Liquibase changelog path, GraphQL subscriptions path, and all JWT/JWE keys, issuer, and expiration configurations.</p>
</li>
<li>
<p><code>liquibase/master.xml</code>
The Liquibase master changelog, with includes and DBMS-specific properties for H2 and PostgreSQL.</p>
</li>
<li>
<p><code>liquibase/changelog/changelog-user.xml</code>
Your core schema changelog defining <code>user_identity</code>, <code>authority</code>, and <code>user_authority_mapping</code> tables, indexes, FKs, and initial <code>&#x3C;loadData></code> steps.</p>
</li>
<li>
<p><code>liquibase/data/user.csv</code>
Initial user records (UUID, username, bcrypt-hashed password, email, enabled flag, timestamps, auditor).</p>
</li>
<li>
<p><code>liquibase/data/authority.csv</code>
Initial authority records (UUID, name, description, timestamps, auditor).</p>
</li>
<li>
<p><code>liquibase/data/user_authority_mapping.csv</code>
Initial mapping of users ↔ authorities (composite PK, timestamps, auditor).</p>
</li>
<li>
<p><code>graphql/schema.graphqls</code>
GraphQL schema definitions for the API, including custom scalars, query, subscription and mutation types, and DTO definitions.</p>
</li>
<li>
<p><code>META-INF/native-image/liquibase/reflect-config.json</code>
Native-image reflection configuration for Liquibase classes to ensure compatibility when building a GraalVM native image.</p>
</li>
</ul>
<p>application.yml</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  h2:
    console:
      enabled: true
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate.format_sql: true
  liquibase:
    change-log: classpath:liquibase/master.xml
  graphql:
    websocket:
      path: /subscriptions
    graphiql:
      enabled: true
security:
  admin:
    username: admin
    password: adminpass
  user:
    username: user
    password: userpass
  jwt:
    issuer: demo-issuer
    expiration-duration: 3600s
    signing:
      key-id: signing-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjZEcz5itWkDGOSqZdB5P
        JE0ccOCckskp0hN3kQbT1qnv+9/k66jlWgVi4HSTppwdNF/Ylu5u541Qj+Okyg+u
        8o2PvKo5CfgvTDsFLTrZHUXU6hCSGatLAQoeN6lT8wzov2r4DFecXrIqcO6SvMB5
        ecPqsfiTi4trsNKgJ4cWS6gILH62ISd1ipUadfpnUzDMO1OulV0CJNV6bcBk7Es9
        RW6AHfg9j8osSanpwvRM4MJkB0SRxYUnrN9faGpkBZISZJ8TShhaTHEGfSNgKe5y
        8iu+AMGGZu8DYczVmqS3Ske1fq6y5HEGCma7Mo019GmwKeHBo1obuET6cZRygj4y
        twIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNkRzPmK1aQMY5
        Kpl0Hk8kTRxw4JySySnSE3eRBtPWqe/73+TrqOVaBWLgdJOmnB00X9iW7m7njVCP
        46TKD67yjY+8qjkJ+C9MOwUtOtkdRdTqEJIZq0sBCh43qVPzDOi/avgMV5xesipw
        7pK8wHl5w+qx+JOLi2uw0qAnhxZLqAgsfrYhJ3WKlRp1+mdTMMw7U66VXQIk1Xpt
        wGTsSz1FboAd+D2PyixJqenC9EzgwmQHRJHFhSes319oamQFkhJknxNKGFpMcQZ9
        I2Ap7nLyK74AwYZm7wNhzNWapLdKR7V+rrLkcQYKZrsyjTX0abAp4cGjWhu4RPpx
        lHKCPjK3AgMBAAECggEABMo/sNCIMREXA/EqjxDFecN4LmOTOK5A0YBiH9Cc01sd
        qSaavtSQqhqbjV+0bUNEA7UohXbc3s3bx3qa5VFhiIh8sBQMaQdyRkCK/MxMc16d
        BSx5XQ/8MjVO73A1zHgH2g47BWPjgRrDL94KrgNPOO0FoG76QxL9GlcOHzhFa1rf
        xbb2auLBtflKy+5TZNnB/sbFC9ISpWywzfblD2Fsvxupl0vNWtA0Y2rPbKMZRnY+
        V6NC38yxW3HHAdWQRYLGOitM59I0HBgnjQKzTBNIsjPnJA2BSZvMBtQ1e3RQV0qE
        8VVoktZ8A0KDUxqdKWYlGYAY5hIw1y3UUsPRUbS3fQKBgQDAZehu+Kb5Sw+h00Qu
        vViNHDvi0PwmhQrHVpgjrppCnWsTk6FJFm+EBLY+87usXuRCnfZ1semb6P+yjLh2
        bJ9IXIrFAztcyE+7eZfn0H7mHA7E2ICKTjNdKbjy8kHViHpgdXREnAOrFTTk/BqW
        dq+weG2OuxYczWsY4XliWaVyiwKBgQC8XYExpSmoyI37o+jr9405k30tbGUw4X+/
        xYOvSUuafI8IOSLfKTpsvkMN3hxpJO6apIRAjOALbotKUA5FrHQPenuBWOr3mBJT
        euLiaWphqU8YN5dbNd+JhC/Jh5DLhY5FpB2Fv2MKSoF7+onpjDy9pEU5aReeBB0v
        ekUw4nWiBQKBgHxyrnjxP1frFG5xMB4nfZqw04+v7BmiXsl3mqsh6kgCeNtN17pl
        17YGMjfgAdnJ+02XzW5tqRSfDp3YZgy7z//HVD+BCqnGK8SxLu/ULfD73xW2kNZl
        JNYzAZ2r06eiQr4X2x/x5nGIIxGmfDAtDxFPpFX5b6ErwgVy+sgCAoFnAoGAWSpu
        EMEdQk+FnnwNsz2g9YNSuyDXmdb08SOfXWd1yXBzCLJ7RmYuyPEbrsHYcxFPfZap
        ICFPoTm35/qTdvnWiskxE56yw3eSHUBLjF/YQtixn0YZeMy8v0z6jgyFR0I2gdLZ
        QsnBKUrxlm2XwR1oV2Eef7m2u085PZNEk4pvor0CgYA6n060r48/CgRVslAeGder
        fZ3n+2ru8q2UVB38evepjLifn5+tKdFzZ7/NckXMC0NOVzm74qG46VFLMw5TtyT4
        /hRvGSYxA6dMjXy+tcrAns9Isrz2PnYFntlbhU6hw0um809tFunbvITfyeOjAsDW
        stC4thnhzXXT1Y3RfFtYEg==
        -----END PRIVATE KEY-----
    encryption:
      key-id: encryption-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtU7Vu4tML8qg9BISaUH0
        BTU0+qkHJS3TUl3R0hEPttZJGf8EgT7bpWqRjdhMC2SYt2ifNF82EgMo/eva1rLn
        wYtCsxjJ7oB4U2m16/JKHtphmMUGhSLgbp+Y8pGAQ+P6u4HLlI0qbL92Syb6QCTr
        nYIlzZ2uUAvUQg/SbuAvdS4kdauZtpMNbhryusMVvILVBC5yUhfLJAkjbU3qWo/n
        2NKPr4kjwwFh6FaAf0HLsXCFmJbPPok6WMZeDqPORPZCt5gU2t04fS7s/2SSbhQ/
        i+7yyT4pl44cUlNLLN3Qo73sai5H90q7Dvmk3M5/YMJB0Ueyb705LHyOO5xA618s
        qQIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1TtW7i0wvyqD0
        EhJpQfQFNTT6qQclLdNSXdHSEQ+21kkZ/wSBPtulapGN2EwLZJi3aJ80XzYSAyj9
        69rWsufBi0KzGMnugHhTabXr8koe2mGYxQaFIuBun5jykYBD4/q7gcuUjSpsv3ZL
        JvpAJOudgiXNna5QC9RCD9Ju4C91LiR1q5m2kw1uGvK6wxW8gtUELnJSF8skCSNt
        Tepaj+fY0o+viSPDAWHoVoB/QcuxcIWYls8+iTpYxl4Oo85E9kK3mBTa3Th9Luz/
        ZJJuFD+L7vLJPimXjhxSU0ss3dCjvexqLkf3SrsO+aTczn9gwkHRR7JvvTksfI47
        nEDrXyypAgMBAAECggEAREnJHrY8n9OGA+e6n4KD0mJT7gUz98Dm0yMbIC/k50yW
        hAAVRkjSmd8lq3NIURI2ov342NTznJ0sF1d6OVtxBujY2nP/uqEQsPoj1xaO7Ef8
        cnnjFsooFgJurQ44bVm02mLstqrky8jhWTT5FKfTRqP6cRNu0B9kdu1WqQQvW8a/
        C8xD8XUFzzJFQsEstJ076NjlZNH3EeqFiLxyVdV7tPEJRnOe2V2BAAgda3ByA6Jc
        Nd4xYHTVr5rZ+KES9gaAgb86+S7hNigsxz5Gp58NimLEyz1Qd+d9QCgPRgKOV5Uq
        0ci6c28ZrxW9wa/2CGCpoow1V7hRy1fZ24g2Tz91cQKBgQDbSdP/FQYuem0pUf8u
        mA/niEC8Jz30FW3ix5aqbQ8YRudK48Xy8F7AWnlS0wT2k/Xaeppn9YSPK4HlsNk2
        1gnpNfzdqXpJQtuxcPPeFNzUdXzBrsLXYqtlhOX/afrzqMhp73gkqoxXtqwD7BqN
        VfMcVXsWrEZQrRPBn/RUnnLdJwKBgQDTqUGupm93UlhoPgLXHGF/seQwPPHcU6lS
        SS9l512Ie21B+vZecgO4r77BXJwbY4rZVA5/m+wrsNmsT2f8yfdbs84cBDDWiKy5
        t2Um8zrus+BCIffxc9Fck9/htK33jLLcYwdSAm3a9pJxR9f/kAdredSTGBNJ3W/2
        rfh3cdNprwKBgDi7WfBFRSsjGzi0cPth9cNlubGzyVBrdtlT34PJ4Tzboxz53o1i
        aHEFNxwZYdBVKSbTzzyUBS5xCBMfdKK+LyQ5hmjmXq+zb1jxqvXKmfMRTixhhSDp
        8wO5pTM1/Omqwea+QGvj/5j1tnzxSVFFajbrWoPcH/jhPho6wqBducPDAoGBALOJ
        MEOavZXy7TaO0w3v7uvH0wzvxR/kfw1jMqc3l2j7ePOskmoOQAXaXO3bRjcdOlua
        Jyoq8islOZ4lRMlx7zWD0OKG035GNGzbmRtu2aA8R48RDSVr3jyu2gqznZULbXPv
        M/hmQxSmbhVUoW0PmJubnaqfk0zmXeBaNRXsIS3VAoGBALdsAtqR23PVeL6sYj2l
        dRNTDXpfvjFqJ5NThRO/4mT0CrPHQVj+Mz2bTc/Dxiwi8s8m3L+g23i3hodh/QMb
        Iz+UbcJZBGAKsFbPKGOtj6Bi07y/L11mcuNJzOWe61/JbVmJss0s+N/v3XucK/Ge
        CUaGOccsMO221v6JoBh9J3Hz
        -----END PRIVATE KEY-----
</code></pre>
<p>master.xml</p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
  xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
      http://www.liquibase.org/xml/ns/dbchangelog
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

  &#x3C;!-- H2 defaults -->
  &#x3C;property name="now" value="now()" dbms="h2"/>
  &#x3C;property name="floatType" value="float4" dbms="h2"/>
  &#x3C;property name="uuidType" value="uuid" dbms="h2"/>
  &#x3C;property name="datetimeType" value="datetime(6)" dbms="h2"/>
  &#x3C;property name="binaryType" value="LONGVARBINARY" dbms="h2"/>
  &#x3C;property name="clobType" value="longvarchar" dbms="h2"/>
  &#x3C;property name="blobType" value="blob" dbms="h2"/>

  &#x3C;!-- PostgreSQL overrides -->
  &#x3C;property name="now" value="current_timestamp" dbms="postgresql"/>
  &#x3C;property name="floatType" value="float4" dbms="postgresql"/>
  &#x3C;property name="uuidType" value="uuid" dbms="postgresql"/>
  &#x3C;property name="datetimeType" value="datetime" dbms="postgresql"/>
  &#x3C;property name="binaryType" value="BYTEA" dbms="postgresql"/>
  &#x3C;property name="clobType" value="clob" dbms="postgresql"/>
  &#x3C;property name="blobType" value="blob" dbms="postgresql"/>

  &#x3C;!-- include the core auth changelog -->
  &#x3C;include file="classpath:db/changelog/changelog-user.xml"/>

&#x3C;/databaseChangeLog>
</code></pre>
<p>changelog-user.xml</p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
  xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
      http://www.liquibase.org/xml/ns/dbchangelog
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

  &#x3C;changeSet id="20250510-1" author="susimsek">
    &#x3C;createTable tableName="user_identity">
      &#x3C;column name="id" type="varchar(36)">
        &#x3C;constraints primaryKey="true" primaryKeyName="pk_user_identity" nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="username" type="varchar(50)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="password" type="varchar(100)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="email" type="varchar(100)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="first_name" type="varchar(50)"/>
      &#x3C;column name="last_name" type="varchar(50)"/>
      &#x3C;column name="enabled" type="boolean">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;createIndex indexName="idx_user_identity_username" tableName="user_identity">
      &#x3C;column name="username"/>
    &#x3C;/createIndex>
    &#x3C;createIndex indexName="idx_user_identity_email" tableName="user_identity">
      &#x3C;column name="email"/>
    &#x3C;/createIndex>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-2" author="susimsek">
    &#x3C;createTable tableName="authority">
      &#x3C;column name="id" type="varchar(36)">
        &#x3C;constraints primaryKey="true" primaryKeyName="pk_authority" nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="name" type="varchar(50)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="description" type="varchar(255)"/>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;createIndex indexName="idx_authority_name" tableName="authority">
      &#x3C;column name="name"/>
    &#x3C;/createIndex>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-3" author="susimsek">
    &#x3C;createTable tableName="user_authority_mapping">
      &#x3C;column name="user_id" type="varchar(36)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="authority_id" type="varchar(36)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;addPrimaryKey
      tableName="user_authority_mapping"
      columnNames="user_id, authority_id"
      constraintName="pk_user_authority_mapping"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-4" author="susimsek">
    &#x3C;addForeignKeyConstraint
      baseTableName="user_authority_mapping"
      baseColumnNames="user_id"
      constraintName="fk_user_authority_user"
      referencedTableName="user_identity"
      referencedColumnNames="id"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-5" author="susimsek">
    &#x3C;addForeignKeyConstraint
      baseTableName="user_authority_mapping"
      baseColumnNames="authority_id"
      constraintName="fk_user_authority_authority"
      referencedTableName="authority"
      referencedColumnNames="id"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-1-data" author="susimsek">
    &#x3C;loadData
      file="db/data/user.csv"
      separator=";"
      tableName="user_identity">
      &#x3C;column name="id" type="string"/>
      &#x3C;column name="username" type="string"/>
      &#x3C;column name="password" type="string"/>
      &#x3C;column name="email" type="string"/>
      &#x3C;column name="first_name" type="string"/>
      &#x3C;column name="last_name" type="string"/>
      &#x3C;column name="enabled" type="boolean"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-2-data" author="susimsek">
    &#x3C;loadData
      file="db/data/authority.csv"
      separator=";"
      tableName="authority">
      &#x3C;column name="id" type="string"/>
      &#x3C;column name="name" type="string"/>
      &#x3C;column name="description" type="string"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-3-data" author="susimsek">
    &#x3C;loadData
      file="db/data/user_authority_mapping.csv"
      separator=";"
      tableName="user_authority_mapping">
      &#x3C;column name="user_id" type="string"/>
      &#x3C;column name="authority_id" type="string"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

&#x3C;/databaseChangeLog>
</code></pre>
<p>user.csv</p>
<pre><code class="language-csv">id;username;password;email;first_name;last_name;enabled;created_at;created_by;updated_at;updated_by
a1b2c3d4-e5f6-7890-abcd-ef1234567890;admin;$2a$10$sva6wl8pmGKJE6NIWrxwcuJK1Jaa2I/LOI43iHVpbR4YB8KjGViiK;admin@example.com;Admin;User;true;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
09876543-21fe-dcba-0987-654321fedcba;user;$2a$10$5Py4PyteLuXEqnGpSigzfu0V55C7Hi7zX18lmh.J8Bpmft.h23voG;user@example.com;Normal;User;true;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<p>authority.csv</p>
<pre><code class="language-csv">id;name;description;created_at;created_by;updated_at;updated_by
f47ac10b-58cc-4372-a567-0e02b2c3d479;ROLE_ADMIN;Administrator role;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
9c858901-8a57-4791-81fe-4c455b099bc9;ROLE_USER;User role;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<p>user_authority_mapping.csv</p>
<pre><code class="language-csv">user_id;authority_id;created_at;created_by;updated_at;updated_by
a1b2c3d4-e5f6-7890-abcd-ef1234567890;9c858901-8a57-4791-81fe-4c455b099bc9;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
a1b2c3d4-e5f6-7890-abcd-ef1234567890;f47ac10b-58cc-4372-a567-0e02b2c3d479;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
09876543-21fe-dcba-0987-654321fedcba;9c858901-8a57-4791-81fe-4c455b099bc9;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<p>schema.graphqls</p>
<pre><code class="language-graphql">scalar Long
scalar Date
scalar Instant

type Query {
  helloAll: String!
  helloAdmin: String!
}

type Mutation {
  login(input: LoginInput!): TokenDTO!
  logout: Boolean!
  greet(input: GreetInput!): GreetDTO!
}

type Subscription {
  greetStream(input: GreetInput!): GreetDTO!
  greetStreamAdmin(input: GreetInput!): GreetDTO!
}

input LoginInput {
  username: String!
  password: String!
}

input GreetInput {
  message: String!
}

type TokenDTO {
  accessToken: String!
  tokenType: String!
  accessTokenExpiresIn: Long!
}

type GreetDTO {
  greeting: String!
  timestamp: Instant!
}
</code></pre>
<p>reflect-config.json</p>
<pre><code class="language-json">[
  {
    "name": "liquibase.logging.mdc.MdcManagerFactory",
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.change.core.LoadDataColumnConfig",
    "queryAllPublicMethods": true,
    "allDeclaredMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.command.CommandFactory",
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.changelog.ChangeLogHistoryServiceFactory",
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.BigIntType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.BlobType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.BooleanType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.CharType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.ClobType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.CurrencyType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.DatabaseFunctionType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.DateTimeType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.DateType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.DecimalType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.DoubleType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.FloatType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.IntType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.MediumIntType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.NCharType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.NumberType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.NVarcharType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.SmallIntType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.TimestampType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.TimeType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.TinyIntType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.UnknownType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.UUIDType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.VarcharType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  },
  {
    "name": "liquibase.datatype.core.XMLType",
    "allPublicMethods": true,
    "methods": [{ "name": "&#x3C;init>", "parameterTypes": [] }]
  }
]
</code></pre>
<hr>
<h2>🛠️ Step 3: GraphQL &#x26; Security &#x26; Database Configuration</h2>
<p>In this section, we define the beans and properties RSA keys, HTTP security filters, and JPA repository/auditing setup for JWE‑based authentication, GraalVM native-image runtime hints, GraphQL wiring and custom scalars:</p>
<ul>
<li>JwtProperties: Configures JWT issuer, expiration, and signing/encryption key pairs.</li>
<li>SecurityJwtConfig: Builds RSA keys and JWK sources; configures JWT encoder/decoder, authentication converter, token resolver, and WebSocket interceptor beans.</li>
<li>SecurityConfig: Integrates <code>DomainUserDetailsService</code>, configures authentication manager, password encoder, and stateless security filter chain with JWE support.</li>
<li>DatabaseConfig: Enables JPA repositories, auditing, and transaction management.</li>
<li>GraphQLConfig: Registers custom scalars for GraphQL (<code>Long</code>, <code>Date</code>, <code>Instant</code>).</li>
<li>InstantScalar: Defines an ISO-8601 <code>Instant</code> scalar for GraphQL.</li>
<li>NativeConfig: Registers runtime hints for GraalVM native-image, including reflection and resource patterns.</li>
</ul>
<p>SecurityJwtConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.github.susimsek.springbootgraphqljwedemo.security.CookieAuthenticationWebSocketInterceptor;
import io.github.susimsek.springbootgraphqljwedemo.security.CookieBearerTokenResolver;
import io.github.susimsek.springbootgraphqljwedemo.security.KeyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;

import java.util.List;

import static io.github.susimsek.springbootgraphqljwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Configuration
public class SecurityJwtConfig {

  private final JwtProperties props;

  public SecurityJwtConfig(JwtProperties props) {
    this.props = props;
  }

  @Bean
  public RSAKey signingKey() throws Exception {
    return KeyUtils.buildRsaKey(
      props.getSigning().getPublicKey(),
      props.getSigning().getPrivateKey(),
      props.getSigning().getKeyId(),
      true
    );
  }

  @Bean
  public RSAKey encryptionKey() throws Exception {
    return KeyUtils.buildRsaKey(
      props.getEncryption().getPublicKey(),
      props.getEncryption().getPrivateKey(),
      props.getEncryption().getKeyId(),
      false
    );
  }

  @Bean
  public JWKSource&#x3C;SecurityContext> jwkSource(RSAKey signingKey, RSAKey encryptionKey) {
    JWKSet jwkSet = new JWKSet(List.of(signingKey, encryptionKey));
    return (jwkSelector, context) -> jwkSelector.select(jwkSet);
  }

  @Bean
  public JwtDecoder jwtDecoder(JWKSource&#x3C;SecurityContext> jwkSource) {
    DefaultJWTProcessor&#x3C;SecurityContext> jwtProcessor = new DefaultJWTProcessor&#x3C;>();

    jwtProcessor.setJWEKeySelector(new JWEDecryptionKeySelector&#x3C;>(
      JWEAlgorithm.RSA_OAEP_256,
      EncryptionMethod.A128GCM,
      jwkSource
    ));
    jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector&#x3C;>(
      JWSAlgorithm.RS256,
      jwkSource
    ));
    jwtProcessor.setJWTClaimsSetVerifier((claims, ctx) -> {});

    return new NimbusJwtDecoder(jwtProcessor);
  }

  @Bean
  public JwtEncoder jwtEncoder(JWKSource&#x3C;SecurityContext> jwkSource) {
    return new NimbusJwtEncoder(jwkSource);
  }

  @Bean
  public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
    converter.setAuthorityPrefix("");
    converter.setAuthoritiesClaimName(AUTHORITIES_KEY);

    JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
    authConverter.setJwtGrantedAuthoritiesConverter(converter);
    return authConverter;
  }

  @Bean
  public BearerTokenResolver bearerTokenResolver() {
    CookieBearerTokenResolver resolver = new CookieBearerTokenResolver();
    resolver.setAllowUriQueryParameter(false);
    resolver.setAllowFormEncodedBodyParameter(false);
    resolver.setAllowCookie(true);
    return resolver;
  }

  @Bean
  public WebSocketGraphQlInterceptor authenticationInterceptor(JwtDecoder jwtDecoder) {
    return new CookieAuthenticationWebSocketInterceptor(
      new BearerTokenAuthenticationExtractor(),
      new ProviderManager(new JwtAuthenticationProvider(jwtDecoder))
    );
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.JWSVerificationKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import io.github.susimsek.springbootgraphqljwedemo.security.CookieAuthenticationWebSocketInterceptor
import io.github.susimsek.springbootgraphqljwedemo.security.CookieBearerTokenResolver
import io.github.susimsek.springbootgraphqljwedemo.security.KeyUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.graphql.server.WebSocketGraphQlInterceptor
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver

@Configuration
class SecurityJwtConfig(
  private val props: JwtProperties
) {

  @Bean
  @Throws(Exception::class)
  fun signingKey(): RSAKey = KeyUtils.buildRsaKey(
    props.signing.publicKey,
    props.signing.privateKey,
    props.signing.keyId,
    true
  )

  @Bean
  @Throws(Exception::class)
  fun encryptionKey(): RSAKey = KeyUtils.buildRsaKey(
    props.encryption.publicKey,
    props.encryption.privateKey,
    props.encryption.keyId,
    false
  )

  @Bean
  fun jwkSource(
    signingKey: RSAKey,
    encryptionKey: RSAKey
  ): JWKSource&#x3C;SecurityContext> {
    val jwkSet = JWKSet(listOf(signingKey, encryptionKey))
    return JWKSource { jwkSelector, _ -> jwkSelector.select(jwkSet) }
  }

  @Bean
  fun jwtDecoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtDecoder {
    val processor = DefaultJWTProcessor&#x3C;SecurityContext>().apply {
      setJWEKeySelector(JWEDecryptionKeySelector(
        JWEAlgorithm.RSA_OAEP_256,
        EncryptionMethod.A128GCM,
        jwkSource
      ))
      setJWSKeySelector(JWSVerificationKeySelector(
        JWSAlgorithm.RS256,
        jwkSource
      ))
      setJWTClaimsSetVerifier { _, _ -> }
    }
    return NimbusJwtDecoder(processor)
  }

  @Bean
  fun jwtEncoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtEncoder =
    NimbusJwtEncoder(jwkSource)

  @Bean
  fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val granted = JwtGrantedAuthoritiesConverter().apply {
      authorityPrefix = ""
      authoritiesClaimName = SecurityUtils.AUTHORITIES_KEY
    }
    return JwtAuthenticationConverter().apply {
      setJwtGrantedAuthoritiesConverter(granted)
    }
  }

  @Bean
  fun bearerTokenResolver(): BearerTokenResolver =
    CookieBearerTokenResolver().apply {
      setAllowUriQueryParameter(false)
      setAllowFormEncodedBodyParameter(false)
      setAllowCookie(true)
    }

  @Bean
  fun authenticationInterceptor(jwtDecoder: JwtDecoder): WebSocketGraphQlInterceptor =
    CookieAuthenticationWebSocketInterceptor(
      BearerTokenAuthenticationExtractor(),
      ProviderManager(JwtAuthenticationProvider(jwtDecoder))
    )
}
</code></pre>
<p>SecurityConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import io.github.susimsek.springbootgraphqljwedemo.repository.UserRepository;
import io.github.susimsek.springbootgraphqljwedemo.security.DomainUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                 MvcRequestMatcher.Builder mvc) throws Exception {
    http
      .cors(withDefaults())
      .csrf(AbstractHttpConfigurer::disable)
      .authorizeHttpRequests(authz ->
        authz
          .requestMatchers(
            mvc.pattern("/webjars/**"),
            mvc.pattern("/css/**"),
            mvc.pattern("/js/**")
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/*.ico"),
            mvc.pattern("/*.png"),
            mvc.pattern("/*.svg"),
            mvc.pattern("/*.webapp")
          ).permitAll()
          .requestMatchers("/actuator/**").permitAll()
          .requestMatchers(
            "/v3/api-docs/**",
            "/swagger-ui.html",
            "/swagger-ui/**"
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/graphql"),
            mvc.pattern("/graphiql"),
            mvc.pattern("/subscriptions")
          ).permitAll()
          .anyRequest().authenticated()
      )
      .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .oauth2ResourceServer(oauth2 -> oauth2
        .jwt(withDefaults())
      );

    return http.build();
  }

  @Bean
  public UserDetailsService userDetailsService(UserRepository userRepository) {
    return new DomainUserDetailsService(userRepository);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public AuthenticationManager authenticationManager(
    UserDetailsService userDetailsService,
    PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder);
    return new ProviderManager(provider);
  }

  @Bean
  public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
    return new MvcRequestMatcher.Builder(introspector);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import io.github.susimsek.springbootgraphqljwedemo.repository.UserRepository
import io.github.susimsek.springbootgraphqljwedemo.security.DomainUserDetailsService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
import org.springframework.security.config.Customizer.withDefaults

@Configuration
@EnableMethodSecurity(securedEnabled = true)
class SecurityConfig {

  @Bean
  @Throws(Exception::class)
  fun securityFilterChain(
    http: HttpSecurity,
    mvc: MvcRequestMatcher.Builder
  ): SecurityFilterChain {
    http
      .cors(withDefaults())
      .csrf { it.disable() }
      .authorizeHttpRequests { authz ->
        authz
          .requestMatchers(
            mvc.pattern("/webjars/**"),
            mvc.pattern("/css/**"),
            mvc.pattern("/js/**")
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/*.ico"),
            mvc.pattern("/*.png"),
            mvc.pattern("/*.svg"),
            mvc.pattern("/*.webapp")
          ).permitAll()
          .requestMatchers("/actuator/**").permitAll()
          .requestMatchers(
            "/v3/api-docs/**",
            "/swagger-ui.html",
            "/swagger-ui/**"
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/graphql"),
            mvc.pattern("/graphiql"),
            mvc.pattern("/subscriptions")
          ).permitAll()
          .anyRequest().authenticated()
      }
      .sessionManagement {
        it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
      }
      .oauth2ResourceServer {
        it.jwt(withDefaults())
      }

    return http.build()
  }

  @Bean
  fun userDetailsService(userRepository: UserRepository): UserDetailsService =
    DomainUserDetailsService(userRepository)

  @Bean
  fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

  @Bean
  fun authenticationManager(
    userDetailsService: UserDetailsService,
    passwordEncoder: PasswordEncoder
  ): AuthenticationManager {
    val provider = DaoAuthenticationProvider().apply {
      setUserDetailsService(userDetailsService)
      setPasswordEncoder(passwordEncoder)
    }
    return ProviderManager(provider)
  }

  @Bean
  fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
    MvcRequestMatcher.Builder(introspector)
}
</code></pre>
<p>JwtProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
@Data
public class JwtProperties {

    @Data
    public static class Pair {
        private String publicKey;
        private String privateKey;
        private String keyId;
    }

    private Pair signing;
    private Pair encryption;
    private String issuer;
    private Duration expirationDuration;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
class JwtProperties {

  class Pair {
    lateinit var publicKey: String
    lateinit var privateKey: String
    lateinit var keyId: String
  }

  lateinit var signing: Pair
  lateinit var encryption: Pair
  lateinit var issuer: String
  lateinit var expirationDuration: Duration
}
</code></pre>
<p>DatabaseConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableJpaRepositories("io.github.susimsek.springbootgraphqljwedemo.repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfig {

}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableJpaRepositories("io.github.susimsek.springbootgraphqljwedemo.repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
class DatabaseConfig
</code></pre>
<p>GraphQLConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import graphql.scalars.ExtendedScalars;
import io.github.susimsek.springbootgraphqljwedemo.scalar.InstantScalar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;

@Configuration
public class GraphQLConfig {

  @Bean
  public RuntimeWiringConfigurer runtimeWiringConfigurer() {
    return wiring -> wiring
      .scalar(ExtendedScalars.GraphQLLong)
      .scalar(ExtendedScalars.Date)
      .scalar(InstantScalar.INSTANCE);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import graphql.scalars.ExtendedScalars
import io.github.susimsek.springbootgraphqljwedemo.scalar.InstantScalar
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.graphql.execution.RuntimeWiringConfigurer

@Configuration
class GraphQLConfig {

  @Bean
  fun runtimeWiringConfigurer(): RuntimeWiringConfigurer {
    return RuntimeWiringConfigurer { wiring ->
      wiring
        .scalar(ExtendedScalars.GraphQLLong)
        .scalar(ExtendedScalars.Date)
        .scalar(InstantScalar.INSTANCE)
    }
  }
}
</code></pre>
<p>InstantScalar</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.scalar;

import graphql.GraphQLContext;
import graphql.execution.CoercedVariables;
import graphql.language.StringValue;
import graphql.language.Value;
import graphql.scalars.util.Kit;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import graphql.schema.GraphQLScalarType;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.lang.NonNull;

import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Locale;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class InstantScalar {

  public static final GraphQLScalarType INSTANCE;

  static {
    Coercing&#x3C;Instant, String> coercing = new Coercing&#x3C;>() {
      @Override
      public String serialize(
        @NonNull Object input,
        @NonNull GraphQLContext context,
        @NonNull Locale locale
      ) throws CoercingSerializeException {
        if (input instanceof Instant instant) {
          return instant.toString();
        }
        throw new CoercingSerializeException(
          "Expected java.time.Instant but was: " + Kit.typeName(input) + "'."
        );
      }

      @Override
      public Instant parseValue(
        @NonNull Object input,
        @NonNull GraphQLContext context,
        @NonNull Locale locale
      ) throws CoercingParseValueException {
        if (input instanceof String s) {
          try {
            return Instant.parse(s);
          } catch (DateTimeParseException e) {
            throw new CoercingParseValueException("Invalid Instant value: " + e.getMessage() + "'.");
          }
        }
        throw new CoercingParseValueException(
          "Expected a String for Instant but was: " + Kit.typeName(input) + "'."
        );
      }

      @Override
      public Instant parseLiteral(
        @NonNull Value&#x3C;?> input,
        @NonNull CoercedVariables variables,
        @NonNull GraphQLContext context,
        @NonNull Locale locale
      ) throws CoercingParseLiteralException {
        if (input instanceof StringValue sv) {
          String s = sv.getValue();
          try {
            return Instant.parse(s);
          } catch (DateTimeParseException e) {
            throw new CoercingParseLiteralException("Invalid Instant literal: " + e.getMessage() + "'");
          }
        }
        throw new CoercingParseLiteralException(
          "Expected AST type 'StringValue' but was: " + Kit.typeName(input) + "'."
        );
      }

      @Override
      @NonNull
      public Value&#x3C;?> valueToLiteral(
        @NonNull Object input,
        @NonNull GraphQLContext context,
        @NonNull Locale locale
      ) {
        String serialized = serialize(input, context, locale);
        return StringValue.newStringValue(serialized).build();
      }
    };

    INSTANCE = GraphQLScalarType.newScalar()
      .name("Instant")
      .description("An ISO-8601 compliant java.time.Instant scalar")
      .specifiedByUrl("https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6")
      .coercing(coercing)
      .build();
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.scalar

import graphql.GraphQLContext
import graphql.execution.CoercedVariables
import graphql.language.StringValue
import graphql.language.Value
import graphql.scalars.util.Kit
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingParseValueException
import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import java.time.Instant
import java.time.format.DateTimeParseException
import java.util.Locale

object InstantScalar {

  @JvmField
  val INSTANCE: GraphQLScalarType = GraphQLScalarType.newScalar()
    .name("Instant")
    .description("An ISO-8601 compliant java.time.Instant scalar")
    .specifiedByUrl("https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6")
    .coercing(object : Coercing&#x3C;Instant, String> {
      override fun serialize(
        input: Any,
        context: GraphQLContext,
        locale: Locale
      ): String {
        return if (input is Instant) {
          input.toString()
        } else {
          throw CoercingSerializeException("Expected java.time.Instant but was: ${Kit.typeName(input)}'.")
        }
      }

      override fun parseValue(
        input: Any,
        context: GraphQLContext,
        locale: Locale
      ): Instant {
        if (input is String) {
          return try {
            Instant.parse(input)
          } catch (e: DateTimeParseException) {
            throw CoercingParseValueException("Invalid Instant value: ${e.message}'.")
          }
        }
        throw CoercingParseValueException("Expected a String for Instant but was: ${Kit.typeName(input)}'.")
      }

      override fun parseLiteral(
        input: Value&#x3C;*>,
        variables: CoercedVariables,
        context: GraphQLContext,
        locale: Locale
      ): Instant {
        if (input is StringValue) {
          return try {
            Instant.parse(input.value)
          } catch (e: DateTimeParseException) {
            throw CoercingParseLiteralException("Invalid Instant literal: ${e.message}'")
          }
        }
        throw CoercingParseLiteralException("Expected AST type 'StringValue' but was: ${Kit.typeName(input)}'.")
      }

      override fun valueToLiteral(
        input: Any,
        context: GraphQLContext,
        locale: Locale
      ): Value&#x3C;*> {
        val serialized = serialize(input, context, locale)
        return StringValue.newStringValue(serialized).build()
      }
    })
    .build()
}
</code></pre>
<p>NativeConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.config;

import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;

public class NativeConfig {

  public static class AppNativeRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
      hints.reflection()
        .registerType(sun.misc.Unsafe.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
      hints.reflection()
        .registerType(java.util.Locale.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
      hints.reflection()
        .registerType(org.hibernate.binder.internal.BatchSizeBinder.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
      hints.resources().registerPattern("liquibase/*");
      hints.reflection()
        .registerType(liquibase.ui.LoggerUIService.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
      hints.reflection()
        .registerType(liquibase.database.LiquibaseTableNamesFactory.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
      hints.reflection()
        .registerType(liquibase.report.ShowSummaryGeneratorFactory.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
      hints.reflection()
        .registerType(liquibase.changelog.FastCheckService.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
      hints.reflection()
        .registerType(liquibase.changelog.visitor.ValidatingVisitorGeneratorFactory.class,
          hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
    }
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.config

import org.springframework.aot.hint.MemberCategory
import org.springframework.aot.hint.RuntimeHints
import org.springframework.aot.hint.RuntimeHintsRegistrar

class NativeConfig {

  class AppNativeRuntimeHints : RuntimeHintsRegistrar {
    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader) {
      hints.reflection().registerType(
        sun.misc.Unsafe::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS) }
      )
      hints.reflection().registerType(
        java.util.Locale::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS) }
      )
      hints.reflection().registerType(
        org.hibernate.binder.internal.BatchSizeBinder::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS) }
      )
      hints.resources().registerPattern("liquibase/*")
      hints.reflection().registerType(
        liquibase.ui.LoggerUIService::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS) }
      )
      hints.reflection().registerType(
        liquibase.database.LiquibaseTableNamesFactory::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) }
      )
      hints.reflection().registerType(
        liquibase.report.ShowSummaryGeneratorFactory::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) }
      )
      hints.reflection().registerType(
        liquibase.changelog.FastCheckService::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) }
      )
      hints.reflection().registerType(
        liquibase.changelog.visitor.ValidatingVisitorGeneratorFactory::class.java,
        hint = hint@{ it.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) }
      )
    }
  }
}
</code></pre>
<p>Main</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo;

import io.github.susimsek.springbootgraphqljwedemo.config.NativeConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportRuntimeHints;

@SpringBootApplication
@ImportRuntimeHints({ NativeConfig.AppNativeRuntimeHints.class })
public class SpringBootJweAuthJpaDemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringBootJweAuthJpaDemoApplication.class, args);
  }

}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo

import io.github.susimsek.springbootgraphqljwedemo.config.NativeConfig
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ImportRuntimeHints

@SpringBootApplication
@ImportRuntimeHints(NativeConfig.AppNativeRuntimeHints::class)
class SpringBootJweAuthJpaDemoApplication

fun main(args: Array&#x3C;String>) {
  runApplication&#x3C;SpringBootJweAuthJpaDemoApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 4: JPA Integration</h2>
<p>In this section, we define the JPA entities representing users, authorities, and their mappings, along with the Spring Data repository for loading users with their authorities.</p>
<ul>
<li>BaseEntity: Abstract superclass providing audit fields (<code>createdAt</code>, <code>createdBy</code>, <code>updatedAt</code>, <code>updatedBy</code>).</li>
<li>Authority: <code>authority</code> table entity storing role data.</li>
<li>User: <code>user_identity</code> table entity storing user credentials and profile.</li>
<li>UserAuthorityMapping: <code>user_authority_mapping</code> join table entity linking users and authorities.</li>
<li>UserAuthorityMappingId: Composite key class for <code>UserAuthorityMapping</code>.</li>
<li>UserRepository: Spring Data JPA repository for <code>User</code> with an entity graph to load authorities.</li>
</ul>
<p>BaseEntity</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
public abstract class BaseEntity {

  @CreatedDate
  @Column(name = "created_at", nullable = false, updatable = false)
  private Instant createdAt;

  @CreatedBy
  @Column(name = "created_by", nullable = false, updatable = false, length = 50)
  private String createdBy;

  @LastModifiedDate
  @Column(name = "updated_at")
  private Instant updatedAt;

  @LastModifiedBy
  @Column(name = "updated_by", length = 50)
  private String updatedBy;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.entity

import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {

  @CreatedDate
  @Column(name = "created_at", nullable = false, updatable = false)
  lateinit var createdAt: Instant

  @CreatedBy
  @Column(name = "created_by", nullable = false, updatable = false, length = 50)
  lateinit var createdBy: String

  @LastModifiedDate
  @Column(name = "updated_at")
  var updatedAt: Instant? = null

  @LastModifiedBy
  @Column(name = "updated_by", length = 50)
  var updatedBy: String? = null
}
</code></pre>
<p>Authority</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.Objects;

@Entity
@Table(name = "authority")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Authority extends BaseEntity {

  @Id
  @Column(length = 36, nullable = false)
  private String id;

  @Column(length = 50, nullable = false, unique = true)
  private String name;

  @Column(length = 255)
  private String description;

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof Authority other)) {
      return false;
    }
    Class&#x3C;?> objClass = obj instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass();
    Class&#x3C;?> thisClass = this instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass();
    if (!thisClass.equals(objClass)) {
      return false;
    }
    return id != null &#x26;&#x26; Objects.equals(id, other.id);
  }

  @Override
  public final int hashCode() {
    return this instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : getClass().hashCode();
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.proxy.HibernateProxy
import java.util.Objects

@Entity
@Table(name = "authority")
class Authority(
  @Id
  @Column(length = 36, nullable = false)
  var id: String,

  @Column(length = 50, nullable = false, unique = true)
  var name: String,

  @Column(length = 255)
  var description: String?
) : BaseEntity() {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is Authority) return false
    val objClass = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass ?: other::class.java
    val thisClass = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass ?: this::class.java
    if (thisClass != objClass) return false
    return id == other.id
  }

  override fun hashCode(): Int {
    return (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: javaClass.hashCode()
  }
}
</code></pre>
<p>User</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedSubgraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "user_identity")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@NamedEntityGraph(
  name = "User.withAuthorities",
  attributeNodes = @NamedAttributeNode(value = "authorities", subgraph = "auth-subgraph"),
  subgraphs = @NamedSubgraph(
    name = "auth-subgraph",
    attributeNodes = @NamedAttributeNode("authority")
  )
)
public class User extends BaseEntity {

  @Id
  @Column(name = "id", length = 36, nullable = false, updatable = false)
  private String id;

  @Column(name = "username", length = 50, nullable = false, unique = true)
  private String username;

  @Column(name = "password", length = 100, nullable = false)
  private String password;

  @Column(name = "email", length = 100, nullable = false, unique = true)
  private String email;

  @Column(name = "first_name", length = 50)
  private String firstName;

  @Column(name = "last_name", length = 50)
  private String lastName;

  @Column(name = "enabled", nullable = false)
  private boolean enabled;

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY,
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set&#x3C;UserAuthorityMapping> authorities = new HashSet&#x3C;>();

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof User other)) return false;
    Class&#x3C;?> objClass = (obj instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass());
    Class&#x3C;?> thisClass = (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass());
    if (!thisClass.equals(objClass)) return false;
    return id != null &#x26;&#x26; id.equals(other.id);
  }

  @Override
  public final int hashCode() {
    return (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : getClass().hashCode());
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.entity

import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.Id
import jakarta.persistence.NamedAttributeNode
import jakarta.persistence.NamedEntityGraph
import jakarta.persistence.NamedSubgraph
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import org.hibernate.proxy.HibernateProxy
import java.util.*

@Entity
@Table(name = "user_identity")
@NamedEntityGraph(
  name = "User.withAuthorities",
  attributeNodes = [NamedAttributeNode(value = "authorities", subgraph = "auth-subgraph")],
  subgraphs = [
    NamedSubgraph(
      name = "auth-subgraph",
      attributeNodes = [NamedAttributeNode("authority")]
    )
  ]
)
class User(

  @Id
  @Column(name = "id", length = 36, nullable = false, updatable = false)
  var id: String,

  @Column(name = "username", length = 50, nullable = false, unique = true)
  var username: String,

  @Column(name = "password", length = 100, nullable = false)
  var password: String,

  @Column(name = "email", length = 100, nullable = false, unique = true)
  var email: String,

  @Column(name = "first_name", length = 50)
  var firstName: String? = null,

  @Column(name = "last_name", length = 50)
  var lastName: String? = null,

  @Column(name = "enabled", nullable = false)
  var enabled: Boolean = true

) : BaseEntity() {

  @OneToMany(
    mappedBy = "user",
    fetch = FetchType.LAZY,
    cascade = [CascadeType.ALL],
    orphanRemoval = true
  )
  var authorities: MutableSet&#x3C;UserAuthorityMapping> = HashSet()

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is User) return false
    val objClass = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: other::class.java
    val thisClass = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: this::class.java
    if (thisClass != objClass) return false
    return id == other.id
  }

  override fun hashCode(): Int =
    ((this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: javaClass.hashCode())
}
</code></pre>
<p>UserAuthorityMapping</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.Objects;

@Entity
@Table(name = "user_authority_mapping")
@IdClass(UserAuthorityMappingId.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserAuthorityMapping extends BaseEntity {

  @Id
  @Column(name = "user_id", length = 36, nullable = false)
  private String userId;

  @Id
  @Column(name = "authority_id", length = 36, nullable = false)
  private String authorityId;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id", insertable = false, updatable = false)
  private User user;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "authority_id", insertable = false, updatable = false)
  private Authority authority;

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof UserAuthorityMapping other)) return false;
    Class&#x3C;?> objCls = (obj instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass());
    Class&#x3C;?> thisCls = (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass());
    if (!thisCls.equals(objCls)) return false;
    return userId != null &#x26;&#x26; userId.equals(other.userId)
      &#x26;&#x26; authorityId != null &#x26;&#x26; authorityId.equals(other.authorityId);
  }

  @Override
  public final int hashCode() {
    return (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : Objects.hash(userId, authorityId));
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.entity

import jakarta.persistence.*
import org.hibernate.proxy.HibernateProxy
import java.util.*

@Entity
@Table(name = "user_authority_mapping")
@IdClass(UserAuthorityMappingId::class)
data class UserAuthorityMapping(

  @Id
  @Column(name = "user_id", length = 36, nullable = false)
  var userId: String = "",

  @Id
  @Column(name = "authority_id", length = 36, nullable = false)
  var authorityId: String = "",

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id", insertable = false, updatable = false)
  var user: User? = null,

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "authority_id", insertable = false, updatable = false)
  var authority: Authority? = null

) : BaseEntity() {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is UserAuthorityMapping) return false

    val objCls = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: other::class.java
    val thisCls = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: this::class.java

    if (thisCls != objCls) return false
    return userId == other.userId &#x26;&#x26; authorityId == other.authorityId
  }

  override fun hashCode(): Int {
    return (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: Objects.hash(userId, authorityId)
  }
}
</code></pre>
<p>UserAuthorityMappingId</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.entity;

import java.io.Serializable;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserAuthorityMappingId implements Serializable {

  private String userId;
  private String authorityId;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserAuthorityMappingId that = (UserAuthorityMappingId) o;
    return Objects.equals(userId, that.userId) &#x26;&#x26;
      Objects.equals(authorityId, that.authorityId);
  }

  @Override
  public int hashCode() {
    return Objects.hash(userId, authorityId);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.entity

import java.io.Serializable
import java.util.Objects

data class UserAuthorityMappingId(
  var userId: String = "",
  var authorityId: String = ""
) : Serializable {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || this::class != other::class) return false
    other as UserAuthorityMappingId
    return userId == other.userId &#x26;&#x26; authorityId == other.authorityId
  }

  override fun hashCode(): Int {
    return Objects.hash(userId, authorityId)
  }
}
</code></pre>
<p>UserRepository</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.repository;

import io.github.susimsek.springbootgraphqljwedemo.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository&#x3C;User, String> {

  @EntityGraph("User.withAuthorities")
  Optional&#x3C;User> findOneWithAuthoritiesByUsername(String username);
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.repository

import io.github.susimsek.springbootgraphqljwedemo.entity.User
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.Optional

@Repository
interface UserRepository : JpaRepository&#x3C;User, String> {

  @EntityGraph("User.withAuthorities")
  fun findOneWithAuthoritiesByUsername(username: String): Optional&#x3C;User>
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Secure JWE Token Utilities</h2>
<p>In this section, we define the core utility classes and constants needed to generate, encrypt, and resolve JSON Web Encryption (JWE) tokens in your Spring Boot application, integrate auditing, and implement a JPA-based user details service:</p>
<ul>
<li>AuthoritiesConstants: Centralize role names with the <code>ROLE_</code> prefix.</li>
<li>CookieBearerTokenResolver: Resolve bearer tokens from Authorization headers or HTTP cookies.</li>
<li>CookieUtils: Generates HTTP-only, secure <code>ResponseCookie</code> for new or expired tokens, and extracts the raw <code>accessToken</code> from <code>HttpHeaders</code></li>
<li>JweUtil: Sign (JWS) and encrypt (JWE) JWTs using RSA keys and Nimbus.</li>
<li>KeyUtils: Build RSA JWKs from PEM‐encoded key material.</li>
<li>SecurityUtils: Extract the current user’s login from the security context.</li>
<li>SpringSecurityAuditorAware: Implement <code>AuditorAware</code> to provide the current user for auditing.</li>
<li>DomainUserDetailsService: JPA-based <code>UserDetailsService</code> loading user and authorities for authentication.</li>
<li>GraphQlTokenCookieInterceptor: Intercepts GraphQL responses to set or clear the access token cookie based on <code>accessToken</code> and <code>clearAccessToken</code> flags in the <code>GraphQLContext</code>.</li>
<li>CookieAuthenticationWebSocketInterceptor: Intercept WebSocket connections to authenticate using JWE tokens from headers or cookies.</li>
</ul>
<p>These utilities form the foundation for a stateless, JWE‐based authentication flow in Spring Security.</p>
<p>AuthoritiesConstants</p>
<h3>Java</h3>
<pre><code class="language-java">
package io.github.susimsek.springbootgraphqljwedemo.security;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {
    public static final String ADMIN = "ROLE_ADMIN";
    public static final String USER = "ROLE_USER";
    public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">
package io.github.susimsek.springbootgraphqljwedemo.security

object AuthoritiesConstants {
  const val ADMIN = "ROLE_ADMIN"
  const val USER = "ROLE_USER"
  const val ANONYMOUS = "ROLE_ANONYMOUS"
}
</code></pre>
<p>CookieBearerTokenResolver</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.springframework.http.HttpMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Setter
public class CookieBearerTokenResolver implements BearerTokenResolver {
    private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
    private static final Pattern AUTHORIZATION_PATTERN =
        Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);

    private boolean allowFormEncodedBodyParameter = false;
    private boolean allowUriQueryParameter       = false;
    private boolean allowCookie                  = true;
    private String  bearerTokenHeaderName       = "Authorization";
    private String  cookieName                  = "accessToken";

    @Override
    public String resolve(HttpServletRequest request) {
        String headerToken = resolveFromAuthorizationHeader(request);
        String queryToken  = resolveAccessTokenFromQueryString(request);
        String bodyToken   = resolveAccessTokenFromBody(request);
        String cookieToken = (headerToken == null &#x26;&#x26; queryToken == null &#x26;&#x26; bodyToken == null)
            ? resolveFromCookie(request)
            : null;

        return resolveToken(headerToken, queryToken, bodyToken, cookieToken);
    }

    private String resolveFromAuthorizationHeader(HttpServletRequest request) {
        String authorization = request.getHeader(this.bearerTokenHeaderName);
        if (!StringUtils.hasText(authorization) || !authorization.toLowerCase().startsWith("bearer")) {
            return null;
        }
        Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization.trim());
        if (!matcher.matches()) {
            BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
            throw new OAuth2AuthenticationException(error);
        }
        return matcher.group("token");
    }

    private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
        if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.getMethod())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveAccessTokenFromBody(HttpServletRequest request) {
        if (allowFormEncodedBodyParameter
            &#x26;&#x26; HttpMethod.POST.matches(request.getMethod())
            &#x26;&#x26; "application/x-www-form-urlencoded".equals(request.getContentType())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveFromCookie(HttpServletRequest request) {
        if (!allowCookie || request.getCookies() == null) {
            return null;
        }
        for (Cookie cookie : request.getCookies()) {
            if (cookieName.equals(cookie.getName())) {
                String val = cookie.getValue();
                return StringUtils.hasText(val) ? val : null;
            }
        }
        return null;
    }

    private String resolveToken(String... tokens) {
        String found = null;
        for (String token : tokens) {
            if (token == null) continue;
            if (found != null) {
                BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
                throw new OAuth2AuthenticationException(error);
            }
            found = token;
        }
        if (found != null &#x26;&#x26; found.isBlank()) {
            BearerTokenError error = BearerTokenErrors.invalidRequest("The requested token parameter is an empty string");
            throw new OAuth2AuthenticationException(error);
        }
        return found;
    }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpMethod
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.server.resource.BearerTokenError
import org.springframework.security.oauth2.server.resource.BearerTokenErrors
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.util.StringUtils
import java.util.regex.Pattern

class CookieBearerTokenResolver {
    var allowFormEncodedBodyParameter: Boolean = false
    var allowUriQueryParameter: Boolean = false
    var allowCookie: Boolean = true
    var bearerTokenHeaderName: String = "Authorization"
    var cookieName: String = "accessToken"

    companion object {
        private const val ACCESS_TOKEN_PARAMETER_NAME = "access_token"
        private val AUTHORIZATION_PATTERN =
            Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE)
    }

    fun resolve(request: HttpServletRequest): String? {
        val header = resolveFromAuthorizationHeader(request)
        val query  = if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.method))
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val body   = if (allowFormEncodedBodyParameter &#x26;&#x26; HttpMethod.POST.matches(request.method)
            &#x26;&#x26; request.contentType == "application/x-www-form-urlencoded")
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val cookie = if (header == null &#x26;&#x26; query == null &#x26;&#x26; body == null)
            resolveFromCookie(request)
        else null
      return listOf(header, query, body, cookie).filterNotNull().let {
            if (it.size > 1) throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Found multiple bearer tokens"))
            it.firstOrNull()?: null
        }
    }

    private fun resolveFromAuthorizationHeader(request: HttpServletRequest): String? {
        val auth = request.getHeader(bearerTokenHeaderName) ?: return null
        if (!auth.startsWith("Bearer ", true)) return null
        val matcher = AUTHORIZATION_PATTERN.matcher(auth.trim())
        return if (matcher.matches()) matcher.group("token")
        else throw OAuth2AuthenticationException(BearerTokenErrors.invalidToken("Malformed token"))
    }

    private fun resolveFromCookie(request: HttpServletRequest): String? {
        if (!allowCookie) return null
        return request.cookies?.firstOrNull { it.name == cookieName }?.value?.takeIf { it.isNotBlank() }
    }

    private fun resolveToken(tokens: Array&#x3C;String>): String? {
        return tokens.filter { it.isNotBlank() }.let {
            when {
                it.isEmpty() -> null
                it.size > 1   -> throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Multiple tokens"))
                else          -> it[0]
            }
        }
    }
</code></pre>
<p>CookieUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;

import java.net.HttpCookie;
import java.util.Arrays;
import java.util.Optional;

@UtilityClass
public class CookieUtils {

  public static final String COOKIE_NAME = "accessToken";

  public ResponseCookie createAccessTokenCookie(TokenDTO tokenDto) {
    return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(tokenDto.accessTokenExpiresIn())
      .sameSite("Strict")
      .build();
  }

  public static ResponseCookie removeAccessTokenCookie() {
    return ResponseCookie.from(COOKIE_NAME, "")
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(0)
      .sameSite("Strict")
      .build();
  }

  public String resolveToken(HttpHeaders headers) {
    return Optional.ofNullable(headers.getFirst(HttpHeaders.COOKIE))
      .map(h -> h.split(";"))
      .stream()
      .flatMap(Arrays::stream)
      .flatMap(part -> HttpCookie.parse(part).stream())
      .filter(c -> COOKIE_NAME.equals(c.getName()))
      .map(HttpCookie::getValue)
      .findFirst()
      .orElse(null);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import java.net.HttpCookie

object CookieUtils {

  const val COOKIE_NAME = "accessToken"

  fun createAccessTokenCookie(tokenDto: TokenDTO): ResponseCookie =
    ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(tokenDto.accessTokenExpiresIn())
      .sameSite("Strict")
      .build()

  fun removeAccessTokenCookie(): ResponseCookie =
    ResponseCookie.from(COOKIE_NAME, "")
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(0)
      .sameSite("Strict")
      .build()

  fun resolveToken(headers: HttpHeaders): String? =
    headers.getFirst(HttpHeaders.COOKIE)
      ?.split(";")
      .orEmpty()
      .asSequence()
      .flatMap { HttpCookie.parse(it).asSequence() }
      .firstOrNull { it.name == COOKIE_NAME }
      ?.value
}
</code></pre>
<p>JweUtil</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.jwk.RSAKey;
import io.github.susimsek.springbootgraphqljwedemo.config.JwtProperties;
import io.github.susimsek.springbootgraphqljwedemo.dto.TokenDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;

import static io.github.susimsek.springbootgraphqljwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Component
@RequiredArgsConstructor
public class JweUtil {

    private final JwtEncoder jwtEncoder;
    private final RSAKey signingKey;
    private final RSAKey encryptionKey;
    private final JwtProperties props;

    public TokenDTO generateToken(Authentication authentication) throws JOSEException {
        String subject = authentication.getName();
        List&#x3C;String> roles = authentication.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        Instant now = Instant.now();
        long expiresIn = props.getExpirationDuration().getSeconds();
        Instant exp = now.plusSeconds(expiresIn);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer(props.getIssuer())
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build();

        JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.getKeyID())
            .build();

        String jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .getTokenValue();

        JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.getKeyID())
            .build();

        JWEObject jweObject = new JWEObject(jweHeader, new Payload(jws));
        jweObject.encrypt(new RSAEncrypter(encryptionKey.toRSAPublicKey()));
        String token = jweObject.serialize();

        return new TokenDTO(token, "Bearer", expiresIn);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JOSEException
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.RSAKey
import io.github.susimsek.springbootgraphqljwedemo.config.JwtProperties
import io.github.susimsek.springbootgraphqljwedemo.dto.TokenDTO
import lombok.RequiredArgsConstructor
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
import org.springframework.security.oauth2.jwt.JwtClaimsSet
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.JwtEncoderParameters
import org.springframework.stereotype.Component

import java.time.Instant

@Component
@RequiredArgsConstructor
class JweUtil(
    private val jwtEncoder: JwtEncoder,
    private val signingKey: RSAKey,
    private val encryptionKey: RSAKey,
    private val props: JwtProperties
) {
    fun generateToken(authentication: Authentication): TokenDTO {
        val subject = authentication.name
        val roles = authentication.authorities.map { it.authority }

        val now = Instant.now()
        val expiresIn = props.expirationDuration.seconds
        val exp = now.plusSeconds(expiresIn)

        val claims = JwtClaimsSet.builder()
            .issuer(props.issuer)
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build()

        val jwsHeader = org.springframework.security.oauth2.jwt.JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.keyID)
            .build()

        val jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .tokenValue

        val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.keyID)
            .build()

        val jweObject = JWEObject(jweHeader, Payload(jws))
        jweObject.encrypt(RSAEncrypter(encryptionKey.toRSAPublicKey()))
        val token = jweObject.serialize()

        return TokenDTO(token, "Bearer", expiresIn)
    }
}
</code></pre>
<p>KeyUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.experimental.UtilityClass;

import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@UtilityClass
public class KeyUtils {
    public RSAKey buildRsaKey(
            String pubPem,
            String privPem,
            String kid,
            boolean forSign
    ) throws Exception {
        String pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");
        String privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");

        byte[] decodedPub = Base64.getDecoder().decode(pubContent);
        byte[] decodedPriv = Base64.getDecoder().decode(privContent);

        KeyFactory kf = KeyFactory.getInstance("RSA");
        RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(decodedPub));
        RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedPriv));

        RSAKey.Builder builder = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid);

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE);
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION);
        }

        return builder.build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.RSAKey
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

object KeyUtils {
    @JvmStatic
    @Throws(Exception::class)
    fun buildRsaKey(
        pubPem: String,
        privPem: String,
        kid: String,
        forSign: Boolean
    ): RSAKey {
        val pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replace(Regex("\\s"), "")
        val privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace(Regex("\\s"), "")

        val decodedPub = Base64.getDecoder().decode(pubContent)
        val decodedPriv = Base64.getDecoder().decode(privContent)

        val kf = KeyFactory.getInstance("RSA")
        val publicKey = kf.generatePublic(X509EncodedKeySpec(decodedPub)) as RSAPublicKey
        val privateKey = kf.generatePrivate(PKCS8EncodedKeySpec(decodedPriv)) as RSAPrivateKey

        val builder = RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid)

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE)
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION)
        }

        return builder.build()
    }
}
</code></pre>
<p>SecurityUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.Optional;

@UtilityClass
public class SecurityUtils {

    public static final String AUTHORITIES_KEY = "auth";

    public Optional&#x3C;String> getCurrentUserLogin() {
        var ctx = SecurityContextHolder.getContext();
        return Optional.ofNullable(extractPrincipal(ctx.getAuthentication()));
    }

    private String extractPrincipal(Authentication authentication) {
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getSubject();
        }
        if (principal instanceof UserDetails ud) {
            return ud.getUsername();
        }
        if (principal instanceof String username) {
            return username;
        }
        return null;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.jwt.Jwt
import java.util.Optional

object SecurityUtils {
  const val AUTHORITIES_KEY = "auth"

  fun getCurrentUserLogin(): Optional&#x3C;String> {
    val authentication: Authentication? = SecurityContextHolder.getContext().authentication
    return Optional.ofNullable(extractPrincipal(authentication))
  }

  private fun extractPrincipal(authentication: Authentication?): String? {
    return when (val principal = authentication?.principal) {
      is Jwt -> principal.subject
      is UserDetails -> principal.username
      is String -> principal
      else -> null
    }
  }
}
</code></pre>
<p>DomainUserDetailsService</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import io.github.susimsek.springbootgraphqljwedemo.entity.User;
import io.github.susimsek.springbootgraphqljwedemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class DomainUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findOneWithAuthoritiesByUsername(username)
      .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

    List&#x3C;GrantedAuthority> authorities = user.getAuthorities().stream()
      .map(mapping -> mapping.getAuthority().getName())
      .map(SimpleGrantedAuthority::new)
      .collect(Collectors.toList());

    return new org.springframework.security.core.userdetails.User(
      user.getUsername(),
      user.getPassword(),
      user.isEnabled(),  // enabled
      true,              // accountNonExpired
      true,              // credentialsNonExpired
      true,              // accountNonLocked
      authorities
    );
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import io.github.susimsek.springbootgraphqljwedemo.repository.UserRepository
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import java.util.stream.Collectors

class DomainUserDetailsService(
  private val userRepository: UserRepository
) : UserDetailsService {
  override fun loadUserByUsername(username: String): UserDetails {
    val user = userRepository.findOneWithAuthoritiesByUsername(username)
      .orElseThrow { UsernameNotFoundException("User not found with username: $username") }

    val authorities: List&#x3C;GrantedAuthority> = user.authorities.stream()
      .map { SimpleGrantedAuthority(it.authority.name) }
      .collect(Collectors.toList())

    return org.springframework.security.core.userdetails.User(
      user.username,
      user.password,
      user.enabled,
      true,
      true,
      true,
      authorities
    )
  }
}
</code></pre>
<p>SpringSecurityAuditorAware</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import org.springframework.data.domain.AuditorAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class SpringSecurityAuditorAware implements AuditorAware&#x3C;String> {

  @Override
  @NonNull
  public Optional&#x3C;String> getCurrentAuditor() {
    return Optional.of(SecurityUtils.getCurrentUserLogin().orElse("system"));
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import org.springframework.data.domain.AuditorAware
import org.springframework.lang.NonNull
import org.springframework.stereotype.Component

@Component
class SpringSecurityAuditorAware : AuditorAware&#x3C;String> {
    @NonNull
    override fun getCurrentAuditor(): Optional&#x3C;String> =
        Optional.of(SecurityUtils.getCurrentUserLogin().orElse("system"))
}
</code></pre>
<p>GraphQlTokenCookieInterceptor</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import graphql.GraphQLContext;
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class GraphQlTokenCookieInterceptor implements WebGraphQlInterceptor {

  @Override
  @NonNull
  public Mono&#x3C;WebGraphQlResponse> intercept(@NonNull WebGraphQlRequest request, Chain chain) {
    return chain.next(request)
      .doOnNext(response -> {
        GraphQLContext ctx = response.getExecutionInput().getGraphQLContext();

        if (Boolean.TRUE.equals(ctx.get("clearAccessToken"))) {
          ResponseCookie expired = CookieUtils.removeAccessTokenCookie();
          response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, expired.toString());
          return;
        }

        TokenDTO tokenDto = ctx.get("accessToken");
        if (tokenDto != null) {
          ResponseCookie cookie = CookieUtils.createAccessTokenCookie(tokenDto);
          response.getResponseHeaders()
            .add(HttpHeaders.SET_COOKIE, cookie.toString());
        }
      });
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import graphql.GraphQLContext
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import org.springframework.graphql.server.WebGraphQlInterceptor
import org.springframework.graphql.server.WebGraphQlRequest
import org.springframework.graphql.server.WebGraphQlResponse
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono

@Component
class GraphQlTokenCookieInterceptor : WebGraphQlInterceptor {

  override fun intercept(
    request: WebGraphQlRequest,
    chain: WebGraphQlInterceptor.Chain
  ): Mono&#x3C;WebGraphQlResponse> {
    return chain.next(request)
      .doOnNext { response ->
        val ctx: GraphQLContext = response.executionInput.graphQLContext

        if (ctx.get&#x3C;Boolean>("clearAccessToken") == true) {
          val expired: ResponseCookie = CookieUtils.removeAccessTokenCookie()
          response.responseHeaders.add(HttpHeaders.SET_COOKIE, expired.toString())
          return@doOnNext
        }

        ctx.get&#x3C;TokenDTO>("accessToken")?.let { tokenDto ->
          val cookie: ResponseCookie = CookieUtils.createAccessTokenCookie(tokenDto)
          response.responseHeaders.add(HttpHeaders.SET_COOKIE, cookie.toString())
        }
      }
  }
}
</code></pre>
<p>CookieAuthenticationWebSocketInterceptor</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.security;

import io.github.susimsek.springbootgraphqljwedemo.security.CookieUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
import org.springframework.graphql.server.WebSocketGraphQlRequest;
import org.springframework.graphql.server.WebSocketSessionInfo;
import org.springframework.graphql.server.support.AuthenticationExtractor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import reactor.util.context.ContextView;

import java.util.Map;

@RequiredArgsConstructor
public class CookieAuthenticationWebSocketInterceptor implements WebSocketGraphQlInterceptor {

  private final String authenticationAttribute = this.getClass().getName() + ".AUTHENTICATION";
  private final AuthenticationExtractor authExtractor;
  private final AuthenticationManager authenticationManager;

  @Override
  @NonNull
  public Mono&#x3C;Object> handleConnectionInitialization(@NonNull WebSocketSessionInfo info,
                                                     @NonNull Map&#x3C;String, Object> payload) {

    Mono&#x3C;Authentication> headerAuth = authExtractor.getAuthentication(payload);

    Mono&#x3C;Authentication> cookieAuth = Mono.defer(() -> {
      String token = CookieUtils.resolveToken(info.getHeaders());
      return token != null
        ? Mono.just(new BearerTokenAuthenticationToken(token))
        : Mono.empty();
    });

    return headerAuth
      .switchIfEmpty(cookieAuth)
      .flatMap(this::authenticate)
      .doOnNext(auth -> {
        SecurityContext securityContext = new SecurityContextImpl(auth);
        info.getAttributes().put(this.authenticationAttribute, securityContext);
      })
      .then(Mono.empty());
  }

  @Override
  @NonNull
  public Mono&#x3C;WebGraphQlResponse> intercept(@NonNull WebGraphQlRequest request,
                                            @NonNull WebGraphQlInterceptor.Chain chain) {
    if (request instanceof WebSocketGraphQlRequest webSocketRequest) {
      Map&#x3C;String, Object> attributes = webSocketRequest.getSessionInfo().getAttributes();
      SecurityContext securityContext = (SecurityContext) attributes.get(this.authenticationAttribute);
      ContextView contextView = this.getContextToWrite(securityContext);
      return chain.next(request).contextWrite(contextView);
    } else {
      return chain.next(request);
    }
  }

  protected Mono&#x3C;Authentication> authenticate(Authentication authentication) {
    return Mono.just(this.authenticationManager.authenticate(authentication));
  }

  protected ContextView getContextToWrite(SecurityContext securityContext) {
    return Context.of(SecurityContext.class.getName(), securityContext);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.security

import io.github.susimsek.springbootgraphqljwedemo.security.CookieUtils
import org.springframework.graphql.server.WebGraphQlInterceptor
import org.springframework.graphql.server.WebGraphQlRequest
import org.springframework.graphql.server.WebGraphQlResponse
import org.springframework.graphql.server.WebSocketGraphQlInterceptor
import org.springframework.graphql.server.WebSocketGraphQlRequest
import org.springframework.graphql.server.WebSocketSessionInfo
import org.springframework.graphql.server.support.AuthenticationExtractor
import org.springframework.lang.NonNull
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken
import reactor.core.publisher.Mono
import reactor.util.context.Context
import reactor.util.context.ContextView

class CookieAuthenticationWebSocketInterceptor(
  private val authExtractor: AuthenticationExtractor,
  private val authenticationManager: AuthenticationManager
) : WebSocketGraphQlInterceptor {

  private val authenticationAttribute: String = "${this::class.java.name}.AUTHENTICATION"

  @NonNull
  override fun handleConnectionInitialization(
    @NonNull info: WebSocketSessionInfo,
    @NonNull payload: Map&#x3C;String, Any>
  ): Mono&#x3C;Any> {
    val headerAuth: Mono&#x3C;Authentication> = authExtractor.getAuthentication(payload)

    val cookieAuth: Mono&#x3C;Authentication> = Mono.defer {
      val token = CookieUtils.resolveToken(info.headers)
      if (token != null) {
        Mono.just(BearerTokenAuthenticationToken(token))
      } else {
        Mono.empty()
      }
    }

    return headerAuth
      .switchIfEmpty(cookieAuth)
      .flatMap { authenticate(it) }
      .doOnNext { auth ->
        val securityContext = SecurityContextImpl(auth)
        info.attributes[authenticationAttribute] = securityContext
      }
      .then(Mono.empty())
  }

  @NonNull
  override fun intercept(
    @NonNull request: WebGraphQlRequest,
    @NonNull chain: WebGraphQlInterceptor.Chain
  ): Mono&#x3C;WebGraphQlResponse> {
    return if (request is WebSocketGraphQlRequest) {
      val attributes = request.sessionInfo.attributes
      val securityContext = attributes[authenticationAttribute] as SecurityContext
      val contextView: ContextView = getContextToWrite(securityContext)
      chain.next(request).contextWrite(contextView)
    } else {
      chain.next(request)
    }
  }

  protected fun authenticate(authentication: Authentication): Mono&#x3C;Authentication> =
    Mono.just(authenticationManager.authenticate(authentication))

  protected fun getContextToWrite(securityContext: SecurityContext): ContextView =
    Context.of(SecurityContext::class.java.name, securityContext)
}
</code></pre>
<hr>
<h2>🛠️ Step 6: Authentication &#x26; Protected Endpoints</h2>
<p>In this section, we define the GraphQL controllers and DTOs necessary for:</p>
<ul>
<li>AuthController: Authenticates users, issues JWE tokens via a GraphQL mutation, and places <code>accessToken</code> or <code>clearAccessToken</code> flags into the GraphQLContext.</li>
<li>HelloController: Expose protected GraphQL queries, subscriptions and mutations for authenticated users and admin-only operations.</li>
<li>LoginInput: GraphQL input type for login (username/password).</li>
<li>GreetInput &#x26; GreetDTO: GraphQL mutation input and response for a greeting operation.</li>
<li>TokenDTO: Model the JWE token response including token, type, and expiration.</li>
</ul>
<p>These components complete the stateless authentication flow in a GraphQL API using JWE tokens and a JPA-backed user store.</p>
<p>AuthController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.controller;

import graphql.GraphQLContext;
import io.github.susimsek.springbootjweauthjpademo.dto.LoginInput;
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import io.github.susimsek.springbootjweauthjpademo.security.JweUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class AuthController {

  private final AuthenticationManager authenticationManager;
  private final JweUtil jweUtil;

  @MutationMapping
  public TokenDTO login(@Argument LoginInput input,
                        GraphQLContext context) {
    // Authenticate user
    Authentication auth = authenticationManager.authenticate(
      new UsernamePasswordAuthenticationToken(input.username(), input.password())
    );
    SecurityContextHolder.getContext().setAuthentication(auth);

    // Generate JWE token
    TokenDTO tokenDto = jweUtil.generateToken(auth);

    // Store token in GraphQLContext for the interceptor to set cookie
    context.put("accessToken", tokenDto);

    return tokenDto;
  }

  @PreAuthorize("isAuthenticated()")
  @MutationMapping
  public Boolean logout(GraphQLContext context) {
    // Signal the interceptor to clear the cookie
    context.put("clearAccessToken", Boolean.TRUE);
    return true;
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.controller

import graphql.GraphQLContext
import io.github.susimsek.springbootjweauthjpademo.dto.LoginInput
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import io.github.susimsek.springbootjweauthjpademo.security.JweUtil
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.MutationMapping
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Controller

@Controller
class AuthController(
  private val authenticationManager: AuthenticationManager,
  private val jweUtil: JweUtil
) {

  @MutationMapping
  fun login(
    @Argument input: LoginInput,
    context: GraphQLContext
  ): TokenDTO {
    // Authenticate user
    val auth = authenticationManager.authenticate(
      UsernamePasswordAuthenticationToken(input.username, input.password)
    )
    SecurityContextHolder.getContext().authentication = auth

    // Generate JWE token
    val tokenDto = jweUtil.generateToken(auth)

    // Store token in GraphQLContext for the interceptor to set cookie
    context.put("accessToken", tokenDto)

    return tokenDto
  }

  @PreAuthorize("isAuthenticated()")
  @MutationMapping
  fun logout(context: GraphQLContext): Boolean {
    // Signal the interceptor to clear the cookie
    context.put("clearAccessToken", true)
    return true
  }
}
</code></pre>
<p>HelloController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.controller;

import io.github.susimsek.springbootgraphqljwedemo.dto.GreetDTO;
import io.github.susimsek.springbootgraphqljwedemo.dto.GreetInput;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.Instant;

import static io.github.susimsek.springbootgraphqljwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Controller
@RequiredArgsConstructor
@PreAuthorize("isAuthenticated()")
public class HelloController {

  @QueryMapping
  public String helloAll(@AuthenticationPrincipal Jwt jwt) {
    var roles = jwt.getClaimAsStringList(AUTHORITIES_KEY);
    return "Hello, " + jwt.getSubject() + "! Your roles: " + roles;
  }

  @PreAuthorize("hasAuthority('ROLE_ADMIN')")
  @QueryMapping
  public String helloAdmin(@AuthenticationPrincipal Jwt jwt) {
    return "Hello Admin, " + jwt.getSubject() + "!";
  }

  @MutationMapping
  public GreetDTO greet(
    @Argument GreetInput input,
    @AuthenticationPrincipal Jwt jwt
  ) {
    String msg = "Hello " + jwt.getSubject() + ", you said: " + input.getMessage();
    return new GreetDTO(msg, Instant.now());
  }

  @SubscriptionMapping("greetStream")
  public Flux&#x3C;GreetDTO> greetStream(
    @Argument GreetInput input,
    @AuthenticationPrincipal Jwt jwt
  ) {
    return Flux.interval(Duration.ofSeconds(1))
      .map(i -> {
        String msg = "Hello " + jwt.getSubject()
          + ", you said: " + input.getMessage()
          + " (event " + (i + 1) + ")";
        return new GreetDTO(msg, Instant.now());
      });
  }

  @PreAuthorize("hasAuthority('ROLE_ADMIN')")
  @SubscriptionMapping("greetStreamAdmin")
  public Flux&#x3C;GreetDTO> greetStreamAdmin(
    @Argument GreetInput input,
    @AuthenticationPrincipal Jwt jwt
  ) {
    return Flux.interval(Duration.ofSeconds(2))
      .map(i -> {
        String msg = "Hello Admin " + jwt.getSubject()
          + ", you said: " + input.getMessage()
          + " (admin event " + (i + 1) + ")";
        return new GreetDTO(msg, Instant.now());
      });
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin"></code></pre>
<p>LoginInput</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.dto;

public record LoginInput(
  String username,
  String password
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.dto

data class LoginInput(
  val username: String,
  val password: String
)
</code></pre>
<p>GreetInput</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.dto;

public record GreetInput(
  String message
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.dto

data class GreetInput(
  val message: String
)
</code></pre>
<p>GreetDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.dto;

import java.time.Instant;

public record GreetDTO(
  String greeting,
  Instant timestamp
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.dto

import java.time.Instant

data class GreetDTO(
  val greeting: String,
  val timestamp: Instant
)
</code></pre>
<p>TokenDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootgraphqljwedemo.dto;

public record TokenDTO(
  String accessToken,
  String tokenType,
  long accessTokenExpiresIn
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootgraphqljwedemo.dto

data class TokenDTO(
  val accessToken: String,
  val tokenType: String,
  val accessTokenExpiresIn: Long
)
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<p>If you have GraalVM 22.3+ installed, you can compile a native image with the <code>native</code> profile:</p>
<pre><code class="language-bash">./mvnw native:compile -Pnative
</code></pre>
<p>After successful native-image compilation, the executable will be generated under <code>target/</code> (e.g., <code>target/spring-boot-graphql-jwe-auth-demo</code>). Run it directly:</p>
<pre><code class="language-bash">./target/spring-boot-graphql-jwe-auth-demo
</code></pre>
<p>Optionally, compress the native executable using UPX for a smaller file size (if UPX is installed):</p>
<pre><code class="language-bash">upx --ultra-brute --lzma target/spring-boot-graphql-jwe-auth-demo
</code></pre>
<hr>
<h2>🧪 Test GraphQL Endpoints</h2>
<p>In this section, we clarify Test GraphQL Endpoints and summarize the key points you will apply in implementation.</p>
<h3>Admin Flow</h3>
<p>Login as admin and capture the JWE token from the <code>Set-Cookie</code> header:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { login(input: { username: \"admin\", password: \"adminpass\" }) { accessToken tokenType accessTokenExpiresIn } }" }'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
<li>Response body:</li>
</ul>
<pre><code class="language-json">{
  "data": {
    "login": {
      "accessToken": "&#x3C;jwe-token>",
      "tokenType": "Bearer",
      "accessTokenExpiresIn": 3600
    }
  }
}
</code></pre>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAll }" }' \
  http://localhost:8080/graphql
</code></pre>
<p>Use Authorization header instead:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAll }" }' \
  http://localhost:8080/graphql
</code></pre>
<p>Call the admin-restricted <code>helloAdmin</code> query:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAdmin }" }' \
  http://localhost:8080/graphql
</code></pre>
<p>Execute the <code>logout</code> mutation to clear the session cookie:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { logout }" }' \
  http://localhost:8080/graphql
</code></pre>
<h3>User Flow</h3>
<p>Login as user and capture JWE token from cookie:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { login(input: { username: \"user\", password: \"userpass\" }) { accessToken tokenType accessTokenExpiresIn } }" }'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
</ul>
<p>Use cookie to call <code>helloAll</code>:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAll }" }' \
  http://localhost:8080/graphql
</code></pre>
<p>Use Authorization header:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAll }" }' \
  http://localhost:8080/graphql
</code></pre>
<p>Attempt the <code>helloAdmin</code> query (should return errors with 403):</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ helloAdmin }" }' \
  http://localhost:8080/graphql
</code></pre>
<h3>Subscription Test via GraphiQL</h3>
<p>Test subscriptions directly in the GraphiQL UI at <code>http://localhost:8080/graphiql</code>:</p>
<ol>
<li>
<p>Token Retrieval in GraphiQL</p>
</li>
<li>
<p>In the Query pane, enter:</p>
<pre><code class="language-graphql">mutation Login($in: LoginInput!) {
  login(input: $in) {
    accessToken
  }
}
</code></pre>
</li>
<li>
<p>Switch to the Variables pane and add:</p>
<pre><code class="language-json">{
  "in": { "username": "admin", "password": "adminpass" }
}
</code></pre>
</li>
<li>
<p>Click ▶️ to execute and copy the returned <code>accessToken</code> from the response.</p>
</li>
<li>
<p>Configure Headers</p>
</li>
<li>
<p>Click the Headers tab in the sidebar.</p>
</li>
<li>
<p>Add:</p>
<pre><code class="language-json">{
  "Authorization": "Bearer &#x3C;accessToken>"
}
</code></pre>
</li>
<li>
<p>Subscribe as User</p>
</li>
<li>
<p>Switch to the Subscriptions pane.</p>
</li>
<li>
<p>Enter:</p>
<pre><code class="language-graphql">subscription UserSubscribe($in: GreetInput!) {
  greetStream(input: $in) {
    greeting
    timestamp
  }
}
</code></pre>
</li>
<li>
<p>In Variables, set:</p>
<pre><code class="language-json">{
  "in": { "message": "Hello via GraphiQL!" }
}
</code></pre>
</li>
<li>
<p>Click ▶️ to start streaming messages from <code>greetStream</code>.</p>
</li>
<li>
<p>Subscribe as Admin</p>
</li>
<li>
<p>In the same Subscriptions pane, enter:</p>
<pre><code class="language-graphql">subscription AdminSubscribe($in: GreetInput!) {
  greetStreamAdmin(input: $in) {
    greeting
    timestamp
  }
}
</code></pre>
</li>
<li>
<p>Use the same Variables panel.</p>
</li>
<li>
<p>Click ▶️. Only tokens with <code>ROLE_ADMIN</code> receive data; others see an authorization error.</p>
</li>
</ol>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot GraphQL JWE Authentication implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Security]]></category>
      <category><![CDATA[JWT]]></category>
      <category><![CDATA[JWE]]></category>
      <category><![CDATA[GraphQL]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-graphql-jwe-auth-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot JWE Authentication with JPA]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-jwe-authentication-with-jpa</link>
      <description><![CDATA[Learn how to use stateless encrypted JWTs (JWE) to secure your Spring Boot APIs while persisting user identities and roles in a JPA-backed database.]]></description>
      <pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-05-12T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-jwe-authentication-with-jpa</guid>
      <content:encoded><![CDATA[<p>Spring Boot lets you use stateless encrypted JWTs (JWE) to secure your APIs, while persisting user identities and roles in a JPA-backed database.</p>
<hr>
<h2>🌟 Why Use JWE + JPA Authentication?</h2>
<p>In this section, we clarify Why Use JWE + JPA Authentication? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Stateless Security: Tokens are self-contained and require no server-side storage.</li>
<li>Integrity: Signed tokens ensure tamper evidence.</li>
<li>Confidentiality: Encrypted JWTs hide sensitive claims.</li>
<li>User Management: Persist and manage users and roles via familiar JPA repositories.</li>
<li>Standards-based: Leverage JOSE, Spring Security, and Spring Data JPA.</li>
<li>Scalable: Scale horizontally without session replication or sticky sessions.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 17 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse)</li>
<li>🛢️ PostgreSQL (or H2 for dev)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include these in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.springframework.security&#x3C;/groupId>
  &#x3C;artifactId>spring-security-oauth2-resource-server&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-data-jpa&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.liquibase&#x3C;/groupId>
  &#x3C;artifactId>liquibase-core&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.projectlombok&#x3C;/groupId>
  &#x3C;artifactId>lombok&#x3C;/artifactId>
  &#x3C;optional>true&#x3C;/optional>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>com.h2database&#x3C;/groupId>
  &#x3C;artifactId>h2&#x3C;/artifactId>
  &#x3C;scope>runtime&#x3C;/scope>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.liquibase:liquibase-core'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configuration Files</h2>
<p>In this section, we define all of the application- and database-level configuration files required to wire up our Spring Boot app with H2/PostgreSQL, JPA, Liquibase changelogs, initial data loads, and JWE key properties.</p>
<ul>
<li>
<p><code>application.yml</code>
Holds Spring datasource, H2 console, JPA/Hibernate, Liquibase changelog path, and all JWT/JWE key, issuer, and expiration settings.</p>
</li>
<li>
<p><code>db/master.xml</code>
The Liquibase master changelog, with includes and DBMS-specific properties for H2 and PostgreSQL.</p>
</li>
<li>
<p><code>db/changelog/changelog-user.xml</code>
Your core schema changelog defining <code>user_identity</code>, <code>authority</code>, and <code>user_authority_mapping</code> tables, indexes, FKs, and initial <code>&#x3C;loadData></code> steps.</p>
</li>
<li>
<p><code>db/data/user.csv</code>
Initial user records (UUID, username, bcrypt-hashed password, email, enabled flag, timestamps, auditor).</p>
</li>
<li>
<p><code>db/data/authority.csv</code>
Initial authority records (UUID, name, description, timestamps, auditor).</p>
</li>
<li>
<p><code>db/data/user_authority_mapping.csv</code>
Initial mapping of users ↔ authorities (composite PK, timestamps, auditor).</p>
</li>
</ul>
<p>application.yml</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  h2:
    console:
      enabled: true
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate.format_sql: true
  liquibase:
    change-log: classpath:db/master.xml
security:
  admin:
    username: admin
    password: adminpass
  user:
    username: user
    password: userpass
  jwt:
    issuer: demo-issuer
    expiration-duration: 3600s
    signing:
      key-id: signing-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjZEcz5itWkDGOSqZdB5P
        JE0ccOCckskp0hN3kQbT1qnv+9/k66jlWgVi4HSTppwdNF/Ylu5u541Qj+Okyg+u
        8o2PvKo5CfgvTDsFLTrZHUXU6hCSGatLAQoeN6lT8wzov2r4DFecXrIqcO6SvMB5
        ecPqsfiTi4trsNKgJ4cWS6gILH62ISd1ipUadfpnUzDMO1OulV0CJNV6bcBk7Es9
        RW6AHfg9j8osSanpwvRM4MJkB0SRxYUnrN9faGpkBZISZJ8TShhaTHEGfSNgKe5y
        8iu+AMGGZu8DYczVmqS3Ske1fq6y5HEGCma7Mo019GmwKeHBo1obuET6cZRygj4y
        twIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNkRzPmK1aQMY5
        Kpl0Hk8kTRxw4JySySnSE3eRBtPWqe/73+TrqOVaBWLgdJOmnB00X9iW7m7njVCP
        46TKD67yjY+8qjkJ+C9MOwUtOtkdRdTqEJIZq0sBCh43qVPzDOi/avgMV5xesipw
        7pK8wHl5w+qx+JOLi2uw0qAnhxZLqAgsfrYhJ3WKlRp1+mdTMMw7U66VXQIk1Xpt
        wGTsSz1FboAd+D2PyixJqenC9EzgwmQHRJHFhSes319oamQFkhJknxNKGFpMcQZ9
        I2Ap7nLyK74AwYZm7wNhzNWapLdKR7V+rrLkcQYKZrsyjTX0abAp4cGjWhu4RPpx
        lHKCPjK3AgMBAAECggEABMo/sNCIMREXA/EqjxDFecN4LmOTOK5A0YBiH9Cc01sd
        qSaavtSQqhqbjV+0bUNEA7UohXbc3s3bx3qa5VFhiIh8sBQMaQdyRkCK/MxMc16d
        BSx5XQ/8MjVO73A1zHgH2g47BWPjgRrDL94KrgNPOO0FoG76QxL9GlcOHzhFa1rf
        xbb2auLBtflKy+5TZNnB/sbFC9ISpWywzfblD2Fsvxupl0vNWtA0Y2rPbKMZRnY+
        V6NC38yxW3HHAdWQRYLGOitM59I0HBgnjQKzTBNIsjPnJA2BSZvMBtQ1e3RQV0qE
        8VVoktZ8A0KDUxqdKWYlGYAY5hIw1y3UUsPRUbS3fQKBgQDAZehu+Kb5Sw+h00Qu
        vViNHDvi0PwmhQrHVpgjrppCnWsTk6FJFm+EBLY+87usXuRCnfZ1semb6P+yjLh2
        bJ9IXIrFAztcyE+7eZfn0H7mHA7E2ICKTjNdKbjy8kHViHpgdXREnAOrFTTk/BqW
        dq+weG2OuxYczWsY4XliWaVyiwKBgQC8XYExpSmoyI37o+jr9405k30tbGUw4X+/
        xYOvSUuafI8IOSLfKTpsvkMN3hxpJO6apIRAjOALbotKUA5FrHQPenuBWOr3mBJT
        euLiaWphqU8YN5dbNd+JhC/Jh5DLhY5FpB2Fv2MKSoF7+onpjDy9pEU5aReeBB0v
        ekUw4nWiBQKBgHxyrnjxP1frFG5xMB4nfZqw04+v7BmiXsl3mqsh6kgCeNtN17pl
        17YGMjfgAdnJ+02XzW5tqRSfDp3YZgy7z//HVD+BCqnGK8SxLu/ULfD73xW2kNZl
        JNYzAZ2r06eiQr4X2x/x5nGIIxGmfDAtDxFPpFX5b6ErwgVy+sgCAoFnAoGAWSpu
        EMEdQk+FnnwNsz2g9YNSuyDXmdb08SOfXWd1yXBzCLJ7RmYuyPEbrsHYcxFPfZap
        ICFPoTm35/qTdvnWiskxE56yw3eSHUBLjF/YQtixn0YZeMy8v0z6jgyFR0I2gdLZ
        QsnBKUrxlm2XwR1oV2Eef7m2u085PZNEk4pvor0CgYA6n060r48/CgRVslAeGder
        fZ3n+2ru8q2UVB38evepjLifn5+tKdFzZ7/NckXMC0NOVzm74qG46VFLMw5TtyT4
        /hRvGSYxA6dMjXy+tcrAns9Isrz2PnYFntlbhU6hw0um809tFunbvITfyeOjAsDW
        stC4thnhzXXT1Y3RfFtYEg==
        -----END PRIVATE KEY-----
    encryption:
      key-id: encryption-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtU7Vu4tML8qg9BISaUH0
        BTU0+qkHJS3TUl3R0hEPttZJGf8EgT7bpWqRjdhMC2SYt2ifNF82EgMo/eva1rLn
        wYtCsxjJ7oB4U2m16/JKHtphmMUGhSLgbp+Y8pGAQ+P6u4HLlI0qbL92Syb6QCTr
        nYIlzZ2uUAvUQg/SbuAvdS4kdauZtpMNbhryusMVvILVBC5yUhfLJAkjbU3qWo/n
        2NKPr4kjwwFh6FaAf0HLsXCFmJbPPok6WMZeDqPORPZCt5gU2t04fS7s/2SSbhQ/
        i+7yyT4pl44cUlNLLN3Qo73sai5H90q7Dvmk3M5/YMJB0Ueyb705LHyOO5xA618s
        qQIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1TtW7i0wvyqD0
        EhJpQfQFNTT6qQclLdNSXdHSEQ+21kkZ/wSBPtulapGN2EwLZJi3aJ80XzYSAyj9
        69rWsufBi0KzGMnugHhTabXr8koe2mGYxQaFIuBun5jykYBD4/q7gcuUjSpsv3ZL
        JvpAJOudgiXNna5QC9RCD9Ju4C91LiR1q5m2kw1uGvK6wxW8gtUELnJSF8skCSNt
        Tepaj+fY0o+viSPDAWHoVoB/QcuxcIWYls8+iTpYxl4Oo85E9kK3mBTa3Th9Luz/
        ZJJuFD+L7vLJPimXjhxSU0ss3dCjvexqLkf3SrsO+aTczn9gwkHRR7JvvTksfI47
        nEDrXyypAgMBAAECggEAREnJHrY8n9OGA+e6n4KD0mJT7gUz98Dm0yMbIC/k50yW
        hAAVRkjSmd8lq3NIURI2ov342NTznJ0sF1d6OVtxBujY2nP/uqEQsPoj1xaO7Ef8
        cnnjFsooFgJurQ44bVm02mLstqrky8jhWTT5FKfTRqP6cRNu0B9kdu1WqQQvW8a/
        C8xD8XUFzzJFQsEstJ076NjlZNH3EeqFiLxyVdV7tPEJRnOe2V2BAAgda3ByA6Jc
        Nd4xYHTVr5rZ+KES9gaAgb86+S7hNigsxz5Gp58NimLEyz1Qd+d9QCgPRgKOV5Uq
        0ci6c28ZrxW9wa/2CGCpoow1V7hRy1fZ24g2Tz91cQKBgQDbSdP/FQYuem0pUf8u
        mA/niEC8Jz30FW3ix5aqbQ8YRudK48Xy8F7AWnlS0wT2k/Xaeppn9YSPK4HlsNk2
        1gnpNfzdqXpJQtuxcPPeFNzUdXzBrsLXYqtlhOX/afrzqMhp73gkqoxXtqwD7BqN
        VfMcVXsWrEZQrRPBn/RUnnLdJwKBgQDTqUGupm93UlhoPgLXHGF/seQwPPHcU6lS
        SS9l512Ie21B+vZecgO4r77BXJwbY4rZVA5/m+wrsNmsT2f8yfdbs84cBDDWiKy5
        t2Um8zrus+BCIffxc9Fck9/htK33jLLcYwdSAm3a9pJxR9f/kAdredSTGBNJ3W/2
        rfh3cdNprwKBgDi7WfBFRSsjGzi0cPth9cNlubGzyVBrdtlT34PJ4Tzboxz53o1i
        aHEFNxwZYdBVKSbTzzyUBS5xCBMfdKK+LyQ5hmjmXq+zb1jxqvXKmfMRTixhhSDp
        8wO5pTM1/Omqwea+QGvj/5j1tnzxSVFFajbrWoPcH/jhPho6wqBducPDAoGBALOJ
        MEOavZXy7TaO0w3v7uvH0wzvxR/kfw1jMqc3l2j7ePOskmoOQAXaXO3bRjcdOlua
        Jyoq8islOZ4lRMlx7zWD0OKG035GNGzbmRtu2aA8R48RDSVr3jyu2gqznZULbXPv
        M/hmQxSmbhVUoW0PmJubnaqfk0zmXeBaNRXsIS3VAoGBALdsAtqR23PVeL6sYj2l
        dRNTDXpfvjFqJ5NThRO/4mT0CrPHQVj+Mz2bTc/Dxiwi8s8m3L+g23i3hodh/QMb
        Iz+UbcJZBGAKsFbPKGOtj6Bi07y/L11mcuNJzOWe61/JbVmJss0s+N/v3XucK/Ge
        CUaGOccsMO221v6JoBh9J3Hz
        -----END PRIVATE KEY-----
</code></pre>
<p>master.xml</p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
  xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
      http://www.liquibase.org/xml/ns/dbchangelog
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

  &#x3C;!-- H2 defaults -->
  &#x3C;property name="now" value="now()" dbms="h2"/>
  &#x3C;property name="floatType" value="float4" dbms="h2"/>
  &#x3C;property name="uuidType" value="uuid" dbms="h2"/>
  &#x3C;property name="datetimeType" value="datetime(6)" dbms="h2"/>
  &#x3C;property name="binaryType" value="LONGVARBINARY" dbms="h2"/>
  &#x3C;property name="clobType" value="longvarchar" dbms="h2"/>
  &#x3C;property name="blobType" value="blob" dbms="h2"/>

  &#x3C;!-- PostgreSQL overrides -->
  &#x3C;property name="now" value="current_timestamp" dbms="postgresql"/>
  &#x3C;property name="floatType" value="float4" dbms="postgresql"/>
  &#x3C;property name="uuidType" value="uuid" dbms="postgresql"/>
  &#x3C;property name="datetimeType" value="datetime" dbms="postgresql"/>
  &#x3C;property name="binaryType" value="BYTEA" dbms="postgresql"/>
  &#x3C;property name="clobType" value="clob" dbms="postgresql"/>
  &#x3C;property name="blobType" value="blob" dbms="postgresql"/>

  &#x3C;!-- include the core auth changelog -->
  &#x3C;include file="classpath:db/changelog/changelog-user.xml"/>

&#x3C;/databaseChangeLog>
</code></pre>
<p>changelog-user.xml</p>
<pre><code class="language-xml">&#x3C;?xml version="1.0" encoding="UTF-8"?>
&#x3C;databaseChangeLog
  xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
      http://www.liquibase.org/xml/ns/dbchangelog
      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

  &#x3C;changeSet id="20250510-1" author="susimsek">
    &#x3C;createTable tableName="user_identity">
      &#x3C;column name="id" type="varchar(36)">
        &#x3C;constraints primaryKey="true" primaryKeyName="pk_user_identity" nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="username" type="varchar(50)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="password" type="varchar(100)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="email" type="varchar(100)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="first_name" type="varchar(50)"/>
      &#x3C;column name="last_name" type="varchar(50)"/>
      &#x3C;column name="enabled" type="boolean">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;createIndex indexName="idx_user_identity_username" tableName="user_identity">
      &#x3C;column name="username"/>
    &#x3C;/createIndex>
    &#x3C;createIndex indexName="idx_user_identity_email" tableName="user_identity">
      &#x3C;column name="email"/>
    &#x3C;/createIndex>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-2" author="susimsek">
    &#x3C;createTable tableName="authority">
      &#x3C;column name="id" type="varchar(36)">
        &#x3C;constraints primaryKey="true" primaryKeyName="pk_authority" nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="name" type="varchar(50)">
        &#x3C;constraints nullable="false" unique="true"/>
      &#x3C;/column>
      &#x3C;column name="description" type="varchar(255)"/>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;createIndex indexName="idx_authority_name" tableName="authority">
      &#x3C;column name="name"/>
    &#x3C;/createIndex>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-3" author="susimsek">
    &#x3C;createTable tableName="user_authority_mapping">
      &#x3C;column name="user_id" type="varchar(36)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="authority_id" type="varchar(36)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_at" type="datetime">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="created_by" type="varchar(50)">
        &#x3C;constraints nullable="false"/>
      &#x3C;/column>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="varchar(50)"/>
    &#x3C;/createTable>

    &#x3C;addPrimaryKey
      tableName="user_authority_mapping"
      columnNames="user_id, authority_id"
      constraintName="pk_user_authority_mapping"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-4" author="susimsek">
    &#x3C;addForeignKeyConstraint
      baseTableName="user_authority_mapping"
      baseColumnNames="user_id"
      constraintName="fk_user_authority_user"
      referencedTableName="user_identity"
      referencedColumnNames="id"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-5" author="susimsek">
    &#x3C;addForeignKeyConstraint
      baseTableName="user_authority_mapping"
      baseColumnNames="authority_id"
      constraintName="fk_user_authority_authority"
      referencedTableName="authority"
      referencedColumnNames="id"/>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-1-data" author="susimsek">
    &#x3C;loadData
      file="db/data/user.csv"
      separator=";"
      tableName="user_identity">
      &#x3C;column name="id" type="string"/>
      &#x3C;column name="username" type="string"/>
      &#x3C;column name="password" type="string"/>
      &#x3C;column name="email" type="string"/>
      &#x3C;column name="first_name" type="string"/>
      &#x3C;column name="last_name" type="string"/>
      &#x3C;column name="enabled" type="boolean"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-2-data" author="susimsek">
    &#x3C;loadData
      file="db/data/authority.csv"
      separator=";"
      tableName="authority">
      &#x3C;column name="id" type="string"/>
      &#x3C;column name="name" type="string"/>
      &#x3C;column name="description" type="string"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

  &#x3C;changeSet id="20250510-3-data" author="susimsek">
    &#x3C;loadData
      file="db/data/user_authority_mapping.csv"
      separator=";"
      tableName="user_authority_mapping">
      &#x3C;column name="user_id" type="string"/>
      &#x3C;column name="authority_id" type="string"/>
      &#x3C;column name="created_at" type="datetime"/>
      &#x3C;column name="created_by" type="string"/>
      &#x3C;column name="updated_at" type="datetime"/>
      &#x3C;column name="updated_by" type="string"/>
    &#x3C;/loadData>
  &#x3C;/changeSet>

&#x3C;/databaseChangeLog>
</code></pre>
<p>user.csv</p>
<pre><code class="language-csv">id;username;password;email;first_name;last_name;enabled;created_at;created_by;updated_at;updated_by
a1b2c3d4-e5f6-7890-abcd-ef1234567890;admin;$2a$10$sva6wl8pmGKJE6NIWrxwcuJK1Jaa2I/LOI43iHVpbR4YB8KjGViiK;admin@example.com;Admin;User;true;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
09876543-21fe-dcba-0987-654321fedcba;user;$2a$10$5Py4PyteLuXEqnGpSigzfu0V55C7Hi7zX18lmh.J8Bpmft.h23voG;user@example.com;Normal;User;true;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<p>authority.csv</p>
<pre><code class="language-csv">id;name;description;created_at;created_by;updated_at;updated_by
f47ac10b-58cc-4372-a567-0e02b2c3d479;ROLE_ADMIN;Administrator role;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
9c858901-8a57-4791-81fe-4c455b099bc9;ROLE_USER;User role;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<p>user_authority_mapping.csv</p>
<pre><code class="language-csv">user_id;authority_id;created_at;created_by;updated_at;updated_by
a1b2c3d4-e5f6-7890-abcd-ef1234567890;9c858901-8a57-4791-81fe-4c455b099bc9;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
a1b2c3d4-e5f6-7890-abcd-ef1234567890;f47ac10b-58cc-4372-a567-0e02b2c3d479;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
09876543-21fe-dcba-0987-654321fedcba;9c858901-8a57-4791-81fe-4c455b099bc9;2025-05-10 12:00:00;system;2025-05-10 12:00:00;system
</code></pre>
<hr>
<h2>🛠️ Step 3: Security &#x26; Database Configuration</h2>
<p>In this section, we define the beans and properties RSA keys, HTTP security filters, and JPA repository/auditing setup for JWE‑based authentication:</p>
<ul>
<li>JwtProperties: Configures JWT issuer, expiration, and signing/encryption key pairs.</li>
<li>SecurityJwtConfig: Builds RSA JWKs, JWT encoder/decoder, authentication converter, and token resolver.</li>
<li>SecurityConfig: Integrates <code>DomainUserDetailsService</code>, configures authentication manager, password encoder, and stateless security filter chain with JWE support.</li>
<li>DatabaseConfig: Enables JPA repositories, auditing, and transaction management.</li>
</ul>
<p>SecurityJwtConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.config;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.github.susimsek.springbootjweauthjpademo.security.CookieBearerTokenResolver;
import io.github.susimsek.springbootjweauthjpademo.security.KeyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;

import java.util.List;

import static io.github.susimsek.springbootjweauthjpademo.security.SecurityUtils.AUTHORITIES_KEY;

@Configuration
public class SecurityJwtConfig {

    private final JwtProperties props;
    public SecurityJwtConfig(JwtProperties props) { this.props = props; }

    @Bean
    public RSAKey signingKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getSigning().getPublicKey(),
            props.getSigning().getPrivateKey(),
            props.getSigning().getKeyId(),
            true
        );
    }

    @Bean
    public RSAKey encryptionKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getEncryption().getPublicKey(),
            props.getEncryption().getPrivateKey(),
            props.getEncryption().getKeyId(),
            false
        );
    }

    @Bean
    public JWKSource&#x3C;SecurityContext> jwkSource(RSAKey signingKey, RSAKey encryptionKey) {
        JWKSet jwkSet = new JWKSet(List.of(
            signingKey,
            encryptionKey
        ));
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        DefaultJWTProcessor&#x3C;SecurityContext> jwtProcessor = new DefaultJWTProcessor&#x3C;>();
        JWEDecryptionKeySelector&#x3C;SecurityContext> jweKeySelector =
            new JWEDecryptionKeySelector&#x3C;>(
                JWEAlgorithm.RSA_OAEP_256,
                EncryptionMethod.A128GCM,
                jwkSource
            );
        jwtProcessor.setJWEKeySelector(jweKeySelector);

        JWSVerificationKeySelector&#x3C;SecurityContext> jwsKeySelector =
            new JWSVerificationKeySelector&#x3C;>(
                JWSAlgorithm.RS256,
                jwkSource
            );
        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});

        return new NimbusJwtDecoder(jwtProcessor);
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthorityPrefix("");
        converter.setAuthoritiesClaimName(AUTHORITIES_KEY);

        JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
        authConverter.setJwtGrantedAuthoritiesConverter(converter);
        return authConverter;
    }

    @Bean
    public BearerTokenResolver bearerTokenResolver() {
        CookieBearerTokenResolver resolver = new CookieBearerTokenResolver();
        resolver.setAllowUriQueryParameter(false);
        resolver.setAllowFormEncodedBodyParameter(false);
        resolver.setAllowCookie(true);
        return resolver;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.config

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.JWSVerificationKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import io.github.susimsek.springbootjweauthjpademo.security.CookieBearerTokenResolver
import io.github.susimsek.springbootjweauthjpademo.security.KeyUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver

@Configuration
class SecurityJwtConfig(private val props: JwtProperties) {

    @Bean
    @Throws(Exception::class)
    fun signingKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.signing.publicKey,
            props.signing.privateKey,
            props.signing.keyId,
            true
        )

    @Bean
    @Throws(Exception::class)
    fun encryptionKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.encryption.publicKey,
            props.encryption.privateKey,
            props.encryption.keyId,
            false
        )

    @Bean
    fun jwkSource(signingKey: RSAKey, encryptionKey: RSAKey): JWKSource&#x3C;SecurityContext> {
        val jwkSet = JWKSet(listOf(signingKey, encryptionKey))
        return JWKSource { jwkSelector, context -> jwkSelector.select(jwkSet) }
    }

    @Bean
    fun jwtDecoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtDecoder {
        val jwtProcessor = DefaultJWTProcessor&#x3C;SecurityContext>()
        val jweKeySelector = JWEDecryptionKeySelector(
            JWEAlgorithm.RSA_OAEP_256,
            EncryptionMethod.A128GCM,
            jwkSource
        )
        jwtProcessor.jweKeySelector = jweKeySelector
        val jwsKeySelector = JWSVerificationKeySelector(
            JWSAlgorithm.RS256,
            jwkSource
        )
        jwtProcessor.jwsKeySelector = jwsKeySelector
        jwtProcessor.jwtClaimsSetVerifier = { _, _ -> }
        return NimbusJwtDecoder(jwtProcessor)
    }

    @Bean
    fun jwtEncoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtEncoder =
        NimbusJwtEncoder(jwkSource)

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val converter = JwtGrantedAuthoritiesConverter().apply {
            setAuthorityPrefix("")
            setAuthoritiesClaimName(AUTHORITIES_KEY)
        }
        return JwtAuthenticationConverter().apply {
            setJwtGrantedAuthoritiesConverter(converter)
        }
    }

    @Bean
    fun bearerTokenResolver(): BearerTokenResolver =
        CookieBearerTokenResolver().apply {
            setAllowUriQueryParameter(false)
            setAllowFormEncodedBodyParameter(false)
            setAllowCookie(true)
        }
}
</code></pre>
<p>SecurityConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.config;

import io.github.susimsek.springbootjweauthjpademo.repository.UserRepository;
import io.github.susimsek.springbootjweauthjpademo.security.AuthoritiesConstants;
import io.github.susimsek.springbootjweauthjpademo.security.DomainUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                 MvcRequestMatcher.Builder mvc) throws Exception {
    http
      .cors(withDefaults())
      .csrf(AbstractHttpConfigurer::disable)
      .authorizeHttpRequests(authz -> authz
        .requestMatchers(
          mvc.pattern("/webjars/**"),
          mvc.pattern("/css/**"),
          mvc.pattern("/js/**")
        ).permitAll()
        .requestMatchers(
          mvc.pattern("/*.ico"),
          mvc.pattern("/*.png"),
          mvc.pattern("/*.svg"),
          mvc.pattern("/*.webapp")
        ).permitAll()
        .requestMatchers("/actuator/**").permitAll()
        .requestMatchers(
          "/v3/api-docs/**",
          "/swagger-ui.html",
          "/swagger-ui/**"
        ).permitAll()
        .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
        .requestMatchers(mvc.pattern("/api/hello/admin")).hasAuthority(AuthoritiesConstants.ADMIN)
        .anyRequest().authenticated()
      )
      .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
      )
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));

    return http.build();
  }

  @Bean
  public UserDetailsService userDetailsService(UserRepository userRepository) {
    return new DomainUserDetailsService(userRepository);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public AuthenticationManager authenticationManager(
    UserDetailsService userDetailsService,
    PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(userDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder);

    return new ProviderManager(authenticationProvider);
  }

  @Bean
  public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
    return new MvcRequestMatcher.Builder(introspector);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.config

import io.github.susimsek.springbootjweauthjpademo.repository.UserRepository
import io.github.susimsek.springbootjweauthjpademo.security.AuthoritiesConstants
import io.github.susimsek.springbootjweauthjpademo.security.DomainUserDetailsService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.configurers.AbstractHttpConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector

@Configuration
@EnableMethodSecurity(securedEnabled = true)
class SecurityConfig {

  @Bean
  @Throws(Exception::class)
  fun securityFilterChain(http: HttpSecurity,
                          mvc: MvcRequestMatcher.Builder): SecurityFilterChain {
    http
      .cors(withDefaults())
      .csrf { it.disable() }
      .authorizeHttpRequests { authz ->
        authz
          .requestMatchers(
            mvc.pattern("/webjars/**"),
            mvc.pattern("/css/**"),
            mvc.pattern("/js/**")
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/*.ico"),
            mvc.pattern("/*.png"),
            mvc.pattern("/*.svg"),
            mvc.pattern("/*.webapp")
          ).permitAll()
          .requestMatchers("/actuator/**").permitAll()
          .requestMatchers(
            "/v3/api-docs/**",
            "/swagger-ui.html",
            "/swagger-ui/**"
          ).permitAll()
          .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
          .requestMatchers(mvc.pattern("/api/hello/admin")).hasAuthority(AuthoritiesConstants.ADMIN)
          .anyRequest().authenticated()
      }
      .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
      .oauth2ResourceServer { it.jwt(withDefaults()) }

    return http.build()
  }

  @Bean
  fun userDetailsService(userRepository: UserRepository): UserDetailsService =
    DomainUserDetailsService(userRepository)

  @Bean
  fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

  @Bean
  fun authenticationManager(
    userDetailsService: UserDetailsService,
    passwordEncoder: PasswordEncoder
  ): AuthenticationManager {
    val provider = DaoAuthenticationProvider().apply {
      setUserDetailsService(userDetailsService)
      setPasswordEncoder(passwordEncoder)
    }
    return ProviderManager(provider)
  }

  @Bean
  fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
    MvcRequestMatcher.Builder(introspector)
}
</code></pre>
<p>JwtProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
@Data
public class JwtProperties {

    @Data
    public static class Pair {
        private String publicKey;
        private String privateKey;
        private String keyId;
    }

    private Pair signing;
    private Pair encryption;
    private String issuer;
    private Duration expirationDuration;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
class JwtProperties {

  class Pair {
    lateinit var publicKey: String
    lateinit var privateKey: String
    lateinit var keyId: String
  }

  lateinit var signing: Pair
  lateinit var encryption: Pair
  lateinit var issuer: String
  lateinit var expirationDuration: Duration
}
</code></pre>
<p>DatabaseConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableJpaRepositories("io.github.susimsek.springbootjweauthjpademo.repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfig {

}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.config

import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableJpaRepositories("io.github.susimsek.springbootjweauthjpademo.repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
class DatabaseConfig
</code></pre>
<hr>
<h2>🛠️ Step 4: JPA Integration</h2>
<p>In this section, we define the JPA entities representing users, authorities, and their mappings, along with the Spring Data repository for loading users with their authorities.</p>
<ul>
<li>BaseEntity: Abstract superclass providing audit fields (<code>createdAt</code>, <code>createdBy</code>, <code>updatedAt</code>, <code>updatedBy</code>).</li>
<li>Authority: <code>authority</code> table entity storing role data.</li>
<li>User: <code>user_identity</code> table entity storing user credentials and profile.</li>
<li>UserAuthorityMapping: <code>user_authority_mapping</code> join table entity linking users and authorities.</li>
<li>UserAuthorityMappingId: Composite key class for <code>UserAuthorityMapping</code>.</li>
<li>UserRepository: Spring Data JPA repository for <code>User</code> with an entity graph to load authorities.</li>
</ul>
<p>BaseEntity</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
public abstract class BaseEntity {

  @CreatedDate
  @Column(name = "created_at", nullable = false, updatable = false)
  private Instant createdAt;

  @CreatedBy
  @Column(name = "created_by", nullable = false, updatable = false, length = 50)
  private String createdBy;

  @LastModifiedDate
  @Column(name = "updated_at")
  private Instant updatedAt;

  @LastModifiedBy
  @Column(name = "updated_by", length = 50)
  private String updatedBy;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.entity

import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {

  @CreatedDate
  @Column(name = "created_at", nullable = false, updatable = false)
  lateinit var createdAt: Instant

  @CreatedBy
  @Column(name = "created_by", nullable = false, updatable = false, length = 50)
  lateinit var createdBy: String

  @LastModifiedDate
  @Column(name = "updated_at")
  var updatedAt: Instant? = null

  @LastModifiedBy
  @Column(name = "updated_by", length = 50)
  var updatedBy: String? = null
}
</code></pre>
<p>Authority</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.Objects;

@Entity
@Table(name = "authority")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Authority extends BaseEntity {

  @Id
  @Column(length = 36, nullable = false)
  private String id;

  @Column(length = 50, nullable = false, unique = true)
  private String name;

  @Column(length = 255)
  private String description;

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof Authority other)) {
      return false;
    }
    Class&#x3C;?> objClass = obj instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass();
    Class&#x3C;?> thisClass = this instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass();
    if (!thisClass.equals(objClass)) {
      return false;
    }
    return id != null &#x26;&#x26; Objects.equals(id, other.id);
  }

  @Override
  public final int hashCode() {
    return this instanceof HibernateProxy proxy
      ? proxy.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : getClass().hashCode();
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.hibernate.proxy.HibernateProxy
import java.util.Objects

@Entity
@Table(name = "authority")
class Authority(
  @Id
  @Column(length = 36, nullable = false)
  var id: String,

  @Column(length = 50, nullable = false, unique = true)
  var name: String,

  @Column(length = 255)
  var description: String?
) : BaseEntity() {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is Authority) return false
    val objClass = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass ?: other::class.java
    val thisClass = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass ?: this::class.java
    if (thisClass != objClass) return false
    return id == other.id
  }

  override fun hashCode(): Int {
    return (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: javaClass.hashCode()
  }
}
</code></pre>
<p>User</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedSubgraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "user_identity")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@NamedEntityGraph(
  name = "User.withAuthorities",
  attributeNodes = @NamedAttributeNode(value = "authorities", subgraph = "auth-subgraph"),
  subgraphs = @NamedSubgraph(
    name = "auth-subgraph",
    attributeNodes = @NamedAttributeNode("authority")
  )
)
public class User extends BaseEntity {

  @Id
  @Column(name = "id", length = 36, nullable = false, updatable = false)
  private String id;

  @Column(name = "username", length = 50, nullable = false, unique = true)
  private String username;

  @Column(name = "password", length = 100, nullable = false)
  private String password;

  @Column(name = "email", length = 100, nullable = false, unique = true)
  private String email;

  @Column(name = "first_name", length = 50)
  private String firstName;

  @Column(name = "last_name", length = 50)
  private String lastName;

  @Column(name = "enabled", nullable = false)
  private boolean enabled;

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY,
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set&#x3C;UserAuthorityMapping> authorities = new HashSet&#x3C;>();

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof User other)) return false;
    Class&#x3C;?> objClass = (obj instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass());
    Class&#x3C;?> thisClass = (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass());
    if (!thisClass.equals(objClass)) return false;
    return id != null &#x26;&#x26; id.equals(other.id);
  }

  @Override
  public final int hashCode() {
    return (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : getClass().hashCode());
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.entity

import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.Id
import jakarta.persistence.NamedAttributeNode
import jakarta.persistence.NamedEntityGraph
import jakarta.persistence.NamedSubgraph
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import org.hibernate.proxy.HibernateProxy
import java.util.*

@Entity
@Table(name = "user_identity")
@NamedEntityGraph(
  name = "User.withAuthorities",
  attributeNodes = [NamedAttributeNode(value = "authorities", subgraph = "auth-subgraph")],
  subgraphs = [
    NamedSubgraph(
      name = "auth-subgraph",
      attributeNodes = [NamedAttributeNode("authority")]
    )
  ]
)
class User(

  @Id
  @Column(name = "id", length = 36, nullable = false, updatable = false)
  var id: String,

  @Column(name = "username", length = 50, nullable = false, unique = true)
  var username: String,

  @Column(name = "password", length = 100, nullable = false)
  var password: String,

  @Column(name = "email", length = 100, nullable = false, unique = true)
  var email: String,

  @Column(name = "first_name", length = 50)
  var firstName: String? = null,

  @Column(name = "last_name", length = 50)
  var lastName: String? = null,

  @Column(name = "enabled", nullable = false)
  var enabled: Boolean = true

) : BaseEntity() {

  @OneToMany(
    mappedBy = "user",
    fetch = FetchType.LAZY,
    cascade = [CascadeType.ALL],
    orphanRemoval = true
  )
  var authorities: MutableSet&#x3C;UserAuthorityMapping> = HashSet()

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is User) return false
    val objClass = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: other::class.java
    val thisClass = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: this::class.java
    if (thisClass != objClass) return false
    return id == other.id
  }

  override fun hashCode(): Int =
    ((this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: javaClass.hashCode())
}
</code></pre>
<p>UserAuthorityMapping</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.proxy.HibernateProxy;

import java.util.Objects;

@Entity
@Table(name = "user_authority_mapping")
@IdClass(UserAuthorityMappingId.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserAuthorityMapping extends BaseEntity {

  @Id
  @Column(name = "user_id", length = 36, nullable = false)
  private String userId;

  @Id
  @Column(name = "authority_id", length = 36, nullable = false)
  private String authorityId;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id", insertable = false, updatable = false)
  private User user;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "authority_id", insertable = false, updatable = false)
  private Authority authority;

  @Override
  public final boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof UserAuthorityMapping other)) return false;
    Class&#x3C;?> objCls = (obj instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : obj.getClass());
    Class&#x3C;?> thisCls = (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass()
      : this.getClass());
    if (!thisCls.equals(objCls)) return false;
    return userId != null &#x26;&#x26; userId.equals(other.userId)
      &#x26;&#x26; authorityId != null &#x26;&#x26; authorityId.equals(other.authorityId);
  }

  @Override
  public final int hashCode() {
    return (this instanceof HibernateProxy hp
      ? hp.getHibernateLazyInitializer().getPersistentClass().hashCode()
      : Objects.hash(userId, authorityId));
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.entity

import jakarta.persistence.*
import org.hibernate.proxy.HibernateProxy
import java.util.*

@Entity
@Table(name = "user_authority_mapping")
@IdClass(UserAuthorityMappingId::class)
data class UserAuthorityMapping(

  @Id
  @Column(name = "user_id", length = 36, nullable = false)
  var userId: String = "",

  @Id
  @Column(name = "authority_id", length = 36, nullable = false)
  var authorityId: String = "",

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id", insertable = false, updatable = false)
  var user: User? = null,

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "authority_id", insertable = false, updatable = false)
  var authority: Authority? = null

) : BaseEntity() {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is UserAuthorityMapping) return false

    val objCls = (other as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: other::class.java
    val thisCls = (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass
      ?: this::class.java

    if (thisCls != objCls) return false
    return userId == other.userId &#x26;&#x26; authorityId == other.authorityId
  }

  override fun hashCode(): Int {
    return (this as? HibernateProxy)?.hibernateLazyInitializer?.persistentClass?.hashCode()
      ?: Objects.hash(userId, authorityId)
  }
}
</code></pre>
<p>UserAuthorityMappingId</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.entity;

import java.io.Serializable;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserAuthorityMappingId implements Serializable {

  private String userId;
  private String authorityId;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserAuthorityMappingId that = (UserAuthorityMappingId) o;
    return Objects.equals(userId, that.userId) &#x26;&#x26;
      Objects.equals(authorityId, that.authorityId);
  }

  @Override
  public int hashCode() {
    return Objects.hash(userId, authorityId);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.entity

import java.io.Serializable
import java.util.Objects

data class UserAuthorityMappingId(
  var userId: String = "",
  var authorityId: String = ""
) : Serializable {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || this::class != other::class) return false
    other as UserAuthorityMappingId
    return userId == other.userId &#x26;&#x26; authorityId == other.authorityId
  }

  override fun hashCode(): Int {
    return Objects.hash(userId, authorityId)
  }
}
</code></pre>
<p>UserRepository</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.repository;

import io.github.susimsek.springbootjweauthjpademo.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository&#x3C;User, String> {

  @EntityGraph("User.withAuthorities")
  Optional&#x3C;User> findOneWithAuthoritiesByUsername(String username);
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.repository

import io.github.susimsek.springbootjweauthjpademo.entity.User
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.Optional

@Repository
interface UserRepository : JpaRepository&#x3C;User, String> {

  @EntityGraph("User.withAuthorities")
  fun findOneWithAuthoritiesByUsername(username: String): Optional&#x3C;User>
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Secure JWE Token Utilities</h2>
<p>In this section, we define the core utility classes and constants needed to generate, encrypt, and resolve JSON Web Encryption (JWE) tokens in your Spring Boot application, integrate auditing, and implement a JPA-based user details service:</p>
<ul>
<li>AuthoritiesConstants: Centralize role names with the <code>ROLE_</code> prefix.</li>
<li>CookieBearerTokenResolver: Resolve bearer tokens from Authorization headers or HTTP cookies.</li>
<li>CookieUtils: Create HTTP-only, secure cookies for access tokens.</li>
<li>JweUtil: Sign (JWS) and encrypt (JWE) JWTs using RSA keys and Nimbus.</li>
<li>KeyUtils: Build RSA JWKs from PEM‐encoded key material.</li>
<li>SecurityUtils: Extract the current user’s login from the security context.</li>
<li>SpringSecurityAuditorAware: Implement <code>AuditorAware</code> to provide the current user for auditing.</li>
<li>DomainUserDetailsService: JPA-based <code>UserDetailsService</code> loading user and authorities for authentication.</li>
</ul>
<p>These utilities form the foundation for a stateless, JWE‐based authentication flow in Spring Security.</p>
<p>AuthoritiesConstants</p>
<h3>Java</h3>
<pre><code class="language-java">
package io.github.susimsek.springbootjweauthjpademo.security;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {
    public static final String ADMIN = "ROLE_ADMIN";
    public static final String USER = "ROLE_USER";
    public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">
package io.github.susimsek.springbootjweauthjpademo.security

object AuthoritiesConstants {
  const val ADMIN = "ROLE_ADMIN"
  const val USER = "ROLE_USER"
  const val ANONYMOUS = "ROLE_ANONYMOUS"
}
</code></pre>
<p>CookieBearerTokenResolver</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.springframework.http.HttpMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Setter
public class CookieBearerTokenResolver implements BearerTokenResolver {
    private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
    private static final Pattern AUTHORIZATION_PATTERN =
        Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);

    private boolean allowFormEncodedBodyParameter = false;
    private boolean allowUriQueryParameter       = false;
    private boolean allowCookie                  = true;
    private String  bearerTokenHeaderName       = "Authorization";
    private String  cookieName                  = "accessToken";

    @Override
    public String resolve(HttpServletRequest request) {
        String headerToken = resolveFromAuthorizationHeader(request);
        String queryToken  = resolveAccessTokenFromQueryString(request);
        String bodyToken   = resolveAccessTokenFromBody(request);
        String cookieToken = (headerToken == null &#x26;&#x26; queryToken == null &#x26;&#x26; bodyToken == null)
            ? resolveFromCookie(request)
            : null;

        return resolveToken(headerToken, queryToken, bodyToken, cookieToken);
    }

    private String resolveFromAuthorizationHeader(HttpServletRequest request) {
        String authorization = request.getHeader(this.bearerTokenHeaderName);
        if (!StringUtils.hasText(authorization) || !authorization.toLowerCase().startsWith("bearer")) {
            return null;
        }
        Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization.trim());
        if (!matcher.matches()) {
            BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
            throw new OAuth2AuthenticationException(error);
        }
        return matcher.group("token");
    }

    private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
        if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.getMethod())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveAccessTokenFromBody(HttpServletRequest request) {
        if (allowFormEncodedBodyParameter
            &#x26;&#x26; HttpMethod.POST.matches(request.getMethod())
            &#x26;&#x26; "application/x-www-form-urlencoded".equals(request.getContentType())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveFromCookie(HttpServletRequest request) {
        if (!allowCookie || request.getCookies() == null) {
            return null;
        }
        for (Cookie cookie : request.getCookies()) {
            if (cookieName.equals(cookie.getName())) {
                String val = cookie.getValue();
                return StringUtils.hasText(val) ? val : null;
            }
        }
        return null;
    }

    private String resolveToken(String... tokens) {
        String found = null;
        for (String token : tokens) {
            if (token == null) continue;
            if (found != null) {
                BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
                throw new OAuth2AuthenticationException(error);
            }
            found = token;
        }
        if (found != null &#x26;&#x26; found.isBlank()) {
            BearerTokenError error = BearerTokenErrors.invalidRequest("The requested token parameter is an empty string");
            throw new OAuth2AuthenticationException(error);
        }
        return found;
    }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpMethod
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.server.resource.BearerTokenError
import org.springframework.security.oauth2.server.resource.BearerTokenErrors
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.util.StringUtils
import java.util.regex.Pattern

class CookieBearerTokenResolver {
    var allowFormEncodedBodyParameter: Boolean = false
    var allowUriQueryParameter: Boolean = false
    var allowCookie: Boolean = true
    var bearerTokenHeaderName: String = "Authorization"
    var cookieName: String = "accessToken"

    companion object {
        private const val ACCESS_TOKEN_PARAMETER_NAME = "access_token"
        private val AUTHORIZATION_PATTERN =
            Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE)
    }

    fun resolve(request: HttpServletRequest): String? {
        val header = resolveFromAuthorizationHeader(request)
        val query  = if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.method))
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val body   = if (allowFormEncodedBodyParameter &#x26;&#x26; HttpMethod.POST.matches(request.method)
            &#x26;&#x26; request.contentType == "application/x-www-form-urlencoded")
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val cookie = if (header == null &#x26;&#x26; query == null &#x26;&#x26; body == null)
            resolveFromCookie(request)
        else null
      return listOf(header, query, body, cookie).filterNotNull().let {
            if (it.size > 1) throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Found multiple bearer tokens"))
            it.firstOrNull()?: null
        }
    }

    private fun resolveFromAuthorizationHeader(request: HttpServletRequest): String? {
        val auth = request.getHeader(bearerTokenHeaderName) ?: return null
        if (!auth.startsWith("Bearer ", true)) return null
        val matcher = AUTHORIZATION_PATTERN.matcher(auth.trim())
        return if (matcher.matches()) matcher.group("token")
        else throw OAuth2AuthenticationException(BearerTokenErrors.invalidToken("Malformed token"))
    }

    private fun resolveFromCookie(request: HttpServletRequest): String? {
        if (!allowCookie) return null
        return request.cookies?.firstOrNull { it.name == cookieName }?.value?.takeIf { it.isNotBlank() }
    }

    private fun resolveToken(tokens: Array&#x3C;String>): String? {
        return tokens.filter { it.isNotBlank() }.let {
            when {
                it.isEmpty() -> null
                it.size > 1   -> throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Multiple tokens"))
                else          -> it[0]
            }
        }
    }
</code></pre>
<p>CookieUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import lombok.experimental.UtilityClass;
import org.springframework.http.ResponseCookie;

@UtilityClass
public class CookieUtils {

    private static final String COOKIE_NAME = "accessToken";

    public ResponseCookie createAccessTokenCookie(TokenDTO tokenDto) {
        return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(tokenDto.accessTokenExpiresIn())
            .sameSite("Strict")
            .build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import org.springframework.http.ResponseCookie

object CookieUtils {

  private const val COOKIE_NAME = "accessToken"

  fun createAccessTokenCookie(tokenDto: TokenDTO): ResponseCookie {
    return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(tokenDto.accessTokenExpiresIn())
      .sameSite("Strict")
      .build()
  }
}
</code></pre>
<p>JweUtil</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.jwk.RSAKey;
import io.github.susimsek.springbootjweauthjpademo.config.JwtProperties;
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;

import static io.github.susimsek.springbootjweauthjpademo.security.SecurityUtils.AUTHORITIES_KEY;

@Component
@RequiredArgsConstructor
public class JweUtil {

    private final JwtEncoder jwtEncoder;
    private final RSAKey signingKey;
    private final RSAKey encryptionKey;
    private final JwtProperties props;

    public TokenDTO generateToken(Authentication authentication) throws JOSEException {
        String subject = authentication.getName();
        List&#x3C;String> roles = authentication.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        Instant now = Instant.now();
        long expiresIn = props.getExpirationDuration().getSeconds();
        Instant exp = now.plusSeconds(expiresIn);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer(props.getIssuer())
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build();

        JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.getKeyID())
            .build();

        String jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .getTokenValue();

        JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.getKeyID())
            .build();

        JWEObject jweObject = new JWEObject(jweHeader, new Payload(jws));
        jweObject.encrypt(new RSAEncrypter(encryptionKey.toRSAPublicKey()));
        String token = jweObject.serialize();

        return new TokenDTO(token, "Bearer", expiresIn);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JOSEException
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.RSAKey
import io.github.susimsek.springbootjweauthjpademo.config.JwtProperties
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import lombok.RequiredArgsConstructor
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
import org.springframework.security.oauth2.jwt.JwtClaimsSet
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.JwtEncoderParameters
import org.springframework.stereotype.Component

import java.time.Instant

@Component
@RequiredArgsConstructor
class JweUtil(
    private val jwtEncoder: JwtEncoder,
    private val signingKey: RSAKey,
    private val encryptionKey: RSAKey,
    private val props: JwtProperties
) {
    fun generateToken(authentication: Authentication): TokenDTO {
        val subject = authentication.name
        val roles = authentication.authorities.map { it.authority }

        val now = Instant.now()
        val expiresIn = props.expirationDuration.seconds
        val exp = now.plusSeconds(expiresIn)

        val claims = JwtClaimsSet.builder()
            .issuer(props.issuer)
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build()

        val jwsHeader = org.springframework.security.oauth2.jwt.JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.keyID)
            .build()

        val jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .tokenValue

        val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.keyID)
            .build()

        val jweObject = JWEObject(jweHeader, Payload(jws))
        jweObject.encrypt(RSAEncrypter(encryptionKey.toRSAPublicKey()))
        val token = jweObject.serialize()

        return TokenDTO(token, "Bearer", expiresIn)
    }
}
</code></pre>
<p>KeyUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.experimental.UtilityClass;

import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@UtilityClass
public class KeyUtils {
    public RSAKey buildRsaKey(
            String pubPem,
            String privPem,
            String kid,
            boolean forSign
    ) throws Exception {
        String pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");
        String privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");

        byte[] decodedPub = Base64.getDecoder().decode(pubContent);
        byte[] decodedPriv = Base64.getDecoder().decode(privContent);

        KeyFactory kf = KeyFactory.getInstance("RSA");
        RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(decodedPub));
        RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedPriv));

        RSAKey.Builder builder = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid);

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE);
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION);
        }

        return builder.build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.RSAKey
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

object KeyUtils {
    @JvmStatic
    @Throws(Exception::class)
    fun buildRsaKey(
        pubPem: String,
        privPem: String,
        kid: String,
        forSign: Boolean
    ): RSAKey {
        val pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replace(Regex("\\s"), "")
        val privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace(Regex("\\s"), "")

        val decodedPub = Base64.getDecoder().decode(pubContent)
        val decodedPriv = Base64.getDecoder().decode(privContent)

        val kf = KeyFactory.getInstance("RSA")
        val publicKey = kf.generatePublic(X509EncodedKeySpec(decodedPub)) as RSAPublicKey
        val privateKey = kf.generatePrivate(PKCS8EncodedKeySpec(decodedPriv)) as RSAPrivateKey

        val builder = RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid)

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE)
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION)
        }

        return builder.build()
    }
}
</code></pre>
<p>SecurityUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.Optional;

@UtilityClass
public class SecurityUtils {

    public static final String AUTHORITIES_KEY = "auth";

    public Optional&#x3C;String> getCurrentUserLogin() {
        var ctx = SecurityContextHolder.getContext();
        return Optional.ofNullable(extractPrincipal(ctx.getAuthentication()));
    }

    private String extractPrincipal(Authentication authentication) {
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getSubject();
        }
        if (principal instanceof UserDetails ud) {
            return ud.getUsername();
        }
        if (principal instanceof String username) {
            return username;
        }
        return null;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.jwt.Jwt
import java.util.Optional

object SecurityUtils {
  const val AUTHORITIES_KEY = "auth"

  fun getCurrentUserLogin(): Optional&#x3C;String> {
    val authentication: Authentication? = SecurityContextHolder.getContext().authentication
    return Optional.ofNullable(extractPrincipal(authentication))
  }

  private fun extractPrincipal(authentication: Authentication?): String? {
    return when (val principal = authentication?.principal) {
      is Jwt -> principal.subject
      is UserDetails -> principal.username
      is String -> principal
      else -> null
    }
  }
}
</code></pre>
<p>DomainUserDetailsService</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import io.github.susimsek.springbootjweauthjpademo.entity.User;
import io.github.susimsek.springbootjweauthjpademo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class DomainUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findOneWithAuthoritiesByUsername(username)
      .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

    List&#x3C;GrantedAuthority> authorities = user.getAuthorities().stream()
      .map(mapping -> mapping.getAuthority().getName())
      .map(SimpleGrantedAuthority::new)
      .collect(Collectors.toList());

    return new org.springframework.security.core.userdetails.User(
      user.getUsername(),
      user.getPassword(),
      user.isEnabled(),  // enabled
      true,              // accountNonExpired
      true,              // credentialsNonExpired
      true,              // accountNonLocked
      authorities
    );
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import io.github.susimsek.springbootjweauthjpademo.repository.UserRepository
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import java.util.stream.Collectors

class DomainUserDetailsService(
  private val userRepository: UserRepository
) : UserDetailsService {
  override fun loadUserByUsername(username: String): UserDetails {
    val user = userRepository.findOneWithAuthoritiesByUsername(username)
      .orElseThrow { UsernameNotFoundException("User not found with username: $username") }

    val authorities: List&#x3C;GrantedAuthority> = user.authorities.stream()
      .map { SimpleGrantedAuthority(it.authority.name) }
      .collect(Collectors.toList())

    return org.springframework.security.core.userdetails.User(
      user.username,
      user.password,
      user.enabled,
      true,
      true,
      true,
      authorities
    )
  }
}
</code></pre>
<p>SpringSecurityAuditorAware</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.security;

import org.springframework.data.domain.AuditorAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class SpringSecurityAuditorAware implements AuditorAware&#x3C;String> {

  @Override
  @NonNull
  public Optional&#x3C;String> getCurrentAuditor() {
    return Optional.of(SecurityUtils.getCurrentUserLogin().orElse("system"));
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.security

import org.springframework.data.domain.AuditorAware
import org.springframework.lang.NonNull
import org.springframework.stereotype.Component

@Component
class SpringSecurityAuditorAware : AuditorAware&#x3C;String> {
    @NonNull
    override fun getCurrentAuditor(): Optional&#x3C;String> =
        Optional.of(SecurityUtils.getCurrentUserLogin().orElse("system"))
}
</code></pre>
<hr>
<h2>🛠️ Step 6: Authentication &#x26; Protected Endpoints</h2>
<p>In this section, we define the REST controllers and DTOs necessary for:</p>
<ul>
<li>AuthController: Authenticate users, issue JWE tokens, and set secure cookies.</li>
<li>HelloController: Expose protected resource endpoints for authenticated users and admin-specific paths.</li>
<li>LoginRequestDTO: Model the login request payload (username/password).</li>
<li>TokenDTO: Model the authentication response including token and expiration.</li>
</ul>
<p>These components complete the stateless authentication flow by handling login, token issuance, cookie management, and resource protection.</p>
<p>In this section, we expose REST controllers and DTOs to handle user authentication, token issuance, and protected resource access.</p>
<p>AuthController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.controller;

import io.github.susimsek.springbootjweauthjpademo.dto.LoginRequestDTO;
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO;
import io.github.susimsek.springbootjweauthjpademo.security.CookieUtils;
import io.github.susimsek.springbootjweauthjpademo.security.JweUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JweUtil jweUtil;

    @PostMapping("/login")
    public ResponseEntity&#x3C;TokenDTO> login(@RequestBody LoginRequestDTO loginRequest) throws Exception {
        // Authenticate user
        var authToken = new UsernamePasswordAuthenticationToken(
            loginRequest.username(), loginRequest.password()
        );
        Authentication auth = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);

        // Generate JWE token and cookie
        TokenDTO tokenDto = jweUtil.generateToken(auth);
        ResponseCookie cookie = CookieUtils.createAccessTokenCookie(tokenDto);

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.controller

import io.github.susimsek.springbootjweauthjpademo.dto.LoginRequestDTO
import io.github.susimsek.springbootjweauthjpademo.dto.TokenDTO
import io.github.susimsek.springbootjweauthjpademo.security.CookieUtils
import io.github.susimsek.springbootjweauthjpademo.security.JweUtil
import lombok.RequiredArgsConstructor
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import org.springframework.http.ResponseEntity
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
class AuthController(
    private val authenticationManager: AuthenticationManager,
    private val jweUtil: JweUtil
) {

    @PostMapping("/login")
    fun login(@RequestBody loginRequest: LoginRequestDTO): ResponseEntity&#x3C;TokenDTO> {
        val authToken = UsernamePasswordAuthenticationToken(
            loginRequest.username, loginRequest.password
        )
        val auth: Authentication = authenticationManager.authenticate(authToken)
        SecurityContextHolder.getContext().authentication = auth

        val tokenDto: TokenDTO = jweUtil.generateToken(auth)
        val cookie: ResponseCookie = CookieUtils.createAccessTokenCookie(tokenDto)

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto)
    }
}
</code></pre>
<p>HelloController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

import static io.github.susimsek.springbootjweauthjpademo.security.SecurityUtils.AUTHORITIES_KEY;

@RestController
@RequestMapping("/api/hello")
public class HelloController {

    @GetMapping
    public String helloAll(@AuthenticationPrincipal Jwt jwt) {
        String user = jwt.getSubject();
        var roles = jwt.getClaimAsStringList(AUTHORITIES_KEY);
        return "Hello, " + user + "! Your roles: " + roles;
    }

    @GetMapping("/admin")
    public String helloAdmin(@AuthenticationPrincipal Jwt jwt) {
        return "Hello Admin, " + jwt.getSubject() + "!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.controller

import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*

import io.github.susimsek.springbootjweauthjpademo.security.SecurityUtils.AUTHORITIES_KEY

@RestController
@RequestMapping("/api/hello")
class HelloController {

    @GetMapping
    fun helloAll(@AuthenticationPrincipal jwt: Jwt): String {
        val user = jwt.subject
        val roles = jwt.getClaimAsStringList(AUTHORITIES_KEY)
        return "Hello, \$user! Your roles: \$roles"
    }

    @GetMapping("/admin")
    fun helloAdmin(@AuthenticationPrincipal jwt: Jwt): String {
        return "Hello Admin, \${jwt.subject}!"
    }
}
</code></pre>
<p>LoginRequestDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.dto;

public record LoginRequestDTO(
    String username,
    String password
) { }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.dto

data class LoginRequestDTO(
    val username: String,
    val password: String
)
</code></pre>
<p>TokenDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjweauthjpademo.dto;

public record TokenDTO(
    String accessToken,
    String tokenType,
    long accessTokenExpiresIn
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjweauthjpademo.dto

import kotlin.Long

data class TokenDTO(
    val accessToken: String,
    val tokenType: String,
    val accessTokenExpiresIn: Long
)
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Test Endpoints</h2>
<p>In this section, we clarify Test Endpoints and summarize the key points you will apply in implementation.</p>
<h3>Admin Flow</h3>
<p>Login as admin and capture the JWE token from the <code>Set-Cookie</code> header:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"adminpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
<li>Response body:</li>
</ul>
<pre><code class="language-json">{
  "accessToken": "&#x3C;jwe-token>",
  "tokenType": "Bearer",
  "accessTokenExpiresIn": 3600
}
</code></pre>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header instead:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Access admin-only endpoint:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
</code></pre>
<h3>User Flow</h3>
<p>Login as user and capture JWE token from cookie:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"userpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
</ul>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Attempt admin endpoint (should be 403 Forbidden):</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
# HTTP/1.1 403 Forbidden
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot JWE Authentication with JPA implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Security]]></category>
      <category><![CDATA[JWT]]></category>
      <category><![CDATA[JWE]]></category>
      <category><![CDATA[JPA]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-jwe-auth-with-jpa-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot LDAP and JWE Authentication]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-ldap-jwe-authentication</link>
      <description><![CDATA[Learn how to use LDAP authentication with JWE-encrypted JWTs to secure your Spring Boot APIs with both external directory integration and token confidentiality.]]></description>
      <pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-05-11T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-ldap-jwe-authentication</guid>
      <content:encoded><![CDATA[<p>Spring Boot lets you combine LDAP authentication with JWE-encrypted JWTs to secure your Spring Boot APIs with both external directory integration and token confidentiality.</p>
<hr>
<h2>🌟 Why Combine LDAP &#x26; JWE?</h2>
<p>In this section, we clarify Why Combine LDAP &#x26; JWE? and summarize the key points you will apply in implementation.</p>
<ul>
<li>External Directory: Centralize user management with LDAP (e.g., corporate or embedded directory).</li>
<li>Integrity &#x26; Confidentiality: Sign tokens (JWS) and encrypt payloads (JWE) for secure claims transport.</li>
<li>Standards-based: Leverage JOSE (JWS &#x26; JWE) and Spring Security’s OAuth2 Resource Server.</li>
<li>Role-based Access: Map LDAP groups to <code>ROLE_USER</code> / <code>ROLE_ADMIN</code> automatically.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 17 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🗄️ LDAP (Embedded via Spring Boot or external)</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include these in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.springframework.security&#x3C;/groupId>
  &#x3C;artifactId>spring-security-oauth2-resource-server&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.springframework.security&#x3C;/groupId>
  &#x3C;artifactId>spring-security-ldap&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>com.unboundid&#x3C;/groupId>
  &#x3C;artifactId>unboundid-ldapsdk&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.projectlombok&#x3C;/groupId>
  &#x3C;artifactId>lombok&#x3C;/artifactId>
  &#x3C;optional>true&#x3C;/optional>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-ldap'
implementation 'com.unboundid:unboundid-ldapsdk'
compileOnly 'org.projectlombok:lombok'
</code></pre>
<hr>
<h2>🛠️ Step 2: LDAP and Keys Configuration</h2>
<p>Define your embedded LDAP directory, user/group structure, and RSA key properties in your <code>application.yml</code> and <code>schema.ldif</code>:</p>
<p>application.yml</p>
<pre><code class="language-yaml">spring:
  ldap:
    embedded:
      ldif: classpath:schema.ldif
      base-dn: dc=suaybsimsek,dc=com
      port: 8389
    urls: ldap://localhost:8389
    base: dc=suaybsimsek,dc=com
security:
  jwt:
    issuer: demo-issuer
    expiration-duration: 3600s
    signing:
      key-id: signing-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjZEcz5itWkDGOSqZdB5P
        JE0ccOCckskp0hN3kQbT1qnv+9/k66jlWgVi4HSTppwdNF/Ylu5u541Qj+Okyg+u
        8o2PvKo5CfgvTDsFLTrZHUXU6hCSGatLAQoeN6lT8wzov2r4DFecXrIqcO6SvMB5
        ecPqsfiTi4trsNKgJ4cWS6gILH62ISd1ipUadfpnUzDMO1OulV0CJNV6bcBk7Es9
        RW6AHfg9j8osSanpwvRM4MJkB0SRxYUnrN9faGpkBZISZJ8TShhaTHEGfSNgKe5y
        8iu+AMGGZu8DYczVmqS3Ske1fq6y5HEGCma7Mo019GmwKeHBo1obuET6cZRygj4y
        twIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNkRzPmK1aQMY5
        Kpl0Hk8kTRxw4JySySnSE3eRBtPWqe/73+TrqOVaBWLgdJOmnB00X9iW7m7njVCP
        46TKD67yjY+8qjkJ+C9MOwUtOtkdRdTqEJIZq0sBCh43qVPzDOi/avgMV5xesipw
        7pK8wHl5w+qx+JOLi2uw0qAnhxZLqAgsfrYhJ3WKlRp1+mdTMMw7U66VXQIk1Xpt
        wGTsSz1FboAd+D2PyixJqenC9EzgwmQHRJHFhSes319oamQFkhJknxNKGFpMcQZ9
        I2Ap7nLyK74AwYZm7wNhzNWapLdKR7V+rrLkcQYKZrsyjTX0abAp4cGjWhu4RPpx
        lHKCPjK3AgMBAAECggEABMo/sNCIMREXA/EqjxDFecN4LmOTOK5A0YBiH9Cc01sd
        qSaavtSQqhqbjV+0bUNEA7UohXbc3s3bx3qa5VFhiIh8sBQMaQdyRkCK/MxMc16d
        BSx5XQ/8MjVO73A1zHgH2g47BWPjgRrDL94KrgNPOO0FoG76QxL9GlcOHzhFa1rf
        xbb2auLBtflKy+5TZNnB/sbFC9ISpWywzfblD2Fsvxupl0vNWtA0Y2rPbKMZRnY+
        V6NC38yxW3HHAdWQRYLGOitM59I0HBgnjQKzTBNIsjPnJA2BSZvMBtQ1e3RQV0qE
        8VVoktZ8A0KDUxqdKWYlGYAY5hIw1y3UUsPRUbS3fQKBgQDAZehu+Kb5Sw+h00Qu
        vViNHDvi0PwmhQrHVpgjrppCnWsTk6FJFm+EBLY+87usXuRCnfZ1semb6P+yjLh2
        bJ9IXIrFAztcyE+7eZfn0H7mHA7E2ICKTjNdKbjy8kHViHpgdXREnAOrFTTk/BqW
        dq+weG2OuxYczWsY4XliWaVyiwKBgQC8XYExpSmoyI37o+jr9405k30tbGUw4X+/
        xYOvSUuafI8IOSLfKTpsvkMN3hxpJO6apIRAjOALbotKUA5FrHQPenuBWOr3mBJT
        euLiaWphqU8YN5dbNd+JhC/Jh5DLhY5FpB2Fv2MKSoF7+onpjDy9pEU5aReeBB0v
        ekUw4nWiBQKBgHxyrnjxP1frFG5xMB4nfZqw04+v7BmiXsl3mqsh6kgCeNtN17pl
        17YGMjfgAdnJ+02XzW5tqRSfDp3YZgy7z//HVD+BCqnGK8SxLu/ULfD73xW2kNZl
        JNYzAZ2r06eiQr4X2x/x5nGIIxGmfDAtDxFPpFX5b6ErwgVy+sgCAoFnAoGAWSpu
        EMEdQk+FnnwNsz2g9YNSuyDXmdb08SOfXWd1yXBzCLJ7RmYuyPEbrsHYcxFPfZap
        ICFPoTm35/qTdvnWiskxE56yw3eSHUBLjF/YQtixn0YZeMy8v0z6jgyFR0I2gdLZ
        QsnBKUrxlm2XwR1oV2Eef7m2u085PZNEk4pvor0CgYA6n060r48/CgRVslAeGder
        fZ3n+2ru8q2UVB38evepjLifn5+tKdFzZ7/NckXMC0NOVzm74qG46VFLMw5TtyT4
        /hRvGSYxA6dMjXy+tcrAns9Isrz2PnYFntlbhU6hw0um809tFunbvITfyeOjAsDW
        stC4thnhzXXT1Y3RfFtYEg==
        -----END PRIVATE KEY-----
    encryption:
      key-id: encryption-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtU7Vu4tML8qg9BISaUH0
        BTU0+qkHJS3TUl3R0hEPttZJGf8EgT7bpWqRjdhMC2SYt2ifNF82EgMo/eva1rLn
        wYtCsxjJ7oB4U2m16/JKHtphmMUGhSLgbp+Y8pGAQ+P6u4HLlI0qbL92Syb6QCTr
        nYIlzZ2uUAvUQg/SbuAvdS4kdauZtpMNbhryusMVvILVBC5yUhfLJAkjbU3qWo/n
        2NKPr4kjwwFh6FaAf0HLsXCFmJbPPok6WMZeDqPORPZCt5gU2t04fS7s/2SSbhQ/
        i+7yyT4pl44cUlNLLN3Qo73sai5H90q7Dvmk3M5/YMJB0Ueyb705LHyOO5xA618s
        qQIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1TtW7i0wvyqD0
        EhJpQfQFNTT6qQclLdNSXdHSEQ+21kkZ/wSBPtulapGN2EwLZJi3aJ80XzYSAyj9
        69rWsufBi0KzGMnugHhTabXr8koe2mGYxQaFIuBun5jykYBD4/q7gcuUjSpsv3ZL
        JvpAJOudgiXNna5QC9RCD9Ju4C91LiR1q5m2kw1uGvK6wxW8gtUELnJSF8skCSNt
        Tepaj+fY0o+viSPDAWHoVoB/QcuxcIWYls8+iTpYxl4Oo85E9kK3mBTa3Th9Luz/
        ZJJuFD+L7vLJPimXjhxSU0ss3dCjvexqLkf3SrsO+aTczn9gwkHRR7JvvTksfI47
        nEDrXyypAgMBAAECggEAREnJHrY8n9OGA+e6n4KD0mJT7gUz98Dm0yMbIC/k50yW
        hAAVRkjSmd8lq3NIURI2ov342NTznJ0sF1d6OVtxBujY2nP/uqEQsPoj1xaO7Ef8
        cnnjFsooFgJurQ44bVm02mLstqrky8jhWTT5FKfTRqP6cRNu0B9kdu1WqQQvW8a/
        C8xD8XUFzzJFQsEstJ076NjlZNH3EeqFiLxyVdV7tPEJRnOe2V2BAAgda3ByA6Jc
        Nd4xYHTVr5rZ+KES9gaAgb86+S7hNigsxz5Gp58NimLEyz1Qd+d9QCgPRgKOV5Uq
        0ci6c28ZrxW9wa/2CGCpoow1V7hRy1fZ24g2Tz91cQKBgQDbSdP/FQYuem0pUf8u
        mA/niEC8Jz30FW3ix5aqbQ8YRudK48Xy8F7AWnlS0wT2k/Xaeppn9YSPK4HlsNk2
        1gnpNfzdqXpJQtuxcPPeFNzUdXzBrsLXYqtlhOX/afrzqMhp73gkqoxXtqwD7BqN
        VfMcVXsWrEZQrRPBn/RUnnLdJwKBgQDTqUGupm93UlhoPgLXHGF/seQwPPHcU6lS
        SS9l512Ie21B+vZecgO4r77BXJwbY4rZVA5/m+wrsNmsT2f8yfdbs84cBDDWiKy5
        t2Um8zrus+BCIffxc9Fck9/htK33jLLcYwdSAm3a9pJxR9f/kAdredSTGBNJ3W/2
        rfh3cdNprwKBgDi7WfBFRSsjGzi0cPth9cNlubGzyVBrdtlT34PJ4Tzboxz53o1i
        aHEFNxwZYdBVKSbTzzyUBS5xCBMfdKK+LyQ5hmjmXq+zb1jxqvXKmfMRTixhhSDp
        8wO5pTM1/Omqwea+QGvj/5j1tnzxSVFFajbrWoPcH/jhPho6wqBducPDAoGBALOJ
        MEOavZXy7TaO0w3v7uvH0wzvxR/kfw1jMqc3l2j7ePOskmoOQAXaXO3bRjcdOlua
        Jyoq8islOZ4lRMlx7zWD0OKG035GNGzbmRtu2aA8R48RDSVr3jyu2gqznZULbXPv
        M/hmQxSmbhVUoW0PmJubnaqfk0zmXeBaNRXsIS3VAoGBALdsAtqR23PVeL6sYj2l
        dRNTDXpfvjFqJ5NThRO/4mT0CrPHQVj+Mz2bTc/Dxiwi8s8m3L+g23i3hodh/QMb
        Iz+UbcJZBGAKsFbPKGOtj6Bi07y/L11mcuNJzOWe61/JbVmJss0s+N/v3XucK/Ge
        CUaGOccsMO221v6JoBh9J3Hz
        -----END PRIVATE KEY-----
</code></pre>
<p>LDAP Schema (schema.ldif)</p>
<pre><code class="language-ldif"># 1) Root DN
 dn: dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: domain
 objectClass: extensibleObject
 dc: suaybsimsek

# 2) People OU
 dn: ou=people,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: organizationalUnit
 ou: people

# 3) Admin user
 dn: uid=admin,ou=people,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: person
 objectClass: inetOrgPerson
 cn: Admin User
 sn: User
 uid: admin
 userPassword: $2a$10$sva6wl8pmGKJE6NIWrxwcuJK1Jaa2I/LOI43iHVpbR4YB8KjGViiK

# 4) Normal user
 dn: uid=user,ou=people,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: person
 objectClass: inetOrgPerson
 cn: Normal User
 sn: User
 uid: user
 userPassword: $2a$10$5Py4PyteLuXEqnGpSigzfu0V55C7Hi7zX18lmh.J8Bpmft.h23voG

# 5) Groups OU
 dn: ou=groups,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: organizationalUnit
 ou: groups

# 6) USER group → maps to ROLE_USER
 dn: cn=USER,ou=groups,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: groupOfUniqueNames
 cn: USER
 uniqueMember: uid=user,ou=people,dc=suaybsimsek,dc=com
 uniqueMember: uid=admin,ou=people,dc=suaybsimsek,dc=com

# 7) ADMIN group → maps to ROLE_ADMIN
 dn: cn=ADMIN,ou=groups,dc=suaybsimsek,dc=com
 objectClass: top
 objectClass: groupOfUniqueNames
 cn: ADMIN
 uniqueMember: uid=admin,ou=people,dc=suaybsimsek,dc=com
</code></pre>
<p>This combined configuration ensures your application boots an embedded LDAP server with the defined schema, and loads RSA keys for JWE signing and encryption. You can now proceed to configure Spring Security to use LDAP for authentication and JWE for token issuance.</p>
<hr>
<h2>🛠️ Step 3: Security Configuration</h2>
<p>In this section, we define the beans and properties configure LDAP authentication, set up RSA keys, and enforce HTTP security filters for a JWE-based authentication:</p>
<ul>
<li>JwtProperties: Configures JWT issuer, expiration, and signing/encryption key pairs.</li>
<li>SecurityJwtConfig: Builds RSA JWKs, JWT encoder/decoder, authentication converter, and token resolver.</li>
<li>SecurityConfig: Defines embedded LDAP authentication, JWE resource server, and stateless security filter chain with route authorization.</li>
</ul>
<p>SecurityJwtConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.config;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.github.susimsek.springbootldapjwedemo.security.CookieBearerTokenResolver;
import io.github.susimsek.springbootldapjwedemo.security.KeyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;

import java.util.List;

import static io.github.susimsek.springbootldapjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Configuration
public class SecurityJwtConfig {

    private final JwtProperties props;
    public SecurityJwtConfig(JwtProperties props) { this.props = props; }

    @Bean
    public RSAKey signingKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getSigning().getPublicKey(),
            props.getSigning().getPrivateKey(),
            props.getSigning().getKeyId(),
            true
        );
    }

    @Bean
    public RSAKey encryptionKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getEncryption().getPublicKey(),
            props.getEncryption().getPrivateKey(),
            props.getEncryption().getKeyId(),
            false
        );
    }

    @Bean
    public JWKSource&#x3C;SecurityContext> jwkSource(RSAKey signingKey, RSAKey encryptionKey) {
        JWKSet jwkSet = new JWKSet(List.of(
            signingKey,
            encryptionKey
        ));
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        DefaultJWTProcessor&#x3C;SecurityContext> jwtProcessor = new DefaultJWTProcessor&#x3C;>();
        JWEDecryptionKeySelector&#x3C;SecurityContext> jweKeySelector =
            new JWEDecryptionKeySelector&#x3C;>(
                JWEAlgorithm.RSA_OAEP_256,
                EncryptionMethod.A128GCM,
                jwkSource
            );
        jwtProcessor.setJWEKeySelector(jweKeySelector);

        JWSVerificationKeySelector&#x3C;SecurityContext> jwsKeySelector =
            new JWSVerificationKeySelector&#x3C;>(
                JWSAlgorithm.RS256,
                jwkSource
            );
        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});

        return new NimbusJwtDecoder(jwtProcessor);
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthorityPrefix("");
        converter.setAuthoritiesClaimName(AUTHORITIES_KEY);

        JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
        authConverter.setJwtGrantedAuthoritiesConverter(converter);
        return authConverter;
    }

    @Bean
    public BearerTokenResolver bearerTokenResolver() {
        CookieBearerTokenResolver resolver = new CookieBearerTokenResolver();
        resolver.setAllowUriQueryParameter(false);
        resolver.setAllowFormEncodedBodyParameter(false);
        resolver.setAllowCookie(true);
        return resolver;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.config

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.JWSVerificationKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import io.github.susimsek.springbootldapjwedemo.security.CookieBearerTokenResolver
import io.github.susimsek.springbootldapjwedemo.security.KeyUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver

@Configuration
class SecurityJwtConfig(private val props: JwtProperties) {

    @Bean
    @Throws(Exception::class)
    fun signingKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.signing.publicKey,
            props.signing.privateKey,
            props.signing.keyId,
            true
        )

    @Bean
    @Throws(Exception::class)
    fun encryptionKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.encryption.publicKey,
            props.encryption.privateKey,
            props.encryption.keyId,
            false
        )

    @Bean
    fun jwkSource(signingKey: RSAKey, encryptionKey: RSAKey): JWKSource&#x3C;SecurityContext> {
        val jwkSet = JWKSet(listOf(signingKey, encryptionKey))
        return JWKSource { jwkSelector, context -> jwkSelector.select(jwkSet) }
    }

    @Bean
    fun jwtDecoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtDecoder {
        val jwtProcessor = DefaultJWTProcessor&#x3C;SecurityContext>()
        val jweKeySelector = JWEDecryptionKeySelector(
            JWEAlgorithm.RSA_OAEP_256,
            EncryptionMethod.A128GCM,
            jwkSource
        )
        jwtProcessor.jweKeySelector = jweKeySelector
        val jwsKeySelector = JWSVerificationKeySelector(
            JWSAlgorithm.RS256,
            jwkSource
        )
        jwtProcessor.jwsKeySelector = jwsKeySelector
        jwtProcessor.jwtClaimsSetVerifier = { _, _ -> }
        return NimbusJwtDecoder(jwtProcessor)
    }

    @Bean
    fun jwtEncoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtEncoder =
        NimbusJwtEncoder(jwkSource)

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val converter = JwtGrantedAuthoritiesConverter().apply {
            setAuthorityPrefix("")
            setAuthoritiesClaimName(AUTHORITIES_KEY)
        }
        return JwtAuthenticationConverter().apply {
            setJwtGrantedAuthoritiesConverter(converter)
        }
    }

    @Bean
    fun bearerTokenResolver(): BearerTokenResolver =
        CookieBearerTokenResolver().apply {
            setAllowUriQueryParameter(false)
            setAllowFormEncodedBodyParameter(false)
            setAllowCookie(true)
        }
}
</code></pre>
<p>SecurityConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.config;

import io.github.susimsek.springbootldapjwedemo.security.AuthoritiesConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.ldap.LdapPasswordComparisonAuthenticationManagerFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                 MvcRequestMatcher.Builder mvc) throws Exception {
    http
      .cors(withDefaults())
      .csrf(AbstractHttpConfigurer::disable)
      .authorizeHttpRequests(authz ->
        authz
          .requestMatchers(
            mvc.pattern("/webjars/**"),
            mvc.pattern("/css/**"),
            mvc.pattern("/js/**")
          ).permitAll()
          .requestMatchers(
            mvc.pattern("/*.ico"),
            mvc.pattern("/*.png"),
            mvc.pattern("/*.svg"),
            mvc.pattern("/*.webapp")
          ).permitAll()
          .requestMatchers("/actuator/**").permitAll()
          .requestMatchers(
            "/v3/api-docs/**",
            "/swagger-ui.html",
            "/swagger-ui/**"
          ).permitAll()
          .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
          .requestMatchers(mvc.pattern("/api/hello/admin"))
          .hasAuthority(AuthoritiesConstants.ADMIN)
          .anyRequest().authenticated()
      )
      .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .oauth2ResourceServer(oauth2 -> oauth2
        .jwt(withDefaults())
      );
    return http.build();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {
    DefaultLdapAuthoritiesPopulator populator =
      new DefaultLdapAuthoritiesPopulator(contextSource, "ou=groups");
    populator.setGroupSearchFilter("uniqueMember={0}");
    populator.setGroupRoleAttribute("cn");
    populator.setRolePrefix("ROLE_");
    return populator;
  }

  @Bean
  public AuthenticationManager authenticationManager(
    BaseLdapPathContextSource contextSource,
    PasswordEncoder passwordEncoder,
    LdapAuthoritiesPopulator authorities
  ) {
    LdapPasswordComparisonAuthenticationManagerFactory factory =
      new LdapPasswordComparisonAuthenticationManagerFactory(contextSource, passwordEncoder);
    factory.setUserDnPatterns("uid={0},ou=people");
    factory.setPasswordAttribute("userPassword");
    factory.setLdapAuthoritiesPopulator(authorities);
    return factory.createAuthenticationManager();
  }

  @Bean
  public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
    return new MvcRequestMatcher.Builder(introspector);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.config

import io.github.susimsek.springbootldapjwedemo.security.AuthoritiesConstants
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.ldap.core.support.BaseLdapPathContextSource
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.config.ldap.LdapPasswordComparisonAuthenticationManagerFactory
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
import org.springframework.security.config.Customizer.withDefaults

@Configuration
@EnableMethodSecurity(securedEnabled = true)
class SecurityConfig {

  @Bean
  fun securityFilterChain(
    http: HttpSecurity,
    mvc: MvcRequestMatcher.Builder
  ): SecurityFilterChain = http
    .cors(withDefaults())
    .csrf { it.disable() }
    .authorizeHttpRequests { authz ->
      authz
        .requestMatchers(
          mvc.pattern("/webjars/**"),
          mvc.pattern("/css/**"),
          mvc.pattern("/js/**")
        ).permitAll()
        .requestMatchers(
          mvc.pattern("/*.ico"),
          mvc.pattern("/*.png"),
          mvc.pattern("/*.svg"),
          mvc.pattern("/*.webapp")
        ).permitAll()
        .requestMatchers("/actuator/**").permitAll()
        .requestMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**")
        .permitAll()
        .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
        .requestMatchers(mvc.pattern("/api/hello/admin"))
        .hasAuthority(AuthoritiesConstants.ADMIN)
        .anyRequest().authenticated()
    }
    .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
    .oauth2ResourceServer { it.jwt(withDefaults()) }
    .build()

  @Bean
  fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

  @Bean
  fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopulator {
    return DefaultLdapAuthoritiesPopulator(contextSource, "ou=groups").apply {
      setGroupSearchFilter("uniqueMember={0}")
      setGroupRoleAttribute("cn")
      setRolePrefix("ROLE_")
    }
  }

  @Bean
  fun authenticationManager(
    contextSource: BaseLdapPathContextSource,
    passwordEncoder: PasswordEncoder,
    authorities: LdapAuthoritiesPopulator
  ): AuthenticationManager {
    return LdapPasswordComparisonAuthenticationManagerFactory(contextSource, passwordEncoder).apply {
      setUserDnPatterns("uid={0},ou=people")
      setPasswordAttribute("userPassword")
      setLdapAuthoritiesPopulator(authorities)
    }.createAuthenticationManager()
  }

  @Bean
  fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
    MvcRequestMatcher.Builder(introspector)
}
</code></pre>
<p>JwtProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
@Data
public class JwtProperties {

    @Data
    public static class Pair {
        private String publicKey;
        private String privateKey;
        private String keyId;
    }

    private Pair signing;
    private Pair encryption;
    private String issuer;
    private Duration expirationDuration;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
class JwtProperties {

  class Pair {
    lateinit var publicKey: String
    lateinit var privateKey: String
    lateinit var keyId: String
  }

  lateinit var signing: Pair
  lateinit var encryption: Pair
  lateinit var issuer: String
  lateinit var expirationDuration: Duration
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Secure JWE Token Utilities</h2>
<p>In this section, we define the core utility classes and constants needed to generate, encrypt, and resolve JSON Web Encryption (JWE) tokens in your Spring Boot application. These components work together to:</p>
<ul>
<li>AuthoritiesConstants: Centralize role names with the <code>ROLE_</code> prefix.</li>
<li>CookieBearerTokenResolver: Resolve bearer tokens from Authorization headers or HTTP cookies.</li>
<li>CookieUtils: Create HTTP-only, secure cookies for access tokens.</li>
<li>JweUtil: Sign (JWS) and encrypt (JWE) JWTs using RSA keys and Nimbus.</li>
<li>KeyUtils: Build RSA JWKs from PEM‐encoded key material.</li>
<li>SecurityUtils: Extract the current user’s login from the security context.</li>
</ul>
<p>These utilities form the foundation for a stateless, JWE‐based authentication flow in Spring Security.</p>
<p>AuthoritiesConstants</p>
<h3>Java</h3>
<pre><code class="language-java">
package io.github.susimsek.springbootldapjwedemo.security;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {
    public static final String ADMIN = "ROLE_ADMIN";
    public static final String USER = "ROLE_USER";
    public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">
package io.github.susimsek.springbootldapjwedemo.security

object AuthoritiesConstants {
  const val ADMIN = "ROLE_ADMIN"
  const val USER = "ROLE_USER"
  const val ANONYMOUS = "ROLE_ANONYMOUS"
}
</code></pre>
<p>CookieBearerTokenResolver</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.security;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.springframework.http.HttpMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Setter
public class CookieBearerTokenResolver implements BearerTokenResolver {
    private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
    private static final Pattern AUTHORIZATION_PATTERN =
        Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);

    private boolean allowFormEncodedBodyParameter = false;
    private boolean allowUriQueryParameter       = false;
    private boolean allowCookie                  = true;
    private String  bearerTokenHeaderName       = "Authorization";
    private String  cookieName                  = "accessToken";

    @Override
    public String resolve(HttpServletRequest request) {
        String headerToken = resolveFromAuthorizationHeader(request);
        String queryToken  = resolveAccessTokenFromQueryString(request);
        String bodyToken   = resolveAccessTokenFromBody(request);
        String cookieToken = (headerToken == null &#x26;&#x26; queryToken == null &#x26;&#x26; bodyToken == null)
            ? resolveFromCookie(request)
            : null;

        return resolveToken(headerToken, queryToken, bodyToken, cookieToken);
    }

    private String resolveFromAuthorizationHeader(HttpServletRequest request) {
        String authorization = request.getHeader(this.bearerTokenHeaderName);
        if (!StringUtils.hasText(authorization) || !authorization.toLowerCase().startsWith("bearer")) {
            return null;
        }
        Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization.trim());
        if (!matcher.matches()) {
            BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
            throw new OAuth2AuthenticationException(error);
        }
        return matcher.group("token");
    }

    private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
        if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.getMethod())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveAccessTokenFromBody(HttpServletRequest request) {
        if (allowFormEncodedBodyParameter
            &#x26;&#x26; HttpMethod.POST.matches(request.getMethod())
            &#x26;&#x26; "application/x-www-form-urlencoded".equals(request.getContentType())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveFromCookie(HttpServletRequest request) {
        if (!allowCookie || request.getCookies() == null) {
            return null;
        }
        for (Cookie cookie : request.getCookies()) {
            if (cookieName.equals(cookie.getName())) {
                String val = cookie.getValue();
                return StringUtils.hasText(val) ? val : null;
            }
        }
        return null;
    }

    private String resolveToken(String... tokens) {
        String found = null;
        for (String token : tokens) {
            if (token == null) continue;
            if (found != null) {
                BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
                throw new OAuth2AuthenticationException(error);
            }
            found = token;
        }
        if (found != null &#x26;&#x26; found.isBlank()) {
            BearerTokenError error = BearerTokenErrors.invalidRequest("The requested token parameter is an empty string");
            throw new OAuth2AuthenticationException(error);
        }
        return found;
    }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.security

import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpMethod
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.server.resource.BearerTokenError
import org.springframework.security.oauth2.server.resource.BearerTokenErrors
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.util.StringUtils
import java.util.regex.Pattern

class CookieBearerTokenResolver {
    var allowFormEncodedBodyParameter: Boolean = false
    var allowUriQueryParameter: Boolean = false
    var allowCookie: Boolean = true
    var bearerTokenHeaderName: String = "Authorization"
    var cookieName: String = "accessToken"

    companion object {
        private const val ACCESS_TOKEN_PARAMETER_NAME = "access_token"
        private val AUTHORIZATION_PATTERN =
            Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE)
    }

    fun resolve(request: HttpServletRequest): String? {
        val header = resolveFromAuthorizationHeader(request)
        val query  = if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.method))
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val body   = if (allowFormEncodedBodyParameter &#x26;&#x26; HttpMethod.POST.matches(request.method)
            &#x26;&#x26; request.contentType == "application/x-www-form-urlencoded")
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val cookie = if (header == null &#x26;&#x26; query == null &#x26;&#x26; body == null)
            resolveFromCookie(request)
        else null
      return listOf(header, query, body, cookie).filterNotNull().let {
            if (it.size > 1) throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Found multiple bearer tokens"))
            it.firstOrNull()?: null
        }
    }

    private fun resolveFromAuthorizationHeader(request: HttpServletRequest): String? {
        val auth = request.getHeader(bearerTokenHeaderName) ?: return null
        if (!auth.startsWith("Bearer ", true)) return null
        val matcher = AUTHORIZATION_PATTERN.matcher(auth.trim())
        return if (matcher.matches()) matcher.group("token")
        else throw OAuth2AuthenticationException(BearerTokenErrors.invalidToken("Malformed token"))
    }

    private fun resolveFromCookie(request: HttpServletRequest): String? {
        if (!allowCookie) return null
        return request.cookies?.firstOrNull { it.name == cookieName }?.value?.takeIf { it.isNotBlank() }
    }

    private fun resolveToken(tokens: Array&#x3C;String>): String? {
        return tokens.filter { it.isNotBlank() }.let {
            when {
                it.isEmpty() -> null
                it.size > 1   -> throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Multiple tokens"))
                else          -> it[0]
            }
        }
    }
</code></pre>
<p>CookieUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.security;

import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO;
import lombok.experimental.UtilityClass;
import org.springframework.http.ResponseCookie;

@UtilityClass
public class CookieUtils {

    private static final String COOKIE_NAME = "accessToken";

    public ResponseCookie createAccessTokenCookie(TokenDTO tokenDto) {
        return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(tokenDto.accessTokenExpiresIn())
            .sameSite("Strict")
            .build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.security

import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO
import org.springframework.http.ResponseCookie

object CookieUtils {

  private const val COOKIE_NAME = "accessToken"

  fun createAccessTokenCookie(tokenDto: TokenDTO): ResponseCookie {
    return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(tokenDto.accessTokenExpiresIn())
      .sameSite("Strict")
      .build()
  }
}
</code></pre>
<p>JweUtil</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.security;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.jwk.RSAKey;
import io.github.susimsek.springbootldapjwedemo.config.JwtProperties;
import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;

import static io.github.susimsek.springbootldapjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Component
@RequiredArgsConstructor
public class JweUtil {

    private final JwtEncoder jwtEncoder;
    private final RSAKey signingKey;
    private final RSAKey encryptionKey;
    private final JwtProperties props;

    public TokenDTO generateToken(Authentication authentication) throws JOSEException {
        String subject = authentication.getName();
        List&#x3C;String> roles = authentication.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        Instant now = Instant.now();
        long expiresIn = props.getExpirationDuration().getSeconds();
        Instant exp = now.plusSeconds(expiresIn);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer(props.getIssuer())
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build();

        JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.getKeyID())
            .build();

        String jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .getTokenValue();

        JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.getKeyID())
            .build();

        JWEObject jweObject = new JWEObject(jweHeader, new Payload(jws));
        jweObject.encrypt(new RSAEncrypter(encryptionKey.toRSAPublicKey()));
        String token = jweObject.serialize();

        return new TokenDTO(token, "Bearer", expiresIn);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.security

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JOSEException
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.RSAKey
import io.github.susimsek.springbootldapjwedemo.config.JwtProperties
import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO
import lombok.RequiredArgsConstructor
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
import org.springframework.security.oauth2.jwt.JwtClaimsSet
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.JwtEncoderParameters
import org.springframework.stereotype.Component

import java.time.Instant

@Component
@RequiredArgsConstructor
class JweUtil(
    private val jwtEncoder: JwtEncoder,
    private val signingKey: RSAKey,
    private val encryptionKey: RSAKey,
    private val props: JwtProperties
) {
    fun generateToken(authentication: Authentication): TokenDTO {
        val subject = authentication.name
        val roles = authentication.authorities.map { it.authority }

        val now = Instant.now()
        val expiresIn = props.expirationDuration.seconds
        val exp = now.plusSeconds(expiresIn)

        val claims = JwtClaimsSet.builder()
            .issuer(props.issuer)
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build()

        val jwsHeader = org.springframework.security.oauth2.jwt.JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.keyID)
            .build()

        val jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .tokenValue

        val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.keyID)
            .build()

        val jweObject = JWEObject(jweHeader, Payload(jws))
        jweObject.encrypt(RSAEncrypter(encryptionKey.toRSAPublicKey()))
        val token = jweObject.serialize()

        return TokenDTO(token, "Bearer", expiresIn)
    }
}
</code></pre>
<p>KeyUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.security;

import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.experimental.UtilityClass;

import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@UtilityClass
public class KeyUtils {
    public RSAKey buildRsaKey(
            String pubPem,
            String privPem,
            String kid,
            boolean forSign
    ) throws Exception {
        String pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");
        String privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");

        byte[] decodedPub = Base64.getDecoder().decode(pubContent);
        byte[] decodedPriv = Base64.getDecoder().decode(privContent);

        KeyFactory kf = KeyFactory.getInstance("RSA");
        RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(decodedPub));
        RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedPriv));

        RSAKey.Builder builder = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid);

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE);
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION);
        }

        return builder.build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.security

import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.RSAKey
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

object KeyUtils {
    @JvmStatic
    @Throws(Exception::class)
    fun buildRsaKey(
        pubPem: String,
        privPem: String,
        kid: String,
        forSign: Boolean
    ): RSAKey {
        val pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replace(Regex("\\s"), "")
        val privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace(Regex("\\s"), "")

        val decodedPub = Base64.getDecoder().decode(pubContent)
        val decodedPriv = Base64.getDecoder().decode(privContent)

        val kf = KeyFactory.getInstance("RSA")
        val publicKey = kf.generatePublic(X509EncodedKeySpec(decodedPub)) as RSAPublicKey
        val privateKey = kf.generatePrivate(PKCS8EncodedKeySpec(decodedPriv)) as RSAPrivateKey

        val builder = RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid)

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE)
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION)
        }

        return builder.build()
    }
}
</code></pre>
<p>SecurityUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.security;

import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.Optional;

@UtilityClass
public class SecurityUtils {

    public static final String AUTHORITIES_KEY = "auth";

    public Optional&#x3C;String> getCurrentUserLogin() {
        var ctx = SecurityContextHolder.getContext();
        return Optional.ofNullable(extractPrincipal(ctx.getAuthentication()));
    }

    private String extractPrincipal(Authentication authentication) {
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getSubject();
        }
        if (principal instanceof UserDetails ud) {
            return ud.getUsername();
        }
        if (principal instanceof String username) {
            return username;
        }
        return null;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.security

import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.jwt.Jwt
import java.util.Optional

object SecurityUtils {
  const val AUTHORITIES_KEY = "auth"

  fun getCurrentUserLogin(): Optional&#x3C;String> {
    val authentication: Authentication? = SecurityContextHolder.getContext().authentication
    return Optional.ofNullable(extractPrincipal(authentication))
  }

  private fun extractPrincipal(authentication: Authentication?): String? {
    return when (val principal = authentication?.principal) {
      is Jwt -> principal.subject
      is UserDetails -> principal.username
      is String -> principal
      else -> null
    }
  }
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Authentication &#x26; Protected Endpoints</h2>
<p>In this section, we define the REST controllers and DTOs necessary for:</p>
<ul>
<li>AuthController: Authenticate users, issue JWE tokens, and set secure cookies.</li>
<li>HelloController: Expose protected resource endpoints for authenticated users and admin-specific paths.</li>
<li>LoginRequestDTO: Model the login request payload (username/password).</li>
<li>TokenDTO: Model the authentication response including token and expiration.</li>
</ul>
<p>These components complete the stateless authentication flow by handling login, token issuance, cookie management, and resource protection.</p>
<p>In this section, we expose REST controllers and DTOs to handle user authentication, token issuance, and protected resource access.</p>
<p>AuthController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.controller;

import io.github.susimsek.springbootldapjwedemo.dto.LoginRequestDTO;
import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO;
import io.github.susimsek.springbootldapjwedemo.security.CookieUtils;
import io.github.susimsek.springbootldapjwedemo.security.JweUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JweUtil jweUtil;

    @PostMapping("/login")
    public ResponseEntity&#x3C;TokenDTO> login(@RequestBody LoginRequestDTO loginRequest) throws Exception {
        // Authenticate user
        var authToken = new UsernamePasswordAuthenticationToken(
            loginRequest.username(), loginRequest.password()
        );
        Authentication auth = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);

        // Generate JWE token and cookie
        TokenDTO tokenDto = jweUtil.generateToken(auth);
        ResponseCookie cookie = CookieUtils.createAccessTokenCookie(tokenDto);

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.controller

import io.github.susimsek.springbootldapjwedemo.dto.LoginRequestDTO
import io.github.susimsek.springbootldapjwedemo.dto.TokenDTO
import io.github.susimsek.springbootldapjwedemo.security.CookieUtils
import io.github.susimsek.springbootldapjwedemo.security.JweUtil
import lombok.RequiredArgsConstructor
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import org.springframework.http.ResponseEntity
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
class AuthController(
    private val authenticationManager: AuthenticationManager,
    private val jweUtil: JweUtil
) {

    @PostMapping("/login")
    fun login(@RequestBody loginRequest: LoginRequestDTO): ResponseEntity&#x3C;TokenDTO> {
        val authToken = UsernamePasswordAuthenticationToken(
            loginRequest.username, loginRequest.password
        )
        val auth: Authentication = authenticationManager.authenticate(authToken)
        SecurityContextHolder.getContext().authentication = auth

        val tokenDto: TokenDTO = jweUtil.generateToken(auth)
        val cookie: ResponseCookie = CookieUtils.createAccessTokenCookie(tokenDto)

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto)
    }
}
</code></pre>
<p>HelloController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

import static io.github.susimsek.springbootldapjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@RestController
@RequestMapping("/api/hello")
public class HelloController {

    @GetMapping
    public String helloAll(@AuthenticationPrincipal Jwt jwt) {
        String user = jwt.getSubject();
        var roles = jwt.getClaimAsStringList(AUTHORITIES_KEY);
        return "Hello, " + user + "! Your roles: " + roles;
    }

    @GetMapping("/admin")
    public String helloAdmin(@AuthenticationPrincipal Jwt jwt) {
        return "Hello Admin, " + jwt.getSubject() + "!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.controller

import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*

import io.github.susimsek.springbootldapjwedemo.security.SecurityUtils.AUTHORITIES_KEY

@RestController
@RequestMapping("/api/hello")
class HelloController {

    @GetMapping
    fun helloAll(@AuthenticationPrincipal jwt: Jwt): String {
        val user = jwt.subject
        val roles = jwt.getClaimAsStringList(AUTHORITIES_KEY)
        return "Hello, \$user! Your roles: \$roles"
    }

    @GetMapping("/admin")
    fun helloAdmin(@AuthenticationPrincipal jwt: Jwt): String {
        return "Hello Admin, \${jwt.subject}!"
    }
}
</code></pre>
<p>LoginRequestDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.dto;

public record LoginRequestDTO(
    String username,
    String password
) { }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.dto

data class LoginRequestDTO(
    val username: String,
    val password: String
)
</code></pre>
<p>TokenDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootldapjwedemo.dto;

public record TokenDTO(
    String accessToken,
    String tokenType,
    long accessTokenExpiresIn
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootldapjwedemo.dto

import kotlin.Long

data class TokenDTO(
    val accessToken: String,
    val tokenType: String,
    val accessTokenExpiresIn: Long
)
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Test Endpoints</h2>
<p>In this section, we clarify Test Endpoints and summarize the key points you will apply in implementation.</p>
<h3>Admin Flow</h3>
<p>Login as admin and capture the JWE token from the <code>Set-Cookie</code> header:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"adminpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
<li>Response body:</li>
</ul>
<pre><code class="language-json">{
  "accessToken": "&#x3C;jwe-token>",
  "tokenType": "Bearer",
  "accessTokenExpiresIn": 3600
}
</code></pre>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header instead:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Access admin-only endpoint:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
</code></pre>
<h3>User Flow</h3>
<p>Login as user and capture JWE token from cookie:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"userpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
</ul>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Attempt admin endpoint (should be 403 Forbidden):</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
# HTTP/1.1 403 Forbidden
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot LDAP and JWE Authentication implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Security]]></category>
      <category><![CDATA[JWT]]></category>
      <category><![CDATA[JWE]]></category>
      <category><![CDATA[LDAP]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-ldap-jwe-auth-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot JWE Authentication]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-jwe-authentication</link>
      <description><![CDATA[Learn how to use JWE to encrypt your JWTs and secure your Spring Boot APIs for confidentiality and integrity.]]></description>
      <pubDate>Sat, 03 May 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-05-04T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-jwe-authentication</guid>
      <content:encoded><![CDATA[<p>Spring Boot lets you combine JWS signing with JWE encryption to protect both the integrity and confidentiality of your JWTs in Spring-powered microservices.</p>
<hr>
<h2>🌟 Why Use JWE Authentication?</h2>
<p>In this section, we clarify Why Use JWE Authentication? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Integrity: Ensure tokens are signed (JWS) and tamper-evident.</li>
<li>Confidentiality: Encrypt sensitive claims so only holders of the private key can read them (JWE).</li>
<li>Standards-based: Built on the JOSE (JWS, JWE) specifications.</li>
<li>Spring Support: Spring Security’s OAuth2 Resource Server handles JWE+JWS seamlessly.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 17 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include these in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.springframework.security&#x3C;/groupId>
  &#x3C;artifactId>spring-security-oauth2-resource-server&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.projectlombok&#x3C;/groupId>
  &#x3C;artifactId>lombok&#x3C;/artifactId>
  &#x3C;optional>true&#x3C;/optional>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
compileOnly 'org.projectlombok:lombok'
</code></pre>
<hr>
<h2>🛠️ Step 2: Keys Configuration</h2>
<p>Define RSA keys and user credentials in <code>application.yml</code>:</p>
<pre><code class="language-yaml">security:
  admin:
    username: admin
    password: adminpass
  user:
    username: user
    password: userpass
  jwt:
    issuer: demo-issuer
    expiration-duration: 3600s
    signing:
      key-id: signing-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjZEcz5itWkDGOSqZdB5P
        JE0ccOCckskp0hN3kQbT1qnv+9/k66jlWgVi4HSTppwdNF/Ylu5u541Qj+Okyg+u
        8o2PvKo5CfgvTDsFLTrZHUXU6hCSGatLAQoeN6lT8wzov2r4DFecXrIqcO6SvMB5
        ecPqsfiTi4trsNKgJ4cWS6gILH62ISd1ipUadfpnUzDMO1OulV0CJNV6bcBk7Es9
        RW6AHfg9j8osSanpwvRM4MJkB0SRxYUnrN9faGpkBZISZJ8TShhaTHEGfSNgKe5y
        8iu+AMGGZu8DYczVmqS3Ske1fq6y5HEGCma7Mo019GmwKeHBo1obuET6cZRygj4y
        twIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNkRzPmK1aQMY5
        Kpl0Hk8kTRxw4JySySnSE3eRBtPWqe/73+TrqOVaBWLgdJOmnB00X9iW7m7njVCP
        46TKD67yjY+8qjkJ+C9MOwUtOtkdRdTqEJIZq0sBCh43qVPzDOi/avgMV5xesipw
        7pK8wHl5w+qx+JOLi2uw0qAnhxZLqAgsfrYhJ3WKlRp1+mdTMMw7U66VXQIk1Xpt
        wGTsSz1FboAd+D2PyixJqenC9EzgwmQHRJHFhSes319oamQFkhJknxNKGFpMcQZ9
        I2Ap7nLyK74AwYZm7wNhzNWapLdKR7V+rrLkcQYKZrsyjTX0abAp4cGjWhu4RPpx
        lHKCPjK3AgMBAAECggEABMo/sNCIMREXA/EqjxDFecN4LmOTOK5A0YBiH9Cc01sd
        qSaavtSQqhqbjV+0bUNEA7UohXbc3s3bx3qa5VFhiIh8sBQMaQdyRkCK/MxMc16d
        BSx5XQ/8MjVO73A1zHgH2g47BWPjgRrDL94KrgNPOO0FoG76QxL9GlcOHzhFa1rf
        xbb2auLBtflKy+5TZNnB/sbFC9ISpWywzfblD2Fsvxupl0vNWtA0Y2rPbKMZRnY+
        V6NC38yxW3HHAdWQRYLGOitM59I0HBgnjQKzTBNIsjPnJA2BSZvMBtQ1e3RQV0qE
        8VVoktZ8A0KDUxqdKWYlGYAY5hIw1y3UUsPRUbS3fQKBgQDAZehu+Kb5Sw+h00Qu
        vViNHDvi0PwmhQrHVpgjrppCnWsTk6FJFm+EBLY+87usXuRCnfZ1semb6P+yjLh2
        bJ9IXIrFAztcyE+7eZfn0H7mHA7E2ICKTjNdKbjy8kHViHpgdXREnAOrFTTk/BqW
        dq+weG2OuxYczWsY4XliWaVyiwKBgQC8XYExpSmoyI37o+jr9405k30tbGUw4X+/
        xYOvSUuafI8IOSLfKTpsvkMN3hxpJO6apIRAjOALbotKUA5FrHQPenuBWOr3mBJT
        euLiaWphqU8YN5dbNd+JhC/Jh5DLhY5FpB2Fv2MKSoF7+onpjDy9pEU5aReeBB0v
        ekUw4nWiBQKBgHxyrnjxP1frFG5xMB4nfZqw04+v7BmiXsl3mqsh6kgCeNtN17pl
        17YGMjfgAdnJ+02XzW5tqRSfDp3YZgy7z//HVD+BCqnGK8SxLu/ULfD73xW2kNZl
        JNYzAZ2r06eiQr4X2x/x5nGIIxGmfDAtDxFPpFX5b6ErwgVy+sgCAoFnAoGAWSpu
        EMEdQk+FnnwNsz2g9YNSuyDXmdb08SOfXWd1yXBzCLJ7RmYuyPEbrsHYcxFPfZap
        ICFPoTm35/qTdvnWiskxE56yw3eSHUBLjF/YQtixn0YZeMy8v0z6jgyFR0I2gdLZ
        QsnBKUrxlm2XwR1oV2Eef7m2u085PZNEk4pvor0CgYA6n060r48/CgRVslAeGder
        fZ3n+2ru8q2UVB38evepjLifn5+tKdFzZ7/NckXMC0NOVzm74qG46VFLMw5TtyT4
        /hRvGSYxA6dMjXy+tcrAns9Isrz2PnYFntlbhU6hw0um809tFunbvITfyeOjAsDW
        stC4thnhzXXT1Y3RfFtYEg==
        -----END PRIVATE KEY-----
    encryption:
      key-id: encryption-key
      public-key: |-
        -----BEGIN PUBLIC KEY-----
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtU7Vu4tML8qg9BISaUH0
        BTU0+qkHJS3TUl3R0hEPttZJGf8EgT7bpWqRjdhMC2SYt2ifNF82EgMo/eva1rLn
        wYtCsxjJ7oB4U2m16/JKHtphmMUGhSLgbp+Y8pGAQ+P6u4HLlI0qbL92Syb6QCTr
        nYIlzZ2uUAvUQg/SbuAvdS4kdauZtpMNbhryusMVvILVBC5yUhfLJAkjbU3qWo/n
        2NKPr4kjwwFh6FaAf0HLsXCFmJbPPok6WMZeDqPORPZCt5gU2t04fS7s/2SSbhQ/
        i+7yyT4pl44cUlNLLN3Qo73sai5H90q7Dvmk3M5/YMJB0Ueyb705LHyOO5xA618s
        qQIDAQAB
        -----END PUBLIC KEY-----
      private-key: |-
        -----BEGIN PRIVATE KEY-----
        MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1TtW7i0wvyqD0
        EhJpQfQFNTT6qQclLdNSXdHSEQ+21kkZ/wSBPtulapGN2EwLZJi3aJ80XzYSAyj9
        69rWsufBi0KzGMnugHhTabXr8koe2mGYxQaFIuBun5jykYBD4/q7gcuUjSpsv3ZL
        JvpAJOudgiXNna5QC9RCD9Ju4C91LiR1q5m2kw1uGvK6wxW8gtUELnJSF8skCSNt
        Tepaj+fY0o+viSPDAWHoVoB/QcuxcIWYls8+iTpYxl4Oo85E9kK3mBTa3Th9Luz/
        ZJJuFD+L7vLJPimXjhxSU0ss3dCjvexqLkf3SrsO+aTczn9gwkHRR7JvvTksfI47
        nEDrXyypAgMBAAECggEAREnJHrY8n9OGA+e6n4KD0mJT7gUz98Dm0yMbIC/k50yW
        hAAVRkjSmd8lq3NIURI2ov342NTznJ0sF1d6OVtxBujY2nP/uqEQsPoj1xaO7Ef8
        cnnjFsooFgJurQ44bVm02mLstqrky8jhWTT5FKfTRqP6cRNu0B9kdu1WqQQvW8a/
        C8xD8XUFzzJFQsEstJ076NjlZNH3EeqFiLxyVdV7tPEJRnOe2V2BAAgda3ByA6Jc
        Nd4xYHTVr5rZ+KES9gaAgb86+S7hNigsxz5Gp58NimLEyz1Qd+d9QCgPRgKOV5Uq
        0ci6c28ZrxW9wa/2CGCpoow1V7hRy1fZ24g2Tz91cQKBgQDbSdP/FQYuem0pUf8u
        mA/niEC8Jz30FW3ix5aqbQ8YRudK48Xy8F7AWnlS0wT2k/Xaeppn9YSPK4HlsNk2
        1gnpNfzdqXpJQtuxcPPeFNzUdXzBrsLXYqtlhOX/afrzqMhp73gkqoxXtqwD7BqN
        VfMcVXsWrEZQrRPBn/RUnnLdJwKBgQDTqUGupm93UlhoPgLXHGF/seQwPPHcU6lS
        SS9l512Ie21B+vZecgO4r77BXJwbY4rZVA5/m+wrsNmsT2f8yfdbs84cBDDWiKy5
        t2Um8zrus+BCIffxc9Fck9/htK33jLLcYwdSAm3a9pJxR9f/kAdredSTGBNJ3W/2
        rfh3cdNprwKBgDi7WfBFRSsjGzi0cPth9cNlubGzyVBrdtlT34PJ4Tzboxz53o1i
        aHEFNxwZYdBVKSbTzzyUBS5xCBMfdKK+LyQ5hmjmXq+zb1jxqvXKmfMRTixhhSDp
        8wO5pTM1/Omqwea+QGvj/5j1tnzxSVFFajbrWoPcH/jhPho6wqBducPDAoGBALOJ
        MEOavZXy7TaO0w3v7uvH0wzvxR/kfw1jMqc3l2j7ePOskmoOQAXaXO3bRjcdOlua
        Jyoq8islOZ4lRMlx7zWD0OKG035GNGzbmRtu2aA8R48RDSVr3jyu2gqznZULbXPv
        M/hmQxSmbhVUoW0PmJubnaqfk0zmXeBaNRXsIS3VAoGBALdsAtqR23PVeL6sYj2l
        dRNTDXpfvjFqJ5NThRO/4mT0CrPHQVj+Mz2bTc/Dxiwi8s8m3L+g23i3hodh/QMb
        Iz+UbcJZBGAKsFbPKGOtj6Bi07y/L11mcuNJzOWe61/JbVmJss0s+N/v3XucK/Ge
        CUaGOccsMO221v6JoBh9J3Hz
        -----END PRIVATE KEY-----
</code></pre>
<hr>
<h2>🛠️ Step 3: Security Configuration</h2>
<p>In this section, we define the beans and properties required to load credentials, RSA keys, and HTTP security filters for JWE-based authentication:</p>
<ul>
<li>AdminProperties: Loads admin username and password from application properties.</li>
<li>UserProperties: Loads normal user credentials from application properties.</li>
<li>JwtProperties: Configures JWT issuer, expiration, and signing/encryption key pairs.</li>
<li>SecurityJwtConfig: Builds RSA JWKs, JWT encoder/decoder, authentication converter, and token resolver.</li>
<li>SecurityConfig: Defines in-memory users and the stateless security filter chain with route authorization.</li>
</ul>
<p>AdminProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "security.admin")
@Data
public class AdminProperties {
    private String username;
    private String password;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "security.admin")
class AdminProperties {
  lateinit var username: String
  lateinit var password: String
}
</code></pre>
<p>SecurityJwtConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.config;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.github.susimsek.springbootjwedemo.security.CookieBearerTokenResolver;
import io.github.susimsek.springbootjwedemo.security.KeyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;

import java.util.List;

import static io.github.susimsek.springbootjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Configuration
public class SecurityJwtConfig {

    private final JwtProperties props;
    public SecurityJwtConfig(JwtProperties props) { this.props = props; }

    @Bean
    public RSAKey signingKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getSigning().getPublicKey(),
            props.getSigning().getPrivateKey(),
            props.getSigning().getKeyId(),
            true
        );
    }

    @Bean
    public RSAKey encryptionKey() throws Exception {
        return KeyUtils.buildRsaKey(
            props.getEncryption().getPublicKey(),
            props.getEncryption().getPrivateKey(),
            props.getEncryption().getKeyId(),
            false
        );
    }

    @Bean
    public JWKSource&#x3C;SecurityContext> jwkSource(RSAKey signingKey, RSAKey encryptionKey) {
        JWKSet jwkSet = new JWKSet(List.of(
            signingKey,
            encryptionKey
        ));
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        DefaultJWTProcessor&#x3C;SecurityContext> jwtProcessor = new DefaultJWTProcessor&#x3C;>();
        JWEDecryptionKeySelector&#x3C;SecurityContext> jweKeySelector =
            new JWEDecryptionKeySelector&#x3C;>(
                JWEAlgorithm.RSA_OAEP_256,
                EncryptionMethod.A128GCM,
                jwkSource
            );
        jwtProcessor.setJWEKeySelector(jweKeySelector);

        JWSVerificationKeySelector&#x3C;SecurityContext> jwsKeySelector =
            new JWSVerificationKeySelector&#x3C;>(
                JWSAlgorithm.RS256,
                jwkSource
            );
        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});

        return new NimbusJwtDecoder(jwtProcessor);
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource&#x3C;SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthorityPrefix("");
        converter.setAuthoritiesClaimName(AUTHORITIES_KEY);

        JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
        authConverter.setJwtGrantedAuthoritiesConverter(converter);
        return authConverter;
    }

    @Bean
    public BearerTokenResolver bearerTokenResolver() {
        CookieBearerTokenResolver resolver = new CookieBearerTokenResolver();
        resolver.setAllowUriQueryParameter(false);
        resolver.setAllowFormEncodedBodyParameter(false);
        resolver.setAllowCookie(true);
        return resolver;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.config

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.JWSVerificationKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import io.github.susimsek.springbootjwedemo.security.CookieBearerTokenResolver
import io.github.susimsek.springbootjwedemo.security.KeyUtils
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver

@Configuration
class SecurityJwtConfig(private val props: JwtProperties) {

    @Bean
    @Throws(Exception::class)
    fun signingKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.signing.publicKey,
            props.signing.privateKey,
            props.signing.keyId,
            true
        )

    @Bean
    @Throws(Exception::class)
    fun encryptionKey(): RSAKey =
        KeyUtils.buildRsaKey(
            props.encryption.publicKey,
            props.encryption.privateKey,
            props.encryption.keyId,
            false
        )

    @Bean
    fun jwkSource(signingKey: RSAKey, encryptionKey: RSAKey): JWKSource&#x3C;SecurityContext> {
        val jwkSet = JWKSet(listOf(signingKey, encryptionKey))
        return JWKSource { jwkSelector, context -> jwkSelector.select(jwkSet) }
    }

    @Bean
    fun jwtDecoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtDecoder {
        val jwtProcessor = DefaultJWTProcessor&#x3C;SecurityContext>()
        val jweKeySelector = JWEDecryptionKeySelector(
            JWEAlgorithm.RSA_OAEP_256,
            EncryptionMethod.A128GCM,
            jwkSource
        )
        jwtProcessor.jweKeySelector = jweKeySelector
        val jwsKeySelector = JWSVerificationKeySelector(
            JWSAlgorithm.RS256,
            jwkSource
        )
        jwtProcessor.jwsKeySelector = jwsKeySelector
        jwtProcessor.jwtClaimsSetVerifier = { _, _ -> }
        return NimbusJwtDecoder(jwtProcessor)
    }

    @Bean
    fun jwtEncoder(jwkSource: JWKSource&#x3C;SecurityContext>): JwtEncoder =
        NimbusJwtEncoder(jwkSource)

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val converter = JwtGrantedAuthoritiesConverter().apply {
            setAuthorityPrefix("")
            setAuthoritiesClaimName(AUTHORITIES_KEY)
        }
        return JwtAuthenticationConverter().apply {
            setJwtGrantedAuthoritiesConverter(converter)
        }
    }

    @Bean
    fun bearerTokenResolver(): BearerTokenResolver =
        CookieBearerTokenResolver().apply {
            setAllowUriQueryParameter(false)
            setAllowFormEncodedBodyParameter(false)
            setAllowCookie(true)
        }
}
</code></pre>
<p>SecurityConfig</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.config;

import io.github.susimsek.springbootjwedemo.security.AuthoritiesConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    @Bean
    public InMemoryUserDetailsManager userDetailsService(AdminProperties adminProps,
                                                         UserProperties userProps,
                                                         PasswordEncoder passwordEncoder) {
        var admin = User.withUsername(adminProps.getUsername())
            .password(passwordEncoder.encode(adminProps.getPassword()))
            .authorities(AuthoritiesConstants.ADMIN)
            .build();

        var user = User.withUsername(userProps.getUsername())
            .password(passwordEncoder.encode(userProps.getPassword()))
            .authorities(AuthoritiesConstants.USER)
            .build();

        return new InMemoryUserDetailsManager(admin, user);
    }

    @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService uds) {
        var provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(uds);
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                           MvcRequestMatcher.Builder mvc) throws Exception {
        http
            .cors(withDefaults())
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authz ->
                authz
                    .requestMatchers(mvc.pattern("/*.ico"), mvc.pattern("/*.png"), mvc.pattern("/*.svg"),
                        mvc.pattern("/*.webapp")
                    ).permitAll()
                    .requestMatchers("/actuator/**").permitAll()
                    .requestMatchers(
                        "/v3/api-docs/**",
                        "/swagger-ui.html",
                        "/swagger-ui/**"
                    ).permitAll()
                    .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
                    .requestMatchers(mvc.pattern("/api/hello/admin")).hasAuthority(AuthoritiesConstants.ADMIN)
                .anyRequest().authenticated()
            )
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(withDefaults())
            );
        return http.build();
    }

    @Bean
    public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
        return new MvcRequestMatcher.Builder(introspector);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.config

import io.github.susimsek.springbootjwedemo.security.AuthoritiesConstants
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
import org.springframework.security.config.Customizer.withDefaults

@Configuration
@EnableMethodSecurity(securedEnabled = true)
class SecurityConfig {

    @Bean
    fun userDetailsService(
        adminProps: AdminProperties,
        userProps: UserProperties,
        passwordEncoder: PasswordEncoder
    ): UserDetailsService {
        val admin = User.withUsername(adminProps.username)
            .password(passwordEncoder.encode(adminProps.password))
            .authorities(AuthoritiesConstants.ADMIN)
            .build()
        val user = User.withUsername(userProps.username)
            .password(passwordEncoder.encode(userProps.password))
            .authorities(AuthoritiesConstants.USER)
            .build()
        return InMemoryUserDetailsManager(admin, user)
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    @Bean
    fun authenticationManager(uds: UserDetailsService): AuthenticationManager {
        val provider = DaoAuthenticationProvider().apply {
            setUserDetailsService(uds)
            setPasswordEncoder(passwordEncoder())
        }
        return ProviderManager(provider)
    }

    @Bean
    fun filterChain(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
        http.cors(withDefaults())
            .csrf { it.disable() }
            .authorizeHttpRequests { authz ->
                authz
                    .requestMatchers(mvc.pattern("/*.ico"), mvc.pattern("/*.png"), mvc.pattern("/*.svg"), mvc.pattern("/*.webapp")).permitAll()
                    .requestMatchers("/actuator/**").permitAll()
                    .requestMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll()
                    .requestMatchers(mvc.pattern("/api/auth/login")).permitAll()
                    .requestMatchers(mvc.pattern("/api/hello/admin")).hasAuthority(AuthoritiesConstants.ADMIN)
                    .anyRequest().authenticated()
            }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .oauth2ResourceServer { it.jwt(withDefaults()) }
            .build()

    @Bean
    fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
        MvcRequestMatcher.Builder(introspector)
}
</code></pre>
<p>JwtProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
@Data
public class JwtProperties {

    @Data
    public static class Pair {
        private String publicKey;
        private String privateKey;
        private String keyId;
    }

    private Pair signing;
    private Pair encryption;
    private String issuer;
    private Duration expirationDuration;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@ConfigurationProperties(prefix = "security.jwt")
class JwtProperties {

  class Pair {
    lateinit var publicKey: String
    lateinit var privateKey: String
    lateinit var keyId: String
  }

  lateinit var signing: Pair
  lateinit var encryption: Pair
  lateinit var issuer: String
  lateinit var expirationDuration: Duration
}
</code></pre>
<p>UserProperties</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "security.user")
@Data
public class UserProperties {
    private String username;
    private String password;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "security.user")
class UserProperties {
  lateinit var username: String
  lateinit var password: String
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Secure JWE Token Utilities</h2>
<p>In this section, we define the core utility classes and constants needed to generate, encrypt, and resolve JSON Web Encryption (JWE) tokens in your Spring Boot application. These components work together to:</p>
<ul>
<li>AuthoritiesConstants: Centralize role names with the <code>ROLE_</code> prefix.</li>
<li>CookieBearerTokenResolver: Resolve bearer tokens from Authorization headers or HTTP cookies.</li>
<li>CookieUtils: Create HTTP-only, secure cookies for access tokens.</li>
<li>JweUtil: Sign (JWS) and encrypt (JWE) JWTs using RSA keys and Nimbus.</li>
<li>KeyUtils: Build RSA JWKs from PEM‐encoded key material.</li>
<li>SecurityUtils: Extract the current user’s login from the security context.</li>
</ul>
<p>These utilities form the foundation for a stateless, JWE‐based authentication flow in Spring Security.</p>
<p>AuthoritiesConstants</p>
<h3>Java</h3>
<pre><code class="language-java">
package io.github.susimsek.springbootjwedemo.security;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {
    public static final String ADMIN = "ROLE_ADMIN";
    public static final String USER = "ROLE_USER";
    public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">
package io.github.susimsek.springbootjwedemo.security

object AuthoritiesConstants {
  const val ADMIN = "ROLE_ADMIN"
  const val USER = "ROLE_USER"
  const val ANONYMOUS = "ROLE_ANONYMOUS"
}
</code></pre>
<p>CookieBearerTokenResolver</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.security;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.springframework.http.HttpMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Setter
public class CookieBearerTokenResolver implements BearerTokenResolver {
    private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
    private static final Pattern AUTHORIZATION_PATTERN =
        Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);

    private boolean allowFormEncodedBodyParameter = false;
    private boolean allowUriQueryParameter       = false;
    private boolean allowCookie                  = true;
    private String  bearerTokenHeaderName       = "Authorization";
    private String  cookieName                  = "accessToken";

    @Override
    public String resolve(HttpServletRequest request) {
        String headerToken = resolveFromAuthorizationHeader(request);
        String queryToken  = resolveAccessTokenFromQueryString(request);
        String bodyToken   = resolveAccessTokenFromBody(request);
        String cookieToken = (headerToken == null &#x26;&#x26; queryToken == null &#x26;&#x26; bodyToken == null)
            ? resolveFromCookie(request)
            : null;

        return resolveToken(headerToken, queryToken, bodyToken, cookieToken);
    }

    private String resolveFromAuthorizationHeader(HttpServletRequest request) {
        String authorization = request.getHeader(this.bearerTokenHeaderName);
        if (!StringUtils.hasText(authorization) || !authorization.toLowerCase().startsWith("bearer")) {
            return null;
        }
        Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization.trim());
        if (!matcher.matches()) {
            BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
            throw new OAuth2AuthenticationException(error);
        }
        return matcher.group("token");
    }

    private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
        if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.getMethod())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveAccessTokenFromBody(HttpServletRequest request) {
        if (allowFormEncodedBodyParameter
            &#x26;&#x26; HttpMethod.POST.matches(request.getMethod())
            &#x26;&#x26; "application/x-www-form-urlencoded".equals(request.getContentType())) {
            return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
        }
        return null;
    }

    private String resolveFromCookie(HttpServletRequest request) {
        if (!allowCookie || request.getCookies() == null) {
            return null;
        }
        for (Cookie cookie : request.getCookies()) {
            if (cookieName.equals(cookie.getName())) {
                String val = cookie.getValue();
                return StringUtils.hasText(val) ? val : null;
            }
        }
        return null;
    }

    private String resolveToken(String... tokens) {
        String found = null;
        for (String token : tokens) {
            if (token == null) continue;
            if (found != null) {
                BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
                throw new OAuth2AuthenticationException(error);
            }
            found = token;
        }
        if (found != null &#x26;&#x26; found.isBlank()) {
            BearerTokenError error = BearerTokenErrors.invalidRequest("The requested token parameter is an empty string");
            throw new OAuth2AuthenticationException(error);
        }
        return found;
    }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.security

import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpMethod
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.server.resource.BearerTokenError
import org.springframework.security.oauth2.server.resource.BearerTokenErrors
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.util.StringUtils
import java.util.regex.Pattern

class CookieBearerTokenResolver {
    var allowFormEncodedBodyParameter: Boolean = false
    var allowUriQueryParameter: Boolean = false
    var allowCookie: Boolean = true
    var bearerTokenHeaderName: String = "Authorization"
    var cookieName: String = "accessToken"

    companion object {
        private const val ACCESS_TOKEN_PARAMETER_NAME = "access_token"
        private val AUTHORIZATION_PATTERN =
            Pattern.compile("^Bearer (?&#x3C;token>[A-Za-z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE)
    }

    fun resolve(request: HttpServletRequest): String? {
        val header = resolveFromAuthorizationHeader(request)
        val query  = if (allowUriQueryParameter &#x26;&#x26; HttpMethod.GET.matches(request.method))
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val body   = if (allowFormEncodedBodyParameter &#x26;&#x26; HttpMethod.POST.matches(request.method)
            &#x26;&#x26; request.contentType == "application/x-www-form-urlencoded")
            request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)?.let(::resolveToken)
        else null
        val cookie = if (header == null &#x26;&#x26; query == null &#x26;&#x26; body == null)
            resolveFromCookie(request)
        else null
      return listOf(header, query, body, cookie).filterNotNull().let {
            if (it.size > 1) throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Found multiple bearer tokens"))
            it.firstOrNull()?: null
        }
    }

    private fun resolveFromAuthorizationHeader(request: HttpServletRequest): String? {
        val auth = request.getHeader(bearerTokenHeaderName) ?: return null
        if (!auth.startsWith("Bearer ", true)) return null
        val matcher = AUTHORIZATION_PATTERN.matcher(auth.trim())
        return if (matcher.matches()) matcher.group("token")
        else throw OAuth2AuthenticationException(BearerTokenErrors.invalidToken("Malformed token"))
    }

    private fun resolveFromCookie(request: HttpServletRequest): String? {
        if (!allowCookie) return null
        return request.cookies?.firstOrNull { it.name == cookieName }?.value?.takeIf { it.isNotBlank() }
    }

    private fun resolveToken(tokens: Array&#x3C;String>): String? {
        return tokens.filter { it.isNotBlank() }.let {
            when {
                it.isEmpty() -> null
                it.size > 1   -> throw OAuth2AuthenticationException(BearerTokenErrors.invalidRequest("Multiple tokens"))
                else          -> it[0]
            }
        }
    }
</code></pre>
<p>CookieUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.security;

import io.github.susimsek.springbootjwedemo.dto.TokenDTO;
import lombok.experimental.UtilityClass;
import org.springframework.http.ResponseCookie;

@UtilityClass
public class CookieUtils {

    private static final String COOKIE_NAME = "accessToken";

    public ResponseCookie createAccessTokenCookie(TokenDTO tokenDto) {
        return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(tokenDto.accessTokenExpiresIn())
            .sameSite("Strict")
            .build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.security

import io.github.susimsek.springbootjwedemo.dto.TokenDTO
import org.springframework.http.ResponseCookie

object CookieUtils {

  private const val COOKIE_NAME = "accessToken"

  fun createAccessTokenCookie(tokenDto: TokenDTO): ResponseCookie {
    return ResponseCookie.from(COOKIE_NAME, tokenDto.accessToken())
      .httpOnly(true)
      .secure(true)
      .path("/")
      .maxAge(tokenDto.accessTokenExpiresIn())
      .sameSite("Strict")
      .build()
  }
}
</code></pre>
<p>JweUtil</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.security;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.jwk.RSAKey;
import io.github.susimsek.springbootjwedemo.config.JwtProperties;
import io.github.susimsek.springbootjwedemo.dto.TokenDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.List;

import static io.github.susimsek.springbootjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@Component
@RequiredArgsConstructor
public class JweUtil {

    private final JwtEncoder jwtEncoder;
    private final RSAKey signingKey;
    private final RSAKey encryptionKey;
    private final JwtProperties props;

    public TokenDTO generateToken(Authentication authentication) throws JOSEException {
        String subject = authentication.getName();
        List&#x3C;String> roles = authentication.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        Instant now = Instant.now();
        long expiresIn = props.getExpirationDuration().getSeconds();
        Instant exp = now.plusSeconds(expiresIn);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer(props.getIssuer())
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build();

        JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.getKeyID())
            .build();

        String jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .getTokenValue();

        JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.getKeyID())
            .build();

        JWEObject jweObject = new JWEObject(jweHeader, new Payload(jws));
        jweObject.encrypt(new RSAEncrypter(encryptionKey.toRSAPublicKey()));
        String token = jweObject.serialize();

        return new TokenDTO(token, "Bearer", expiresIn);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.security

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JOSEException
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.RSAKey
import io.github.susimsek.springbootjwedemo.config.JwtProperties
import io.github.susimsek.springbootjwedemo.dto.TokenDTO
import lombok.RequiredArgsConstructor
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
import org.springframework.security.oauth2.jwt.JwtClaimsSet
import org.springframework.security.oauth2.jwt.JwtEncoder
import org.springframework.security.oauth2.jwt.JwtEncoderParameters
import org.springframework.stereotype.Component

import java.time.Instant

@Component
@RequiredArgsConstructor
class JweUtil(
    private val jwtEncoder: JwtEncoder,
    private val signingKey: RSAKey,
    private val encryptionKey: RSAKey,
    private val props: JwtProperties
) {
    fun generateToken(authentication: Authentication): TokenDTO {
        val subject = authentication.name
        val roles = authentication.authorities.map { it.authority }

        val now = Instant.now()
        val expiresIn = props.expirationDuration.seconds
        val exp = now.plusSeconds(expiresIn)

        val claims = JwtClaimsSet.builder()
            .issuer(props.issuer)
            .issuedAt(now)
            .expiresAt(exp)
            .subject(subject)
            .claim(AUTHORITIES_KEY, roles)
            .build()

        val jwsHeader = org.springframework.security.oauth2.jwt.JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId(signingKey.keyID)
            .build()

        val jws = jwtEncoder
            .encode(JwtEncoderParameters.from(jwsHeader, claims))
            .tokenValue

        val jweHeader = JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
            .contentType("JWT")
            .keyID(encryptionKey.keyID)
            .build()

        val jweObject = JWEObject(jweHeader, Payload(jws))
        jweObject.encrypt(RSAEncrypter(encryptionKey.toRSAPublicKey()))
        val token = jweObject.serialize()

        return TokenDTO(token, "Bearer", expiresIn)
    }
}
</code></pre>
<p>KeyUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.security;

import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.experimental.UtilityClass;

import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@UtilityClass
public class KeyUtils {
    public RSAKey buildRsaKey(
            String pubPem,
            String privPem,
            String kid,
            boolean forSign
    ) throws Exception {
        String pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");
        String privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");

        byte[] decodedPub = Base64.getDecoder().decode(pubContent);
        byte[] decodedPriv = Base64.getDecoder().decode(privContent);

        KeyFactory kf = KeyFactory.getInstance("RSA");
        RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(decodedPub));
        RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedPriv));

        RSAKey.Builder builder = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid);

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE);
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION);
        }

        return builder.build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.security

import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.RSAKey
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

object KeyUtils {
    @JvmStatic
    @Throws(Exception::class)
    fun buildRsaKey(
        pubPem: String,
        privPem: String,
        kid: String,
        forSign: Boolean
    ): RSAKey {
        val pubContent = pubPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replace(Regex("\\s"), "")
        val privContent = privPem
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace(Regex("\\s"), "")

        val decodedPub = Base64.getDecoder().decode(pubContent)
        val decodedPriv = Base64.getDecoder().decode(privContent)

        val kf = KeyFactory.getInstance("RSA")
        val publicKey = kf.generatePublic(X509EncodedKeySpec(decodedPub)) as RSAPublicKey
        val privateKey = kf.generatePrivate(PKCS8EncodedKeySpec(decodedPriv)) as RSAPrivateKey

        val builder = RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(kid)

        if (forSign) {
            builder
                .algorithm(JWSAlgorithm.RS256)
                .keyUse(KeyUse.SIGNATURE)
        } else {
            builder
                .algorithm(JWEAlgorithm.RSA_OAEP_256)
                .keyUse(KeyUse.ENCRYPTION)
        }

        return builder.build()
    }
}
</code></pre>
<p>SecurityUtils</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.security;

import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.Optional;

@UtilityClass
public class SecurityUtils {

    public static final String AUTHORITIES_KEY = "auth";

    public Optional&#x3C;String> getCurrentUserLogin() {
        var ctx = SecurityContextHolder.getContext();
        return Optional.ofNullable(extractPrincipal(ctx.getAuthentication()));
    }

    private String extractPrincipal(Authentication authentication) {
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getSubject();
        }
        if (principal instanceof UserDetails ud) {
            return ud.getUsername();
        }
        if (principal instanceof String username) {
            return username;
        }
        return null;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.security

import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.jwt.Jwt
import java.util.Optional

object SecurityUtils {
  const val AUTHORITIES_KEY = "auth"

  fun getCurrentUserLogin(): Optional&#x3C;String> {
    val authentication: Authentication? = SecurityContextHolder.getContext().authentication
    return Optional.ofNullable(extractPrincipal(authentication))
  }

  private fun extractPrincipal(authentication: Authentication?): String? {
    return when (val principal = authentication?.principal) {
      is Jwt -> principal.subject
      is UserDetails -> principal.username
      is String -> principal
      else -> null
    }
  }
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Authentication &#x26; Protected Endpoints</h2>
<p>In this section, we define the REST controllers and DTOs necessary for:</p>
<ul>
<li>AuthController: Authenticate users, issue JWE tokens, and set secure cookies.</li>
<li>HelloController: Expose protected resource endpoints for authenticated users and admin-specific paths.</li>
<li>LoginRequestDTO: Model the login request payload (username/password).</li>
<li>TokenDTO: Model the authentication response including token and expiration.</li>
</ul>
<p>These components complete the stateless authentication flow by handling login, token issuance, cookie management, and resource protection.</p>
<p>In this section, we expose REST controllers and DTOs to handle user authentication, token issuance, and protected resource access.</p>
<p>AuthController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.controller;

import io.github.susimsek.springbootjwedemo.dto.LoginRequestDTO;
import io.github.susimsek.springbootjwedemo.dto.TokenDTO;
import io.github.susimsek.springbootjwedemo.security.CookieUtils;
import io.github.susimsek.springbootjwedemo.security.JweUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JweUtil jweUtil;

    @PostMapping("/login")
    public ResponseEntity&#x3C;TokenDTO> login(@RequestBody LoginRequestDTO loginRequest) throws Exception {
        // Authenticate user
        var authToken = new UsernamePasswordAuthenticationToken(
            loginRequest.username(), loginRequest.password()
        );
        Authentication auth = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);

        // Generate JWE token and cookie
        TokenDTO tokenDto = jweUtil.generateToken(auth);
        ResponseCookie cookie = CookieUtils.createAccessTokenCookie(tokenDto);

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.controller

import io.github.susimsek.springbootjwedemo.dto.LoginRequestDTO
import io.github.susimsek.springbootjwedemo.dto.TokenDTO
import io.github.susimsek.springbootjwedemo.security.CookieUtils
import io.github.susimsek.springbootjwedemo.security.JweUtil
import lombok.RequiredArgsConstructor
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import org.springframework.http.ResponseEntity
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
class AuthController(
    private val authenticationManager: AuthenticationManager,
    private val jweUtil: JweUtil
) {

    @PostMapping("/login")
    fun login(@RequestBody loginRequest: LoginRequestDTO): ResponseEntity&#x3C;TokenDTO> {
        val authToken = UsernamePasswordAuthenticationToken(
            loginRequest.username, loginRequest.password
        )
        val auth: Authentication = authenticationManager.authenticate(authToken)
        SecurityContextHolder.getContext().authentication = auth

        val tokenDto: TokenDTO = jweUtil.generateToken(auth)
        val cookie: ResponseCookie = CookieUtils.createAccessTokenCookie(tokenDto)

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenDto)
    }
}
</code></pre>
<p>HelloController</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

import static io.github.susimsek.springbootjwedemo.security.SecurityUtils.AUTHORITIES_KEY;

@RestController
@RequestMapping("/api/hello")
public class HelloController {

    @GetMapping
    public String helloAll(@AuthenticationPrincipal Jwt jwt) {
        String user = jwt.getSubject();
        var roles = jwt.getClaimAsStringList(AUTHORITIES_KEY);
        return "Hello, " + user + "! Your roles: " + roles;
    }

    @GetMapping("/admin")
    public String helloAdmin(@AuthenticationPrincipal Jwt jwt) {
        return "Hello Admin, " + jwt.getSubject() + "!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.controller

import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*

import io.github.susimsek.springbootjwedemo.security.SecurityUtils.AUTHORITIES_KEY

@RestController
@RequestMapping("/api/hello")
class HelloController {

    @GetMapping
    fun helloAll(@AuthenticationPrincipal jwt: Jwt): String {
        val user = jwt.subject
        val roles = jwt.getClaimAsStringList(AUTHORITIES_KEY)
        return "Hello, \$user! Your roles: \$roles"
    }

    @GetMapping("/admin")
    fun helloAdmin(@AuthenticationPrincipal jwt: Jwt): String {
        return "Hello Admin, \${jwt.subject}!"
    }
}
</code></pre>
<p>LoginRequestDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.dto;

public record LoginRequestDTO(
    String username,
    String password
) { }
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.dto

data class LoginRequestDTO(
    val username: String,
    val password: String
)
</code></pre>
<p>TokenDTO</p>
<h3>Java</h3>
<pre><code class="language-java">package io.github.susimsek.springbootjwedemo.dto;

public record TokenDTO(
    String accessToken,
    String tokenType,
    long accessTokenExpiresIn
) {}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package io.github.susimsek.springbootjwedemo.dto

import kotlin.Long

data class TokenDTO(
    val accessToken: String,
    val tokenType: String,
    val accessTokenExpiresIn: Long
)
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Test Endpoints</h2>
<p>In this section, we clarify Test Endpoints and summarize the key points you will apply in implementation.</p>
<h3>Admin Flow</h3>
<p>Login as admin and capture the JWE token from the <code>Set-Cookie</code> header:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"adminpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
<li>Response body:</li>
</ul>
<pre><code class="language-json">{
  "accessToken": "&#x3C;jwe-token>",
  "tokenType": "Bearer",
  "accessTokenExpiresIn": 3600
}
</code></pre>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header instead:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Access admin-only endpoint:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
</code></pre>
<h3>User Flow</h3>
<p>Login as user and capture JWE token from cookie:</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"userpass"}'
</code></pre>
<ul>
<li>Set-Cookie header contains <code>accessToken=&#x3C;jwe-token></code></li>
</ul>
<p>Use cookie to access hello endpoint:</p>
<pre><code class="language-bash">curl -b "accessToken=&#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Use Authorization header:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello
</code></pre>
<p>Attempt admin endpoint (should be 403 Forbidden):</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer &#x3C;jwe-token>" http://localhost:8080/api/hello/admin
# HTTP/1.1 403 Forbidden
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot JWE Authentication implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Security]]></category>
      <category><![CDATA[JWT]]></category>
      <category><![CDATA[JWE]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-jwe-auth-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot HTTP Interface Client]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-http-interface-client</link>
      <description><![CDATA[Learn how to use Spring Boot 3.2+ HTTP Interface Client for declarative, type-safe HTTP calls.]]></description>
      <pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-04-28T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-http-interface-client</guid>
      <content:encoded><![CDATA[<p>Spring Boot 3.2+ introduces the HTTP Interface Client, enabling you to define declarative, type-safe HTTP clients with simple Java or Kotlin interfaces. In this article, you’ll learn to set it up from scratch using the JSONPlaceholder API.</p>
<hr>
<h2>🌟 Why HTTP Interface Client?</h2>
<p>In this section, we clarify Why HTTP Interface Client? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Declarative: Define an interface and annotate methods for HTTP calls.</li>
<li>Type-safe: Compiler catches mismatched signatures or misconfigured paths.</li>
<li>Minimal boilerplate: One bean definition, no manual proxy or template code.</li>
<li>Spring-friendly: Leverages Spring Framework 6.2’s <code>@HttpExchange</code>, <code>@GetExchange</code>, and <code>WebClientAdapter</code>.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 21 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse, etc.)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the Web starter to your build:</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure Base URL</h2>
<p>Add to <code>application.yml</code> or <code>application.properties</code>:</p>
<pre><code class="language-yaml">jsonplaceholder:
  base-url: https://jsonplaceholder.typicode.com
</code></pre>
<pre><code class="language-properties">jsonplaceholder.base-url=https://jsonplaceholder.typicode.com
</code></pre>
<hr>
<h2>🛠️ Step 3: Define DTO and Client Interface</h2>
<h3>Java</h3>
<pre><code class="language-java">// src/main/java/com/example/client/dto/PostDTO.java
package com.example.client.dto;

public record PostDTO(
    Integer userId,
    Integer id,
    String title,
    String body
) {}

// src/main/java/com/example/client/JsonPlaceholderClient.java
package com.example.client;

import com.example.client.dto.PostDTO;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;

@HttpExchange(url = "${jsonplaceholder.base-url}", accept = "application/json")
public interface JsonPlaceholderClient {

    @GetExchange("/posts")
    List&#x3C;PostDTO> getPosts();

    @GetExchange("/posts/{id}")
    PostDTO getPost(@PathVariable("id") Integer id);
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">// src/main/kotlin/com/example/client/dto/PostDTO.kt
package com.example.client.dto

data class PostDTO(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

// src/main/kotlin/com/example/client/JsonPlaceholderClient.kt
package com.example.client

import com.example.client.dto.PostDTO
import org.springframework.web.service.annotation.GetExchange
import org.springframework.web.service.annotation.HttpExchange
import org.springframework.web.bind.annotation.PathVariable

@HttpExchange(url = "${jsonplaceholder.base-url}", accept = "application/json")
interface JsonPlaceholderClient {

    @GetExchange("/posts")
    fun getPosts(): List&#x3C;PostDTO>

    @GetExchange("/posts/{id}")
    fun getPost(@PathVariable("id") id: Int): PostDTO
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Configure the Client Bean</h2>
<h3>Java</h3>
<pre><code class="language-java">// src/main/java/com/example/config/HttpClientConfig.java
package com.example.config;

import com.example.client.JsonPlaceholderClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.invoker.RestClient;

@Configuration
public class HttpClientConfig {

    @Bean
    public JsonPlaceholderClient jsonPlaceholderClient(RestClient.Builder restClientBuilder) {
        RestClient restClient = restClientBuilder
            .baseUrl("https://jsonplaceholder.typicode.com")
            .build();

        var factory = HttpServiceProxyFactory
            .builderFor(RestClientAdapter.create(restClient))
            .build();

        return factory.createClient(JsonPlaceholderClient.class);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">// src/main/kotlin/com/example/config/HttpClientConfig.kt
package com.example.config

import com.example.client.JsonPlaceholderClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.support.RestClientAdapter
import org.springframework.web.service.invoker.HttpServiceProxyFactory
import org.springframework.web.service.invoker.RestClient

@Configuration
class HttpClientConfig {

    @Bean
    fun jsonPlaceholderClient(restClientBuilder: RestClient.Builder): JsonPlaceholderClient {
        val restClient = restClientBuilder
            .baseUrl("https://jsonplaceholder.typicode.com")
            .build()

        val factory = HttpServiceProxyFactory
            .builderFor(RestClientAdapter.create(restClient))
            .build()

        return factory.createClient(JsonPlaceholderClient::class.java)
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Implement Service and Controller</h2>
<h3>Java</h3>
<pre><code class="language-java">// src/main/java/com/example/service/PostService.java
package com.example.service;

import com.example.client.JsonPlaceholderClient;
import com.example.client.dto.PostDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {
    private final JsonPlaceholderClient client;

    public List&#x3C;PostDTO> getAllPosts() {
        return client.getPosts();
    }

    public PostDTO getPostById(Integer id) {
        return client.getPost(id);
    }
}

// src/main/java/com/example/controller/PostController.java
package com.example.controller;

import com.example.client.dto.PostDTO;
import com.example.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/posts")
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;

    @GetMapping
    public List&#x3C;PostDTO> getAllPosts() {
        return postService.getAllPosts();
    }

    @GetMapping("/{id}")
    public ResponseEntity&#x3C;PostDTO> getPostById(@PathVariable Integer id) {
        PostDTO post = postService.getPostById(id);
        return post != null ? ResponseEntity.ok(post) : ResponseEntity.notFound().build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">// src/main/kotlin/com/example/service/PostService.kt
package com.example.service

import com.example.client.JsonPlaceholderClient
import com.example.client.dto.PostDTO
import org.springframework.stereotype.Service

@Service
class PostService(private val client: JsonPlaceholderClient) {
    fun getAllPosts(): List&#x3C;PostDTO> = client.getPosts()
    fun getPostById(id: Int): PostDTO = client.getPost(id)
}

// src/main/kotlin/com/example/controller/PostController.kt
package com.example.controller

import com.example.client.dto.PostDTO
import com.example.service.PostService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/posts")
class PostController(private val postService: PostService) {
    @GetMapping
    fun getAllPosts(): List&#x3C;PostDTO> = postService.getAllPosts()

    @GetMapping("/{id}")
    fun getPostById(@PathVariable id: Int): ResponseEntity&#x3C;PostDTO> =
        ResponseEntity.ok(postService.getPostById(id))
}
</code></pre>
<hr>
<h2>▶️ Run the Application</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Test Endpoints</h2>
<pre><code class="language-bash">curl http://localhost:8080/posts
curl http://localhost:8080/posts/1
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot HTTP Interface Client implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[REST API]]></category>
      <category><![CDATA[Rest Client]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-http-interface-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Async Tasks with Virtual Thread]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-async-tasks-virtual</link>
      <description><![CDATA[Learn how to run asynchronous tasks with `@Async` on JDK 21 virtual threads in Spring Boot.]]></description>
      <pubDate>Sat, 26 Apr 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-04-27T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-async-tasks-virtual</guid>
      <content:encoded><![CDATA[<p>Spring Boot 3.2+ lets you combine the simplicity of <code>@Async</code> with JDK 21 virtual threads for ultra-lightweight concurrency. Offload work to isolated virtual threads without complex pool configs.</p>
<hr>
<h2>🌟 Why Use Virtual Thread in Spring Boot?</h2>
<p>In this section, we clarify Why Use Virtual Thread in Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Ultra-Lightweight: Virtual threads are thousands of times cheaper than platform threads.</li>
<li>Non-Blocking: <code>@Async</code> methods run off the main thread, improving responsiveness.</li>
<li>Scalable: Handle high concurrency with minimal resource overhead.</li>
<li>Simple Config: Enable with a single property, no custom executors needed.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>In this section, we clarify Prerequisites and summarize the key points you will apply in implementation.</p>
<ul>
<li>☕ Java Development Kit (JDK) 21 or higher</li>
<li>📦 Spring Boot 3.2+</li>
<li>🔤 IDE (IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable async processing, include spring-boot-starter-web in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
</code></pre>
<hr>
<h2>🛠️ Step 2: Enable Virtual Threads</h2>
<p>Add to <code>application.yml</code> or <code>application.properties</code>:</p>
<pre><code class="language-yaml">spring:
  threads:
    virtual:
      enabled: true
</code></pre>
<pre><code class="language-properties">spring.threads.virtual.enabled=true
</code></pre>
<p>This setting auto-configures the following:</p>
<ul>
<li><code>applicationTaskExecutor</code> for <code>@Async</code> support</li>
<li>Task scheduler for <code>@Scheduled</code> methods</li>
<li>Servlet container thread pools (Tomcat/Jetty) to use virtual threads</li>
</ul>
<hr>
<h2>🛠️ Step 3: Enable Async Support</h2>
<p>Annotate your main application class in Java or Kotlin:</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class AsyncVirtualApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncVirtualApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableAsync

@SpringBootApplication
@EnableAsync
class AsyncVirtualApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;AsyncVirtualApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Define an Async Service</h2>
<p>Create a service with <code>@Async</code>. It will run each call on a new virtual thread.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalTime;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AsyncVirtualService {

  @Async
  public void runTask() {
    log.info("[{}] Async start on {}", LocalTime.now(), Thread.currentThread());
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ignored) {}
    log.info("[{}] Async end on {}", LocalTime.now(), Thread.currentThread());
  }

  @Async
  public CompletableFuture&#x3C;String> runAndReturn() throws InterruptedException {
    Thread.sleep(500);
    return CompletableFuture.completedFuture("Completed");
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service

import java.time.LocalTime
import java.util.concurrent.CompletableFuture

@Service
class AsyncVirtualService {
  private val log = LoggerFactory.getLogger(AsyncVirtualService::class.java)

  @Async
  fun runTask() {
    log.info("[{}] Async start on {}", LocalTime.now(), Thread.currentThread())
    try {
      Thread.sleep(1000)
    } catch (_: InterruptedException) {}
    log.info("[{}] Async end on {}", LocalTime.now(), Thread.currentThread())
  }

  @Async
  fun runAndReturn(): CompletableFuture&#x3C;String> {
    Thread.sleep(500)
    return CompletableFuture.completedFuture("Completed")
  }
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Trigger via REST Controller</h2>
<p>Expose endpoints to invoke your async methods:</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
public class AsyncVirtualController {

  private final AsyncVirtualService service;

  @GetMapping("/run")
  public String triggerRun() {
    service.runTask();
    return "Async virtual thread task triggered";
  }

  @GetMapping("/run-return")
  public String triggerRunAndReturn() throws Exception {
    var future = service.runAndReturn();
    return future.get();
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

import lombok.RequiredArgsConstructor

@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
class AsyncVirtualController(private val service: AsyncVirtualService) {

  @GetMapping("/run")
  fun triggerRun(): String {
    service.runTask()
    return "Async virtual thread task triggered"
  }

  @GetMapping("/run-return")
  @Throws(Exception::class)
  fun triggerRunAndReturn(): String {
    val future = service.runAndReturn()
    return future.get()
  }
}
</code></pre>
<hr>
<h2>▶️ Run the App</h2>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Test Endpoints</h2>
<p>Trigger void task</p>
<pre><code class="language-bash">curl http://localhost:8080/async/run
</code></pre>
<p>Check logs for virtual thread start/end.</p>
<p>Trigger task with return</p>
<pre><code class="language-bash">curl http://localhost:8080/async/run-return
# returns "Completed"
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Async Tasks with Virtual Thread implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Async Tasks]]></category>
      <category><![CDATA[Virtual Thread]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-async-virtual-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot AI Integration]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-ai</link>
      <description><![CDATA[Learn how to integrate AI models into Spring Boot applications using Spring AI for modular, portable, and observable AI workflows.]]></description>
      <pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-04-24T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-ai</guid>
      <content:encoded><![CDATA[<p>Spring AI is a framework for AI engineering that applies Spring’s portability and modular design principles to AI workloads. It lets you build AI-driven applications using familiar Spring idioms and POJOs.</p>
<hr>
<h2>🌟 Why Use Spring AI?</h2>
<p>In this section, we clarify Why Use Spring AI? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Provider-agnostic portability: Write code once and switch between AI providers (OpenAI, Anthropic, Azure, etc.) without changes.</li>
<li>POJO-based design: Model AI inputs and outputs as plain Java/Kotlin objects for type safety and easy integration.</li>
<li>Structured outputs: Automatically map model responses to your domain objects.</li>
<li>Vector store integration: Seamless support for major vector databases (Chroma, Pinecone, Redis, etc.) via a unified API.</li>
<li>Tool/function calling: Enable LLMs to invoke custom functions or services for real-time data.</li>
<li>Observability &#x26; evaluation: Built-in metrics and evaluation utilities to monitor AI operations and detect hallucinations.</li>
<li>Chat abstractions: Fluent ChatClient API, similar to Spring’s WebClient, for building conversational agents.</li>
<li>Retrieval-augmented generation (RAG): Simplify document-based QA and memory-backed chat with advisors and memory APIs.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🐳 Spring Boot 3+</li>
<li>🔑 OpenAI API Key (set as environment variable <code>OPENAI_API_KEY</code>)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include Spring AI starter for OpenAI, Spring Web, and Lombok.</p>
<h3>Maven</h3>
<pre><code class="language-xml">&#x3C;dependencies>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.springframework.ai&#x3C;/groupId>
    &#x3C;artifactId>spring-ai-starter-model-openai&#x3C;/artifactId>
  &#x3C;/dependency>
  &#x3C;dependency>
    &#x3C;groupId>org.projectlombok&#x3C;/groupId>
    &#x3C;artifactId>lombok&#x3C;/artifactId>
    &#x3C;scope>provided&#x3C;/scope>
  &#x3C;/dependency>
&#x3C;/dependencies>
</code></pre>
<h3>Gradle</h3>
<pre><code class="language-groovy">plugins {
  id 'org.springframework.boot' version '3.2.0'
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.ai:spring-ai-starter-model-openai'
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Configuration</h2>
<p>In <code>application.yml</code>, configure your OpenAI key and set the ChatClient model:</p>
<pre><code class="language-yaml">spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4.1-mini
</code></pre>
<hr>
<h2>🛠️ Step 3: Implement the Service Layer</h2>
<p>Create an <code>AIService</code> to wrap your ChatClient. Inject <code>ChatClient.Builder</code> and build the client.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.ai.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
public class AIService {
    private final ChatClient chatClient;

    public AIService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String getJoke(String topic) {
        return chatClient.prompt()
                .user(u -> u.text("Tell me a joke about {topic}").param("topic", topic))
                .call()
                .content();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.ai.service

import org.springframework.ai.chat.client.ChatClient
import org.springframework.stereotype.Service

@Service
class AIService(builder: ChatClient.Builder) {
    private val chatClient: ChatClient = builder.build()

    fun getJoke(topic: String): String {
        return chatClient.prompt()
            .user { it.text("Tell me a joke about {topic}").param("topic", topic) }
            .call()
            .content()
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Expose a REST Controller</h2>
<p>Create a <code>ChatController</code> to expose your AIService over HTTP.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.ai.controller;

import com.example.ai.service.AIService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class ChatController {

    private final AIService aiService;

    @GetMapping("/joke")
    public String getJoke(@RequestParam(defaultValue = "dogs") String topic) {
        return aiService.getJoke(topic);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.ai.controller

import com.example.ai.service.AIService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
class ChatController(private val aiService: AIService) {

    @GetMapping("/joke")
    fun getJoke(@RequestParam(defaultValue = "dogs") topic: String): String {
        return aiService.getJoke(topic)
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Start your Spring Boot app:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
# or
gradle bootRun
</code></pre>
<hr>
<h2>🧪 Testing the Integration</h2>
<p>Call your service via HTTP:</p>
<pre><code class="language-bash">curl -X GET "http://localhost:8080/joke?topic=dogs"
# Returns a dog joke generated by the AI model.
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot AI Integration implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Spring AI]]></category>
      <category><![CDATA[Artificial Intelligence]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-ai-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot JPA Auditing]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-jpa-auditing</link>
      <description><![CDATA[Learn how to automatically track created and modified fields in your Spring Boot applications using JPA Auditing.]]></description>
      <pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-04-11T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-jpa-auditing</guid>
      <content:encoded><![CDATA[<p>JPA Auditing in Spring Boot allows you to automatically track who created or modified an entity and when. This is extremely useful for audit logs, history tracking, and debugging.</p>
<hr>
<h2>🌟 Why Use JPA Auditing?</h2>
<p>In this section, we clarify Why Use JPA Auditing? and summarize the key points you will apply in implementation.</p>
<ul>
<li>📅 Auto track <code>createdDate</code>, <code>lastModifiedDate</code></li>
<li>👤 Record <code>createdBy</code>, <code>modifiedBy</code></li>
<li>🧼 Cleaner code by avoiding manual field setting</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Before you start, prepare the following:</p>
<ul>
<li>Java 17+</li>
<li>Spring Boot 3.x project with Spring Data JPA</li>
<li>A configured datasource (H2, PostgreSQL, MySQL, etc.)</li>
<li>Basic familiarity with JPA entities and repository flow</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Make sure you have the following dependencies in your project:</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-data-jpa&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Enable JPA Auditing</h2>
<p>Add <code>@EnableJpaAuditing</code> to your main class or a configuration class.</p>
<h3>Java</h3>
<pre><code class="language-java">import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class DemoApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;DemoApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Create AuditorAware Bean</h2>
<p>This bean tells Spring Security who the current user is. Here's a basic static user example:</p>
<h3>Java</h3>
<pre><code class="language-java">@Configuration
public class AuditingConfig {

  @Bean
  public AuditorAware&#x3C;String> auditorProvider() {
    return () -> Optional.of("Şuayb");
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">@Configuration
class AuditingConfig {

  @Bean
  fun auditorProvider(): AuditorAware&#x3C;String> = AuditorAware { Optional.of("Şuayb") }
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Annotate Your Entity</h2>
<p>Use auditing annotations like <code>@CreatedDate</code>, <code>@LastModifiedDate</code>, etc.</p>
<h3>Java</h3>
<pre><code class="language-java">@Entity
@EntityListeners(AuditingEntityListener.class)
public class Article {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  @CreatedDate
  private LocalDateTime createdDate;

  @LastModifiedDate
  private LocalDateTime lastModifiedDate;

  @CreatedBy
  private String createdBy;

  @LastModifiedBy
  private String lastModifiedBy;

  // getters &#x26; setters
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">@Entity
@EntityListeners(AuditingEntityListener::class)
data class Article(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long? = null,

  var title: String? = null,

  @CreatedDate
  var createdDate: LocalDateTime? = null,

  @LastModifiedDate
  var lastModifiedDate: LocalDateTime? = null,

  @CreatedBy
  var createdBy: String? = null,

  @LastModifiedBy
  var lastModifiedBy: String? = null
)
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Now when you save an entity using <code>JpaRepository</code>, auditing fields will be populated automatically.</p>
<hr>
<h2>🧪 Testing</h2>
<p>You can test JPA Auditing functionality by saving an entity and checking whether the audit fields are populated.</p>
<h3>Java</h3>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureTestDatabase
@Transactional
class ArticleRepositoryTest {

  @Autowired
  private ArticleRepository articleRepository;

  @Test
  void testAuditFieldsAreSet() {
    Article article = new Article();
    article.setTitle("Test Title");

    Article saved = articleRepository.save(article);

    assertNotNull(saved.getCreatedDate());
    assertNotNull(saved.getLastModifiedDate());
    assertEquals("Şuayb", saved.getCreatedBy());
    assertEquals("Şuayb", saved.getLastModifiedBy());
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">@SpringBootTest
@AutoConfigureTestDatabase
@Transactional
class ArticleRepositoryTest {

  @Autowired
  lateinit var articleRepository: ArticleRepository

  @Test
  fun `should set audit fields`() {
    val article = Article().apply {
      title = "Test Title"
    }

    val saved = articleRepository.save(article)

    assertNotNull(saved.createdDate)
    assertNotNull(saved.lastModifiedDate)
    assertEquals("Şuayb", saved.createdBy)
    assertEquals("Şuayb", saved.lastModifiedBy)
  }
}
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot JPA Auditing implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Spring Data]]></category>
      <category><![CDATA[JPA]]></category>
      <category><![CDATA[Auditing]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-jpa-auditing-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Test-Driven Development]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-tdd</link>
      <description><![CDATA[Learn how to apply TDD in Spring Boot by writing unit tests first, implementing logic, and ensuring code quality through testing.]]></description>
      <pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-04-10T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-tdd</guid>
      <content:encoded><![CDATA[<p>Test-Driven Development (TDD) helps you build robust applications by writing tests before implementing business logic. With Spring Boot, writing unit tests is fast, clean, and efficient.</p>
<hr>
<h2>🌟 Why Use TDD in Spring Boot?</h2>
<p>In this section, we clarify Why Use TDD in Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Fail Fast: Catch issues early during development.</li>
<li>Clean Design: Forces modular, testable code structure.</li>
<li>Refactor with Confidence: Tests serve as a safety net.</li>
<li>Documentation: Tests describe intended behavior clearly.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Make sure you have:</p>
<ul>
<li>☕ JDK 17+ installed</li>
<li>📦 Maven or Gradle</li>
<li>🔤 A Java IDE like IntelliJ IDEA or Eclipse</li>
</ul>
<hr>
<h2>🧪 Step 1: Add Test Dependencies</h2>
<p>Add Spring Boot's test starter to your project.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-test&#x3C;/artifactId>
  &#x3C;scope>test&#x3C;/scope>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">testImplementation 'org.springframework.boot:spring-boot-starter-test'
</code></pre>
<hr>
<h2>🧪 Step 2: Write Your First Test</h2>
<p>Create a simple unit test before implementing the service.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.tdd;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class GreetingServiceTest {

    @Test
    void shouldReturnGreetingMessage() {
        GreetingService service = new GreetingService();
        String result = service.greet("World");
        assertEquals("Hello, World!", result);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.tdd

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class GreetingServiceTest {

    @Test
    fun shouldReturnGreetingMessage() {
        val service = GreetingService()
        val result = service.greet("World")
        assertEquals("Hello, World!", result)
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Implement the Service</h2>
<p>Now implement the <code>GreetingService</code> to pass the test.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.tdd;

public class GreetingService {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.tdd

class GreetingService {
    fun greet(name: String): String {
        return "Hello, $name!"
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Tests</h2>
<p>Use your build tool to run the tests:</p>
<pre><code class="language-bash">./mvnw test
</code></pre>
<p>Or with Gradle:</p>
<pre><code class="language-bash">gradle test
</code></pre>
<p>You should see the test pass ✅</p>
<hr>
<h3>Refactor and Repeat</h3>
<p>Now that your test passes, you can safely refactor your code. TDD is a loop:</p>
<ol>
<li>Red – Write a failing test</li>
<li>Green – Make it pass</li>
<li>Refactor – Improve the code</li>
</ol>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Test-Driven Development implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[TDD]]></category>
      <category><![CDATA[Testing]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-tdd-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Circuit Breaker]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-circuit-breaker</link>
      <description><![CDATA[Learn how to implement Circuit Breaker in Spring Boot applications for resilient microservices.]]></description>
      <pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-03-14T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-circuit-breaker</guid>
      <content:encoded><![CDATA[<p>Spring Boot Circuit Breaker is a fault-tolerance mechanism used to prevent cascading failures in a microservices architecture. It helps applications handle failures gracefully by detecting failures and stopping excessive requests to unhealthy services. This guide will walk you through implementing Circuit Breaker using Resilience4j in Spring Boot.</p>
<hr>
<h2>🌟 Why Use Circuit Breaker?</h2>
<p>In this section, we clarify Why Use Circuit Breaker? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Prevents cascading failures in microservices.</li>
<li>Improves application resilience by stopping excessive failed requests.</li>
<li>Automatically recovers when services become healthy again.</li>
<li>Reduces latency by preventing unnecessary waits for failed services.</li>
<li>Configurable retry strategies to manage failures efficiently.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🌐 Spring Boot &#x26; Spring Cloud</li>
<li>🛠 A REST API to demonstrate Circuit Breaker behavior</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>In this section, we clarify Step 1: Add Dependencies and summarize the key points you will apply in implementation.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.cloud&#x3C;/groupId>
  &#x3C;artifactId>spring-cloud-starter-circuitbreaker-resilience4j&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure Circuit Breaker Properties</h2>
<p>Define Circuit Breaker settings in <code>application.yml</code>.</p>
<pre><code class="language-yaml">resilience4j:
  circuitbreaker:
    instances:
      externalService:
        failure-rate-threshold: 50
        slow-call-rate-threshold: 50
        slow-call-duration-threshold: 2000ms
        permitted-number-of-calls-in-half-open-state: 3
        sliding-window-size: 10
        minimum-number-of-calls: 5
        wait-duration-in-open-state: 5s
</code></pre>
<hr>
<h2>🛠️ Step 3: Implement Circuit Breaker in a REST Service</h2>
<p>In this section, we clarify Step 3: Implement Circuit Breaker in a REST Service and summarize the key points you will apply in implementation.</p>
<h3>Create a Service to Call an External API</h3>
<p>The following example gives practical context for Create a Service to Call an External API and can be applied directly.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.circuitbreaker.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class ExternalService {

    private final RestTemplate restTemplate = new RestTemplate();

    @CircuitBreaker(name = "externalService", fallbackMethod = "fallbackResponse")
    public String callExternalAPI() {
        return restTemplate.getForObject("http://unreliable-service/api/data", String.class);
    }

    public String fallbackResponse(Exception e) {
        return "Fallback response: Service is unavailable!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.circuitbreaker.service

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate

@Service
class ExternalService {
    private val restTemplate = RestTemplate()

    @CircuitBreaker(name = "externalService", fallbackMethod = "fallbackResponse")
    fun callExternalAPI(): String {
        return restTemplate.getForObject("http://unreliable-service/api/data", String::class.java) ?: ""
    }

    fun fallbackResponse(e: Exception): String {
        return "Fallback response: Service is unavailable!"
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Create a REST Controller</h2>
<h3>Java</h3>
<pre><code class="language-java">package com.example.circuitbreaker.controller;

import com.example.circuitbreaker.service.ExternalService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class CircuitBreakerController {

    private final ExternalService externalService;

    public CircuitBreakerController(ExternalService externalService) {
        this.externalService = externalService;
    }

    @GetMapping("/data")
    public String fetchData() {
        return externalService.callExternalAPI();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.circuitbreaker.controller

import com.example.circuitbreaker.service.ExternalService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
class CircuitBreakerController(private val externalService: ExternalService) {

    @GetMapping("/data")
    fun fetchData(): String {
        return externalService.callExternalAPI()
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Start the application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<h2>🧪 Test the Circuit Breaker</h2>
<p>Test the Circuit Breaker:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/data
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Circuit Breaker implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Spring Cloud]]></category>
      <category><![CDATA[Circuit Breaker]]></category>
      <category><![CDATA[Microservice]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-circuit-breaker-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Eureka Server]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-eureka-server</link>
      <description><![CDATA[Learn how to set up and configure a Spring Boot Eureka Server for service discovery in microservices architecture.]]></description>
      <pubDate>Sun, 23 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-24T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-eureka-server</guid>
      <content:encoded><![CDATA[<p>Spring Boot Eureka Server is a service registry that enables service discovery in a microservices architecture. It allows microservices to register themselves and discover other services dynamically. This guide will walk you through setting up and configuring a Spring Boot Eureka Server.</p>
<hr>
<h2>🌟 Why Use Eureka Server?</h2>
<p>In this section, we clarify Why Use Eureka Server? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Service Discovery: Automatically register and discover microservices.</li>
<li>Load Balancing: Enables client-side load balancing with Ribbon.</li>
<li>Failover Support: Services can find alternative instances if one fails.</li>
<li>Scalability: Easily scale microservices without manual configuration.</li>
<li>Spring Cloud Integration: Works seamlessly with Spring Boot applications.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>In this section, we clarify Step 1: Add Dependencies and summarize the key points you will apply in implementation.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.cloud&#x3C;/groupId>
    &#x3C;artifactId>spring-cloud-starter-netflix-eureka-server&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
</code></pre>
<hr>
<h2>🛠️ Step 2: Create the Eureka Server Application</h2>
<h3>Java</h3>
<pre><code class="language-java">package com.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.eurekaserver

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
class EurekaServerApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;EurekaServerApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Configure the Eureka Server</h2>
<p>Create an <code>application.yml</code> file for Eureka Server configuration.</p>
<pre><code class="language-yaml">server:
  port: 8761
spring:
  application:
    name: eureka-server
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    wait-time-in-ms-when-sync-empty: 5
</code></pre>
<blockquote>
<p>Note: The Eureka Server does not register itself.</p>
</blockquote>
<hr>
<h2>▶️ Running the Eureka Server</h2>
<p>Start the Eureka Server application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<p>Access the Eureka Server dashboard:</p>
<pre><code class="language-bash">http://localhost:8761/
</code></pre>
<hr>
<h2>🛠️ Step 4: Register a Client Application</h2>
<p>In this section, we clarify Step 4: Register a Client Application and summarize the key points you will apply in implementation.</p>
<h3>Add Dependencies</h3>
<p>Add the Eureka Client starter first so this application can register with the server and participate in service discovery.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.cloud&#x3C;/groupId>
    &#x3C;artifactId>spring-cloud-starter-netflix-eureka-client&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
</code></pre>
<h3>Client Application Configuration</h3>
<p>In the client's <code>application.yml</code>, add the following:</p>
<pre><code class="language-yaml">spring:
  application:
    name: eureka-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
</code></pre>
<h3>Client Application Code</h3>
<p>The sample below provides a minimal client implementation so you can validate the server integration end to end.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.eurekaclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@RequestMapping("/client")
public class EurekaClientApplication {

    @GetMapping
    public String getClientMessage() {
        return "Hello from Eureka Client!";
    }

    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.eurekaclient

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
@RestController
@RequestMapping("/client")
class EurekaClientApplication {

    @GetMapping
    fun getClientMessage(): String {
        return "Hello from Eureka Client!"
    }
}

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;EurekaClientApplication>(*args)
}
</code></pre>
<hr>
<h2>▶️ Running the Client Application</h2>
<p>Run the Eureka Client application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<p>Check if the client has registered with Eureka Server by visiting:</p>
<pre><code class="language-bash">http://localhost:8761/
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Eureka Server implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Spring Cloud]]></category>
      <category><![CDATA[Eureka Server]]></category>
      <category><![CDATA[Microservice]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-eureka-server-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Config Server]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-config-server</link>
      <description><![CDATA[Learn how to use Spring Boot Config Server to centralize and manage application configurations efficiently.]]></description>
      <pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-23T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-config-server</guid>
      <content:encoded><![CDATA[<p>Spring Boot Config Server enables centralized configuration management for distributed applications, allowing dynamic configuration updates without requiring service restarts. This guide covers how to set up and use Spring Boot Config Server efficiently.</p>
<hr>
<h2>🌟 Why Use Config Server?</h2>
<p>In this section, we clarify Why Use Config Server? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Centralized Configuration: Manage application settings in a single place.</li>
<li>Dynamic Updates: Update configurations without restarting services.</li>
<li>Environment Profiles: Support different configurations for dev, test, and production environments.</li>
<li>Security &#x26; Access Control: Store sensitive configurations securely.</li>
<li>Integration with Git: Keep configuration versions managed and trackable.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>📁 Git Repository (for storing configurations)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the necessary dependencies for Spring Cloud Config Server.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.cloud&#x3C;/groupId>
    &#x3C;artifactId>spring-cloud-config-server&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.cloud:spring-cloud-config-server'
</code></pre>
<hr>
<h2>🛠️ Step 2: Create the Config Server Application</h2>
<h3>Java</h3>
<pre><code class="language-java">package com.example.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.configserver

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.config.server.EnableConfigServer

@SpringBootApplication
@EnableConfigServer
class ConfigServerApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;ConfigServerApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Configure the Config Server</h2>
<p>Create an <code>application.yml</code> file to specify the Git repository for storing configurations.</p>
<pre><code class="language-yaml">server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/example/config-repo
          default-label: main
</code></pre>
<blockquote>
<p>Note: Replace <code>https://github.com/example/config-repo</code> with your actual Git repository.</p>
</blockquote>
<hr>
<h2>🛠️ Step 4: Create a Configuration Repository</h2>
<p>In your Git repository (e.g., <code>config-repo</code>), create an <code>application.yml</code> file with the following content:</p>
<pre><code class="language-yaml">config.key: 'Hello from Config Server!'
</code></pre>
<p>Commit and push the file to the repository:</p>
<pre><code class="language-bash">git add application.yml
git commit -m "Add config properties"
git push origin main
</code></pre>
<hr>
<h2>▶️ Running the Config Server</h2>
<p>Start the Config Server application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<p>Verify it is running by accessing:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8888/application/default
</code></pre>
<p>Expected Output:</p>
<pre><code class="language-json">{
  "name": "application",
  "profiles": ["default"],
  "propertySources": [
    {
      "name": "https://github.com/example/config-repo/application.yml",
      "source": {
        "config.key": "Hello from Config Server!"
      }
    }
  ]
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Configure a Client Application</h2>
<p>In this section, we clarify Step 5: Configure a Client Application and summarize the key points you will apply in implementation.</p>
<h3>Add Dependencies</h3>
<p>First, include the Config Client starter so this service can pull centralized configuration during bootstrap.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.cloud&#x3C;/groupId>
    &#x3C;artifactId>spring-cloud-starter-config&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.cloud:spring-cloud-starter-config'
</code></pre>
<h3>Client Application Configuration</h3>
<p>In the client's <code>bootstrap.yml</code>, add the following:</p>
<pre><code class="language-yaml">spring:
  application:
    name: config-client
  cloud:
    config:
      uri: http://localhost:8888
</code></pre>
<h3>Client Application Code</h3>
<p>The sample below provides a minimal client implementation so you can validate the server integration end to end.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.configclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;

@SpringBootApplication
@RestController
@RequestMapping("/config")
public class ConfigClientApplication {

  @Value("${config.key:not found}")
  private String configValue;

  @GetMapping
  public String getConfigValue() {
    return "Config Value: " + configValue;
  }

  public static void main(String[] args) {
    SpringApplication.run(ConfigClientApplication.class, args);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.configclient

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.beans.factory.annotation.Value

@SpringBootApplication
@RestController
@RequestMapping("/config")
class ConfigClientApplication {

    @Value("\${config.key:not found}")
    lateinit var configValue: String

    @GetMapping
    fun getConfigValue(): String {
        return "Config Value: \$configValue"
    }
}

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;ConfigClientApplication>(*args)
}
</code></pre>
<hr>
<h2>▶️ Running the Client Application</h2>
<p>Run the Config Client application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<p>Retrieve the configuration from Config Server:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/config
</code></pre>
<p>Expected Output:</p>
<pre><code class="language-plaintext">Config Value: Hello from Config Server!
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Config Server implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Spring Cloud]]></category>
      <category><![CDATA[Config Server]]></category>
      <category><![CDATA[Microservice]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-config-server-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Docker Integration]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-docker</link>
      <description><![CDATA[Learn how to build and deploy Spring Boot applications using Docker with Buildpacks and Jib for efficient containerization.]]></description>
      <pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-22T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-docker</guid>
      <content:encoded><![CDATA[<p>Spring Boot provides multiple ways to containerize applications efficiently using Docker, Buildpacks, and Jib. This guide explores different approaches to building and deploying Spring Boot applications with Docker.</p>
<hr>
<h2>🌟 Why Use Docker for Spring Boot?</h2>
<p>In this section, we clarify Why Use Docker for Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Portable Deployment: Run applications consistently across environments.</li>
<li>Scalability: Easily scale and manage containerized applications.</li>
<li>Lightweight and Efficient: Optimize resource utilization with minimal overhead.</li>
<li>DevOps Friendly: Seamlessly integrate with CI/CD pipelines.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🐳 Docker installed and running</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable Docker support, add the Spring Boot Maven Plugin, Jib Plugin, and Spring Boot Web Starter to your build tool.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependencies>
    &#x3C;dependency>
        &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
        &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
    &#x3C;/dependency>
&#x3C;/dependencies>

&#x3C;build>
    &#x3C;plugins>
        &#x3C;plugin>
            &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
            &#x3C;artifactId>spring-boot-maven-plugin&#x3C;/artifactId>
        &#x3C;/plugin>
        &#x3C;plugin>
            &#x3C;groupId>com.google.cloud.tools&#x3C;/groupId>
            &#x3C;artifactId>jib-maven-plugin&#x3C;/artifactId>
            &#x3C;version>3.3.2&#x3C;/version>
        &#x3C;/plugin>
    &#x3C;/plugins>
&#x3C;/build>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'com.google.cloud.tools.jib' version '3.3.2'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}
</code></pre>
<p>To enable Docker support, add the Spring Boot Maven Plugin and Jib Plugin to your build tool.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;build>
    &#x3C;plugins>
        &#x3C;plugin>
            &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
            &#x3C;artifactId>spring-boot-maven-plugin&#x3C;/artifactId>
        &#x3C;/plugin>
        &#x3C;plugin>
            &#x3C;groupId>com.google.cloud.tools&#x3C;/groupId>
            &#x3C;artifactId>jib-maven-plugin&#x3C;/artifactId>
            &#x3C;version>3.3.2&#x3C;/version>
        &#x3C;/plugin>
    &#x3C;/plugins>
&#x3C;/build>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'com.google.cloud.tools.jib' version '3.3.2'
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Create a Simple Spring Boot Application</h2>
<p>Define a REST endpoint to deploy inside a Docker container.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.docker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DockerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DockerApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello from Spring Boot running in Docker!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.docker

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
@RestController
class DockerApplication {

    @GetMapping("/hello")
    fun hello(): String = "Hello from Spring Boot running in Docker!"
}

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;DockerApplication>(*args)
}
</code></pre>
<hr>
<h2>▶️ Step 3: Build and Run a Docker Image</h2>
<p>In this section, we clarify Step 3: Build and Run a Docker Image and summarize the key points you will apply in implementation.</p>
<h3>Option 1: Using Buildpacks</h3>
<p>Spring Boot provides built-in support for creating Docker images without a <code>Dockerfile</code> using Buildpacks.</p>
<pre><code class="language-bash">mvn spring-boot:build-image
</code></pre>
<pre><code class="language-bash">gradle bootBuildImage
</code></pre>
<p>Run the container:</p>
<pre><code class="language-bash">docker run --rm -p 8080:8080 myproject:0.0.1-SNAPSHOT
</code></pre>
<hr>
<h3>Option 2: Using Jib</h3>
<p>Jib allows building optimized container images without a Docker daemon.</p>
<pre><code class="language-bash">mvn jib:dockerBuild
</code></pre>
<pre><code class="language-bash">gradle jibDockerBuild
</code></pre>
<p>Run the Jib-built container:</p>
<pre><code class="language-bash">docker run --rm -p 8080:8080 myproject:0.0.1-SNAPSHOT
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Once the container is running, test the REST API:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/hello
</code></pre>
<p>Expected Output:</p>
<pre><code class="language-plaintext">Hello from Spring Boot running in Docker!
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Docker Integration implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Docker]]></category>
      <category><![CDATA[Containerization]]></category>
      <category><![CDATA[Jib]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-docker-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Kubernetes Integration]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-kubernetes</link>
      <description><![CDATA[Learn how to deploy Spring Boot applications on Kubernetes for scalable, containerized microservices.]]></description>
      <pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-22T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-kubernetes</guid>
      <content:encoded><![CDATA[<p>Spring Boot seamlessly integrates with Kubernetes to provide scalable, containerized applications. This guide explores how to deploy and manage Spring Boot applications in a Kubernetes cluster.</p>
<hr>
<h2>🌟 Why Use Kubernetes for Spring Boot?</h2>
<p>In this section, we clarify Why Use Kubernetes for Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Scalability: Automatically scale applications based on traffic.</li>
<li>Self-Healing: Kubernetes restarts failed containers automatically.</li>
<li>Declarative Configuration: Manage infrastructure with YAML configurations.</li>
<li>Load Balancing &#x26; Service Discovery: Built-in support for routing requests efficiently.</li>
<li>Efficient Resource Utilization: Optimize CPU and memory usage dynamically.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🛠 Docker installed and running</li>
<li>🌐 Kubernetes Cluster (Minikube or a cloud provider like AWS EKS, GKE, or AKS)</li>
<li>🛠 kubectl installed for managing Kubernetes</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the necessary dependencies for Spring Boot Web and Actuator to expose health endpoints.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependencies>
    &#x3C;dependency>
        &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
        &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
    &#x3C;/dependency>
    &#x3C;dependency>
        &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
        &#x3C;artifactId>spring-boot-starter-actuator&#x3C;/artifactId>
    &#x3C;/dependency>
&#x3C;/dependencies>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Create a Simple Spring Boot Application</h2>
<p>Define a REST endpoint to deploy inside Kubernetes.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.kubernetes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class KubernetesApplication {

    public static void main(String[] args) {
        SpringApplication.run(KubernetesApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello from Spring Boot running in Kubernetes!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.kubernetes

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
@RestController
class KubernetesApplication {

    @GetMapping("/hello")
    fun hello(): String = "Hello from Spring Boot running in Kubernetes!"
}

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;KubernetesApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Create Kubernetes Deployment and Service</h2>
<p>Create a deployment.yaml file:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-boot-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-boot-app
  template:
    metadata:
      labels:
        app: spring-boot-app
    spec:
      containers:
        - name: spring-boot-app
          image: myproject:0.0.1-SNAPSHOT
          ports:
            - containerPort: 8080
</code></pre>
<p>Create a service.yaml file:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: spring-boot-service
spec:
  type: LoadBalancer
  selector:
    app: spring-boot-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
</code></pre>
<p>Apply the configurations:</p>
<pre><code class="language-bash">kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Once deployed, retrieve the service URL:</p>
<pre><code class="language-bash">kubectl get services
</code></pre>
<p>Test the endpoint:</p>
<pre><code class="language-bash">curl -X GET http://your-service-ip/hello
</code></pre>
<p>Expected Output:</p>
<pre><code class="language-plaintext">Hello from Spring Boot running in Kubernetes!
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Kubernetes Integration implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[Microservice]]></category>
      <category><![CDATA[Containerization]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-kubernetes-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Async Tasks]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-async-tasks</link>
      <description><![CDATA[Learn how to use @Async in Spring Boot to execute tasks asynchronously without blocking the main thread.]]></description>
      <pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-21T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-async-tasks</guid>
      <content:encoded><![CDATA[<p>Spring Boot provides an easy way to run asynchronous tasks using the <code>@Async</code> annotation. This is useful for executing tasks in the background, improving performance, and avoiding blocking the main thread.</p>
<hr>
<h2>🌟 Why Use @Async in Spring Boot?</h2>
<p>In this section, we clarify Why Use @Async in Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Non-Blocking Execution: Runs tasks asynchronously without blocking the main thread.</li>
<li>Improved Performance: Executes independent tasks in parallel.</li>
<li>Better Scalability: Frees up resources for other processes.</li>
<li>Seamless Integration: Works with Spring Boot’s dependency injection and lifecycle management.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable async processing, include spring-boot-starter-web in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
</code></pre>
<hr>
<h2>🛠️ Step 2: Enable Async in Your Application</h2>
<p>Annotate your main application class with <code>@EnableAsync</code> to enable asynchronous execution.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class AsyncApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableAsync

@SpringBootApplication
@EnableAsync
class AsyncApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;AsyncApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Create an Async Task</h2>
<p>Define an asynchronous method using <code>@Async</code>.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalTime;

@Service
public class AsyncTask {

    @Async
    public void runTask() {
        System.out.println("Async task executed at: " + LocalTime.now());
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import java.time.LocalTime

@Service
class AsyncTask {

    @Async
    fun runTask() {
        println("Async task executed at: ${LocalTime.now()}")
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Create a Controller to Trigger Async Tasks</h2>
<p>Create a REST controller to trigger the asynchronous task.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.async;

import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
public class AsyncController {

    private final AsyncTask asyncTask;

    @GetMapping("/run")
    public String triggerAsyncTask() {
        asyncTask.runTask();
        return "Async task triggered!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.async

import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/async")
class AsyncController(
    private val asyncTask: AsyncTask
) {

    @GetMapping("/run")
    fun triggerAsyncTask(): String {
        asyncTask.runTask()
        return "Async task triggered!"
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the Spring Boot application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<hr>
<h2>🧪 Testing the Async Task</h2>
<p>In this section, we clarify Testing the Async Task and summarize the key points you will apply in implementation.</p>
<h3>Trigger Async Task:</h3>
<p>Call this endpoint to trigger the async workflow and observe execution flow in logs.</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/async/run
</code></pre>
<h3>Expected Console Output:</h3>
<p>Compare your console logs with this output to quickly confirm the behavior is correct.</p>
<pre><code class="language-plaintext">Async task executed at: 12:00:01
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Async Tasks implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Async Tasks]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-async-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Scheduled Tasks]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-scheduled-tasks</link>
      <description><![CDATA[Learn how to use @Scheduled in Spring Boot to run background tasks at fixed intervals or cron expressions.]]></description>
      <pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-21T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-scheduled-tasks</guid>
      <content:encoded><![CDATA[<p>Spring Boot allows developers to easily schedule and execute background tasks using the <code>@Scheduled</code> annotation. This is useful for running periodic jobs, automation tasks, and batch processing without manual intervention.</p>
<hr>
<h2>🌟 Why Use @Scheduled in Spring Boot?</h2>
<p>In this section, we clarify Why Use @Scheduled in Spring Boot? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Automate Tasks: Run jobs periodically without human interaction.</li>
<li>Efficient Resource Usage: Schedule tasks without blocking main threads.</li>
<li>Supports Fixed Rate, Fixed Delay, and Cron Expressions.</li>
<li>Seamless Integration: Works with Spring Boot’s dependency injection and lifecycle management.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable scheduling, you need to include spring-boot-starter-web in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-web&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-web'
</code></pre>
<blockquote>
<p>Why <code>spring-boot-starter-web</code>?
Spring Boot applications using <code>@Scheduled</code> need a running Spring Context. Including <code>spring-boot-starter-web</code> ensures that the application lifecycle is properly managed.</p>
</blockquote>
<hr>
<h2>🛠️ Step 2: Enable Scheduling in Your Application</h2>
<p>To enable scheduling, annotate your main application class with <code>@EnableScheduling</code>.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.scheduled;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ScheduledApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduledApplication.class, args);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.scheduled

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling

@SpringBootApplication
@EnableScheduling
class ScheduledApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;ScheduledApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Create a Scheduled Task</h2>
<p>Define a scheduled task using <code>@Scheduled</code>.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.scheduled;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalTime;

@Service
public class ScheduledTask {

    @Scheduled(fixedRate = 5000)
    public void runTask() {
        System.out.println("Scheduled task executed at: " + LocalTime.now());
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.scheduled

import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.time.LocalTime

@Service
class ScheduledTask {

    @Scheduled(fixedRate = 5000)
    fun runTask() {
        println("Scheduled task executed at: ${LocalTime.now()}")
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the Spring Boot application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<p>The scheduled task will execute every 5 seconds and print a timestamp.</p>
<hr>
<h2>🧪 Testing the Scheduled Task</h2>
<p>In this section, we clarify Testing the Scheduled Task and summarize the key points you will apply in implementation.</p>
<h3>Expected Console Output:</h3>
<p>Compare your console logs with this output to quickly confirm the behavior is correct.</p>
<pre><code class="language-plaintext">Scheduled task executed at: 12:00:01
Scheduled task executed at: 12:00:06
Scheduled task executed at: 12:00:11
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Scheduled Tasks implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Scheduled Tasks]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-scheduled-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Reactive Programming]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-reactive-programming</link>
      <description><![CDATA[Learn how to build reactive applications using Spring Boot with Project Reactor and WebFlux for high-performance asynchronous processing.]]></description>
      <pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate>
      <atom:updated>2025-02-20T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-reactive-programming</guid>
      <content:encoded><![CDATA[<p>Spring Boot with Reactive Programming enables developers to build non-blocking, event-driven applications that scale efficiently. By leveraging Spring WebFlux and Project Reactor, developers can handle large amounts of concurrent requests with minimal resource consumption, making it ideal for microservices and real-time applications.</p>
<hr>
<h2>🌟 Why Use Reactive Programming?</h2>
<p>In this section, we clarify Why Use Reactive Programming? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Asynchronous &#x26; Non-Blocking: Handle multiple requests efficiently without blocking threads.</li>
<li>Better Scalability: Utilize fewer resources while handling more concurrent users.</li>
<li>Event-Driven Model: Ideal for microservices, real-time applications, and streaming data.</li>
<li>Built-in Backpressure: Prevents overwhelming the system with too many requests.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the necessary dependency for Spring WebFlux in your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
    &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
    &#x3C;artifactId>spring-boot-starter-webflux&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-webflux'
</code></pre>
<hr>
<h2>🛠️ Step 2: Create a Reactive REST Controller</h2>
<p>Define a non-blocking REST endpoint using <code>Mono</code> and <code>Flux</code> to handle asynchronous processing.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.reactive;

import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.List;

@RestController
@RequestMapping("/reactive")
public class ReactiveController {

    @GetMapping("/mono")
    public Mono&#x3C;String> getMono() {
        return Mono.just("Hello from Reactive Mono!");
    }

    @GetMapping("/flux")
    public Flux&#x3C;String> getFlux() {
        return Flux.fromIterable(List.of("Hello", "from", "Reactive", "Flux"))
                   .delayElements(Duration.ofSeconds(1));
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.reactive

import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration

@RestController
@RequestMapping("/reactive")
class ReactiveController {

    @GetMapping("/mono")
    fun getMono(): Mono&#x3C;String> {
        return Mono.just("Hello from Reactive Mono!")
    }

    @GetMapping("/flux")
    fun getFlux(): Flux&#x3C;String> {
        return Flux.just("Hello", "from", "Reactive", "Flux")
            .delayElements(Duration.ofSeconds(1))
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the Spring Boot application:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Or using Gradle:</p>
<pre><code class="language-bash">gradle bootRun
</code></pre>
<hr>
<h2>🧪 Testing the API</h2>
<p>In this section, we clarify Testing the API and summarize the key points you will apply in implementation.</p>
<h3>Test Mono Endpoint:</h3>
<p>Run this check to validate endpoint behavior and confirm the response matches expectations.</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/reactive/mono
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Hello from Reactive Mono!
</code></pre>
<h3>Test Flux Endpoint:</h3>
<p>Run this check to validate endpoint behavior and confirm the response matches expectations.</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/reactive/flux
</code></pre>
<p>Expected output (delayed by 1 second per word):</p>
<pre><code class="language-plaintext">Hello
from
Reactive
Flux
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Reactive Programming implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Reactive Programming]]></category>
      <category><![CDATA[Spring WebFlux]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-reactive-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot GraalVM Native Application]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-graalvm</link>
      <description><![CDATA[Learn how to build and deploy a Spring Boot application as a GraalVM native image for improved startup time and reduced resource usage.]]></description>
      <pubDate>Sat, 28 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-29T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-graalvm</guid>
      <content:encoded><![CDATA[<p>Spring Boot with GraalVM Native enables developers to create applications that offer lightning-fast startup times, reduced memory consumption, and enhanced security. With native image support, your Spring Boot applications can efficiently run in resource-constrained environments, making them ideal for modern cloud-native architectures.</p>
<hr>
<h2>🌟 Why Build a Native Application with GraalVM?</h2>
<p>In this section, we clarify Why Build a Native Application with GraalVM? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Faster Startup Times: Native images start much faster compared to JVM-based applications.</li>
<li>Reduced Resource Usage: Native images consume less memory, making them ideal for cloud and containerized environments.</li>
<li>Enhanced Security: Smaller runtime and reduced attack surface.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>🛠 GraalVM installed with native-image support</li>
<li>🕝 Maven or Gradle</li>
<li>🐳 Docker for building container images (optional)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Add the GraalVM Native Build Tools plugin to your <code>pom.xml</code> or <code>build.gradle</code> file.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;plugin>
    &#x3C;groupId>org.graalvm.buildtools&#x3C;/groupId>
    &#x3C;artifactId>native-maven-plugin&#x3C;/artifactId>
    &#x3C;version>0.9.20&#x3C;/version>
&#x3C;/plugin>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">plugins {
    id 'org.graalvm.buildtools.native' version '0.9.20'
}
</code></pre>
<hr>
<h2>🛠️ Step 2: Write a REST Controller</h2>
<p>Create a simple REST endpoint to test the native image functionality.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.graalvmnative;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @GetMapping("/greeting")
    public String greeting() {
        return "Hello from GraalVM Native!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.graalvmnative

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
@RestController
class MyApplication {

    @GetMapping("/greeting")
    fun greeting(): String = "Hello from GraalVM Native!"
}

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;MyApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Build a Native Image</h2>
<p>In this section, we clarify Step 3: Build a Native Image and summarize the key points you will apply in implementation.</p>
<p>Maven:
Run the following command to create a native image:</p>
<pre><code class="language-bash">mvn -Pnative package
</code></pre>
<p>Gradle:
Run the following command:</p>
<pre><code class="language-bash">gradle nativeCompile
</code></pre>
<hr>
<h2>🛠️ Step 4: Build a Docker Image</h2>
<p>Spring Boot provides built-in support for creating Docker images with native executables.</p>
<p>Maven:</p>
<pre><code class="language-bash">mvn -Pnative spring-boot:build-image
</code></pre>
<p>Gradle:</p>
<pre><code class="language-bash">gradle bootBuildImage
</code></pre>
<p>The Docker image will be available locally and can be run using:</p>
<pre><code class="language-bash">docker run --rm -p 8080:8080 myproject:0.0.1-SNAPSHOT
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>In this section, we clarify Running the Application and summarize the key points you will apply in implementation.</p>
<h3>Running Locally</h3>
<p>You can run the native image directly:</p>
<pre><code class="language-bash">./target/myproject
</code></pre>
<h3>Running in Docker</h3>
<p>If you built the Docker image, run it using:</p>
<pre><code class="language-bash">docker run --rm -p 8080:8080 myproject:0.0.1-SNAPSHOT
</code></pre>
<hr>
<h2>🧪 Testing the API</h2>
<p>Test the REST endpoint using cURL or Postman:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/greeting
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Hello from GraalVM Native!
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot GraalVM Native Application implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[GraalVM]]></category>
      <category><![CDATA[Native Image]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-graalvm-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Kafka Integration]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-kafka</link>
      <description><![CDATA[Learn how to integrate Apache Kafka with Spring Boot to send and consume JSON messages through Kafka topics.]]></description>
      <pubDate>Wed, 25 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-26T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-kafka</guid>
      <content:encoded><![CDATA[<p>Apache Kafka is a distributed event streaming platform that facilitates real-time data processing. This guide demonstrates how to send and consume JSON messages in a Spring Boot application using Kafka.</p>
<hr>
<h2>🌟 Why Use Kafka?</h2>
<p>In this section, we clarify Why Use Kafka? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Scalable Messaging: Handle large volumes of data seamlessly.</li>
<li>Fault Tolerance: Ensure data durability and high availability.</li>
<li>Real-Time Processing: Process and analyze data in real time.</li>
<li>Integration: Easily integrate Kafka with Spring Boot for efficient development.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>🕊 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔠 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
<li>🔠 Apache Kafka installed and running</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To integrate Kafka into your Spring Boot project, add the following dependencies:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.kafka&#x3C;/groupId>
  &#x3C;artifactId>spring-kafka&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springframework.kafka:spring-kafka'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure Kafka</h2>
<p>Set up Kafka connection in your <code>application.properties</code> or <code>application.yml</code> file:</p>
<pre><code class="language-properties">spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=my-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=*
</code></pre>
<hr>
<h2>🛠️ Step 3: Create a Model for JSON Messages</h2>
<p>In this section, we clarify Step 3: Create a Model for JSON Messages and summarize the key points you will apply in implementation.</p>
<h3>Model Class</h3>
<p>This model defines the message contract shared across producer, consumer, and controller layers.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.kafka.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    private String id;
    private String content;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.kafka.model

data class Message(
    val id: String,
    val content: String
)
</code></pre>
<hr>
<h2>🛠️ Step 4: Implement Kafka Producer and Consumer</h2>
<p>In this section, we clarify Step 4: Implement Kafka Producer and Consumer and summarize the key points you will apply in implementation.</p>
<h3>Producer Example</h3>
<p>This producer example shows how to publish messages consistently to the target topic.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.kafka.producer;

import com.example.kafka.model.Message;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class KafkaProducer {

    private final KafkaTemplate&#x3C;String, Message> kafkaTemplate;

    public void sendMessage(String topic, Message message) {
        kafkaTemplate.send(topic, message);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.kafka.producer

import com.example.kafka.model.Message
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Service

@Service
class KafkaProducer(private val kafkaTemplate: KafkaTemplate&#x3C;String, Message>) {

    fun sendMessage(topic: String, message: Message) {
        kafkaTemplate.send(topic, message)
    }
}
</code></pre>
<h3>Consumer Example</h3>
<p>This consumer example demonstrates how incoming messages are read and processed.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.kafka.consumer;

import com.example.kafka.model.Message;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class KafkaConsumer {

    @KafkaListener(topics = "my-topic", groupId = "my-group")
    public void consumeMessage(Message message) {
        System.out.println("Received message: " + message);
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.kafka.consumer

import com.example.kafka.model.Message
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Service

@Service
class KafkaConsumer {

    @KafkaListener(topics = ["my-topic"], groupId = "my-group")
    fun consumeMessage(message: Message) {
        println("Received message: $message")
    }
}
</code></pre>
<hr>
<h3>Controller Example</h3>
<p>This controller exposes a simple endpoint so you can trigger and verify messaging behavior.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.kafka.controller;

import com.example.kafka.model.Message;
import com.example.kafka.producer.KafkaProducer;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/kafka")
@RequiredArgsConstructor
public class KafkaController {

    private final KafkaProducer kafkaProducer;

    @PostMapping("/publish")
    public String publishMessage(@RequestParam String topic, @RequestBody Message message) {
        kafkaProducer.sendMessage(topic, message);
        return "Message sent to topic: " + topic;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.kafka.controller

import com.example.kafka.model.Message
import com.example.kafka.producer.KafkaProducer
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/kafka")
class KafkaController(private val kafkaProducer: KafkaProducer) {

    @PostMapping("/publish")
    fun publishMessage(@RequestParam topic: String, @RequestBody message: Message): String {
        kafkaProducer.sendMessage(topic, message)
        return "Message sent to topic: $topic"
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<hr>
<h2>🧪 Testing the API</h2>
<p>You can test the Kafka Producer endpoint using cURL or Postman:</p>
<pre><code class="language-bash">curl -X POST "http://localhost:8080/kafka/publish" \
-H "Content-Type: application/json" \
-d '{"id": "123", "content": "Hello Kafka!"}'
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Kafka Integration implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Kafka]]></category>
      <category><![CDATA[Messaging]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-kafka-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Redis Caching]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-redis-caching</link>
      <description><![CDATA[Learn how to implement Redis caching in your Spring Boot application to improve performance and efficiency.]]></description>
      <pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-23T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-redis-caching</guid>
      <content:encoded><![CDATA[<p>Redis is a powerful in-memory data store often used for caching, messaging, and real-time data management. This guide explains how to integrate Redis into a Spring Boot application using both Java and Kotlin.</p>
<hr>
<h2>🌟 Why Use Redis?</h2>
<p>In this section, we clarify Why Use Redis? and summarize the key points you will apply in implementation.</p>
<ul>
<li>High Performance: Redis provides extremely low latency for read and write operations.</li>
<li>Versatile Data Structures: Supports strings, hashes, lists, sets, and more.</li>
<li>Scalability: Ideal for distributed caching and real-time analytics.</li>
<li>Integration: Easily integrates with Spring Boot for seamless development.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>🗈 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
<li>💠 Redis Server installed and running locally or accessible via a network</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To integrate Redis into your Spring Boot project, add the following dependencies:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-data-redis&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-data-redis'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure Redis</h2>
<p>Set up the Redis connection in your <code>application.properties</code> or <code>application.yml</code> file.</p>
<h3>Example Configuration:</h3>
<p>Use this configuration as a baseline and adjust values for your local or production environment.</p>
<pre><code class="language-properties">spring.redis.host=localhost
spring.redis.port=6379
</code></pre>
<p>For advanced setups, such as password authentication or SSL, add these properties:</p>
<pre><code class="language-properties">spring.redis.password=yourpassword
spring.redis.ssl=true
</code></pre>
<hr>
<h2>🛠️ Step 3: Enable Caching</h2>
<p>Add the <code>@EnableCaching</code> annotation to your main application class to enable Spring's caching abstraction.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.redis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class RedisApplication {
  public static void main(String[] args) {
    SpringApplication.run(RedisApplication.class, args);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.redis

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class RedisApplication

fun main(args: Array&#x3C;String>) {
  runApplication&#x3C;RedisApplication>(*args)
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Service Layer Example with Caching</h2>
<h3>Java</h3>
<pre><code class="language-java">package com.example.redis.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

  @Cacheable("users")
  public String getUserById(String id) {
    simulateSlowService();
    return "User with ID: " + id;
  }

  private void simulateSlowService() {
    try {
      Thread.sleep(3000L);
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.redis.service

import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service

@Service
class UserService {

  @Cacheable("users")
  fun getUserById(id: String): String {
    simulateSlowService()
    return "User with ID: $id"
  }

  private fun simulateSlowService() {
    Thread.sleep(3000L)
  }
}
</code></pre>
<hr>
<h3>Configuring TTL (Time-To-Live)</h3>
<p>Set the cache expiration time in your <code>application.properties</code> file:</p>
<pre><code class="language-properties">spring.cache.redis.time-to-live=600000
</code></pre>
<p>This sets the TTL to 10 minutes (600,000 milliseconds).</p>
<hr>
<h3>Controller Example</h3>
<p>Create a REST controller to expose the caching functionality.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.redis.controller;

import com.example.redis.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;

  @GetMapping("/{id}")
  public String getUser(@PathVariable String id) {
    return userService.getUserById(id);
  }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.redis.controller

import com.example.redis.service.UserService
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/users")
class UserController(
  private val userService: UserService
) {
  @GetMapping("/{id}")
  fun getUser(@PathVariable id: String): String = userService.getUserById(id)
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<hr>
<h2>🧪 Testing the API</h2>
<p>You can test the API using cURL or Postman:</p>
<ul>
<li>Fetch a User (cached):</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/users/1
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Redis Caching implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Redis]]></category>
      <category><![CDATA[Caching]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-redis-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot with OpenAPI]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-openapi</link>
      <description><![CDATA[Learn how to integrate OpenAPI into your Spring Boot application for API documentation and testing using SpringDoc.]]></description>
      <pubDate>Fri, 20 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-21T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-openapi</guid>
      <content:encoded><![CDATA[<p>OpenAPI simplifies API documentation and testing by providing an interactive interface and machine-readable documentation. This guide will show you how to integrate OpenAPI into your Spring Boot application using SpringDoc.</p>
<hr>
<h2>🌟 Why Use OpenAPI?</h2>
<p>In this section, we clarify Why Use OpenAPI? and summarize the key points you will apply in implementation.</p>
<ul>
<li>Interactive Documentation: Offers a user-friendly interface for exploring APIs.</li>
<li>Standardized Format: Generates machine-readable API definitions.</li>
<li>Ease of Testing: Provides built-in tools for testing endpoints.</li>
<li>Client Code Generation: Allows automatic generation of client code for various programming languages.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To integrate OpenAPI using SpringDoc, add the following dependency to your Spring Boot project:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springdoc&#x3C;/groupId>
  &#x3C;artifactId>springdoc-openapi-starter-webmvc-ui&#x3C;/artifactId>
  &#x3C;version>2.1.0&#x3C;/version>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure OpenAPI</h2>
<p>SpringDoc requires minimal configuration. You can customize your OpenAPI documentation using the <code>application.properties</code> or <code>application.yml</code> file.</p>
<h3>Example Configuration:</h3>
<p>Use this configuration as a baseline and adjust values for your local or production environment.</p>
<pre><code class="language-properties">springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
</code></pre>
<hr>
<h2>🛠️ Step 3: Annotate Your REST Controllers</h2>
<p>Add annotations to your REST controllers and models to generate OpenAPI documentation automatically.</p>
<h3>Java</h3>
<p>Controller Example</p>
<pre><code class="language-java">package com.example.openapi.controller;

import com.example.openapi.model.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

  @Operation(summary = "Get all users", description = "Retrieve a list of users")
  @ApiResponse(responseCode = "200", description = "Successful operation",
    content = @Content(mediaType = "application/json"))
  @GetMapping
  public List&#x3C;User> getAllUsers() {
    return List.of(new User(1L, "John Doe", "john@example.com"));
  }

  @Operation(summary = "Create a new user", description = "Add a new user to the system")
  @ApiResponse(responseCode = "201", description = "User created successfully",
    content = @Content(mediaType = "application/json"))
  @PostMapping
  public User createUser(@RequestBody User user) {
    return user;
  }
}
</code></pre>
<p>User Model</p>
<pre><code class="language-java">package com.example.openapi.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long id;
    private String name;
    private String email;
}
</code></pre>
<h3>Kotlin</h3>
<p>Controller Example</p>
<pre><code class="language-kotlin">package com.example.openapi.controller

import com.example.openapi.model.User
import io.swagger.v3.oas.annotations.*
import io.swagger.v3.oas.annotations.responses.*
import io.swagger.v3.oas.annotations.media.*
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/users")
class UserController {

    @Operation(summary = "Get all users", description = "Retrieve a list of users")
    @ApiResponse(responseCode = "200", description = "Successful operation",
                 content = [Content(mediaType = "application/json")])
    @GetMapping
    fun getAllUsers(): List&#x3C;User> = listOf(User(1L, "John Doe", "john@example.com"))

    @Operation(summary = "Create a new user", description = "Add a new user to the system")
    @ApiResponse(responseCode = "201", description = "User created successfully",
                 content = [Content(mediaType = "application/json")])
    @PostMapping
    fun createUser(@RequestBody user: User): User = user
}
</code></pre>
<p>User Model</p>
<pre><code class="language-kotlin">package com.example.openapi.model

data class User(
  val id: Long,
  val name: String,
  val email: String
)
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Access the OpenAPI documentation at:</p>
<ul>
<li>API Docs: <code>http://localhost:8080/api-docs</code></li>
<li>Swagger UI: <code>http://localhost:8080/swagger-ui.html</code></li>
</ul>
<hr>
<h2>🧪 Testing the API</h2>
<p>You can test the generated API documentation by exploring the Swagger UI interface:</p>
<ol>
<li>Navigate to <code>http://localhost:8080/swagger-ui.html</code> in your browser.</li>
<li>Test endpoints interactively by providing inputs and observing responses.</li>
</ol>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot with OpenAPI implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[OpenAPI]]></category>
      <category><![CDATA[Documentation]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-openapi-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Clean Coding Practices in Spring Boot]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-clean-coding</link>
      <description><![CDATA[Learn how to apply clean coding principles in Spring Boot projects. Includes best practices, examples, and benefits of using Lombok in Java and Kotlin clean code techniques.]]></description>
      <pubDate>Thu, 19 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-20T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-clean-coding</guid>
      <content:encoded><![CDATA[<p>Clean coding practices ensure that your Spring Boot applications are maintainable, readable, and scalable. This guide provides essential tips and code examples to help you write cleaner and more efficient code in both Java and Kotlin.</p>
<hr>
<h2>🌟 Why Focus on Clean Coding?</h2>
<p>Adopting clean coding principles helps to:</p>
<ul>
<li>Improve code readability and maintainability.</li>
<li>Reduce technical debt.</li>
<li>Make onboarding new developers easier.</li>
<li>Enhance scalability and debugging processes.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
<li>🛠️ Familiarity with Spring Boot basics</li>
</ul>
<hr>
<h2>🛠️ Step 1: Structure Your Project</h2>
<p>Organize your Spring Boot project for better clarity:</p>
<ul>
<li>Controller Layer: Handles incoming HTTP requests.</li>
<li>Service Layer: Contains business logic.</li>
<li>Repository Layer: Interacts with the database.</li>
</ul>
<h3>Example Folder Structure:</h3>
<p>This structure keeps layers separated and makes future refactoring safer as the project grows.</p>
<pre><code class="language-filename=&#x22;snippet.txt&#x22;">src/main/java/com/example/cleanproject
├── controller
├── service
├── repository
├── entity
└── dto
</code></pre>
<hr>
<h2>🛠️ Step 2: Use Lombok for Cleaner Java Code</h2>
<p>Lombok reduces boilerplate code in Java, making your classes more concise and readable. Here's how to use Lombok effectively:</p>
<h3>Add Lombok Dependency</h3>
<p>Start by adding Lombok to your build so annotations are resolved correctly before refactoring your entities and DTOs.</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.projectlombok&#x3C;/groupId>
  &#x3C;artifactId>lombok&#x3C;/artifactId>
  &#x3C;scope>provided&#x3C;/scope>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">provided 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
</code></pre>
<h3>Example: Entity with Lombok</h3>
<p>The following example gives practical context for Example: Entity with Lombok and can be applied directly.</p>
<pre><code class="language-java">package com.example.cleanproject.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}
</code></pre>
<p>Benefits:</p>
<ul>
<li><code>@Data</code> generates getters, setters, equals, hashCode, and toString methods.</li>
<li><code>@NoArgsConstructor</code> and <code>@AllArgsConstructor</code> create constructors.</li>
</ul>
<hr>
<h2>🛠️ Step 3: Write Concise and Readable Code in Kotlin</h2>
<p>Kotlin offers modern features that naturally lead to cleaner code:</p>
<h3>Example: Entity in Kotlin</h3>
<p>The following example gives practical context for Example: Entity in Kotlin and can be applied directly.</p>
<pre><code class="language-kotlin">package com.example.cleanproject.entity

import jakarta.persistence.*

@Entity
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    var name: String,
    var email: String
)
</code></pre>
<p>Advantages of Kotlin:</p>
<ul>
<li><code>data class</code> automatically generates <code>toString</code>, <code>equals</code>, and <code>hashCode</code> methods.</li>
<li>Immutable properties (<code>val</code>) ensure better stability.</li>
</ul>
<hr>
<h2>🛠️ Step 4: Follow Dependency Injection Principles</h2>
<p>Use dependency injection to decouple components and improve testability.</p>
<h3>Example: Service Layer with DI</h3>
<p>The following example gives practical context for Example: Service Layer with DI and can be applied directly.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.cleanproject.service;

import com.example.cleanproject.entity.User;
import com.example.cleanproject.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public List&#x3C;User> getAllUsers() {
        return userRepository.findAll();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.cleanproject.service

import com.example.cleanproject.entity.User
import com.example.cleanproject.repository.UserRepository
import org.springframework.stereotype.Service

@Service
class UserService(
    private val userRepository: UserRepository
) {
    fun getAllUsers(): List&#x3C;User> = userRepository.findAll()
}
</code></pre>
<hr>
<h2>🛠️ Step 5: Use DTOs for Data Transfer</h2>
<p>Data Transfer Objects (DTOs) separate your domain and API layers, promoting better encapsulation.</p>
<h3>Example: DTO for User</h3>
<p>The following example gives practical context for Example: DTO for User and can be applied directly.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.cleanproject.dto;

import lombok.Data;

@Data
public class UserDTO {
    private String name;
    private String email;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.cleanproject.dto

data class UserDTO(
    val name: String,
    val email: String
)
</code></pre>
<hr>
<h3>Controller Layer</h3>
<p>Implement a controller to handle HTTP requests and interact with the service layer.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.cleanproject.controller;

import com.example.cleanproject.dto.UserDTO;
import com.example.cleanproject.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping
    public List&#x3C;UserDTO> getAllUsers() {
        return userService.getAllUsers();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.cleanproject.controller

import com.example.cleanproject.dto.UserDTO
import com.example.cleanproject.service.UserService
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {

    @GetMapping
    fun getAllUsers(): List&#x3C;UserDTO> = userService.getAllUsers()
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Test endpoints using a tool like Postman or cURL.</p>
<hr>
<h2>🧪 Testing the API</h2>
<p>You can test the API using the following cURL command:</p>
<ul>
<li>Fetch all users:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Clean Coding Practices in Spring Boot implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Clean Coding]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-clean-coding-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot DevTools]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-devtools</link>
      <description><![CDATA[Learn how to utilize Spring Boot DevTools for rapid development. Includes live reload, caching, and debugging tips.]]></description>
      <pubDate>Thu, 19 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-20T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-devtools</guid>
      <content:encoded><![CDATA[<p>Spring Boot DevTools is a must-have tool for developers looking to speed up their development process. This guide will walk you through the features and setup of DevTools, including live reload, caching, and debugging enhancements.</p>
<hr>
<h2>🌟 Why Use Spring Boot DevTools?</h2>
<p>Spring Boot DevTools provides features such as:</p>
<ul>
<li>Live Reload: Automatically reloads the application on code changes.</li>
<li>Enhanced Caching: Disables template caching for faster iteration.</li>
<li>Debugging Tools: Improves development-time debugging experience.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable DevTools, add the following dependency to your Spring Boot project:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-devtools&#x3C;/artifactId>
  &#x3C;scope>runtime&#x3C;/scope>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">runtimeOnly 'org.springframework.boot:spring-boot-devtools'
</code></pre>
<hr>
<h2>🛠️ Step 2: Configure DevTools</h2>
<p>Spring Boot DevTools requires minimal configuration but offers some customization options. Here are some common configurations:</p>
<h3>Enable Live Reload</h3>
<p>Live reload is enabled by default when using DevTools with supported IDEs like IntelliJ IDEA or Eclipse.</p>
<h3>Disable Caching</h3>
<p>Disable caching for templates to see changes instantly:</p>
<pre><code class="language-properties">spring.thymeleaf.cache=false
spring.freemarker.cache=false
</code></pre>
<h3>Restart Exclusions</h3>
<p>Exclude specific files or directories from triggering a restart:</p>
<pre><code class="language-properties">spring.devtools.restart.exclude=static/**,public/**
</code></pre>
<hr>
<h2>🛠️ Step 3: Debugging Enhancements</h2>
<p>Spring Boot DevTools improves debugging by:</p>
<ul>
<li>Automatic Restart: Restarts the application context when code changes are detected.</li>
<li>Remote Debugging: Provides the ability to debug applications running on remote servers.</li>
</ul>
<p>To enable remote debugging:</p>
<pre><code class="language-properties">spring.devtools.remote.secret=mysecretkey
</code></pre>
<p>Use the following command to connect:</p>
<pre><code class="language-bash">java -jar myapp.jar -Dspring.devtools.remote.secret=mysecretkey
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Make changes to your code, and observe live reload and application context restarts.</p>
<hr>
<h2>🧪 Test DevTools</h2>
<p>You can test Spring Boot DevTools features as follows:</p>
<ul>
<li>Live Reload: Make a change in your controller or template and save the file. The application should reload automatically.</li>
<li>Template Updates: Update a <code>.html</code> file and verify the changes without restarting the server.</li>
<li>Debugging: Add breakpoints in your IDE and test remote debugging with <code>DevTools</code> enabled.</li>
</ul>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot DevTools implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Dev Tools]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-devtools-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot Actuator]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-actuator</link>
      <description><![CDATA[Learn how to use Spring Boot Actuator to monitor and manage your applications with ease. Includes endpoints, customization, and security examples.]]></description>
      <pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-19T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-actuator</guid>
      <content:encoded><![CDATA[<p>Spring Boot Actuator provides production-ready features to monitor and manage your applications. This guide explores Actuator's capabilities, how to enable it, and secure its endpoints.</p>
<hr>
<h2>🌟 Why Use Spring Boot Actuator?</h2>
<p>Spring Boot Actuator allows developers to:</p>
<ul>
<li>Monitor application health</li>
<li>Gather metrics and information</li>
<li>Expose management endpoints for operational tasks</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>💼 Maven or Gradle installed</li>
<li>🄄 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To enable Actuator, add the following dependency to your Spring Boot project:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-actuator&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-actuator'
</code></pre>
<hr>
<h2>🛠️ Step 2: Enable Actuator Endpoints</h2>
<p>By default, Actuator exposes several endpoints to provide application insights. You can enable these endpoints in your <code>application.properties</code> or <code>application.yml</code> file.</p>
<h3>Configuration Example:</h3>
<p>Use this configuration as a baseline and adjust values for your local or production environment.</p>
<pre><code class="language-properties">management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
</code></pre>
<hr>
<h2>🛠️ Step 3: Using Actuator Endpoints</h2>
<p>Here are some commonly used Actuator endpoints:</p>
<ul>
<li><code>/actuator/health</code>: Provides application health status.</li>
<li><code>/actuator/info</code>: Displays application metadata.</li>
<li><code>/actuator/metrics</code>: Offers application performance metrics.</li>
</ul>
<p>You can access these endpoints via a browser or API tools like cURL.</p>
<p>Example:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/actuator/health
</code></pre>
<hr>
<h2>🛠️ Step 4: Customize Actuator Endpoints</h2>
<p>You can customize Actuator endpoints to suit your needs. For example, you can define additional metadata in the <code>application.properties</code> file for the <code>/actuator/info</code> endpoint:</p>
<pre><code class="language-properties">info.app.name=My Application
info.app.version=1.0.0
info.app.description=Spring Boot Actuator Example
</code></pre>
<hr>
<h2>🛠️ Step 5: Secure Actuator Endpoints</h2>
<p>For production environments, it's essential to secure Actuator endpoints. Use Spring Security to restrict access.</p>
<h3>Java</h3>
<p>Example Security Configuration</p>
<pre><code class="language-java">package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .httpBasic();
        return http.build();
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Example Security Configuration</p>
<pre><code class="language-kotlin">package com.example.demo.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain

@Configuration
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .authorizeHttpRequests()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .httpBasic()
        return http.build()
    }
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following command:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Access the Actuator endpoints at:</p>
<pre><code class="language-filename=&#x22;snippet.txt&#x22;">http://localhost:8080/actuator/health
</code></pre>
<hr>
<h2>🧪 Test the API</h2>
<p>You can test the Actuator endpoints using cURL or browser:</p>
<ul>
<li>Health Endpoint:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/actuator/health
</code></pre>
<ul>
<li>Info Endpoint:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/actuator/info
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot Actuator implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Actuator]]></category>
      <category><![CDATA[Monitoring]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-actuator-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Aspect-Oriented Programming in Spring Boot]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-aop</link>
      <description><![CDATA[Learn how to implement Aspect-Oriented Programming (AOP) in Spring Boot using Java and Kotlin. Covers core concepts, use cases, and practical examples.]]></description>
      <pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-19T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-aop</guid>
      <content:encoded><![CDATA[<p>Aspect-Oriented Programming (AOP) provides a way to modularize cross-cutting concerns, such as logging, transaction management, and security. This guide demonstrates how to implement AOP in Spring Boot with examples in Java and Kotlin.</p>
<hr>
<h2>🌟 Why Use AOP?</h2>
<p>AOP enables developers to separate concerns that affect multiple parts of an application, like logging or security, into reusable aspects. This approach promotes cleaner code, improved maintainability, and reduced redundancy.</p>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include the following dependencies in your project:</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-aop&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-aop'
</code></pre>
<hr>
<h2>🛠️ Step 2: Define a Service</h2>
<p>Create a simple service to demonstrate AOP.</p>
<h3>Java</h3>
<p>Service</p>
<pre><code class="language-java">package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public String getUserById(String id) {
        return "User with ID: " + id;
    }

    public String getAllUsers() {
        return "Fetching all users.";
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Service</p>
<pre><code class="language-kotlin">package com.example.demo.service

import org.springframework.stereotype.Service

@Service
class UserService {

    fun getUserById(id: String): String {
        return "User with ID: $id"
    }

    fun getAllUsers(): String {
        return "Fetching all users."
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Create an Aspect</h2>
<p>Define an aspect to log method execution details.</p>
<h3>Java</h3>
<p>Aspect</p>
<pre><code class="language-java">package com.example.demo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Before("execution(* com.example.demo.service.UserService.*(..))")
    public void logBefore() {
        logger.info("Method execution started.");
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Aspect</p>
<pre><code class="language-kotlin">package com.example.demo.aspect

import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

@Aspect
@Component
class LoggingAspect {

    private val logger = LoggerFactory.getLogger(LoggingAspect::class.java)

    @Before("execution(* com.example.demo.service.UserService.*(..))")
    fun logBefore() {
        logger.info("Method execution started.")
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Create a Controller</h2>
<p>Expose the service methods through a REST controller.</p>
<h3>Java</h3>
<p>Controller</p>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public String getUserById(@PathVariable String id) {
        return userService.getUserById(id);
    }

    @GetMapping
    public String getAllUsers() {
        return userService.getAllUsers();
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Controller</p>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.service.UserService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {

    @GetMapping("/{id}")
    fun getUserById(@PathVariable id: String): String = userService.getUserById(id)

    @GetMapping
    fun getAllUsers(): String = userService.getAllUsers()
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following commands:</p>
<p>Spring Boot (Java/Kotlin):
Run the application with either stack to confirm the baseline setup is working before deeper tests.</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Access the API at <code>http://localhost:8080/api/users</code>.</p>
<hr>
<h2>🧪 Testing the API</h2>
<p>You can test the API using the following cURL commands:</p>
<ul>
<li>Fetch all users:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users
</code></pre>
<ul>
<li>Fetch a user by ID:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users/1
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Aspect-Oriented Programming in Spring Boot implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[AOP]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-aop-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Dependency Injection in Spring Boot]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-di</link>
      <description><![CDATA[Learn how Dependency Injection (DI) works in Spring Boot, including examples in Java and Kotlin. Covers key concepts, annotations, and practical use cases.]]></description>
      <pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-19T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-di</guid>
      <content:encoded><![CDATA[<p>Dependency Injection (DI) is a fundamental concept in Spring Boot that helps achieve loose coupling and increased testability. This guide demonstrates how to use DI in Spring Boot with practical examples in Java and Kotlin.</p>
<hr>
<h2>🌟 Why Use Dependency Injection?</h2>
<p>Dependency Injection allows developers to manage and inject dependencies into classes without manually instantiating them. This leads to:</p>
<ul>
<li>Better code modularity</li>
<li>Simplified testing</li>
<li>Easier maintenance</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include the necessary Spring Boot dependencies:</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter'
</code></pre>
<hr>
<h2>🛠️ Step 2: Create a Service</h2>
<p>Define a simple service to demonstrate DI.</p>
<h3>Java</h3>
<p>Service</p>
<pre><code class="language-java">package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public String getUserById(String id) {
        return "User with ID: " + id;
    }

    public String getAllUsers() {
        return "Fetching all users.";
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Service</p>
<pre><code class="language-kotlin">package com.example.demo.service

import org.springframework.stereotype.Service

@Service
class UserService {

    fun getUserById(id: String): String {
        return "User with ID: $id"
    }

    fun getAllUsers(): String {
        return "Fetching all users."
    }
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Inject Dependencies</h2>
<p>Use annotations to inject the service into other components.</p>
<h3>Java</h3>
<p>Controller</p>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public String getUserById(@PathVariable String id) {
        return userService.getUserById(id);
    }

    @GetMapping
    public String getAllUsers() {
        return userService.getAllUsers();
    }
}
</code></pre>
<h3>Kotlin</h3>
<p>Controller</p>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.service.UserService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {

    @GetMapping("/{id}")
    fun getUserById(@PathVariable id: String): String = userService.getUserById(id)

    @GetMapping
    fun getAllUsers(): String = userService.getAllUsers()
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following commands:</p>
<p>Spring Boot (Java/Kotlin):
Run the application with either stack to confirm the baseline setup is working before deeper tests.</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Access the API at <code>http://localhost:8080/api/users</code>.</p>
<hr>
<h2>🧪 Test the API</h2>
<p>You can test the API using the following cURL commands:</p>
<h3>Fetch all users</h3>
<p>Use this request to verify the list endpoint and validate the default response shape.</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users
</code></pre>
<h3>Fetch a user by ID</h3>
<p>Use this request to validate path-variable handling and single-resource retrieval behavior.</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users/1
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Dependency Injection in Spring Boot implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Dependency Injection]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-di-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Object-Relational Mapping]]></title>
      <link>https://suaybsimsek.com/posts/object-relational-mapping</link>
      <description><![CDATA[Learn how to set up an ORM-based application with Spring Boot using Java, Kotlin, and Go (Gin). Includes PostgreSQL integration and basic CRUD operations.]]></description>
      <pubDate>Mon, 16 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-17T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/object-relational-mapping</guid>
      <content:encoded><![CDATA[<p>Object-Relational Mapping (ORM) simplifies database operations in modern applications. This guide will show you how to set up ORM with PostgreSQL using Spring Boot and the Gin framework.</p>
<hr>
<h2>🌟 Why Use ORM?</h2>
<p>ORM bridges the gap between object-oriented programming and relational databases by mapping objects directly to database tables. This eliminates the need for manual SQL queries, reducing errors and increasing productivity for developers.</p>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🖥️ A Java IDE</li>
<li>🐘 PostgreSQL installed and running</li>
</ul>
<p>For Go:</p>
<ul>
<li>🔧 Golang 1.17+ installed</li>
<li>🔄 go.mod initialized in your project</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>To set up a Spring Boot project and integrate ORM, follow these steps:</p>
<h3>Add Dependencies</h3>
<p>For Spring Boot projects, include the following dependencies in your project:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-data-jpa&#x3C;/artifactId>
&#x3C;/dependency>
&#x3C;dependency>
  &#x3C;groupId>org.postgresql&#x3C;/groupId>
  &#x3C;artifactId>postgresql&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'
</code></pre>
<h3>Configure the Database</h3>
<p>Set up your PostgreSQL connection in the <code>application.properties</code> or <code>application.yml</code> file:</p>
<pre><code class="language-properties">spring.datasource.url=jdbc:postgresql://localhost:5432/demo
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
</code></pre>
<h3>Initialize the Project</h3>
<p>If you haven't already, create your Spring Boot project using <a href="https://start.spring.io/">Spring Initializr</a>:</p>
<ol>
<li>Choose Spring Web, Spring Data JPA, and PostgreSQL Driver as dependencies.</li>
<li>Download the project and import it into your preferred IDE.</li>
</ol>
<p>For Go projects:</p>
<ul>
<li>Install Gin Framework and GORM:</li>
</ul>
<pre><code class="language-bash"># Install the Gin framework
go get -u github.com/gin-gonic/gin

# Install the GORM PostgreSQL driver
go get -u gorm.io/driver/postgres

# Install the GORM ORM library
go get -u gorm.io/gorm
</code></pre>
<hr>
<h2>🛠️ Step 2: Define the Model and Implement the REST Controller</h2>
<p>This step combines creating the entity/model, repository, and REST controllers.</p>
<h3>Java</h3>
<p>Entity</p>
<pre><code class="language-java">package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.Data;

@Data
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}
</code></pre>
<p>Repository</p>
<pre><code class="language-java">package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository&#x3C;User, Long> {
}
</code></pre>
<p>REST Controller</p>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class UserController {

  private final UserRepository userRepository;

  @GetMapping
  public List&#x3C;User> getAllUsers() {
    return userRepository.findAll();
  }

  @PostMapping
  public User createUser(@RequestBody User user) {
    return userRepository.save(user);
  }
}
</code></pre>
<h3>Kotlin</h3>
<p>Entity</p>
<pre><code class="language-kotlin">package com.example.demo.entity

import jakarta.persistence.*

@Entity
class User(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0,

  var name: String,

  var email: String
)
</code></pre>
<p>Repository</p>
<pre><code class="language-kotlin">package com.example.demo.repository

import com.example.demo.entity.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository&#x3C;User, Long>
</code></pre>
<p>REST Controller</p>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.entity.User
import com.example.demo.repository.UserRepository
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/users")
class UserController(
  private val userRepository: UserRepository
) {

  @GetMapping
  fun getAllUsers(): List&#x3C;User> = userRepository.findAll()

  @PostMapping
  fun createUser(@RequestBody user: User): User = userRepository.save(user)
}
</code></pre>
<h3>Go</h3>
<p>Model and Database Connection</p>
<pre><code class="language-go">package main

import (
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var DB *gorm.DB

func connectDatabase() {
	dsn := "host=localhost user=your_username password=your_password dbname=demo port=5432 sslmode=disable"
	db, err := gorm.Open(postgres.Open(dsn), &#x26;gorm.Config{})
	if err != nil {
		panic("Failed to connect to database!")
	}

	DB = db
	DB.AutoMigrate(&#x26;User{})
}

type User struct {
	ID    uint   `json:"id" gorm:"primaryKey"`
	Name  string `json:"name"`
	Email string `json:"email"`
}
</code></pre>
<p>Handlers</p>
<pre><code class="language-go">package main

import (
	"github.com/gin-gonic/gin"
)

func getUsers(c *gin.Context) {
	var users []User
	DB.Find(&#x26;users)
	c.JSON(200, users)
}

func createUser(c *gin.Context) {
	var user User
	if err := c.ShouldBindJSON(&#x26;user); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	DB.Create(&#x26;user)
	c.JSON(201, user)
}

func main() {
	r := gin.Default()
	connectDatabase()

	r.GET("/api/users", getUsers)
	r.POST("/api/users", createUser)

	r.Run() // Listen and serve on 0.0.0.0:8080
}
</code></pre>
<hr>
<h2>▶️ Running the Applications</h2>
<p>In this section, we clarify Running the Applications and summarize the key points you will apply in implementation.</p>
<ul>
<li>
<p>Spring Boot:</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
</li>
<li>
<p>Gin:</p>
<pre><code class="language-bash">go run main.go
</code></pre>
</li>
</ul>
<hr>
<h2>🧪 Testing with cURL</h2>
<p>You can test the API using the following cURL commands:</p>
<p>Fetch All Users:</p>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/users
</code></pre>
<p>Create a New User:</p>
<pre><code class="language-bash">curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "johndoe@example.com"}'
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Object-Relational Mapping implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Go]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Gin]]></category>
      <category><![CDATA[ORM]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/orm-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Input Validation in REST APIs]]></title>
      <link>https://suaybsimsek.com/posts/rest-api-input-validation</link>
      <description><![CDATA[Learn how to implement input validation in REST APIs using Java, Kotlin, and Go with frameworks like Spring Boot and Gin. Covers annotations, custom validators, and error handling.]]></description>
      <pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-16T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/rest-api-input-validation</guid>
      <content:encoded><![CDATA[<p>Input validation is crucial for building secure and robust REST APIs. In this article, we will explore how to implement input validation in Spring Boot using Java, Kotlin, and Go (with Gin).</p>
<hr>
<h2>🌟 Why Validate Input?</h2>
<p>Validation ensures the data sent to your API adheres to expected formats and prevents potential vulnerabilities like SQL Injection, XSS, and bad data entries.</p>
<hr>
<h2>📋 Prerequisites</h2>
<p>Before implementing validation, make sure you have:</p>
<ul>
<li>Java 17+ for Spring Boot examples</li>
<li>Go 1.21+ for Gin examples</li>
<li>A running Spring Boot or Gin starter project</li>
<li>Basic familiarity with DTOs, JSON payloads, and HTTP status codes</li>
</ul>
<hr>
<h2>🧪 Step 1: Add Validation Dependencies</h2>
<p>For Spring Boot projects, include the following dependencies:</p>
<ul>
<li>Maven:</li>
</ul>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-validation&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<ul>
<li>Gradle:</li>
</ul>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-validation'
</code></pre>
<p>For Go projects with Gin, use the following:</p>
<pre><code class="language-bash"># Install the Gin framework
go get -u github.com/gin-gonic/gin

# Install the validator package
go get -u github.com/go-playground/validator/v10
</code></pre>
<hr>
<h2>🧪 Step 2: Define a DTO with Validation Rules</h2>
<p>Use annotations to define validation constraints on fields. Examples include <code>@NotNull</code>, <code>@Size</code>, and <code>@Pattern</code>.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class TodoRequest {

    @NotNull(message = "Title is required")
    @Size(min = 3, max = 50, message = "Title must be between 3 and 50 characters")
    private String title;

    private boolean completed;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.dto

import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size

data class TodoRequest(
    @field:NotNull(message = "Title is required")
    @field:Size(min = 3, max = 50, message = "Title must be between 3 and 50 characters")
    val title: String?,

    val completed: Boolean = false
)
</code></pre>
<h3>Go</h3>
<pre><code class="language-go">package dto

import (
    "github.com/go-playground/validator/v10"
)

type TodoRequest struct {
    Title     string `validate:"required,min=3,max=50"`
    Completed bool   `validate:""`
}

var validate = validator.New()

func ValidateTodoRequest(todo TodoRequest) error {
    return validate.Struct(todo)
}
</code></pre>
<hr>
<h2>🧪 Step 3: Create a Controller with Validation</h2>
<p>Integrate validation into your REST endpoints.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.dto.TodoRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    @PostMapping
    public String createTodo(@Validated @RequestBody TodoRequest request) {
        return "Todo created: " + request.getTitle();
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.dto.TodoRequest
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/todos")
class TodoController {

    @PostMapping
    fun createTodo(@Validated @RequestBody request: TodoRequest): String {
        return "Todo created: ${request.title}"
    }
}
</code></pre>
<h3>Go</h3>
<pre><code class="language-go">package controller

import (
    "dto"
    "github.com/gin-gonic/gin"
    "net/http"
)

func CreateTodoHandler(c *gin.Context) {
    var todo dto.TodoRequest

    if err := c.ShouldBindJSON(&#x26;todo); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    if err := dto.ValidateTodoRequest(todo); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "Todo created", "title": todo.Title})
}
</code></pre>
<hr>
<h2>🧪 Step 4: Handle Validation Errors</h2>
<p>Customize error handling to return user-friendly responses.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MethodArgumentNotValidException;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map&#x3C;String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map&#x3C;String, String> errors = new HashMap&#x3C;>();
        ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
        return errors;
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.exception

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.bind.MethodArgumentNotValidException

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationExceptions(ex: MethodArgumentNotValidException): Map&#x3C;String, String> {
        return ex.bindingResult.fieldErrors.associate { it.field to it.defaultMessage.orEmpty() }
    }
}
</code></pre>
<h3>Go</h3>
<pre><code class="language-go">package middleware

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        if len(c.Errors) > 0 {
            c.JSON(http.StatusBadRequest, gin.H{"errors": c.Errors.JSON()})
        }
    }
}
</code></pre>
<hr>
<p>main.go Example</p>
<p>Here is an example of the <code>main.go</code> file for setting up a Gin application:</p>
<pre><code class="language-go">package main

import (
	"controller"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.POST("/api/todos", controller.CreateTodoHandler)

	r.Run() // Start the server on http://localhost:8080
}
</code></pre>
<hr>
<h2>▶️ Step 5: Run the Application</h2>
<p>To run the application:</p>
<p>Spring Boot (Java/Kotlin):
Run the Spring Boot application from your IDE or terminal:</p>
<pre><code class="language-bash">./mvnw spring-boot:run # For Maven projects
./gradlew bootRun       # For Gradle projects
</code></pre>
<p>Access the API at <code>http://localhost:8080/api/todos</code>.</p>
<h3>Gin (Go)</h3>
<p>Run the Go application:</p>
<pre><code class="language-bash">go run main.go
</code></pre>
<p>Access the API at <code>http://localhost:8080/api/todos</code>.</p>
<h2>🧪 Testing with cURL</h2>
<p>Here are some example cURL commands to test the API:</p>
<ul>
<li>POST a new Todo:</li>
</ul>
<pre><code class="language-bash">curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-d '{"title": "New Task", "completed": false}'
</code></pre>
<ul>
<li>GET all Todos:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/todos
</code></pre>
<ul>
<li>Handle Validation Errors:</li>
</ul>
<p>Send an invalid request:</p>
<pre><code class="language-bash">curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-d '{"title": ""}'
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Input Validation in REST APIs implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Go]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Gin]]></category>
      <category><![CDATA[Validation]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/input-validation-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Building APIs with Spring for GraphQL]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-graphql</link>
      <description><![CDATA[Learn how to build GraphQL APIs using Spring for GraphQL with Java and Kotlin. Includes schema creation, query handling, and integration.]]></description>
      <pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-16T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-graphql</guid>
      <content:encoded><![CDATA[<p>Spring for GraphQL simplifies the development of GraphQL APIs by integrating with Spring Boot. This guide demonstrates how to create a GraphQL API using Java and Kotlin, with examples for schema definition, query handling, and dependency injection.</p>
<hr>
<h2>🌟 Why Use GraphQL?</h2>
<p>GraphQL allows clients to request specific data, reducing over-fetching and under-fetching compared to REST. It also supports strong typing and facilitates efficient data fetching, making it a popular choice for modern APIs.</p>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+</li>
<li>📦 Maven or Gradle installed</li>
<li>🔤 A Java IDE (e.g., IntelliJ IDEA, Eclipse)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Add Dependencies</h2>
<p>Include the following dependencies in your project to enable Spring for GraphQL.</p>
<p>Maven:</p>
<pre><code class="language-xml">&#x3C;dependency>
  &#x3C;groupId>org.springframework.boot&#x3C;/groupId>
  &#x3C;artifactId>spring-boot-starter-graphql&#x3C;/artifactId>
&#x3C;/dependency>
</code></pre>
<p>Gradle:</p>
<pre><code class="language-groovy">implementation 'org.springframework.boot:spring-boot-starter-graphql'
</code></pre>
<hr>
<h2>🛠️ Step 2: Define the GraphQL Schema</h2>
<p>Create a schema file named <code>schema.graphqls</code> under the <code>src/main/resources/graphql</code> directory.</p>
<pre><code class="language-graphql">type Query {
  getUser(id: ID!): User
  getUsers: [User]
}

type User {
  id: ID!
  name: String!
  email: String!
}
</code></pre>
<hr>
<h2>🛠️ Step 3: Implement the Data Model and Services</h2>
<p>Define the data model and service layer for handling queries.</p>
<h3>Java</h3>
<p>Entity</p>
<pre><code class="language-java">package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String id;
    private String name;
    private String email;
}
</code></pre>
<p>Service</p>
<pre><code class="language-java">package com.example.demo.service;

import com.example.demo.model.User;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class UserService {

    private final Map&#x3C;String, User> userData = Stream.of(
        new User("1", "Alice", "alice@example.com"),
        new User("2", "Bob", "bob@example.com")
    ).collect(Collectors.toMap(User::getId, user -> user));

    public User getUser(String id) {
        return userData.get(id);
    }

    public List&#x3C;User> getUsers() {
        return List.copyOf(userData.values());
    }
}
</code></pre>
<hr>
<h3>Kotlin</h3>
<p>Entity</p>
<pre><code class="language-kotlin">package com.example.demo.model

data class User(
    val id: String,
    val name: String,
    val email: String
)
</code></pre>
<p>Service</p>
<pre><code class="language-kotlin">package com.example.demo.service

import com.example.demo.model.User
import org.springframework.stereotype.Service

@Service
class UserService {

    private val userData = mapOf(
        "1" to User("1", "Alice", "alice@example.com"),
        "2" to User("2", "Bob", "bob@example.com")
    )

    fun getUser(id: String): User? = userData[id]

    fun getUsers(): List&#x3C;User> = userData.values.toList()
}
</code></pre>
<hr>
<h2>🛠️ Step 4: Implement GraphQL Controllers</h2>
<p>Controllers handle GraphQL queries and mutations. Use the <code>@Controller</code> annotation in Spring for GraphQL.</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @QueryMapping
    public User getUser(String id) {
        return userService.getUser(id);
    }

    @QueryMapping
    public List&#x3C;User> getUsers() {
        return userService.getUsers();
    }
}
</code></pre>
<hr>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.model.User
import com.example.demo.service.UserService
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller

@Controller
class UserController(
    private val userService: UserService
) {

    @QueryMapping
    fun getUser(id: String): User? = userService.getUser(id)

    @QueryMapping
    fun getUsers(): List&#x3C;User> = userService.getUsers()
}
</code></pre>
<hr>
<h2>▶️ Running the Application</h2>
<p>Run the application using the following commands:</p>
<p>Spring Boot (Java/Kotlin):
Run the application with either stack to confirm the baseline setup is working before deeper tests.</p>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<p>Access the GraphQL Playground at <code>http://localhost:8080/graphiql</code> to test your API.</p>
<hr>
<h2>🧪 Testing the GraphQL API</h2>
<p>Here are some example queries to test your API:</p>
<ul>
<li>Fetch a user by ID:</li>
</ul>
<pre><code class="language-graphql">query {
  getUser(id: "1") {
    id
    name
    email
  }
}
</code></pre>
<ul>
<li>Fetch all users:</li>
</ul>
<pre><code class="language-graphql">query {
  getUsers {
    id
    name
    email
  }
}
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Building APIs with Spring for GraphQL implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[GraphQL]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-graphql-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Gin - First Application]]></title>
      <link>https://suaybsimsek.com/posts/golang-gin-first-application</link>
      <description><![CDATA[A beginner-friendly guide to creating your first Golang Gin application from scratch. Learn the basics and start your journey with Gin.]]></description>
      <pubDate>Sat, 14 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-15T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/golang-gin-first-application</guid>
      <content:encoded><![CDATA[<p>Gin simplifies the process of building high-performance web applications with Golang. In this guide, we’ll walk through creating your first Gin application step by step.</p>
<hr>
<h2>🌟 What you'll learn</h2>
<p>In this section, we clarify What you'll learn and summarize the key points you will apply in implementation.</p>
<ul>
<li>How to initialize a minimal Gin project structure.</li>
<li>How to build and expose your first JSON endpoint.</li>
<li>How to run and validate the service locally before adding business logic.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Before starting, ensure you have the following:</p>
<ul>
<li>🔧 Golang 1.17+ installed</li>
<li>🗍 A text editor or IDE (e.g., Visual Studio Code, GoLand, or Vim)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Set Up Your Gin Project</h2>
<p>You can set up your first Gin project by following these steps:</p>
<ol>
<li>Initialize a Go Module</li>
</ol>
<pre><code class="language-bash">mkdir gin-first-app
cd gin-first-app
go mod init gin-first-app
</code></pre>
<ol start="2">
<li>Install Gin</li>
</ol>
<pre><code class="language-bash">go get -u github.com/gin-gonic/gin
</code></pre>
<hr>
<h2>🛠️ Step 2: Writing Your First Endpoint</h2>
<p>Let’s write a simple endpoint to say hello:</p>
<p>Create a file named <code>main.go</code> with the following content:</p>
<pre><code class="language-go">package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/hello", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello, Gin!",
		})
	})

	r.Run() // Listen and serve on 0.0.0.0:8080
}
</code></pre>
<hr>
<h2>▶️ Step 3: Run the Application</h2>
<p>In this section, we clarify Step 3: Run the Application and summarize the key points you will apply in implementation.</p>
<ol>
<li>Open a terminal in the project folder.</li>
<li>Execute the following command to run your application:</li>
</ol>
<pre><code class="language-bash">go run main.go
</code></pre>
<ol start="3">
<li>Access the endpoint at:
<pre><code class="language-filename=&#x22;snippet.txt&#x22;">http://localhost:8080/hello
</code></pre>
</li>
</ol>
<p>Response:</p>
<pre><code class="language-json">{
  "message": "Hello, Gin!"
}
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Gin - First Application implementation with a clear, production-friendly structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Go]]></category>
      <category><![CDATA[Gin]]></category>
      <category><![CDATA[Beginner Guide]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/golang-gin-first-app-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Building a REST API with Spring Boot]]></title>
      <link>https://suaybsimsek.com/posts/building-a-rest-api-with-spring-boot</link>
      <description><![CDATA[Learn how to create a REST API using Spring Boot. Includes HTTP methods, JSON handling, Lombok integration, and curl for testing.]]></description>
      <pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-12T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/building-a-rest-api-with-spring-boot</guid>
      <content:encoded><![CDATA[<p>Spring Boot is an excellent tool for quickly developing RESTful web services. In this article, we’ll walk through creating a REST API step by step using Spring Boot.</p>
<hr>
<h2>🌟 What is a REST API?</h2>
<p>REST (Representational State Transfer) is an architectural style for designing web services. It uses standard HTTP methods for communication between clients and servers.</p>
<h3>HTTP Methods</h3>
<p>In this subsection, we frame HTTP Methods with concise context so the next example is easier to follow.</p>






























<table><thead><tr><th>Method</th><th>Description</th><th>Example Endpoint</th></tr></thead><tbody><tr><td>GET</td><td>Retrieves a resource.</td><td><code>/api/todos</code></td></tr><tr><td>POST</td><td>Creates a new resource.</td><td><code>/api/todos</code></td></tr><tr><td>PUT</td><td>Updates or creates a resource.</td><td><code>/api/todos/1</code></td></tr><tr><td>DELETE</td><td>Deletes a resource.</td><td><code>/api/todos/1</code></td></tr></tbody></table>
<hr>
<h2>📋 Prerequisites</h2>
<p>Before you start, make sure the following are ready:</p>
<ul>
<li>Java 17+</li>
<li>Maven or Gradle</li>
<li>An IDE (IntelliJ IDEA, Eclipse, VS Code)</li>
<li>Basic familiarity with HTTP methods and JSON payloads</li>
</ul>
<hr>
<h2>🛠️ Step 1: Create a Project</h2>
<p>You can create a Spring Boot project using <a href="https://start.spring.io/">Spring Initializr</a>. Select the following settings:</p>
<ul>
<li>Project: Maven</li>
<li>Language: Java or Kotlin</li>
<li>Spring Boot Version: 3.0.0+</li>
<li>Dependencies: Spring Web, Spring Boot DevTools, Lombok</li>
</ul>
<p>Download the project, open it in your IDE, and get ready to run it.</p>
<hr>
<h2>🛠️ Step 2: Create a Model Class</h2>
<p>Create a model class for a <code>Todo</code> object:</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.model;

import lombok.Data;

@Data
public class Todo {
    private Long id;
    private String title;
    private boolean completed;
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.model

data class Todo(
    var id: Long,
    var title: String,
    var completed: Boolean
)
</code></pre>
<hr>
<h2>🛠️ Step 3: Create a Controller Class</h2>
<p>Add a <code>TodoController</code> class to define CRUD operations as REST endpoints:</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo.controller;

import com.example.demo.model.Todo;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private List&#x3C;Todo> todos = new ArrayList&#x3C;>();

    @GetMapping
    public List&#x3C;Todo> getAllTodos() {
        return todos;
    }

    @PostMapping
    public Todo createTodo(@RequestBody Todo todo) {
        todo.setId((long) (todos.size() + 1));
        todos.add(todo);
        return todo;
    }

    @PutMapping("/{id}")
    public Todo updateTodo(@PathVariable Long id, @RequestBody Todo updatedTodo) {
        Todo todo = todos.stream().filter(t -> t.getId().equals(id)).findFirst().orElse(null);
        if (todo != null) {
            todo.setTitle(updatedTodo.getTitle());
            todo.setCompleted(updatedTodo.isCompleted());
        }
        return todo;
    }

    @DeleteMapping("/{id}")
    public void deleteTodo(@PathVariable Long id) {
        todos.removeIf(todo -> todo.getId().equals(id));
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo.controller

import com.example.demo.model.Todo
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/todos")
class TodoController {

    private val todos = mutableListOf&#x3C;Todo>()

    @GetMapping
    fun getAllTodos(): List&#x3C;Todo> = todos

    @PostMapping
    fun createTodo(@RequestBody todo: Todo): Todo {
        todo.id = (todos.size + 1).toLong()
        todos.add(todo)
        return todo
    }

    @PutMapping("/{id}")
    fun updateTodo(@PathVariable id: Long, @RequestBody updatedTodo: Todo): Todo? {
        val todo = todos.find { it.id == id }
        todo?.apply {
            title = updatedTodo.title
            completed = updatedTodo.completed
        }
        return todo
    }

    @DeleteMapping("/{id}")
    fun deleteTodo(@PathVariable id: Long) {
        todos.removeIf { it.id == id }
    }
}
</code></pre>
<hr>
<h2>🧪 Step 4: Test the API</h2>
<p>Test your API using <code>curl</code> commands:</p>
<ul>
<li>GET All Todos:</li>
</ul>
<pre><code class="language-bash">curl -X GET http://localhost:8080/api/todos
</code></pre>
<ul>
<li>POST New Todo:</li>
</ul>
<pre><code class="language-bash">curl -X POST http://localhost:8080/api/todos -H "Content-Type: application/json" -d '{"title": "New Todo", "completed": false}'
</code></pre>
<ul>
<li>PUT Update Todo:</li>
</ul>
<pre><code class="language-bash">curl -X PUT http://localhost:8080/api/todos/1 -H "Content-Type: application/json" -d '{"title": "Updated Todo", "completed": true}'
</code></pre>
<ul>
<li>DELETE Delete Todo:</li>
</ul>
<pre><code class="language-bash">curl -X DELETE http://localhost:8080/api/todos/1
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Building a REST API with Spring Boot implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[REST API]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-rest-api-thumbnail.webp" />
    </item>
    <item>
      <title><![CDATA[Spring Boot - First Application]]></title>
      <link>https://suaybsimsek.com/posts/spring-boot-first-application</link>
      <description><![CDATA[A beginner-friendly guide to creating your first Spring Boot application from scratch. Learn the basics and start your journey with Spring Boot.]]></description>
      <pubDate>Tue, 10 Dec 2024 00:00:00 GMT</pubDate>
      <atom:updated>2024-12-11T00:00:00.000Z</atom:updated>
      <guid isPermaLink="true">https://suaybsimsek.com/posts/spring-boot-first-application</guid>
      <content:encoded><![CDATA[<p>Spring Boot simplifies the process of building production-ready applications with the Spring Framework. In this guide, we’ll walk through creating your first Spring Boot application step by step.</p>
<hr>
<h2>🌟 What you'll learn</h2>
<p>In this section, we clarify What you'll learn and summarize the key points you will apply in implementation.</p>
<ul>
<li>How to bootstrap a Spring Boot project with Java or Kotlin.</li>
<li>How to expose your first HTTP endpoint and verify it quickly.</li>
<li>How to run the app locally and prepare a clean base for the next features.</li>
</ul>
<hr>
<h2>📋 Prerequisites</h2>
<p>📋 Before starting, ensure you have the following:</p>
<ul>
<li>☕ Java Development Kit (JDK) 17+ installed</li>
<li>📦 Maven or Gradle installed</li>
<li>🖥️ A Java IDE (e.g., IntelliJ IDEA, Eclipse, or Visual Studio Code)</li>
</ul>
<hr>
<h2>🛠️ Step 1: Create a Spring Boot Project</h2>
<p>You can create your first Spring Boot project in two ways:</p>
<ol>
<li>Using Spring Initializr 🖱️</li>
</ol>
<ul>
<li>Visit <a href="https://start.spring.io/">Spring Initializr</a>.</li>
<li>Configure the following:
<ul>
<li>📂 Project: <code>Maven</code></li>
<li>👨‍💻 Language: <code>Java</code> or <code>Kotlin</code></li>
<li>🔄 Spring Boot Version: <code>3.0.0</code> (or the latest version).</li>
<li>📜 Add dependencies: <code>Spring Web</code></li>
</ul>
</li>
<li>Click Generate to download the project files.</li>
</ul>
<ol start="2">
<li>Using IntelliJ IDEA 💻</li>
</ol>
<ul>
<li>Open IntelliJ IDEA.</li>
<li>Go to <code>New Project > Spring Initializr</code>.</li>
<li>Configure similar parameters as mentioned above.</li>
</ul>
<hr>
<h2>🛠️ Step 2: Writing Your First Endpoint</h2>
<p>Let’s write a simple endpoint to say hello:</p>
<h3>Java</h3>
<pre><code class="language-java">package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String sayHello() {
        return "Hello, Spring Boot!";
    }
}
</code></pre>
<h3>Kotlin</h3>
<pre><code class="language-kotlin">package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
@RestController
class DemoApplication

fun main(args: Array&#x3C;String>) {
    runApplication&#x3C;DemoApplication>(*args)
}

@GetMapping("/hello")
fun sayHello(): String {
    return "Hello, Spring Boot!"
}
</code></pre>
<hr>
<h2>▶️ Step 3: Run the Application</h2>
<p>In this section, we clarify Step 3: Run the Application and summarize the key points you will apply in implementation.</p>
<ol>
<li>Open a terminal in the project folder.</li>
<li>Execute the command to run your application:</li>
</ol>
<pre><code class="language-bash">./mvnw spring-boot:run
</code></pre>
<ol start="3">
<li>Access the endpoint at:
<pre><code class="language-filename=&#x22;snippet.txt&#x22;">http://localhost:8080/hello
</code></pre>
</li>
</ol>
<p>Response:</p>
<pre><code class="language-filename=&#x22;snippet.txt&#x22;">Hello, Spring Boot!
</code></pre>
<hr>
<h2>🏁 Conclusion</h2>
<p>You now have a practical Spring Boot - First Application implementation with a clear, production-friendly Spring Boot structure. As a next step, adapt configuration and tests to your own domain, then validate behavior under realistic traffic and failure scenarios.</p>]]></content:encoded>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Kotlin]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[Beginner Guide]]></category>
      <media:thumbnail url="https://suaybsimsek.com/images/spring-boot-first-app-thumbnail.webp" />
    </item>
  </channel>
</rss>
