夏有木工作室夏有木工作室XymStudio
  • Home
  • Blog
  • About
  • Contact
夏有木工作室夏有木工作室XymStudio
All Articles/
架构设计

使用 Sa-Token 平替 Spring Security,告别繁琐的认证与鉴权

EricEric
2025/01/15
·3 min read (889 words)·

对比传统 Spring Security 的复杂繁重,分享使用轻量 Sa-Token 框架实现无痛多端鉴权、单点登录与路由拦截的最佳实践。

Table of Contents

  • 需求说明
  • 实战
  • 01 环境准备
  • 02 添加依赖
  • 03 设置配置文件
  • 04 使用JWT 生成 双 Token
  • 05 Spring Bean 注入
  • 06 生成 Token
  • 07 前端使用 Token 请求
  • 08 动态鉴权
  • 最后
  • 附录
  • 参考文档
  • 架构设计推荐阅读

最近将项目升级到 SpringBoot 3 后发现 Spring Security 的框架又进行了大幅调整,又得重新学习和应用了一遍。今天准备使用 Sa-Token 进行平替 Spring Security 框架。

需求说明

使用 Sa-Token 替换 Spring Security 框架,那么就需要将之前实现的所有安全相关的需求进行替换,主要有如下:

  • 前后端分离,支持多前端,如:PC Web、小程序、APP 等
  • 在微服务框架中易于 Spring Cloud Gateway 集成,支持 Webflux
  • 支持 JWT,同时可以生成 AccessToken 和 RefreshToken
  • 支持全局过滤器,统一对请求地址进行拦截与认证
  • 密码策略不变,便于后续两个框架的切换
  • 用户登录后,支持灵活的鉴权配置

实战

01 环境准备

  • Spring Boot 3.4
  • JDK 21
  • Gradle 8.12

02 添加依赖

implementation 'cn.dev33:sa-token-spring-boot3-starter:1.39.0'
implementation 'cn.dev33:sa-token-jwt:1.39.0'
implementation 'org.springframework.security:spring-security-crypto'

注:如果你使用的不是 SpringBoot 3.x,只需要将 sa-token-spring-boot3-starter 修改为 sa-token-spring-boot-starter 即可。

建议单独引入 Spring Security 的密码模块。

  • 若当前项目中有历史数据,且用户密码不可逆加密,那么必须引入。
  • 若没有历史数据遗留,也建议引入,便于后续切换回 Spring Security。

03 设置配置文件

在 application.yaml 文件中添加如下配置,定制化使用框架:

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: Authorization
  # token前缀
  token-prefix: Bearer
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效,当前设置为 7 天
  timeout: 604800
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true
  # jwt 密钥
  jwt-secret-key: asdasdasiferichueuiwyurfewbfjsdafjk0212
  # jwt refresh 密钥
  jwt-refresh-secret-key: asdasdasiferichueuiwyurfewbfjsdafjk7788
  # refreshToken 超时时间(单位:秒),当前设置为 30 天
  jwt-refresh-timeout: 2592000

注意:额外补充了 jwt-refresh-secret-key 和 jwt-refresh-timeout 用于 Refresh Token 的生成。

04 使用JWT 生成 双 Token

Sa-Token 当前的 JWT 模块是不支持生成双Token的,不利于移动端的 token 使用,参考官方 StpLogicJwtForStateless 进行扩展:

@Slf4j
@Component
public class StpLogicJwtForRefresh extends StpLogic {

    @Value("${sa-token.jwt-refresh-secret-key}")
    private String refreshSecretKey;

    @Value("${sa-token.jwt-refresh-timeout}")
    private long refreshTimeout;

    public StpLogicJwtForRefresh() {
        super("login"); // 指定 StpLogic 的标识
    }

    // 生成 refreshToken
    public String createRefreshToken(Object loginId, SaLoginModel loginModel) {
        // 自定义 RefreshToken 的生成逻辑
        return SaJwtUtil.createToken(loginType,
                loginId,
                loginModel.getDeviceOrDefault(),
                refreshTimeout,
                loginModel.getExtraData(),
                refreshSecretKey);
    }

    // 校验 refreshToken
    public JSONObject verifyRefreshToken(String refreshToken) {
        try {
            return SaJwtUtil.getPayloads(refreshToken, getLoginType(), refreshSecretKey);
        } catch (Exception e) {
            log.error("StpLogicJwtForRefresh getPayloads failed", e);
            throw new UnauthorizedException(AuthErrorCode.REFRESH_TOKEN_INVALID);
        }
    }
}

