DEV Community

Cover image for Implementing Spring Security in Microservices Architecture: A Deep Dive
Harshit Singh
Harshit Singh

Posted on • Originally published at wittedtech.Medium

Implementing Spring Security in Microservices Architecture: A Deep Dive

In a microservices architecture, each service (or component) acts like a room in a sprawling mansion. And like any good mansion, security needs to be tight. Each room needs its own lock (authentication) and set of rules about who can enter (authorization). But security is more complex than that—there’s also a need for protection from outside threats (encryption, CSRF, CORS), while ensuring that legitimate users can move freely between rooms without too much hassle (SSO, JWT).

Why Spring Security?

Spring Security is a powerful and flexible framework that provides robust authentication and authorization mechanisms. It's built to handle security for web applications, API endpoints, microservices, and more. It works seamlessly with Spring Boot and Spring Cloud, making it the go-to solution for securing Java-based microservices.


Key Concepts Recap:

  1. Authentication: Proving who you are (e.g., logging in with a username and password, or presenting a token).
  2. Authorization: Determining what you’re allowed to do once authenticated (e.g., accessing certain services or data).
  3. Session Management: Deciding how long you’re allowed to stay authenticated, or if we’re even using sessions at all (stateless vs. stateful services).
  4. Encryption: Ensuring sensitive data (passwords, tokens, etc.) is secure, both at rest and in transit.

1. JWT (JSON Web Tokens): Stateless Authentication for Microservices

JWTs are the bread and butter of modern, stateless authentication in microservices. JWT tokens contain all the information a server needs to verify a user’s identity without storing session data.

The What:

A JWT is a token in the form of a string that is passed with every API request. It contains a header, payload, and a signature. The payload holds claims (i.e., details about the user), while the signature ensures the token hasn’t been tampered with.

The Why:

  • Stateless: In a microservices world, storing sessions centrally is a bad idea. JWTs are self-contained, meaning they hold all the information required to authenticate the user in every request.
  • Scalable: Since JWTs don’t need server-side storage, you can easily scale your services horizontally.
  • Secure: JWTs can be signed using algorithms like HMAC or RSA to ensure their integrity.

The How:

Here’s how we implement JWT-based security in a modern Spring Boot microservice:

Step 1: Add Dependencies

In your pom.xml, ensure the required dependencies for Spring Security and JWT are present:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

Step 2: Create a JWT Utility Class

