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.
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: BearerUsing 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: BearerGateway 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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.