同时官方支持三种 JWT Token 模式,按需选择:

功能点Simple 简单模式Mixin 混入模式Stateless 无状态模式
Token风格jwt风格jwt风格jwt风格
登录数据存储Redis中存储Token中存储Token中存储
Session存储Redis中存储Redis中存储无Session
注销下线前后端双清数据前后端双清数据前端清除数据
踢人下线API支持不支持不支持
顶人下线API支持不支持不支持
登录认证支持支持支持
角色认证支持支持支持
权限认证支持支持支持
timeout 有效期支持支持支持
active-timeout 有效期支持支持不支持
id反查Token支持支持不支持
会话管理支持部分支持不支持
注解鉴权支持支持支持
路由拦截鉴权支持支持支持
账号封禁支持支持不支持
身份切换支持支持支持
二级认证支持支持支持
模式总结Token风格替换jwt 与 Redis 逻辑混合完全舍弃Redis,只用jwt

05 Spring Bean 注入

新增一个 SaTokenConfigure 配置类,声明 Sa-Token 所需要的 bean。

@Slf4j
@Configuration
public class SaTokenConfigure {

  // Sa-Token 整合 jwt (Stateless 无状态模式)
  @Bean
  @Primary
  @Qualifier("jwtStatelessLogic")
  public StpLogic jwtStatelessLogic() {
      return new StpLogicJwtForStateless();
  }

  @Bean
  @Qualifier("jwtRefreshLogic")
  public StpLogic jwtRefreshLogic() {
      return new StpLogicJwtForRefresh();
  }

  @Bean
  PasswordEncoder passwordEncoder() {
      return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

	/**
   * 注意:
   * SaServletFilter 默认执行顺序为 -100,如果你要自定义过滤器的执行顺序,可以使用 FilterRegistrationBean 注册
   */
  @Bean
  public SaServletFilter getSaServletFilter() {
      return new SaServletFilter()

        // 指定 拦截路由 与 放行路由
        .addInclude("/**").addExclude(AuthConstants.IGNORE_AUTH_URLS)    /* 如:排除掉 /favicon.ico */

        // 认证函数: 每次请求执行
        .setAuth(obj -> {
            // 登录认证 -- 拦截所有路由,并排除 auth 相关接口用于开放登录
            SaRouter.match("/**", "/api/admin/auth/**", StpUtil::checkLogin);
        })

        // 异常处理函数:每次认证函数发生异常时执行此函数
        .setError(e -> {
            log.error("Sa-Token认证失败, 异常信息:", e);
            throw new UnauthorizedException(AuthErrorCode.TOKEN_INVALID);
        })

        // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
        .setBeforeAuth(r -> {
            // ---------- 设置一些安全响应头 ----------
            SaHolder.getResponse()
                    // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
                    .setHeader("X-Frame-Options", "SAMEORIGIN")
                    // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
                    .setHeader("X-XSS-Protection", "1; mode=block")
                    // 禁用浏览器内容嗅探
                    .setHeader("X-Content-Type-Options", "nosniff")
            ;
        });
  }
}

06 生成 Token

编写 Controller 类,新增用户登录接口。

private final UserAccountRepository userAccountRepository;
private final PasswordEncoder passwordEncoder;
private final StpLogicJwtForRefresh jwtRefreshLogic;
private final UserLoginRecordDOMapper userLoginRecordDOMapper;

@Override
@Transactional
public TokenResponse adminLogin(AdminLoginCmd cmd) {
    // 第一步:验证用户名密码
    UserAccount userAccount = userAccountRepository.findByUsername(cmd.getUsername());
    if (userAccount == null || !passwordEncoder.matches(cmd.getPassword(), userAccount.getAccountPassword())) {
        throw new RuntimeException("用户名或密码错误");
    }

    // 第二步:登录并生成 AccessToken
    SaLoginModel saLoginModel = SaLoginConfig
            .setDevice("PC")
            .setExtra("userId", userAccount.getId())
            .setExtra("username", userAccount.getAccountName())
            .setExtra("applicationType", userAccount.getApplicationType());

    StpUtil.login(userAccount.getId(), saLoginModel);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    String accessToken = tokenInfo.getTokenValue();

    // 第三步:生成 RefreshToken
    String refreshToken = jwtRefreshLogic.createRefreshToken(userAccount.getId(), saLoginModel);

    // 第四步:保存用户登录记录
    UserContext userContext = userAccount.buildUserContext();
    saveLoginRecord(userContext, cmd.getClientIp(), cmd.getClientInfo());

    return TokenResponse.builder().accessToken(accessToken).refreshToken(refreshToken).build();
}

07 前端使用 Token 请求

这里举例 PC Web 端,其他端代码其实类似。

let token = '';
export const getToken = (): string => {
  if (token) return token;
  const value = localStorage.getItem(TOKEN_KEY) ?? sessionStorage.getItem(TOKEN_KEY);
  if (value) token = value;
  return token;
};

export const setToken = (accessToken: string, refreshToken: string, remember: boolean): void => {
  token = accessToken;
  if (remember) {
    localStorage.setItem(TOKEN_KEY, accessToken);
  } else {
    sessionStorage.setItem(TOKEN_KEY, accessToken);
  }
};

interface RequestOptions {
  url: string,
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  body?: Record<string, any> | null;
  headers?: Record<string, string>;
}

type Resp = Record<string, any>;

async function request(options: RequestOptions): Promise<Resp> {
  const {url, method = 'GET', body} = options;
  let apiUrl = `${API_URL}${url}`;

  const headers = new Headers({
    'Authorization': `Bearer ${getToken()}`,
    'Accept-Language': 'zh',
    ...options.headers,
  });

  ...
}

08 动态鉴权

以下是官方的例子,供参考。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor(handle -> {
        // 遍历校验规则,依次鉴权
        Map<String, String> rules = getAuthRules();
        for (String path : rules.keySet()) {
            SaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path)));
        }
    })).addPathPatterns("/**");
}

