<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Thanasis Chousiadas on Medium]]></title>
        <description><![CDATA[Stories by Thanasis Chousiadas on Medium]]></description>
        <link>https://medium.com/@th.chousiadas?source=rss-b6aff825225c------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*dDn2c05NpUkHVsJeTYjYRQ.jpeg</url>
            <title>Stories by Thanasis Chousiadas on Medium</title>
            <link>https://medium.com/@th.chousiadas?source=rss-b6aff825225c------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 24 May 2026 02:24:43 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@th.chousiadas/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Using Redis with Spring Boot application in Docker]]></title>
            <link>https://medium.com/@th.chousiadas/using-redis-with-spring-boot-application-in-docker-678d785a601a?source=rss-b6aff825225c------2</link>
            <guid isPermaLink="false">https://medium.com/p/678d785a601a</guid>
            <category><![CDATA[https]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[spring]]></category>
            <dc:creator><![CDATA[Thanasis Chousiadas]]></dc:creator>
            <pubDate>Tue, 22 Apr 2025 12:50:28 GMT</pubDate>
            <atom:updated>2025-04-22T12:51:06.430Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CVhHu0E1ePWv-gyEgZbDaA.png" /><figcaption>The image is generated with ChatGPT</figcaption></figure><p>In this article, we demonstrate how we can configure a <em>Redis Cache¹ ² </em>for a <em>Spring Boot</em> <em>Application</em> in containerized environment using <em>Docker</em>.</p><p>For that purpose, we will build a small <em>REST API</em> with <em>Spring Boot</em> where it sends request to another free <em>API</em> and it will cache the response in the <em>Redis Cache</em> for subsequent calls. A well-known free API which gives dummy response data is <a href="http://jsonplaceholder.typicode.com/">http://jsonplaceholder.typicode.com</a>. Under the hood, the application will call this API using the <em>Apache HTTP Client</em> library³ for fetching the data for dummy posts.</p><h4>Step 1: Gradle dependencies</h4><p>Firstly, we use the <em>Gradle</em> with the following dependencies,</p><pre>    implementation &#39;org.springframework.boot:spring-boot-starter-web:3.4.4&#39;<br>    implementation &#39;org.apache.httpcomponents:httpclient:4.5.14&#39;<br>    implementation &#39;org.hibernate:hibernate-validator:8.0.2.Final&#39;<br>    implementation &#39;redis.clients:jedis:5.2.0&#39;<br><br>    implementation &#39;org.mapstruct:mapstruct:1.6.3&#39;<br>    annotationProcessor &#39;org.mapstruct:mapstruct-processor:1.6.3&#39;</pre><p>For the <em>Spring Boot</em> we use the classical starter dependency. In addition, we use <em>MapStruct</em> and <em>Hibernate Validation</em> for <em>DTOs</em>. Regarding <em>Redis</em> client we use the <em>Jedis</em> and <em>HTTP client </em>the<em> Apache HTTP Client version 4</em>.</p><h4>Step 2: application.properties file</h4><p>In <em>application.properties</em> file we placed all the necessary properties for configuring <em>HTTP Client</em> and <em>Redis</em>. As you can see, the properties are more or less the same because both of them make use of <em>TCP protocol</em> upon a <em>client-server</em> architecture.</p><pre>spring.redis.host=${SPRING_REDIS_HOST:localhost}<br>spring.redis.port=${SPRING_REDIS_PORT:6379}<br>spring.redis.max-size=100<br>spring.redis.max-idle=30<br>spring.redis.min-idle=10<br>spring.redis.max-wait=2000<br>spring.redis.test-on-borrow=false<br>spring.redis.test-on-return=false<br>spring.redis.test-while-idle=false<br>spring.redis.time-between-eviction=30000<br>spring.redis.min-evictable-idle-time=60000<br>spring.redis.block-when-exhausted=true<br>spring.redis.expiration-in-seconds=3600<br><br>http.client.max-size=100<br>http.client.max-per-route=20<br>http.client.connect-timeout=5000<br>http.client.request-timeout=5000<br>http.client.socket-timeout=10000<br><br>api.protocol=https<br>api.baseUrl=jsonplaceholder.typicode.com</pre><h4>Step 3: Dockerfile and compose.yaml</h4><p>The <em>Spring Boot</em> application and the <em>Redis</em> will be containerized. For that purpose, we want to create a <em>Dockerfile</em> for the <em>Spring Boot</em>⁴,</p><pre>FROM eclipse-temurin:21-jdk<br><br>RUN groupadd -r spring &amp;&amp; useradd -r -g spring springuser<br><br>WORKDIR /app<br><br>COPY --chown=springuser:spring build/libs/cache-service-1.0.jar app.jar<br><br>USER springuser<br><br>ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</pre><p>We use <em>Java 21</em> for running the application, and the base image <em>eclipse-temurin:21-jdk. </em>The<em> cache-service-1.0.jar</em> is the <em>jar</em> file of our application and we copy it inside the filesystem of the image. Also we have created a <em>spring</em> group and the user <em>springuser</em>, as security best practice.</p><p>In the same manner, we use the base image of <em>Redis⁵ </em>as found in the <em>DockerHub</em>, without any modifications. In order to create the container for the application and for <em>Redis</em>, we use a <em>compose.yaml</em> file⁶ as shown below,</p><pre>services:<br>  redis:<br>    image: redis<br>    container_name: redis<br>    ports:<br>      - &quot;6379:6379&quot;<br><br>  spring-app:<br>    build: .<br>    container_name: spring-app<br>    ports:<br>      - &quot;8080:8080&quot;<br>    environment:<br>      SPRING_REDIS_HOST: redis<br>      SPRING_REDIS_PORT: 6379</pre><h4>Step 4: RedisConfig and RedisService classes</h4><p>In <em>RedisConfig</em> class we configure the connection pool and we use <em>JedisPool</em> object as <em>Bean</em> with <em>Spring’s Dependency Injection</em>⁷.</p><pre>@Data<br>@Configuration<br>@ConfigurationProperties(prefix = &quot;spring.redis&quot;)<br>public class RedisConfig {<br><br>    private String host;<br>    private int port;<br>    private int maxSize;<br>    private int maxIdle;<br>    private int minIdle;<br>    private int maxWait;<br>    private boolean testOnBorrow;<br>    private boolean testOnReturn;<br>    private boolean testWhileIdle;<br>    private int timeBetweenEviction;<br>    private int minEvictableIdleTime;<br>    private boolean blockWhenExhausted;<br><br>    @Bean<br>    public JedisPool jedisPool() {<br>        <br>        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();<br><br>        // Configure pool size<br>        jedisPoolConfig.setMaxTotal(maxSize);<br>        jedisPoolConfig.setMaxIdle(maxIdle);<br>        jedisPoolConfig.setMinIdle(minIdle);<br>        // Set eviction settings for the connections<br>        jedisPoolConfig.setMaxWait(Duration.ofMillis(maxWait));<br>        jedisPoolConfig.setTestOnBorrow(testOnBorrow);<br>        jedisPoolConfig.setTestOnReturn(testOnReturn);<br>        jedisPoolConfig.setTestWhileIdle(testWhileIdle);<br>        jedisPoolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(timeBetweenEviction));<br>        jedisPoolConfig.setMinEvictableIdleDuration(Duration.ofMillis(minEvictableIdleTime));<br>        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);<br><br>        return new JedisPool(jedisPoolConfig, host, port);<br>    }<br>}</pre><p>We use <em>@ConfigurationProperties(prefix = “spring.redis”) </em>to get the values of the variables from the <em>application.properties</em> file. As it is shown, we can configure many properties for the <em>Redis Server, </em>the most common are pool size. This is a simple example for non production applications. In production environment a tuning of connection pool is needed for better performance.</p><p>In the <em>RedisService</em> class we declare the method to <em>get, set</em> and <em>delete</em> a <em>key-value pair</em>,</p><pre>@Data<br>@Service<br>@ConfigurationProperties(prefix = &quot;spring.redis&quot;)<br>public class RedisService {<br><br>    private int expirationInSeconds;<br><br>    private final JedisPool jedisPool;<br><br>    public RedisService(JedisPool jedisPool) {<br><br>        this.jedisPool = jedisPool;<br>    }<br><br>    public void setData(String key, String value) {<br><br>        try (Jedis jedis = jedisPool.getResource()) {<br>            jedis.set(key, value);<br>        }<br>    }<br><br>    public void setDataWithExpiration(String key, String value, long requestedExpiration) {<br><br>        long expirationToBeSet = requestedExpiration == 0 ? expirationInSeconds : requestedExpiration;<br><br>        try (Jedis jedis = jedisPool.getResource()) {<br>            jedis.setex(key, expirationToBeSet, value);<br>        }<br>    }<br><br>    public String getData(String key) {<br><br>        try (Jedis jedis = jedisPool.getResource()) {<br>            return jedis.get(key);<br>        }<br>    }<br><br>    public long deleteData(String key) {<br><br>        try (Jedis jedis = jedisPool.getResource()) {<br>            return jedis.del(key);<br>        }<br>    }<br><br>    @PostConstruct<br>    public void checkConnection() {<br><br>        try (Jedis jedis = jedisPool.getResource()) {<br>            String response = jedis.ping();<br>            System.out.println(&quot;Redis connection OK: &quot; + response);<br>        } catch (Exception e) {<br>            System.out.println(&quot;Redis connection failed: &quot; + e.getMessage());<br>        }<br>    }<br>}</pre><p>The <em>setData()</em> sets a <em>key-value</em> without expiration time, default behavior and <em>setDataWithExpiration()</em> method sets a key with expiration time that we can set from a controller, as we will see. In the same manner, we have the <em>getData()</em> method, where we retrieve the value based on the key and the <em>deleteData()</em> method where we delete a <em>key-value pair</em>. The <em>checkConnection()</em> method is a helper method to see if the <em>Redis Server</em> is running after the <em>RedisService</em> is initialized.</p><h4>Step 5: HttpConfig, HttpService, ApiService and ApiProperties</h4><p>In the same way, we have created the relative classes for sending<em> HTTP</em> requests. The <em>HttpConfig</em> provides a <em>CloseableHttpClient</em> object for sending requests in the <a href="http://jsonplaceholder.typicode.com/">http://jsonplaceholder.typicode.com/</a>. In this example, we send <em>GET</em> and <em>PUT</em> requests for the posts <em>API</em> (<a href="http://jsonplaceholder.typicode.com/posts/1">http://jsonplaceholder.typicode.com/posts/1</a>).</p><p>The <em>ApiService</em> and <em>ApiProperties</em> are helper method to construct the <em>URLs</em> for the different endpoints. As we can see the above <em>API</em> has many endpoints. The code for those classes can be found in the <a href="https://github.com/SakisHous/cache-service"><em>GitHub Repository</em></a>.</p><h4>Step 6: Data Transfer Objects</h4><p>In this simple application we have the <em>DTOs</em> for <em>Post</em>, <em>UpdatePost</em> and <em>HttpCallResponse.</em></p><pre>@Data<br>@JsonIgnoreProperties(ignoreUnknown = true)<br>@AllArgsConstructor<br>@NoArgsConstructor<br>public class Post {<br><br>    private long userId;<br>    private long id;<br>    private String title;<br>    private String body;<br>}</pre><pre>@Data<br>@JsonIgnoreProperties(ignoreUnknown = true)<br>@AllArgsConstructor<br>@NoArgsConstructor<br>public class UpdatePost {<br><br>    @NotBlank<br>    private long userId;<br>    @NotBlank<br>    private long id;<br>    private String title;<br>    private String body;<br>}</pre><p>The <em>HttpCallResponse</em> is a custom response object we use for the response entity.</p><pre>@Data<br>@Builder<br>@AllArgsConstructor<br>@NoArgsConstructor<br>public class HttpCallResponse {<br><br>    private int statusCode;<br>    private String body;<br>}</pre><h4>Step 7: PostServiceImpl class</h4><p>The <em>PostServiceImpl</em> class implements <em>PostService</em> interface and it has three methods,</p><pre>public interface PostService {<br><br>    Post getPost(long postId);<br>    Post getPostWithExpirationInRedis(long postId, long expirationInSeconds);<br>    Post updatePostAndRemoveFromCache(UpdatePost updatePost);<br>}</pre><p>In the <em>getPost()</em> method, first we check if the <em>id</em> of the <em>Post</em> is saved as key in <em>Redis</em>. If indeed we have such a key, we retrieve the value. This value is basically a <em>JSON</em> representation of the object saved as string. Retrieving this value from Redis we construct the <em>Post</em> object using <em>Jackson-Databind</em> library⁸ and return a response without sending any request to the public <em>API</em>.</p><p>If we do not have the key, we send a <em>GET request</em> to the public <em>API</em> and save the response body in <em>Redis</em>. The key is a string of Post’s id and the value is a string of the whole response body, e.g.</p><pre>{<br>    &quot;userId&quot;: 1,<br>    &quot;id&quot;: 1,<br>    &quot;title&quot;: &quot;sunt aut facere repellat provident occaecati excepturi optio reprehenderit&quot;,<br>    &quot;body&quot;: &quot;quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto&quot;<br>}</pre><pre>@Service<br>@AllArgsConstructor<br>public class PostServiceImpl implements PostService {<br><br>    private final ApiService apiService;<br>    private final RedisService redisService;<br>    private final HttpService httpService;<br><br>    public Post getPost(long postId) {<br><br>        String result = redisService.getData(Long.toString(postId));<br>        ObjectMapper mapper = new ObjectMapper();<br><br>        if (Objects.nonNull(result)) {<br><br>            System.out.println(&quot;You hit Redis cache&quot;);<br>            try {<br>                return mapper.readValue(result, Post.class);<br>            } catch (JsonProcessingException e) {<br>                System.out.println(e.getMessage());<br>            }<br>        }<br><br>        // Send HTTP GET Request to the API for fetching the Post<br>        String url = apiService.constructGetPostByIdEndpoint(postId);<br><br>        try {<br>            HttpCallResponse response = httpService.getRequest(url);<br><br>            if (response.getStatusCode() == 200) {<br>                String body = response.getBody();<br>                redisService.setData(Long.toString(postId), response.getBody());<br><br>                // Use Jackson to convert JSON to Java object<br>                return mapper.readValue(body, Post.class);<br>            }<br>        } catch (Exception e) {<br>            System.out.println(e.getMessage());<br>        }<br><br>        return null;<br>    }<br><br>    public Post getPostWithExpirationInRedis(long postId, long expirationInSeconds) {<br><br>        String result = redisService.getData(Long.toString(postId));<br>        ObjectMapper mapper = new ObjectMapper();<br><br>        if (Objects.nonNull(result)) {<br><br>            System.out.println(&quot;You hit Redis cache&quot;);<br>            try {<br>                return mapper.readValue(result, Post.class);<br>            } catch (JsonProcessingException e) {<br>                System.out.println(e.getMessage());<br>            }<br>        }<br><br>        // Send HTTP GET Request to the API for fetching the Post<br>        String url = apiService.constructGetPostByIdEndpoint(postId);<br><br>        try {<br>            HttpCallResponse response = httpService.getRequest(url);<br><br>            if (response.getStatusCode() == 200) {<br>                String body = response.getBody();<br>                redisService.setDataWithExpiration(Long.toString(postId), response.getBody(), expirationInSeconds);<br><br>                // Use Jackson to convert JSON to Java object<br>                return mapper.readValue(body, Post.class);<br>            }<br>        } catch (Exception e) {<br>            System.out.println(e.getMessage());<br>        }<br><br>        return null;<br>    }<br><br>    public Post updatePostAndRemoveFromCache(UpdatePost updatePost) {<br><br>        ObjectMapper mapper = new ObjectMapper();<br>        // Send HTTP GET Request to the API for fetching the Post<br>        String url = apiService.constructPutPostByIdEndpoint(updatePost.getId());<br><br>        try {<br>            String jsonPayload = mapper.writeValueAsString(updatePost);<br><br>            HttpCallResponse response = httpService.putRequest(url, jsonPayload);<br><br>            if (response.getStatusCode() == 200) {<br>                String body = response.getBody();<br><br>                // Use Jackson to convert JSON to Java object<br>                Post post = mapper.readValue(body, Post.class);<br><br>                //Invalidate from the Redis Cache<br>                long deletedKey = redisService.deleteData(Long.toString(post.getId()));<br>                System.out.println(&quot;Deleted key: &quot; + deletedKey);<br><br>                return post;<br>            }<br>        } catch (Exception e) {<br>            System.out.println(e.getMessage());<br>        }<br><br>        return null;<br>    }<br>}</pre><h4>Step 8: PostController</h4><p>Our application has the following endpoints that are declared in the <em>PostController</em> class.</p><pre>@AllArgsConstructor<br>@RestController<br>public class PostController {<br><br>    private final PostService postService;<br><br>    @GetMapping(&quot;/posts/{id}&quot;)<br>    public ResponseEntity&lt;Post&gt; getPost(@PathVariable(&quot;id&quot;) long postId) {<br><br>        Long startTime = System.currentTimeMillis();<br>        Post post = postService.getPost(postId);<br>        Long endTime = System.currentTimeMillis();<br><br>        System.out.println(&quot;Elapsed time: &quot; + (endTime - startTime) + &quot;ms&quot;);<br>        return new ResponseEntity&lt;&gt;(post, HttpStatus.OK);<br>    }<br><br>    @GetMapping(&quot;/posts/{id}/set&quot;)<br>    public ResponseEntity&lt;Post&gt; getPostWithExpirationTime(@PathVariable(&quot;id&quot;) long postId,<br>                                                          @RequestParam long expirationInSeconds) {<br><br>        Long startTime = System.currentTimeMillis();<br>        Post post = postService.getPostWithExpirationInRedis(postId, expirationInSeconds);<br>        Long endTime = System.currentTimeMillis();<br><br>        System.out.println(&quot;Elapsed time: &quot; + (endTime - startTime) + &quot;ms&quot;);<br>        return new ResponseEntity&lt;&gt;(post, HttpStatus.OK);<br>    }<br><br>    @PutMapping(&quot;/posts/{id}&quot;)<br>    public ResponseEntity&lt;?&gt; updatePost(@PathVariable(&quot;id&quot;) long id,<br>                                        @Valid @RequestBody UpdatePost updatePost,<br>                                        BindingResult bindingResult) {<br><br>        if (bindingResult.hasErrors()) {<br>            return ResponseEntity.badRequest().body(bindingResult.getAllErrors());<br>        }<br><br>        Post updatedPost = postService.updatePostAndRemoveFromCache(updatePost);<br><br>        return new ResponseEntity&lt;&gt;(updatedPost, HttpStatus.OK);<br>    }<br>}</pre><p>The <em>/posts/{id} </em>endpoint handles <em>HTTP GET</em> method. In the beginning, <em>Redis</em> does not contain any <em>Post</em>, e.g. <em>id = 1</em>. The application sends a <em>GET request</em> to the <a href="http://jsonplaceholder.typicode.com/posts/1">http://jsonplaceholder.typicode.com/posts/1</a> and sets the value in the <em>Redis</em>⁹. In order to understand better how fast we get the response from <em>Redis Cache</em>, in the controller is calculating the elapsed time and outputs the value in the <em>console log. </em>Sending a <em>HTTP</em> request it takes some time, e.g. some hundreds <em>ms. </em>Although, for <em>Redis</em> this operation is executed under 3<em>ms, </em>because it retrieves the data from the <em>RAM.</em></p><p>The <em>/posts/{id} P</em>UT method updates Post title and body and removes the <em>key-value pair</em> from the <em>Redis Server</em>, if it is found. The method <em>RedisService#deleteData() </em>returns 0 if no pair were deleted otherwise a positive value.</p><p>The <em>/posts/{id}/set?expirationInSeconds=3600 </em>endpoint handles also <em>HTTP GET</em> method but it use the <em>setex()</em> method of <em>Jedis</em> which accepts an argument for the expiration time of the key in seconds¹⁰, in our example we use an hour for expiration time.</p><h4>Step 9: CacheServiceApplication</h4><p>This class is the typical starter class of a <em>Spring Boot</em> application. We have also added <em>@ConfigurationPropertiesScan </em>to scan the project’s classes and inject the values from the <em>application.properties</em> file.</p><pre>@SpringBootApplication<br>@ConfigurationPropertiesScan<br>public class CacheServiceApplication {<br><br>    public static void main(String[] args) {<br>        SpringApplication.run(CacheServiceApplication.class, args);<br>    }<br>}</pre><h4>Running the application</h4><p>In order to run the application in <em>Docker</em>, we have to build the <em>jar</em> file, create the images and start the containers. For that purpose we use,</p><pre>./gradlew clean build<br><br>docker compose build // (or docker compose down)<br><br>docker compose up</pre><p>The application is running in <a href="http://localhost:8080">http://localhost:8080</a>. For retrieving the first post with can send a request <a href="http://localhost:8080/posts/1">http://localhost:8080/posts/1</a> using <em>Postman</em>.</p><p>You can find all the code in the <a href="https://github.com/SakisHous/cache-service"><em>GitHub repository</em></a>.</p><h4>References</h4><p><em>[1] </em><a href="https://redis.io/"><em>https://redis.io</em></a><em>/ Redis Official Site.</em></p><p><em>[2] </em><a href="https://redis.io/nosql/key-value-databases/"><em>https://redis.io/nosql/key-value-databases/</em></a><em> Key-value databases article.</em></p><p><em>[3] </em><a href="https://hc.apache.org/httpcomponents-client-4.5.x/index.html"><em>https://hc.apache.org/httpcomponents-client-4.5.x/index.html</em></a><em> Apache HTTP Client documentation.</em></p><p><em>[4] </em><a href="https://spring.io/guides/gs/spring-boot-docker"><em>https://spring.io/guides/gs/spring-boot-docker</em></a><em> Tutorial for creating Spring Boot Container in Docker.</em></p><p><em>[5] </em><a href="https://hub.docker.com/_/redis"><em>https://hub.docker.com/_/redis</em></a><em> Docker Image for Redis Cache.</em></p><p><em>[6] </em><a href="https://docs.docker.com/compose/intro/compose-application-model/"><em>https://docs.docker.com/compose/intro/compose-application-model/</em></a><em> Docker Compose Guide.</em></p><p><em>[7] </em><a href="https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html"><em>https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html</em></a><em> Spring DI tutorial.</em></p><p><em>[8] </em><a href="https://github.com/FasterXML/jackson-databind"><em>https://github.com/FasterXML/jackson-databind</em></a><em> JSON serialize, de-serialize library.</em></p><p><em>[9] </em><a href="https://redis.io/docs/latest/commands/set/"><em>https://redis.io/docs/latest/commands/set/</em></a><em> Set command documentation.</em></p><p><em>[10] </em><a href="https://redis.io/docs/latest/commands/setex/"><em>https://redis.io/docs/latest/commands/setex/</em></a><em> Setex command documentation.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=678d785a601a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Spring Security Architecture of JWT Authentication]]></title>
            <link>https://medium.com/@th.chousiadas/spring-security-architecture-of-jwt-authentication-a7967a8ee309?source=rss-b6aff825225c------2</link>
            <guid isPermaLink="false">https://medium.com/p/a7967a8ee309</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[jwt]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[spring-security]]></category>
            <category><![CDATA[spring]]></category>
            <dc:creator><![CDATA[Thanasis Chousiadas]]></dc:creator>
            <pubDate>Sun, 16 Jun 2024 19:24:25 GMT</pubDate>
            <atom:updated>2024-06-16T19:24:25.253Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rms91PTLhpkL0Qrj6lZR_g.jpeg" /><figcaption>Image from <a href="https://www.pexels.com/photo/security-logo-60504/">pexels.com</a></figcaption></figure><h4>Introduction</h4><p>Nowadays, <em>Web Services</em> using <em>REST architecture</em>, are built in many small and large-scale systems. A key component of theses services is security. In this article, we will demonstrate the basic architectural design of <em>authentication</em> using <em>JWT Bearer token in Spring Security</em>¹.</p><p>To begin with, the <em>JWT (JSON Web Token) is a token format</em>² that it is used both in <em>OAuth2</em> security framework and standalone. In the latter situation, the information of a user is <em>encoding in a token (JWT)</em> and it is passed in each request in the header <em>Authorization</em> as shown below,</p><pre>Authorization: Bearer eyJhbGciQiJIUzC1NiJ9.eyJzdWIiOiJ0ZXN0vnVzZXIiKCJpYXQiOjE3MTc2MDI1ODIsImV4cCI6MTcxNzYwNDM4Mn0.6y6WSTtcQ8Bf2eV4f4TcVrftS4HHJJPEYWRhSnLSu_w</pre><p>The main difference from the classical approach (<em>session</em> and <em>cookies</em>) is that each request has the information for the user and each time the server process this request. This comes in accordance with the <em>stateless</em> concept of <em>REST</em>.</p><p>In Spring Security the request is passing from different filters that transform the request or even send a response. In the bottom of the filter chain is a servlet (<em>DispatcherServlet</em> instance) and it works as front controller. Regarding authentication, is performed in the security chain. This is a separated from the main <em>FilterChain</em> instance using a dedicated <em>FilterChainProxy</em> instance where security filters are registered<em>¹</em>.</p><h4>Application Setup</h4><p>We will make a simple application, named <em>jwt-application</em>, with a table in MySQL database to persist user data and some endpoints. The application is built upon<strong><em> Java 17</em></strong>, <strong><em>Spring 6</em></strong>, <strong><em>Spring Boot 3.3.0</em></strong> and <strong><em>Spring Security 6.3.0. </em></strong>The packages for this application <em>(using Maven)</em> are shown,</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;<br>    &lt;artifactId&gt;jjwt-api&lt;/artifactId&gt;<br>    &lt;version&gt;0.11.5&lt;/version&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;<br>    &lt;artifactId&gt;jjwt-impl&lt;/artifactId&gt;<br>    &lt;version&gt;0.11.5&lt;/version&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;<br>    &lt;artifactId&gt;jjwt-jackson&lt;/artifactId&gt;<br>    &lt;version&gt;0.11.5&lt;/version&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;com.mysql&lt;/groupId&gt;<br>    &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;<br>    &lt;scope&gt;runtime&lt;/scope&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;<br>    &lt;artifactId&gt;lombok&lt;/artifactId&gt;<br>    &lt;optional&gt;true&lt;/optional&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.security&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-security-test&lt;/artifactId&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;</pre><p>In order to connect with the database we need to make a database schema in <em>MySQL</em> and an user with the role of <em>owner</em>. After that, we put the configuration for the <em>JDBC</em> in the <em>application.properties</em> file,</p><pre>spring.application.name=jwt-application<br>spring.main.allow-circular-references=true<br>spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver<br>spring.datasource.url = jdbc:mysql://localhost:3306/${JWT_APP_DB}<br>spring.datasource.username = ${JWT_APP_USERNAME}<br>spring.datasource.password = ${JWT_APP_PASSWORD}<br>spring.jpa.hibernate.ddl-auto = create<br>spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect<br>spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl<br><br>jwt.secret.key = ${JWT_KEY}</pre><p>The Spring Boot scans environment variables and takes the values. For example, we have put <em>JWT_APP_DB</em> environment variable and Spring Boot reads it from the system with the annotation <em>${JWT_APP_DB}</em>.</p><p>The property <em>jwt.secret.key</em> is needed for signing the JWT. It is a long alphanumerical string with symbols<em>²</em>.</p><h4>Entity User</h4><p>First we create the entity <em>User</em> in the package hierarchy <em>jwtapplication &gt; entity &gt; User.java</em>. Hibernate will make the table for us. <em>Lombok</em> is using for brevity and it provides for us the getters, setters, constructors and builders.</p><pre>@Entity<br>@Data<br>@AllArgsConstructor<br>@NoArgsConstructor<br>@Builder<br>public class UserInfo {<br><br>    @Id<br>    @GeneratedValue(strategy = GenerationType.IDENTITY)<br>    private int id;<br>    private String username;<br>    private String email;<br>    private String password;<br>    private String roles;<br>}</pre><h4>User Repository</h4><p>Using the <em>repository pattern</em>, we make a repository interface <em>jwtapplication &gt; repository &gt; UserRepository.java</em></p><pre>@Repository<br>public interface UserRepository extends JpaRepository&lt;User, Integer&gt; {<br>    Optional&lt;User&gt; getUserInfoByUsername(String username);<br>}</pre><h4>Main controller</h4><p>We create a simple controller in a package hierarchy <em>jwtapplication &gt; controller &gt; UserController.java</em> with base routing <em>/api/v1</em> and it has a public endpoint (<em>/welcome</em>)and a private one (<em>/user/profile</em>).</p><pre>@RestController<br>@RequestMapping(&quot;/api/v1&quot;)<br>public class UserController {<br><br>    @GetMapping(&quot;/welcome&quot;)<br>    public String welcome() {<br>        return &quot;Welcome page&quot;;<br>    }<br><br>    @GetMapping(&quot;/user/profile&quot;)<br>    @PreAuthorize(&quot;hasAuthority(&#39;ROLE_USER&#39;)&quot;)<br>    public String userProfile() {<br>        return &quot;User profile is shown here.&quot;;<br>    }<br>}</pre><h4>JWT Service class</h4><p>In addition, we create a <em>JWT</em> service class <em>jwtapplication &gt; authentication &gt; JwtService.java</em>. This class provides the necessary methods for creating the token, validating the token etc. For this purpose we use the <strong><em>jjwt </em></strong><em>library</em>.</p><pre>@Component<br>public class JwtService {<br><br>    @Value(&quot;${jwt.secret.key}&quot;)<br>    public String SECRET;<br><br>    public String generateToken(String username) {<br>        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();<br><br>        return createToken(claims, username);<br>    }<br><br>    private String createToken(Map&lt;String, Object&gt; claims, String username) {<br>        return Jwts.builder()<br>                .setClaims(claims)<br>                .setSubject(username)<br>                .setIssuedAt(new Date(System.currentTimeMillis()))<br>                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30))<br>                .signWith(getSignKey(), SignatureAlgorithm.HS256).compact();<br>    }<br><br>    private Key getSignKey() {<br>        byte[] keyBytes= Decoders.BASE64.decode(SECRET);<br>        return Keys.hmacShaKeyFor(keyBytes);<br>    }<br>    public String extractUsername(String token) {<br>        return extractClaim(token, Claims::getSubject);<br>    }<br><br>    public Date extractExpiration(String token) {<br>        return extractClaim(token, Claims::getExpiration);<br>    }<br><br>    public &lt;T&gt; T extractClaim(String token, Function&lt;Claims, T&gt; claimsResolver) {<br>        final Claims claims = extractAllClaims(token);<br>        return claimsResolver.apply(claims);<br>    }<br>    private Claims extractAllClaims(String token) {<br>        return Jwts<br>                .parserBuilder()<br>                .setSigningKey(getSignKey())<br>                .build()<br>                .parseClaimsJws(token)<br>                .getBody();<br>    }<br><br>    private Boolean isTokenExpired(String token) {<br>        return extractExpiration(token).before(new Date());<br>    }<br><br>    public Boolean validateToken(String token, UserDetails userDetails) {<br>        final String username = extractUsername(token);<br>        return (username.equals(userDetails.getUsername()) &amp;&amp; !isTokenExpired(token));<br>    }<br>}</pre><h4>Authentication controller</h4><p>We continue to write the authentication controller. Basically, this controller handles login and authentication operations. In the package <em>jwtapplication &gt; authentication &gt; AuthController.java </em>we have,</p><pre>@RestController<br>@RequestMapping(&quot;/api/v1/auth&quot;)<br>public class AuthController {<br>    private final AuthService authService;<br><br>    public AuthController(AuthService authService) {<br>        this.authService = authService;<br>    }<br><br>    @PostMapping(&quot;/register&quot;)<br>    public ResponseEntity&lt;AuthResponse&gt; register(@RequestBody RegisterDTO dto) {<br>        return ResponseEntity.ok(authService.register(dto));<br>    }<br><br>    @PostMapping(&quot;/authenticate&quot;)<br>    public ResponseEntity&lt;AuthResponse&gt; authenticate(<br>            @RequestBody AuthRequest authRequest) {<br>        System.out.println(&quot;It is called&quot;);<br>        return ResponseEntity.ok(authService.authenticate(authRequest));<br>    }<br>}</pre><h4>Data Transfer Object for Register</h4><p><em>RegisterDTO.java </em>is the <em>Data Transfer Object,</em> for example data from the form where the user has filled in. For simplicity we put this class in the same package,</p><pre>@Data<br>@AllArgsConstructor<br>@NoArgsConstructor<br>public class RegisterDTO {<br>    private String username;<br>    private String password;<br>    private String email;<br>    private String roles;<br>}</pre><h4>Data Transfer Objects for authentication’s request and response</h4><p><em>AuthRequest.java</em> is an object that contains the login information, username and password. <em>AuthResponse.java</em> is an object that contains the <em>JSON Web Token (JWT)</em> and it is returned from the server.</p><pre>@Data<br>@AllArgsConstructor<br>@NoArgsConstructor<br>public class AuthRequest {<br>    private String username;<br>    private String password;<br>}</pre><pre>@Data<br>@Builder<br>@NoArgsConstructor<br>@AllArgsConstructor<br>public class AuthResponse {<br>    private String token;<br>}</pre><p>In real world application, as described in the beginning of this article the token is placed in the <em>HTTP Authorization request header, </em>where security filters process this information for each request where authentication and endpoint authorization is performed.</p><h4>Authentication services</h4><p>We make a service class with two methods for register and authenticate. The register method persist the user’s data in the database and return a token. In real world applications, we would perform validation in the controller for user’s data. In addition, the authenticate method calls the <em>Authentication Manager</em> for authenticating the user. <em>In this situation, a UsernamePasswordAuthenticationToken⁴ is created with all information about the user.</em> <em>The key point here is that Authentication Manager communicates with a provider, in our situation DaoAuthenticationProvider as we will see below. The provider has the responsibility to communicate with the database and authenticate the user if exists. However, DaoAuthenticationProvider has special objects that perform this work and they are returned back to the filter chain.</em> The class is shown below,</p><pre>@Service<br>public class AuthService {<br>    private final UserRepository userRepository;<br>    private final PasswordEncoder passwordEncoder;<br>    private final JwtService jwtService;<br>    private final AuthenticationManager authenticationManager;<br><br>    public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService, AuthenticationManager authenticationManager) {<br>        this.userRepository = userRepository;<br>        this.passwordEncoder = passwordEncoder;<br>        this.jwtService = jwtService;<br>        this.authenticationManager = authenticationManager;<br>    }<br><br>    public AuthResponse register(RegisterDTO dto) {<br>        var user = User.builder()<br>                .username(dto.getUsername())<br>                .password(passwordEncoder.encode(dto.getPassword()))<br>                .email(dto.getEmail())<br>                .roles(dto.getRoles())<br>                .build();<br><br>        userRepository.save(user);<br>        var jwtToken = jwtService.generateToken(user.getUsername());<br>        return AuthResponse.builder()<br>                .token(jwtToken)<br>                .build();<br>    }<br><br>    public AuthResponse authenticate(AuthRequest request) {<br>        authenticationManager.authenticate(<br>                new UsernamePasswordAuthenticationToken(<br>                        request.getUsername(),<br>                        request.getPassword()<br>                )<br>        );<br>        var user = userRepository.getUserInfoByUsername(request.getUsername()).orElseThrow();<br>        var jwtToken = jwtService.generateToken(user.getUsername());<br>        return AuthResponse.builder()<br>                .token(jwtToken)<br>                .build();<br>    }<br>}</pre><h4>Security Filter for JWT Authentication</h4><p>A custom security filter (<em>JwtAuthFilter.java</em>) for validating<em> JWT</em> could be the following one in the package<em> jwtapplication &gt; config</em></p><pre>@Component<br>public class JwtAuthFilter extends OncePerRequestFilter {<br><br>    private final JwtService jwtService;<br>    private final UserDetailsService userDetailsService;<br><br>    public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {<br>        this.jwtService = jwtService;<br>        this.userDetailsService = userDetailsService;<br>    }<br><br>    @Override<br>    protected void doFilterInternal(<br>            @NonNull HttpServletRequest request,<br>            @NonNull HttpServletResponse response,<br>            @NonNull FilterChain filterChain) throws ServletException, IOException {<br>        String authHeader = request.getHeader(&quot;Authorization&quot;);<br><br>        final String token;<br>        String username;<br><br>        if (authHeader == null || !authHeader.startsWith(&quot;Bearer&quot;)) {<br>            filterChain.doFilter(request, response);<br>            return;<br>        }<br><br>        token = authHeader.substring(7);<br>        username = jwtService.extractUsername(token);<br><br>        if (username != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) {<br>            UserDetails userDetails = userDetailsService.loadUserByUsername(username);<br><br>            if (jwtService.validateToken(token, userDetails)) {<br>                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(<br>                        userDetails, null, userDetails.getAuthorities());<br>                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));<br>                SecurityContextHolder.getContext().setAuthentication(authToken);<br>            }<br>        }<br><br>        filterChain.doFilter(request, response);<br>    }<br>}</pre><h4>DaoAuthenticationProvider specific interfaces</h4><p>In Spring Security <em>UserDetails</em> and<em> UserDetailsService</em> interfaces place an important role in the authentication using username and password. As <em>AuthenticationProvider</em> we have used the <a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/dao-authentication-provider.html#servlet-authentication-daoauthenticationprovider"><em>DaoAuthenticationProvider</em></a><em>.</em> For<em> UserDetails </em>and<em> UserDetailsService </em>we could keep the default implementation provided by Spring Security. Although, we have created our custom implementations as example in order to understand better how beans are declared for security configuration.</p><p>In the package <em>jwtapplication &gt; service </em>we create<em> CustomUserDetails.java </em>and<em> CustomUserDetailsService.java</em>, as shown below<em>.</em></p><pre>public class CustomUserDetails implements UserDetails {<br><br>    private final String name;<br>    private final String password;<br>    private final List&lt;GrantedAuthority&gt; authorities;<br><br>    public CustomUserDetails(User user) {<br>        name = user.getUsername();<br>        password = user.getPassword();<br>        authorities = Arrays.stream(user.getRoles().split(&quot;,&quot;))<br>                .map(SimpleGrantedAuthority::new)<br>                .collect(Collectors.toList());<br>    }<br><br>    @Override<br>    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {<br>        return authorities;<br>    }<br><br>    @Override<br>    public String getPassword() {<br>        return password;<br>    }<br><br>    @Override<br>    public String getUsername() {<br>        return name;<br>    }<br><br>    @Override<br>    public boolean isAccountNonExpired() {<br>        return true;<br>    }<br><br>    @Override<br>    public boolean isAccountNonLocked() {<br>        return true;<br>    }<br><br>    @Override<br>    public boolean isCredentialsNonExpired() {<br>        return true;<br>    }<br><br>    @Override<br>    public boolean isEnabled() {<br>        return true;<br>    }<br>}</pre><pre>@Service<br>public class CustomUserDetailsService implements UserDetailsService {<br><br>    private final UserRepository userRepository;<br><br>    public CustomUserDetailsService(UserRepository userRepository) {<br>        this.userRepository = userRepository;<br>    }<br><br>    @Override<br>    public UserDetails loadUserByUsername(String username) <br>            throws UsernameNotFoundException {<br>        Optional&lt;User&gt; user = userRepository.getUserInfoByUsername(username);<br><br>        // Converting user to UserDetails<br>        return user.map(CustomUserDetails::new)<br>                .orElseThrow(() -&gt; new UsernameNotFoundException(&quot;User not found: &quot; + username));<br>    }<br>}</pre><p><em>CustomUserDetails</em> in not annotated with <em>@ Service</em>. Basically, we implement the <em>UserDetails</em> interface for application’s needs.</p><h4>Configure Security Filter Chain</h4><p>Lastly, we configure the security filter chain with <em>authentication manager</em> and for<em> authentication provider</em> to use <em>DaoAuthenticationProvider³.</em></p><pre>@Configuration<br>@EnableWebSecurity<br>public class SecurityConfig {<br><br>    private final JwtAuthFilter authFilter;<br>    private final CustomUserDetailsService customUserDetailsService;<br><br>    public SecurityConfig(<br>            JwtAuthFilter authFilter,<br>            CustomUserDetailsService customUserDetailsService) {<br>        this.authFilter = authFilter;<br>        this.customUserDetailsService = customUserDetailsService;<br>    }<br><br>    @Bean<br>    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {<br>        http.csrf(AbstractHttpConfigurer::disable)<br>                .authorizeHttpRequests(auth -&gt; {<br>                    auth.requestMatchers(<br>                            &quot;/api/v1/welcome&quot;,<br>                            &quot;/api/v1/auth/authenticate&quot;,<br>                            &quot;/api/v1/auth/register&quot;<br>                            ).permitAll();<br>                }).authorizeHttpRequests(auth -&gt; {<br>                    auth.requestMatchers(&quot;/api/v1/user/**&quot;).authenticated();<br>                }).authorizeHttpRequests(auth -&gt; {<br>                    auth.requestMatchers(&quot;/api/v1/admin/**&quot;).authenticated();<br>                })<br>                .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))<br>                .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)<br>                .authenticationProvider(authenticationProvider());<br><br>        return http.build();<br>    }<br><br>    @Bean<br>    public AuthenticationProvider authenticationProvider() {<br>        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();<br>        authProvider.setUserDetailsService(customUserDetailsService);<br>        authProvider.setPasswordEncoder(passwordEncoder());<br>        return authProvider;<br>    }<br><br>    @Bean<br>    public PasswordEncoder passwordEncoder() {<br>        return new BCryptPasswordEncoder();<br>    }<br><br>    @Bean<br>    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {<br>        return config.getAuthenticationManager();<br>    }<br>}</pre><h4>Testing with Postman</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sJrb0GJUXg7Iywspk3aYcw.png" /><figcaption>POST request for register</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1XEvFyaxhDrtW8okk-IBMw.png" /><figcaption>Post request for login</figcaption></figure><p>To conclude with, you can find the <a href="https://github.com/SakisHous/jwt-application"><strong>whole code in my repository</strong></a>. In addition, I <a href="https://github.com/ali-bouali/spring-boot-3-jwt-security"><strong>highly suggest to read the repository</strong></a> for a similar approach.</p><h4>References</h4><p><em>[1] Servlet Authentication Architecture, </em><a href="https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html"><em>https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html</em></a></p><p><em>[2] JSON Web Tokens, </em><a href="https://jwt.io/"><em>https://jwt.io/</em></a></p><p><em>[3] DaoAuthenticationProvider, </em><a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/dao-authentication-provider.html"><em>https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/dao-authentication-provider.html</em></a></p><p><em>[4] Form Login, </em><a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html#servlet-authentication-form"><em>https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html#servlet-authentication-form</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a7967a8ee309" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>