Simplify Spring Cloud Gateway Authentication with Sa-Token: A Step‑by‑Step Demo
This tutorial shows how to replace heavyweight Spring Security with the lightweight Sa-Token framework by demonstrating token generation, configuration, gateway filtering, role‑based access control, and asynchronous permission fetching for a Spring Cloud microservices architecture.
Requirement Analysis
Traditional authentication and authorization require writing a lot of boilerplate code; Spring Security is heavy and its many configuration options are hard to understand, while a custom implementation forces you to handle token generation, validation, refresh, and permission assignment yourself.
Architecture
Authentication with Sa‑Token
Sa‑Token Module
Generate a token and create a session using the simple API: StpUtil.login(Object id); The generated token is stored as a Token credential together with a Session object.
Configuration
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: BearerIn the configuration we set token-name to define the cookie name and token-prefix to prepend Bearer to the token when it is sent in the HTTP header.
When calling an API the header should contain: weishuang-token = Bearer token123456 Sa‑Token’s session mode stores sessions in Redis, which is required for distributed services; alternatively, Sa‑Token also supports JWT for a stateless token.
Login API Example
User Entity
@Data
public class User {
private String id;
private String userName;
private String password;
}UserController
@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("登录成功");
}
}UserManager Implementation
@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("账号或密码错误");
}
StpUtil.login(user.getId());
StpUtil.getSession().set("USER_DATA", user);
}
}UserService
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
public User getOne(String username, String password) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username)
.eq(User::getPassword, password);
return userMapper.selectOne(queryWrapper);
}
}Gateway Module
Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>
</dependencies>Gateway Configuration
server:
port: 9000
spring:
application:
name: weishuang-gateway
cloud:
loadbalancer:
ribbon:
enabled: false
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: BearerThe same Sa‑Token and Redis settings must be applied in the gateway to share session data with the account service.
Authentication Filter
package com.weishuang.gateway.gateway.config;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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()));
}
}All paths are intercepted; the login endpoint is excluded so that users can obtain a token.
Fine‑Grained Authorization (RBAC)
Beyond simple token validation, we need to verify whether a user has permission to access a specific path. In a classic RBAC model a user can have multiple roles, and each role can have multiple permissions.
Sa‑Token provides the StpInterface to plug in custom role and permission retrieval logic.
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private RoleFacade roleFacade;
@Autowired
private PermissionFacade permissionFacade;
@Autowired
private ThreadPollConfig threadPollConfig;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Object res = StpUtil.getTokenSession().get("PERMS");
if (res == null) {
CompletableFuture<List<String>> permFuture = CompletableFuture.supplyAsync(() -> {
List<PermissionDTO> 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<String> getRoleList(Object loginId, String loginType) {
Object res = StpUtil.getTokenSession().get("ROLES");
if (res == null) {
CompletableFuture<List<String>> roleFuture = CompletableFuture.supplyAsync(() -> {
List<RoleDTO> 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);
}
}The implementation caches role and permission data in Redis to avoid repeated remote calls.
Gateway Role/Permission Retrieval
package com.weishuang.gateway.gateway.config;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Configuration
public class ThreadPollConfig {
private final BlockingQueue<Runnable> asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);
public final ExecutorService USER_ROLE_PERM_THREAD_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
1000 * 60,
TimeUnit.MILLISECONDS,
this.asyncSenderThreadPoolQueue,
new ThreadFactory() {
private final AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "RolePermExecutor_" + threadIndex.incrementAndGet());
}
}
);
}WebFlux Filter Example
@Component
public class ForwardAuthFilter implements WebFilter {
static Set<String> whitePaths = new HashSet<>();
static {
whitePaths.add("/account/user/doLogin");
whitePaths.add("/account/user/logout");
whitePaths.add("/account/user/register");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getPath().toString();
if (!whitePaths.contains(path)) {
if (!StpUtil.hasPermission(path)) {
throw new NotPermissionException(path);
}
}
return chain.filter(exchange);
}
}Alternatively, a fully reactive approach can fetch permissions via a WebClient call instead of implementing StpInterface.
Conclusion
With only a few lines of Sa‑Token code— StpUtil.login(Object id) —the framework handles token creation, session management, role and permission caching, and integrates seamlessly with Spring Cloud Gateway, eliminating the need for complex Spring Security configurations.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
