Implementing Sa-Token Authentication and Authorization in Spring Cloud Gateway
This article demonstrates how to replace heavyweight Spring Security with the lightweight Sa-Token framework by configuring token generation, session management, role and permission retrieval, and global gateway filters in a Spring Cloud micro‑service architecture, including complete code examples and deployment tips.
Introduction
Hello everyone, I am Chen. Previously, implementing authentication and authorization required a lot of boilerplate code, and using Spring Security felt heavy and complex. To avoid writing token generation, validation, and permission checks from scratch, I discovered the simple and efficient Sa-Token framework and will share a gateway authentication demo.
Requirement Analysis
We need a lightweight solution for token generation, session handling, and permission distribution without the overhead of Spring Security.
Architecture
The system consists of an account service handling user login and a gateway service that intercepts all requests, validates tokens, and checks permissions.
Authentication with Sa-Token
Sa-Token Module
Sa-Token provides a convenient session mode. Generating a token is as simple as calling StpUtil.login(Object id) , which creates a Token and a Session for the account.
StpUtil.login(Object id);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 when the token is sent in the HTTP header.
Login Interface
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);
}
}Gateway Module
Dependencies
<dependencies>
<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>
<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:
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 gateway also registers with Nacos for service discovery and uses the same Sa-Token and Redis settings as the account service.
Global Authentication Filter
@Configuration
public class SaTokenConfigure {
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
SaRouter.match("/**", "/account/user/doLogin").check(r -> StpUtil.checkLogin());
})
.setError(e -> SaResult.error(e.getMessage()));
}
}This filter intercepts all paths, excludes the login endpoint, and performs login verification.
Fine‑Grained Permission Check
Beyond simple token validation, we need to verify whether a user has permission to access a specific path. In the classic RBAC model, users have roles, and roles have permissions.
Custom Permission Interface
@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 (Exception e) { throw new RuntimeException(e); }
}
return ListUtil.string2List((String) res);
}
@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 (Exception e) { throw new RuntimeException(e); }
}
return ListUtil.string2List((String) res);
}
}The implementation lazily loads permissions and roles from Redis or, if missing, calls the account service via Feign. As the gateway runs on WebFlux, a dedicated thread pool avoids blocking the reactive event loop.
Thread Pool Configuration
@Configuration
public class ThreadPollConfig {
private final BlockingQueue
asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);
public final ExecutorService USER_ROLE_PERM_THREAD_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
1000 * 60,
TimeUnit.MILLISECONDS,
asyncSenderThreadPoolQueue,
new ThreadFactory() {
private final AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "RolePermExecutor_" + threadIndex.incrementAndGet());
}
}
);
}Role and Permission Services in Account Service
REST endpoints expose role and permission data for a given user ID.
@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);
}
}Implementations store role and permission information in MySQL and cache the results in Redis, setting "ROLES" and "PERMS" fields for the token session.
Gateway Permission Filter
@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)) {
throw new NotPermissionException(path);
}
}
return chain.filter(exchange);
}
}The filter checks the current token session for the required permission and rejects the request if the user lacks it.
Conclusion
By using Sa-Token we achieve lightweight authentication, session sharing via Redis, fine‑grained RBAC permission checks, and a reactive‑compatible gateway filter without the heavy configuration burden of Spring Security.
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.