Backend Development 21 min read

Implementing Sa-Token Authentication in Spring Cloud Gateway with Redis and Nacos

This article demonstrates how to replace heavyweight Spring Security with the lightweight Sa-Token framework by configuring token generation, session storage in Redis, service discovery via Nacos, and permission checks in a Spring Cloud Gateway micro‑service architecture, complete with code examples and deployment settings.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Sa-Token Authentication in Spring Cloud Gateway with Redis and Nacos

Introduction

When implementing authentication and authorization, writing a lot of boilerplate code or learning complex frameworks like Spring Security can be cumbersome. Sa-Token offers a simple and efficient alternative, and this guide shows a complete gateway authentication demo using Sa-Token.

Requirement Analysis

(Illustrative diagram omitted)

Architecture

(Structure diagram omitted)

Authentication – Sa-Token Module

We first create a Sa-Token module to generate tokens and assign permissions. In Sa-Token's session mode, a token is created with a single call:

StpUtil.login(Object id);

The configuration file defines token name, expiration, prefix, and Redis connection details:

server:
  port: 8081

spring:
  application:
    name: weishuang-account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/weishuang_account?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
    username: root
    password: root
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    timeout: 10s
    lettuce:
      pool:
        max-active: 200
        max-wait: -1ms
        max-idle: 10
        min-idle: 0

sa-token:
  token-name: weishuang-token
  timeout: 2592000
  activity-timeout: -1
  is-concurrent: true
  is-share: true
  token-style: uuid
  is-log: false
  token-prefix: Bearer

Using the configuration, the token name defaults to satoken if not set, and the Bearer prefix must be added by the front‑end when sending the token in the HTTP header.

Login Interface

Entity and controller definitions:

@Data
public class User {
    private String id;
    /** Account */
    private String userName;
    /** Password */
    private String password;
}
@RestController
@RequestMapping("/account/user/")
public class UserController {
    @Autowired
    private UserManager userManager;

    @PostMapping("doLogin")
    public SaResult doLogin(@RequestBody AccountUserLoginDTO req) {
        userManager.login(req);
        return SaResult.ok("Login successful");
    }
}
@Component
public class UserManagerImpl implements UserManager {
    @Autowired
    private UserService userService;

    @Override
    public void login(AccountUserLoginDTO req) {
        String password = PasswordUtil.generatePassword(req.getPassword());
        User user = userService.getOne(req.getUserName(), password);
        if (user == null) {
            throw new RuntimeException("Invalid username or password");
        }
        StpUtil.login(user.getId());
        StpUtil.getSession().set("USER_DATA", user);
    }
}
@Service
public class UserServiceImpl extends ServiceImpl
implements UserService {
    @Autowired
    private UserMapper userMapper;

    public User getOne(String username, String password) {
        LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, username)
                    .eq(User::getPassword, password);
        return userMapper.selectOne(queryWrapper);
    }
}

Gateway Module

Dependencies (Maven) required for Spring Cloud, Nacos discovery, Sa-Token reactive starter, and Redis integration are listed, for example:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>

Gateway configuration (application.yml) mirrors the account service settings and adds routing rules:

server:
  port: 9000
spring:
  application:
    name: weishuang-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: localhost:8848
    gateway:
      routes:
        - id: account
          uri: lb://weishuang-account
          order: 1
          predicates:
            - Path=/account/**
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    timeout: 10s
    lettuce:
      pool:
        max-active: 200
        max-wait: -1ms
        max-idle: 10
        min-idle: 0

sa-token:
  token-name: weishuang-token
  timeout: 2592000
  activity-timeout: -1
  is-concurrent: true
  is-share: true
  token-style: uuid
  is-log: false
  token-prefix: Bearer

Gateway Sa-Token Global Filter

Register a Sa‑Reactor filter to intercept all requests, exclude the login endpoint, and perform login checks:

@Configuration
public class SaTokenConfigure {
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            .addInclude("/**")
            .addExclude("/favicon.ico")
            .setAuth(obj -> {
                SaRouter.match("/**", "/account/user/doLogin", r -> StpUtil.checkLogin());
            })
            .setError(e -> SaResult.error(e.getMessage()));
    }
}

The filter ensures that every request is authenticated except the explicitly whitelisted login path.

Role and Permission Management

REST controllers expose role and permission retrieval APIs:

@RestController
@RequestMapping("/account/role/")
public class RoleController {
    @Autowired
    private RoleManager roleManager;

    @PostMapping("/getRoles")
    public List
getRoles(@RequestParam String userId) {
        return roleManager.getRoles(userId);
    }
}
@RestController
@RequestMapping("/account/permission/")
public class PermissionController {
    @Autowired
    private PermissionManager permissionManager;

    @PostMapping("/getPermissions")
    public List
getPermissions(@RequestParam String userId) {
        return permissionManager.getPermissions(userId);
    }
}

Manager implementations query the database, cache results in Redis, and store role/permission lists in the token session for fast lookup:

@Component
public class RoleManagerImpl implements RoleManager {
    @Autowired
    private RoleService roleService;
    @Autowired
    private UserRoleService userRoleService;
    @Autowired
    private Role2RoleDTOCovert role2RoleDTOCovert;

    @Override
    public List
getRoles(String userId) {
        List
userRoles = userRoleService.getByUserId(userId);
        Set
roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
        List
roleDTOS = role2RoleDTOCovert.covertTargetList2SourceList(roleService.getByIds(roleIds));
        String tokenValue = StpUtil.getTokenValueByLoginId(userId);
        if (StringUtils.isNotEmpty(tokenValue)) {
            if (CollectionUtils.isEmpty(roleDTOS)) {
                StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", "");
            } else {
                List
roleNames = roleDTOS.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());
                StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", ListUtil.list2String(roleNames));
            }
        }
        return roleDTOS;
    }
}
@Component
public class PermissionManagerImpl implements PermissionManager {
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private RolePermService rolePermService;
    @Autowired
    private UserRoleService userRoleService;
    @Autowired
    private Permission2PermissionDTOCovert permissionDTOCovert;

    @Override
    public List
getPermissions(String userId) {
        List
roles = userRoleService.getByUserId(userId);
        if (CollectionUtils.isEmpty(roles)) {
            handleUserPermSession(userId, null);
            return Collections.emptyList();
        }
        Set
roleIds = roles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
        List
rolePerms = rolePermService.getByRoleIds(roleIds);
        if (CollectionUtils.isEmpty(rolePerms)) {
            handleUserPermSession(userId, null);
            return Collections.emptyList();
        }
        Set
permIds = rolePerms.stream().map(RolePerm::getPermId).collect(Collectors.toSet());
        List
perms = permissionDTOCovert.covertTargetList2SourceList(permissionService.getByIds(permIds));
        handleUserPermSession(userId, perms);
        return perms;
    }

    private void handleUserPermSession(String userId, List
perms) {
        String tokenValue = StpUtil.getTokenValueByLoginId(userId);
        if (StringUtils.isNotEmpty(tokenValue)) {
            if (CollectionUtils.isEmpty(perms)) {
                StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", "");
            } else {
                List
paths = perms.stream().map(PermissionDTO::getPath).collect(Collectors.toList());
                StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", ListUtil.list2String(paths));
            }
        }
    }
}

Asynchronous Permission Retrieval

Because the gateway runs on a reactive stack, synchronous RPC calls are wrapped in CompletableFuture and executed in a dedicated thread pool:

@Component
public class StpInterfaceImpl implements StpInterface {
    @Autowired
    private RoleFacade roleFacade;
    @Autowired
    private PermissionFacade permissionFacade;
    @Autowired
    private ThreadPollConfig threadPollConfig;

    @Override
    public List
getPermissionList(Object loginId, String loginType) {
        Object res = StpUtil.getTokenSession().get("PERMS");
        if (res == null) {
            CompletableFuture
> permFuture = CompletableFuture.supplyAsync(() -> {
                List
permissions = permissionFacade.getPermissions((String) loginId);
                return permissions.stream().map(PermissionDTO::getPath).collect(Collectors.toList());
            }, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);
            try {
                return permFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
        String paths = (String) res;
        return ListUtil.string2List(paths);
    }

    @Override
    public List
getRoleList(Object loginId, String loginType) {
        Object res = StpUtil.getTokenSession().get("ROLES");
        if (res == null) {
            CompletableFuture
> roleFuture = CompletableFuture.supplyAsync(() -> {
                List
roles = roleFacade.getRoles((String) loginId);
                return roles.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());
            }, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);
            try {
                return roleFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
        String roleNames = (String) res;
        return ListUtil.string2List(roleNames);
    }
}

Gateway Permission Filter

A WebFlux filter checks the token session for the required permission and rejects unauthorized requests:

@Component
public class ForwardAuthFilter implements WebFilter {
    static Set
whitePaths = new HashSet<>();
    static {
        whitePaths.add("/account/user/doLogin");
        whitePaths.add("/account/user/logout");
        whitePaths.add("/account/user/register");
    }

    @Override
    public Mono
filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getPath().toString();
        if (!whitePaths.contains(path)) {
            if (!StpUtil.hasPermission(path)) {
                return Mono.error(new NotPermissionException(path));
            }
        }
        return chain.filter(exchange);
    }
}

An alternative fully‑reactive approach fetches permissions from the account service via a WebClient call, avoiding the need for a custom StpInterfaceImpl implementation.

Custom Sa‑Token Configuration with Whitelist

Finally, the Sa‑Token filter can be refined to use a whitelist defined in a separate class:

@Configuration
public class SaTokenConfigure {
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            .addInclude("/**")
            .addExclude("/favicon.ico")
            .setAuth(obj -> {
                SaRouter.match("/**")
                         .notMatch(new ArrayList<>(WhitePath.whitePaths))
                         .check(r -> StpUtil.checkLogin());
            })
            .setError(e -> SaResult.error(e.getMessage()));
    }
}

public class WhitePath {
    static Set
whitePaths = new HashSet<>();
    static {
        whitePaths.add("/account/user/doLogin");
        whitePaths.add("/account/user/logout");
        whitePaths.add("/account/user/register");
    }
}

With these configurations, the gateway provides lightweight, token‑based authentication and fine‑grained RBAC permission checks without the overhead of Spring Security.

JavaMicroservicesRedisNacosauthenticationSpring Cloud GatewaySa-Token
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login 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.