// 动态获取鉴权规则
public Map<String, String> getAuthRules() {
    // key 代表要拦截的 path,value 代表需要校验的权限
    Map<String, String> authMap = new LinkedHashMap<>();
    authMap.put("/user/**", "user");
    authMap.put("/admin/**", "admin");
    authMap.put("/article/**", "article");
    // 更多规则 ...
    return authMap;
}

最后

使用 Sa-Token 平替 Spring Security 非常的顺利,代码量也非常的少,确实符合官方的口号:

一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!

附录

平替后代码量对比:

image-20250114下午121854578

登录验证:

image-20250114下午122408152

image-20250114下午122546583

image-20250114下午122614417

参考文档

Sa-Token 官方文档

Spring Security+JWT 轻松实现前后端分离的认证架构

架构设计推荐阅读

如果你对企业级单点登录(SSO)、微服务鉴权体系与安全性设计感兴趣,推荐阅读:

  • 单点登录系统(SSO)自建方案总结
  • 微服务鉴权设计:SpringBoot 3.x + OAuth2
  • 微服务鉴权源码解析:SpringBoot 3.x + OAuth2
  • 使用 Sa-Token 平替 Spring Security,告别繁琐的认证与鉴权

👉 查阅更多企业级系统设计与工程实战,欢迎访问 架构设计专栏完整目录。

Previous

为什么写不好 DDD 代码?

Next

域名备案全流程详解

More Articles

为什么写不好 DDD 代码?

为什么写不好 DDD 代码?

深度剖析为什么大部分团队写不好 DDD,从建模思维、业务边界侵蚀到防腐层设计,解析领域驱动的核心痛点与破局解法。

EricEric
2025/01/14
良好的异常处理设计:系统稳定性的基础保障

良好的异常处理设计:系统稳定性的基础保障

探讨企业级后端的全局异常体系设计,规范业务错误码、状态流转异常捕获与微服务防御性兜底保障。

EricEric
2024/11/27
MySQL 创建高性能的索引

MySQL 创建高性能的索引

深入 MySQL 索引底层原理,总结 B+ 树特性、最左匹配原则、覆盖索引与排查慢查询的高效索引设计军规。

EricEric
2024/09/26
夏有木工作室夏有木工作室XymStudio

Professional software engineering partner, crafting high-performance digital products and enterprise solutions.

GitHubGitHubEmail
Built with夏有木工作室夏有木工作室夏有木

川公网安备51012202002478号|蜀ICP备2025156031号-1

Product
  • Xym ERP
Tools
  • WeChat Formatter
Resources
  • Blog
  • Changelog
Company
  • About Us
  • Contact
Legal
  • Cookie Policy
  • Privacy Policy
  • Terms of Service
© 2026 XymStudio All Rights Reserved.