This class will handle the creation and validation of JWT tokens.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

    private String SECRET_KEY = "secret"; // In a real-world application, use a stronger key!

    // Retrieve username from JWT token
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // Retrieve expiration date from JWT token
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    // Generate token for user
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10-hour expiration
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    // Validate token
    public Boolean validateToken(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return (extractedUsername.equals(username) && !isTokenExpired(token));
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Create the JWT Filter

This filter checks for JWT tokens on every request.

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Component;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private JwtTokenUtil jwtTokenUtil;

    public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil) {
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        // Extract JWT token from the Authorization header
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtTokenUtil.extractUsername(jwt);
        }

        // Validate the JWT token
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (jwtTokenUtil.validateToken(jwt, username)) {
                // If the token is valid, set the authentication in the context
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Spring Security

The traditional WebSecurityConfigurerAdapter is deprecated, so we use a more modular approach with SecurityFilterChain:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

@Configuration
public class SecurityConfig {

    private JwtAuthenticationFilter jwtFilter;
    private UserDetailsService userDetailsService;

    public SecurityConfig(JwtAuthenticationFilter jwtFilter, UserDetailsService userDetailsService) {
        this.jwtFilter = jwtFilter;
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable()) // Disable CSRF for APIs (more on this later)
            .authorizeRequests(auth -> auth
                .antMatchers("/auth/**").permitAll() // Allow open access to the auth endpoints
                .anyRequest().authenticated() // Require authentication for all other endpoints
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

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

Enter fullscreen mode Exit fullscreen mode

Detailed Explanation of JWT in Real Projects

In a real-world scenario, JWTs are a go-to solution for REST APIs. For instance, think about e-commerce microservices where customers have access to Product, Order, and Payment services. Instead of each service managing sessions, JWT tokens allow them to trust the identity of the user making a request without querying a database or session store.

Common Issues and Fallbacks with JWT:

  • Token Expiry: If a token expires, you’ll need a strategy to refresh tokens. Implementing a refresh token system allows for long-lived sessions without constantly requiring login.
  • Token Tampering: Always use strong secret keys for signing JWTs. If the secret is weak, malicious users can forge tokens.
  • Replay Attacks: Ensure tokens have short lifetimes to minimize the impact of stolen tokens.

2. OAuth 2.0 and OpenID Connect: Authorization Done Right

In OAuth 2.0, users delegate permission to access their resources without sharing credentials. It’s the gold standard for third-party logins (e.g., logging in with Google, Facebook, etc.).

The What:

OAuth 2.0 is an authorization framework that allows third-party services to use access tokens (instead of credentials) to authenticate requests to APIs.

The Why:

OAuth 2.0 is ideal for microservices that need to authenticate or authorize users via third-party services like Google, GitHub, or Facebook.

The How:

For OAuth 2.0, Spring Boot makes it relatively simple to integrate using the spring-boot-starter-oauth2-client.

Step 1: Add OAuth 2.0 Client Dependencies

Add the following dependency in your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

Enter fullscreen mode Exit fullscreen mode

Step 2: Configure OAuth2.0 in application.yml

Configure your OAuth2.0 client in application.yml. For example, for Google OAuth:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: profile, email
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            client-name: Google
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/auth
            token-uri: https://accounts.google.com/o/oauth2/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo

Enter fullscreen mode Exit fullscreen mode

Step 3: Configure OAuth2.0 in Spring Security

Now configure Spring Security to handle OAuth2.0 authentication:

import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

@Configuration
public class OAuth2SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/oauth2/authorization/google") // Google as OAuth2 provider
            )
            .authorizeRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .build();
    }
}

Enter fullscreen mode Exit fullscreen mode

Real-World OAuth Use Cases in Microservices:

Imagine an educational platform where students can log in via their Google accounts to access course materials (microservices like CourseService, AuthService). With OAuth 2.0, you don’t need to store passwords. The flow looks like this:

  1. User clicks “Log in with Google”.
  2. They are redirected to Google for authentication.
  3. Google returns an OAuth token, which is exchanged for a JWT in your app, authorizing access to your microservices.

Common Issues with OAuth and Fallbacks:

  • Token Lifespan: OAuth tokens have lifespans. Be sure to implement refresh tokens to keep sessions alive without asking the user to re-authenticate.
  • Scope Management: Define scopes carefully. Scopes dictate the level of access, and improper scope settings can lead to over-privileged tokens.

3. SSO (Single Sign-On): Centralized Authentication for a Seamless Experience

Single Sign-On (SSO) lets users access multiple applications with a single set of credentials. It’s ideal for large-scale systems with multiple microservices or even multiple applications.

The What:

With SSO, the user logs in once, and a centralized authentication service (like Keycloak or Okta) manages the login across multiple services.

The Why:

If your architecture consists of many independent services, managing authentication across all of them individually would be a nightmare. SSO makes the user’s life easy by consolidating authentication and avoiding repeated logins.

The How:

To implement SSO in Spring Boot, you can use third-party tools like Keycloak or Okta. Here’s how it works with Okta:

Step 1: Add Dependencies

xml
Copy code
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

Step 2: Configure application.yml

Configure Okta SSO in your application.yml:

yaml
Copy code
spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: openid, profile, email
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"

Enter fullscreen mode Exit fullscreen mode

Step 3: SSO Security Configuration

In SecurityConfig.java, configure the Okta SSO settings:

java
Copy code
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/oauth2/authorization/okta")
            )
            .build();
    }
}

Enter fullscreen mode Exit fullscreen mode

Real-World SSO Use Case:

In a banking platform where users need access to different services like TransactionService, AccountService, and LoanService, SSO enables a seamless user experience. Once authenticated via the centralized SSO provider (e.g., Okta), users can navigate between services without logging in multiple times.

