How to Implement JWT Authentication and Token Refresh in the Mall Project

The mall project implements stateless JWT authentication using Spring Security, where JwtTokenUtil generates HS512-signed tokens, JwtAuthenticationTokenFilter validates them on every request, and a dedicated refresh endpoint re-issues tokens while enforcing a 30-minute safety window.

The mall project (macrozheng/mall) is a comprehensive e-commerce system built on Spring Boot. Its authentication layer resides in the mall-security module and follows a classic stateless JWT architecture that eliminates server-side session storage.

JWT Authentication Architecture Overview

The authentication flow consists of six distinct steps orchestrated across multiple components:

  1. Login Controller (UmsAdminController.login) receives username and password, validates credentials against the database, and delegates token creation to JwtTokenUtil.

  2. Token Generation (JwtTokenUtil.generateToken) constructs a JWT payload containing the username (sub) and a created timestamp, signs it with HS512 using the configured secret, and sets the expiration defined by jwt.expiration.

  3. Request Filtering (JwtAuthenticationTokenFilter) intercepts every incoming request, extracts the token from the Authorization header, strips the Bearer prefix, and validates the token signature.

  4. Security Context Population occurs when the filter builds a UsernamePasswordAuthenticationToken and stores it in SecurityContextHolder, making the user authenticated for the duration of the request.

  5. Spring Security Configuration (SecurityConfig) registers the JWT filter before UsernamePasswordAuthenticationFilter and enforces SessionCreationPolicy.STATELESS.

  6. Token Refresh (UmsAdminController.refreshToken) allows clients to obtain a new token before expiration, provided the current token is valid and was not refreshed within the last 30 minutes.

Token Generation and Validation

In mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java, the generateToken(UserDetails userDetails) method creates tokens using the following structure:

public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
    claims.put(CLAIM_KEY_CREATED, new Date());
    return generateToken(claims);
}

The method packs two critical claims into the JWT payload: sub containing the username and created storing the generation timestamp. The token is signed with the HS512 algorithm using the secret key defined in jwt.secret and expires after the duration specified in jwt.expiration (typically 604800 seconds or 7 days).

Validation occurs through validateToken(String token, UserDetails userDetails), which extracts the username from the token, compares it with the provided UserDetails, and verifies the token has not expired via isTokenExpired(token).

Stateless Security Filter Implementation

The JwtAuthenticationTokenFilter class in mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java extends OncePerRequestFilter to ensure every request undergoes authentication checks:

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain chain) throws ServletException, IOException {
    String header = request.getHeader(tokenHeader);
    if (header != null && header.startsWith(tokenHead)) {
        String token = header.substring(tokenHead.length());
        String username = jwtTokenUtil.getUserNameFromToken(token);
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtTokenUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
    }
    chain.doFilter(request, response);
}

This filter extracts the token from the header defined by jwt.tokenHeader (typically Authorization), removes the Bearer prefix defined by jwt.tokenHead, and populates the security context only if validation succeeds. Because the filter runs on every request, the system remains completely stateless with no server-side session storage.

Token Refresh Mechanism

The mall project implements a controlled refresh strategy to prevent token abuse while ensuring continuous sessions. The refresh endpoint in UmsAdminController.java delegates to JwtTokenUtil.refreshHeadToken:

public String refreshHeadToken(String oldToken) {
    if (StrUtil.isEmpty(oldToken)) return null;
    String token = oldToken.substring(tokenHead.length());
    Claims claims = getClaimsFromToken(token);
    if (claims == null || isTokenExpired(token)) return null;
    if (tokenRefreshJustBefore(token, 30 * 60)) return token; // 30 min window
    claims.put(CLAIM_KEY_CREATED, new Date());
    return generateToken(claims);
}

The refresh logic enforces three critical rules:

  • Expiration Check: Returns null if the token has already expired, forcing re-authentication.
  • Rate Limiting: Returns the original token unchanged if it was refreshed within the last 30 minutes (1800 seconds), preventing excessive token churn.
  • Timestamp Update: Generates a new token with an updated created claim when refresh is permitted, effectively resetting the expiration window.

The controller endpoint exposes this functionality via a GET request to /refreshToken:

@GetMapping("/refreshToken")
@ResponseBody
public CommonResult refreshToken(HttpServletRequest request) {
    String oldToken = request.getHeader(tokenHeader);
    String newToken = adminService.refreshToken(oldToken);
    if (newToken == null) return CommonResult.failed("token已经过期!");
    Map<String, String> map = new HashMap<>();
    map.put("token", newToken);
    map.put("tokenHead", tokenHead);
    return CommonResult.success(map);
}

Spring Security Configuration

The SecurityConfig class in mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java wires the components together:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .exceptionHandling()
            .accessDeniedHandler(restfulAccessDeniedHandler)
            .authenticationEntryPoint(restAuthenticationEntryPoint)
        .and()
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

This configuration disables CSRF protection (appropriate for stateless APIs), prevents session creation, and inserts the JWT filter before the standard username/password authentication filter. Public URLs (login, register, Swagger endpoints) are typically configured separately via ignoreUrlsConfig.

Summary

  • Token Generation: JwtTokenUtil.generateToken creates HS512-signed JWTs with username and timestamp claims in mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java.
  • Request Authentication: JwtAuthenticationTokenFilter validates tokens on every request and populates SecurityContextHolder without server-side sessions.
  • Refresh Safety: The refresh mechanism in UmsAdminController enforces a 30-minute minimum interval between refreshes to prevent abuse while allowing session extension.
  • Stateless Architecture: SecurityConfig ensures no sessions are created, making the system horizontally scalable and suitable for microservices deployment.

Frequently Asked Questions

How does the mall project store user authentication state?

The mall project uses a completely stateless architecture with no server-side session storage. Authentication state lives entirely inside the JWT token, which the client sends with every request in the Authorization header. The JwtAuthenticationTokenFilter validates this token and temporarily populates SecurityContextHolder for the duration of the single request.

What algorithm does JwtTokenUtil use to sign tokens?

According to the source code in mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java, the implementation uses HS512 (HMAC with SHA-512) to sign tokens. The secret key is configured via the jwt.secret property in the application configuration.

Why does the refresh token endpoint return the same token sometimes?

The refreshHeadToken method implements a safety mechanism that returns the original token unchanged if it was refreshed within the last 30 minutes (configurable via the tokenRefreshJustBefore check). This prevents clients from generating excessive new tokens and reduces computational overhead when a valid token is still relatively fresh.

Can I modify the token expiration time in the mall project?

Yes. Token expiration is controlled by the jwt.expiration configuration property (specified in seconds), which feeds into JwtTokenUtil.generateToken. Additionally, you can adjust the refresh rate-limiting window by modifying the 30 * 60 seconds parameter in the tokenRefreshJustBefore method call within JwtTokenUtil.java.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →