Securing Spring Boot Microservices with JWT and Redis

Introduction to Application Security

Spring Security is a powerful framework designed to handle authentication and authorization within Java applications. In modern web architectures, security primarily revolves around two core concepts:

  • Authentication (AuthN): Verifying the identity of a user. This typically involves validating credentials such as a username and password to ensure the user is who they claim to be.
  • Authorization (AuthZ): Determining what an authenticated user is allowed to do. This involves checking permissions or roles associated with the user identity against the requested resource.

Spring Security operates primarily through a chain of filters. Each incoming request passes through these filters before reaching the controller. Depending on the configuration, the framework can intercept requests to validate session data or tokens.

In a stateless architecture, such as one using JSON Web Tokens (JWT), the security filter parses the token from the request header. It validates the signature and extracts the user identity. Permissions are often cached in a high-speed store like Redis to avoid frequent database hits during authorization checks.

Architecture Design

To implement a scalable security model, a token-based approach is recommended. The workflow is as follows:

  1. The user submits credentials to the login endpoint.
  2. Upon successful validation, the system generates a JWT and stores the user's permission list in Redis, keyed by the username.
  3. The JWT is returned to the client, which stores it (e.g., in local storage or cookies).
  4. For subsequent requests, the client includes the JWT in the HTTP header.
  5. A security filter intercepts the request, validates the JWT, retrieves the username, and fetches permissions from Redis to establish the security context.

Project Setup and Depandencies

Create a dedicated module for security configurations, often named security-core. Include the necessary dependencies for Spring Security, JWT processing, and Redis integration.

<dependencies>
    <!-- Common Utility Module -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>common-utils</artifactId>
        <version>1.0.0</version>
    </dependency>
    
    <!-- Spring Security Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- JWT Library -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    <!-- Redis Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

Core Security Configuration

The configuration class defines the security filter chain, password encoding, and exception handling. In modern Spring Security versions, defining a SecurityFilterChain bean is the preferred approach.

package com.example.security.config;

import com.example.security.filter.JwtAuthorizationFilter;
import com.example.security.filter.JwtLoginFilter;
import com.example.security.handler.CustomAccessDeniedHandler;
import com.example.security.handler.CustomAuthEntryPoint;
import com.example.security.service.AccountDetailsService;
import com.example.security.util.PasswordUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    private final AccountDetailsService accountDetailsService;
    private final PasswordUtil passwordUtil;
    private final RedisTemplate<String, Object> redisTemplate;
    private final CustomAuthEntryPoint authEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    public SecurityConfiguration(AccountDetailsService accountDetailsService, 
                                 PasswordUtil passwordUtil, 
                                 RedisTemplate<String, Object> redisTemplate,
                                 CustomAuthEntryPoint authEntryPoint,
                                 CustomAccessDeniedHandler accessDeniedHandler) {
        this.accountDetailsService = accountDetailsService;
        this.passwordUtil = passwordUtil;
        this.redisTemplate = redisTemplate;
        this.authEntryPoint = authEntryPoint;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authManager) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(authEntryPoint)
            .accessDeniedHandler(accessDeniedHandler)
            .and()
            .authorizeHttpRequests()
            .requestMatchers("/api/**", "/swagger/**", "/webjars/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtLoginFilter(authManager, redisTemplate), UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(new JwtAuthorizationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordUtil passwordEncoder() {
        return new PasswordUtil();
    }
}

User Details Implementation

Spring Security requires a implementation of UserDetails to represent the authenticated principal. This class wraps the actual user data and their granted authorities.

package com.example.security.entity;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
public class UserPrincipal implements UserDetails {

    private final UserInfo userInfo;
    private List<String> permissions;

    public UserPrincipal(UserInfo user) {
        this.userInfo = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> auths = new ArrayList<>();
        if (permissions != null) {
            for (String perm : permissions) {
                if (!StringUtils.isEmpty(perm)) {
                    auths.add(new SimpleGrantedAuthority(perm));
                }
            }
        }
        return auths;
    }

    @Override
    public String getPassword() {
        return userInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return userInfo.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() { return true; }
    @Override
    public boolean isAccountNonLocked() { return true; }
    @Override
    public boolean isCredentialsNonExpired() { return true; }
    @Override
    public boolean isEnabled() { return true; }
}

The UserInfo class is a simple POJO representing the database user record.

package com.example.security.entity;

import lombok.Data;
import java.io.Serializable;

@Data
public class UserInfo implements Serializable {
    private String username;
    private String password;
    private String nickname;
    private String avatar;
}

JWT Utility Class

This component handles token creation and parsing. It encapsulates the JWT library logic.

package com.example.security.util;

import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class JwtUtil {

    private static final long EXPIRATION_TIME = 86400000; // 24 hours
    private static final String SECRET_KEY = "secure_secret_key_change_in_prod";

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
            return claims.getSubject();
        } catch (Exception e) {
            return null;
        }
    }
}

