Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing JWT Authentication and User Verification in a Spring Cloud Gateway Architecture

Tech May 12 2

Securing Microservices with Spring Cloud Gateway

Gateway Role and Advantages

A microservice gateway acts as a centralized entry point between external clients and internal services. It abstracts concerns such as authentication, rate limiting, logging, and cross-origin handling—allowing individual services to focus solely on business logic.

Key benefits include:

  • Centralized security: Authentication and authorization enforced once at the edge.
  • Simplified client interaction: Clients communicate with a single endpoint instead of managing multiple service addresses.
  • Improved observability: Unified metrics, tracing, and request logging.
  • Flexible routing and filtering: Dynamic path-based routing, header manipulation, and request/response transformation.
  • Protocol abstraction: Shielding clients from internal protocols (e.g., gRPC or private HTTP variants).

Among available solutions—Nginx, Netflix Zuul, and Spring Cloud Gateway—we adopt Spring Cloud Gateway due to its native Spring Boot integration, reactive foundation, built-in filters, and superior performance over Zuul 1.x.

Gateway Setup

Create heima-leadnews-admin-gateway with the following dependencies:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
    </dependency>
</dependencies>

Boot class:

package com.heima.admin.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

Configuration (application.yml):

server:
  port: 6001
spring:
  application:
    name: leadnews-admin-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.200.130:8848
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins: "*"
            allowed-methods: [GET, POST, PUT, DELETE]
      routes:
        - id: admin
          uri: lb://leadnews-admin
          predicates:
            - Path=/admin/**
          filters:
            - StripPrefix=1
        - id: user
          uri: lb://leadnews-user
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1

JWT Validation via Global Filter

Define a GlobalFilter that intercepts all non-login requests, extracts and validates JWT tokens, and injects authenticated user identifiers in to downstream headers.

First, add a utility for token parsing (e.g., JwtTokenValidator):

package com.heima.admin.gateway.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;

public class JwtTokenValidator {
    private static final String SECRET = "your-32-byte-secret-key-for-hmac-sha256";

    public static Claims parseClaims(String token) {
        SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public static boolean isValid(Claims claims) {
        return claims.getExpiration().after(new java.util.Date());
    }
}

Then implement the filter:

package com.heima.admin.gateway.filter;

import com.heima.admin.gateway.util.JwtTokenValidator;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // Skip validation for login endpoints
        if (request.getURI().getPath().contains("/login/in")) {
            return chain.filter(exchange);
        }

        String token = extractToken(request);
        if (StringUtils.isBlank(token)) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        try {
            Claims claims = JwtTokenValidator.parseClaims(token);
            if (!JwtTokenValidator.isValid(claims)) {
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            Integer userId = (Integer) claims.get("userId");
            if (userId != null) {
                ServerHttpRequest mutated = request.mutate()
                        .header("X-User-ID", userId.toString())
                        .build();
                return chain.filter(exchange.mutate().request(mutated).build());
            }
        } catch (Exception e) {
            log.warn("Invalid JWT token", e);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return response.setComplete();
    }

    private String extractToken(ServerHttpRequest request) {
        String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return request.getHeaders().getFirst("token");
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

Managing App User Identity Verification

Data Model and Query Itnerface

The identity verification records are stored in ap_user_realname. The corresponding entity is:

package com.heima.model.user.pojos;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;

@Data
@TableName("ap_user_realname")
public class ApUserRealname implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Integer id;

    @TableField("user_id")
    private Integer userId;

    @TableField("name")
    private String name;

    @TableField("idno")
    private String idno;

    @TableField("font_image")
    private String frontImage;

    @TableField("back_image")
    private String backImage;

    @TableField("hold_image")
    private String holdImage;

    @TableField("live_image")
    private String liveImage;

    @TableField("status")
    private Short status; // 0: draft, 1: pending, 2: rejected, 9: approved

    @TableField("reason")
    private String rejectionReason;

    @TableField("created_time")
    private Date createdTime;

    @TableField("submited_time")
    private Date submittedTime;

    @TableField("updated_time")
    private Date updatedTime;
}

Expose a REST interface for paginated queries by status:

package com.heima.api.user;

import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.user.dtos.AuthQuery;

public interface UserVerificationApi {
    PageResponseResult listByStatus(AuthQuery query);
}

Where AuthQuery extends pagination support and adds status and optional id:

package com.heima.model.user.dtos;

import com.heima.model.common.dtos.PageRequestDto;
import lombok.Data;

@Data
public class AuthQuery extends PageRequestDto {
    private Integer id;
    private Short status;
    private String rejectionReason;
}

Backend Implementation

Mapper interface:

package com.heima.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.user.pojos.ApUserRealname;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserVerificationMapper extends BaseMapper<ApUserRealname> {}

Service layer:

package com.heima.user.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.dtos.AuthQuery;
import com.heima.model.user.pojos.ApUserRealname;
import com.heima.user.mapper.UserVerificationMapper;
import org.springframework.stereotype.Service;

@Service
public class UserVerificationService extends ServiceImpl<UserVerificationMapper, ApUserRealname> {

    public ResponseResult listByStatus(AuthQuery query) {
        if (query == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        query.checkParam();

        LambdaQueryWrapper<ApUserRealname> wrapper = new LambdaQueryWrapper<>();
        if (query.getStatus() != null) {
            wrapper.eq(ApUserRealname::getStatus, query.getStatus());
        }

        IPage<ApUserRealname> page = new Page<>(query.getPage(), query.getSize());
        page = page(page, wrapper);

        PageResponseResult result = new PageResponseResult(query.getPage(), query.getSize(), (int) page.getTotal());
        result.setData(page.getRecords());
        return result;
    }
}

Controller:

package com.heima.user.controller.v1;

import com.heima.api.user.UserVerificationApi;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.user.dtos.AuthQuery;
import com.heima.user.service.UserVerificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/auth")
public class UserVerificationController implements UserVerificationApi {

    @Autowired
    private UserVerificationService service;

    @PostMapping("/list")
    @Override
    public PageResponseResult listByStatus(@RequestBody AuthQuery query) {
        return (PageResponseResult) service.listByStatus(query);
    }
}

Approving or Rejecting Identity Submissions

Cross-Service Coordination

Upon approval (status = 9) of a verification record, two actions must occur atomically:

  1. Create a corresponding WmUser in the leadnews-wemedia service.
  2. Register an ApAuthor in the leadnews-article service.

Feign clients enable inter-service communication:

@FeignClient(name = "leadnews-article")
public interface ArticleClient {
    @GetMapping("/api/v1/author/findByUserId/{userId}")
    ApAuthor findAuthorByUserId(@PathVariable Integer userId);

    @PostMapping("/api/v1/author/save")
    ResponseResult saveAuthor(@RequestBody ApAuthor author);
}

@FeignClient(name = "leadnews-wemedia")
public interface MediaClient {
    @PostMapping("/api/v1/user/save")
    ResponseResult saveMediaUser(@RequestBody WmUser user);

    @GetMapping("/api/v1/user/findByName/{name}")
    WmUser findMediaUserByName(@PathVariable String name);
}

Approval Logic

Extend UserVerificationService with state transition methods:

@Autowired
private ArticleClient articleClient;

@Autowired
private MediaClient mediaClient;

@Autowired
private ApUserMapper userMapper;

public ResponseResult approve(Integer id, String reason) {
    return updateStatus(id, (short) 9, reason);
}

public ResponseResult reject(Integer id, String reason) {
    return updateStatus(id, (short) 2, reason);
}

private ResponseResult updateStatus(Integer id, Short status, String reason) {
    if (id == null) {
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
    }

    ApUserRealname record = getById(id);
    if (record == null) {
        return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
    }

    record.setStatus(status);
    record.setRejectionReason(reason);
    updateById(record);

    if (status == 9) {
        ensureMediaAccountAndAuthor(record);
    }

    return ResponseResult.okResult();
}

private void ensureMediaAccountAndAuthor(ApUserRealname record) {
    ApUser appUser = userMapper.selectById(record.getUserId());
    if (appUser == null) return;

    WmUser mediaUser = mediaClient.findMediaUserByName(appUser.getName());
    if (mediaUser == null) {
        mediaUser = buildMediaUserFromAppUser(appUser);
        mediaClient.saveMediaUser(mediaUser);
    }

    ApAuthor author = articleClient.findAuthorByUserId(appUser.getId());
    if (author == null) {
        author = new ApAuthor();
        author.setName(appUser.getName());
        author.setUserId(appUser.getId());
        author.setType((short) 2); // platform creator
        author.setCreatedTime(new Date());
        articleClient.saveAuthor(author);
    }
}

private WmUser buildMediaUserFromAppUser(ApUser appUser) {
    WmUser u = new WmUser();
    u.setApUserId(appUser.getId());
    u.setName(appUser.getName());
    u.setPassword(appUser.getPassword());
    u.setSalt(appUser.getSalt());
    u.setPhone(appUser.getPhone());
    u.setStatus(9);
    u.setCreatedTime(new Date());
    return u;
}

Controller endpoints:

@PostMapping("/approve")
public ResponseResult approve(@RequestBody AuthQuery query) {
    return service.approve(query.getId(), query.getRejectionReason());
}

@PostMapping("/reject")
public ResponseResult reject(@RequestBody AuthQuery query) {
    return service.reject(query.getId(), query.getRejectionReason());
}

This architecture ensures secure, scalable, and maintainable identity verification workflows across distributed services.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.