Build a Stateless JWT Authentication System with Spring Boot 3 & Spring Security 6

This tutorial walks you through creating a scalable, stateless authentication and authorization solution for Spring Boot 3.x applications using JWT, covering project setup, token generation, method‑level security, automatic token renewal, blacklist handling, and common troubleshooting tips.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Build a Stateless JWT Authentication System with Spring Boot 3 & Spring Security 6

1. Introduction

In modern front‑back separation projects, user authentication and permission control are essential. Traditional session‑based solutions become cumbersome in micro‑service or cross‑origin environments, so a stateless JWT approach is adopted.

2. Technology Stack and Project Structure

Spring Boot 3.x

Spring Security 6.x

JSON Web Token (jjwt)

MyBatis + MySQL

Redis (optional, for token blacklist)

The project follows a typical layered layout:

com.example.demo
├── config
│   └── SecurityConfig.java
├── controller
│   ├── AuthController.java
│   ├── DemoController.java
│   └── HelloController.java
├── entity
│   └── User.java
├── filter
│   └── JwtAuthenticationFilter.java
├── handler
│   ├── CustomAccessDeniedHandler.java
│   └── CustomLoginFailureHandler.java
├── mapper
│   └── UserMapper.java
├── point
│   └── CustomAuthenticationEntryPoint.java
├── service
│   └── MyUserDetailsService.java
├── utils
│   ├── JwtUtils.java
│   └── TokenBlacklist.java
└── SecurityDemoApplication.java

3. Detailed Implementation

3.1 User Login and Token Generation

Clients send credentials to /auth/login. After successful verification, the server issues a JWT containing the username and role:

String token = Jwts.builder()
    .setSubject(username)
    .claim("role", role)
    .setIssuedAt(now)
    .setExpiration(expiry)
    .signWith(key, SignatureAlgorithm.HS256)
    .compact();

The token is stored in localStorage on the front end and sent in subsequent requests via the Authorization: Bearer <token> header.

3.2 JWT Authentication Filter

The JwtAuthenticationFilter (extending OncePerRequestFilter) extracts the token from the request header, validates it, and populates the Spring Security context:

String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
    Claims claims = jwtUtils.parseToken(token);
    // Build Authentication object after verification
    UsernamePasswordAuthenticationToken authToken = ...;
    SecurityContextHolder.getContext().setAuthentication(authToken);
}

3.3 Method‑Level Permission Control

Use @PreAuthorize annotations to restrict access, e.g.:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminApi() {
    return "管理员接口";
}

Enable method security in SecurityConfig with @EnableMethodSecurity.

3.4 Automatic Token Refresh

When a protected endpoint is accessed, the filter checks the remaining token lifetime. If it falls below a threshold (e.g., 10 minutes), a new token is generated and returned in the response header X-Refresh-Token:

if (jwtUtils.shouldRefresh(claims.getExpiration())) {
    String newToken = jwtUtils.generateToken(username, role);
    response.setHeader("X-Refresh-Token", newToken);
}

The front end listens for this header and updates the stored token automatically.

3.5 Token Blacklist Mechanism

On logout, the current token is added to a Redis blacklist with an appropriate TTL:

redisTemplate.opsForValue().set("blacklist:" + token, "1", ttl, TimeUnit.MILLISECONDS);

The filter rejects any request whose token exists in the blacklist:

if (redisTemplate.hasKey("blacklist:" + token)) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}

3.6 Login Failure and Unauthorized Handling

Custom handlers return a unified JSON error response, for example:

{
  "code": 401,
  "message": "未登录或Token无效"
}

4. Common Issues and Solutions

AuthenticationManager Bean not found : Define it manually with @Bean and inject.

JWT secret key length error : Use Keys.secretKeyFor(SignatureAlgorithm.HS256) or a string of at least 32 characters.

Git history divergence : Resolve with git pull origin main --allow-unrelated-histories.

5. Source Code

https://gitee.com/nidayeyo/spring-security-demo
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootAuthenticationJWTSpring SecurityBlacklisttoken refresh
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.