Common SSO Issues and Fallbacks:

  • Session Hijacking: Make sure tokens or cookies used in SSO flows are secured and encrypted.
  • Logout Propagation: Ensure that logging out from one service propagates to all services in the system to avoid lingering sessions.

4. Form-Login: A Classic Approach Still Relevant in Many Apps

For smaller applications or internal services, form login (think traditional username-password forms) is still quite common.

The What:

Form-based login is a stateful authentication mechanism where the user’s credentials are submitted via a login form and a session is created.

The Why:

If you're not aiming for a stateless, API-driven approach or if your application doesn’t require OAuth/JWT, form-based login can provide simplicity.

The How:

Form login can be implemented easily with Spring Boot:

Step 1: Form-Login Configuration

Here’s how to configure modern form-based login:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .authorizeRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .build();
}

Enter fullscreen mode Exit fullscreen mode
  • loginPage("/login"): This specifies a custom login page.
  • permitAll(): Allows everyone access to the login page, even if they’re not authenticated.
  • anyRequest().authenticated(): Requires authentication for any other page.

Step 2: Customize Login Page and Handling

You can customize the login page and redirect after success or failure:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .formLogin(form -> form
            .loginPage("/custom-login") // Custom login page URL
            .defaultSuccessUrl("/home", true) // Redirect after successful login
            .failureUrl("/custom-login?error=true") // Redirect on failure
            .permitAll()
        )
        .authorizeRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .build();
}

Enter fullscreen mode Exit fullscreen mode

Common Issues with Form-Login:

  • Session Fixation: Prevent attackers from using the same session ID post-login.
  • Brute Force Attacks: Implement login throttling to prevent brute force attacks.

5. Encryption: Protecting Sensitive Data

Encryption ensures that sensitive data (like passwords, tokens) is protected both at rest and in transit.

The What:

  • Symmetric encryption: Uses the same key to encrypt and decrypt (e.g., AES).
  • Asymmetric encryption: Uses a pair of public and private keys (e.g., RSA).

The Why:

Encryption is critical for protecting tokens, passwords, and sensitive data transmitted over the network.

The How:

In Spring Boot, password encryption is easy with BCrypt. You should always encrypt user passwords before storing them in the database.

java
Copy code
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.context.annotation.Bean;

@Configuration
public class PasswordConfig {

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

Enter fullscreen mode Exit fullscreen mode

When storing passwords in the database, encode them:

String rawPassword = "mypassword";
String encodedPassword = passwordEncoder.encode(rawPassword);

Enter fullscreen mode Exit fullscreen mode

Common Encryption Issues:

  • Key Management: Always store encryption keys securely (e.g., AWS KMS, Vault).
  • Weak Algorithms: Don’t use weak algorithms like MD5 or SHA-1 for hashing passwords.

6. CORS (Cross-Origin Resource Sharing): Handling Requests Across Origins

The What:

Cross-Origin Resource Sharing (CORS) is a mechanism that controls how resources on a server can be requested from a different origin. In a microservices setup, especially when front-end services are on a different domain than your back-end microservices, CORS configuration is essential to avoid "Blocked by CORS policy" errors.

The Why:

If you are running a front-end (React, Angular) on a different domain (like frontend.com) and your API microservices are on api.backend.com, browsers will block these requests by default for security reasons. CORS allows the API to tell the browser, "Hey, it's safe to accept requests from frontend.com."

The How:

To configure CORS globally in Spring Security, follow these steps:

Step 1: Global CORS Configuration in Spring Security

Here’s how you can configure CORS at a global level:

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GlobalCorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("https://frontend.com") // Set allowed origins
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowedHeaders("*")
                        .allowCredentials(true);
            }
        };
    }
}

Enter fullscreen mode Exit fullscreen mode

Here’s what this code does:

  • allowedOrigins: Specifies which domains are allowed to access your microservice API. Use "*" if you want to allow any origin.
  • allowedMethods: Specifies which HTTP methods are permitted.
  • allowCredentials(true): Allows cookies or authentication headers.

Step 2: CORS in Security Filter Chain

You can also manage CORS directly in your Spring Security configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .cors()  // Enable CORS
        .and()
        .authorizeRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .build();
}

