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<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 (Exception e) { throw new RuntimeException(e); }
        }
        return ListUtil.string2List((String) res);
    }

    @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 (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<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,
        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<RoleDTO> getRoles(@RequestParam String userId) {
        return roleManager.getRoles(userId);
    }
}

@RestController
@RequestMapping("/account/permission/")
public class PermissionController {
    @Autowired
    private PermissionManager permissionManager;
    @PostMapping("/getPermissions")
    public List<PermissionDTO> 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<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);
    }
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAuthenticationAuthorizationSpring 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

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.