Information Security 20 min read

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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Sa-Token Authentication and Authorization in Spring Cloud Gateway

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: Bearer

In 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: Bearer

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

JavaRedisauthenticationauthorizationSpring Cloud GatewaySa-Token
Code Ape Tech Column
Written by

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

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.