Enter fullscreen mode Exit fullscreen mode

Real-World CORS Scenario in Microservices:

Suppose you have a front-end SPA (Single Page Application) built with React, hosted on a different domain (e.g., https://studentportal.com). Your microservices (e.g., UserService, CourseService) are hosted on https://api.coursesystem.com. The SPA will need to make API calls to your microservices.

Without proper CORS configuration, the browser will block these requests, preventing your front-end from talking to your back-end. The solution? Proper CORS setup, as demonstrated above, ensures that your microservices only accept requests from trusted origins.


7. CSRF (Cross-Site Request Forgery): Protecting Against Unauthorized Requests

The What:

Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks the user into performing actions on another website where they are authenticated (like submitting a form). CSRF protection ensures that requests are legitimate and intentional by the user.

The Why:

In microservices, especially when dealing with stateful sessions (e.g., traditional session-based authentication), CSRF protection is crucial. It ensures that forms or AJAX requests made from authenticated sessions are not hijacked by malicious scripts.

The How:

Spring Security enables CSRF protection by default. You can disable it in specific cases where it’s not needed, but for form-based authentication, you should leave it enabled.

Step 1: CSRF Token Generation

Spring Security automatically generates and manages CSRF tokens. You just need to include this token in your forms or AJAX requests:

<form action="/post" method="POST">
    <input type="hidden" name="_csrf" value="${_csrf.token}"/>
    <!-- Other form inputs -->
</form>

Enter fullscreen mode Exit fullscreen mode

In a JavaScript-based front-end (like Angular or React), you should send the CSRF token as a request header:

fetch('/post', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': csrfToken
    }
});

Enter fullscreen mode Exit fullscreen mode

Step 2: Configuring CSRF in Security

By default, CSRF is enabled. But if you need to disable it (for example, in stateless REST APIs using JWT), you can do it as follows:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())  // Disable CSRF for stateless JWT
        .authorizeRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .build();
}

Enter fullscreen mode Exit fullscreen mode

When to Use CSRF and When Not to Use It:

  • Use CSRF when you’re dealing with stateful sessions (like form-login with session cookies).
  • Disable CSRF for stateless applications (like REST APIs with JWT tokens) because these APIs don’t maintain sessions that can be attacked.

Real-World CSRF Example in Microservices:

Let’s say you have an admin dashboard where users can manage accounts. If CSRF protection isn’t in place, a malicious attacker could create a fake form on another website that submits a POST request to your service, effectively tricking users into performing actions they didn’t intend.


8. JWT (JSON Web Tokens): Stateless Authentication for Modern Microservices

The What:

JWT is a compact, URL-safe token format that can carry user claims (e.g., user roles, permissions) in a self-contained and stateless way. It's commonly used in microservices architectures where scaling and performance are critical.

The Why:

JWT is ideal for stateless authentication in microservices, where storing user sessions on a central server (like Redis or the database) is impractical. It allows each service to authenticate users without a central session store.

The How:

Here’s a quick guide to implement JWT authentication in a Spring Security microservices setup:

Step 1: Add JWT Dependencies

Add the JWT dependencies in your pom.xml:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

Step 2: JWT Token Utility

Create a utility class to generate and validate tokens:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtUtil {

    private String SECRET_KEY = "mysecretkey";

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours expiration
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public String extractUsername(String token) {
        return extractAllClaims(token).getSubject();
    }

    public boolean isTokenExpired(String token) {
        return extractAllClaims(token).getExpiration().before(new Date());
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Configure JWT Authentication Filter

Create a filter to intercept requests and authenticate JWT tokens:

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtRequestFilter extends OncePerRequestFilter {

    private JwtUtil jwtUtil;

    public JwtRequestFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (jwtUtil.isTokenExpired(jwt)) {
                // Proceed with the authentication if JWT is valid
            }
        }

        chain.doFilter(request, response);
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Integrate JWT Filter into Security Chain

Now integrate the filter into your Spring Security configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    JwtRequestFilter jwtRequestFilter = new JwtRequestFilter(new JwtUtil());

    return http
        .csrf(csrf -> csrf.disable()) // Disable CSRF for JWT-based stateless auth
        .authorizeRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) // Add JWT Filter
        .build();
}