Authentication Filters

Two main filters are required: one for handling the login request and generating the token, and another for validating the token on subsequent requests.

Login Filter

Intercepts the login POST request, validates credentials via the AuthenticationManager, and returns a JWT.

package com.example.security.filter;

import com.example.security.entity.UserPrincipal;
import com.example.security.util.JwtUtil;
import com.example.commonutils.ResponseUtil;
import com.example.commonutils.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
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.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authManager;
    private final RedisTemplate<String, Object> redisTemplate;
    private final JwtUtil jwtUtil;

    public JwtLoginFilter(AuthenticationManager authManager, RedisTemplate<String, Object> redisTemplate) {
        this.authManager = authManager;
        this.redisTemplate = redisTemplate;
        this.jwtUtil = new JwtUtil();
        this.setFilterProcessesUrl("/admin/acl/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 
            throws AuthenticationException {
        try {
            Map<String, String> credentials = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            String username = credentials.get("username");
            String password = credentials.get("password");
            return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, 
                                            FilterChain chain, Authentication authResult) throws IOException {
        UserPrincipal principal = (UserPrincipal) authResult.getPrincipal();
        String token = jwtUtil.generateToken(principal.getUsername());
        
        // Cache permissions in Redis
        redisTemplate.opsForValue().set(principal.getUsername(), principal.getPermissions());

        Map<String, String> data = new HashMap<>();
        data.put("token", token);
        ResponseUtil.out(response, Result.ok(data));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, 
                                              AuthenticationException failed) throws IOException {
        ResponseUtil.out(response, Result.error());
    }
}

Authorization Filter

Validates the token in the header for protected resources and loads authorities from Redis.

package com.example.security.filter;

import com.example.security.util.JwtUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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;
import java.util.ArrayList;
import java.util.List;

public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final RedisTemplate<String, Object> redisTemplate;
    private final JwtUtil jwtUtil;

    public JwtAuthorizationFilter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.jwtUtil = new JwtUtil();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
            throws ServletException, IOException {
        
        String uri = request.getRequestURI();
        // Skip login path
        if (uri.contains("/login")) {
            chain.doFilter(request, response);
            return;
        }

        String token = request.getHeader("token");
        if (StringUtils.hasText(token)) {
            String username = jwtUtil.getUsernameFromToken(token);
            if (StringUtils.hasText(username)) {
                List<String> perms = (List<String>) redisTemplate.opsForValue().get(username);
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                if (perms != null) {
                    for (String perm : perms) {
                        if (StringUtils.hasText(perm)) {
                            authorities.add(new SimpleGrantedAuthority(perm));
                        }
                    }
                }
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(username, null, authorities);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

Password Encoding

A custom encoder is implemented to match the database storage format, often using MD5 or BCrypt.

package com.example.security.util;

import org.springframework.security.crypto.password.PasswordEncoder;
import java.security.MessageDigest;

public class PasswordUtil implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        return md5(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(md5(rawPassword.toString()));
    }

    private String md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(str.getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

User Details Service

This service bridges the application's user database with Spring Security. It loads user information and permissions.

package com.example.security.service;

import com.example.security.entity.UserInfo;
import com.example.security.entity.UserPrincipal;
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 java.util.ArrayList;
import java.util.List;

@Service("userDetailsService")
public class AccountDetailsService implements UserDetailsService {

    // Inject actual user service and permission service here
    // private UserService userService;
    // private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Simulate DB fetch
        UserInfo user = new UserInfo();
        user.setUsername(username);
        user.setPassword("encoded_password_here");
        
        // Simulate permission fetch
        List<String> permissions = new ArrayList<>();
        permissions.add("role_admin");

        UserPrincipal principal = new UserPrincipal(user);
        principal.setPermissions(permissions);
        return principal;
    }
}

Exception Handling

Custom handlers ensure that authentication failures and access denied errors return consistent JSON responses instead of default HTML error pages.

package com.example.security.handler;

import com.example.commonutils.ResponseUtil;
import com.example.commonutils.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

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

public class CustomAuthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 
            throws IOException {
        ResponseUtil.out(response, Result.error());
    }
}

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, 
                       org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException {
        ResponseUtil.out(response, Result.error());
    }
}

Tags: spring-security JWT Redis Authentication Authorization

Posted on Wed, 05 Aug 2026 16:48:49 +0000 by Templar