Enter fullscreen mode Exit fullscreen mode

Fallbacks, Error Handling, and Exceptions with JWT:

  1. Expired Token: If the token is expired, send a 401 Unauthorized response with a message like "Token has expired."
  2. Invalid Token: If the token is tampered with, return 403 Forbidden.
  3. Missing Token: If no token is present in the header, reject the request with 401 Unauthorized.

9. SSO (Single Sign-On): Centralized Authentication for Multiple Microservices

The What:

Single Sign-On (SSO) allows users to authenticate once and gain access to multiple services without needing to log in multiple times. In microservices, this simplifies user experience by centralizing authentication.

The Why:

Imagine a scenario where your system consists of multiple services like BillingService, AccountService, and NotificationService. With SSO, users only need to log in once, and the system will propagate their authentication across these services.

The How:

To implement SSO in a microservices architecture, we typically use OAuth 2.0 or OpenID Connect (OIDC) with an identity provider (IdP) like Keycloak, Auth0, or Okta.

Let’s break down the key steps:

Step 1: Identity Provider Setup (e.g., Keycloak or Okta)

  • Set up a centralized identity provider that handles authentication and issues tokens (JWT or OAuth 2.0 tokens).
  • Each microservice delegates authentication to the IdP.

Step 2: OAuth2 Client Configuration in Spring Security

Your microservices need to act as OAuth2 clients, delegating authentication to the identity provider.

Here’s a simple OAuth2 login configuration in Spring Security:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .oauth2Login();  // Enable OAuth2 Login
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Token Propagation Across Services

Once authenticated via the IdP, the user receives a token (e.g., JWT), which can be propagated to other services via the Authorization header.


Handling Common SSO Challenges:

  1. Token Expiration: Tokens have an expiration time. Handle token refreshes by using OAuth2 refresh tokens or silent refresh in the background.
  2. Service Communication: Ensure that microservices can validate tokens. Use shared secret keys or JWKS (JSON Web Key Set) endpoints from the IdP to validate tokens.
  3. Logout Across Services: SSO logout is tricky because logging out of one service should log the user out of all connected services. Implement global logout via the IdP.

10. Form Login: Classic Session-Based Authentication

The What:

Form login is a traditional session-based authentication method where users log in via an HTML form, and their session is maintained using cookies.

The Why:

Even in a microservices setup, form login can be useful for administrative interfaces or internal tools where simplicity is more important than scalability.

The How:

Here’s how to implement form login in Spring Security:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/login", "/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .logout()
                .permitAll();
    }
}

Enter fullscreen mode Exit fullscreen mode

In this setup, users are redirected to /login when they try to access a restricted page. Upon successful login, they receive a session cookie that will authenticate them for subsequent requests.


Real-World Challenges with Form Login in Microservices:

  1. Session Replication: If you have multiple instances of your microservices, session replication can become a bottleneck. You’ll need to use distributed sessions (e.g., backed by Redis) or stateless mechanisms like JWT to avoid this.
  2. CSRF Attacks: Form login is prone to CSRF attacks. Always ensure CSRF protection is enabled (which is by default in Spring Security).

Conclusion:

Implementing Spring Security in a microservices architecture is like building a fortress for each service while ensuring they work harmoniously together. From JWT to OAuth2, SSO, encryption, and CORS, each aspect of security has its unique role to play. A carefully designed security system not only keeps your services safe but also provides flexibility, scalability, and a great user experience.

So, the next time you’re building a microservices architecture, don’t just think about the fancy features—ensure that your security implementation is rock solid.

TL;DR: Whether it’s JWT for stateless auth, SSO for seamless user experience, or CORS to handle cross-origin requests, Spring Security has you covered for every scenario. Just remember to configure things properly, test for edge cases, and keep your security updates in check!


Want to dive deeper into Spring Security or explore how to integrate advanced features like OAuth 2.0 with external IdPs like Okta or Keycloak? Follow me on my socials and check out my blog for more tutorials, deep dives, and live coding sessions.

Top comments (0)