Sa-Token 使用方法及实现原理

3 阅读20分钟

Sa-Token 使用方法及实现原理

目录


一、Sa-Token 简介

1.1 什么是 Sa-Token

Sa-Token 是一个轻量级的 Java 权限认证框架,由国内开源社区 Dromara 孵化,专注于解决:登录认证、权限认证、Session 会话、单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。

核心理念:  让权限认证变得简单、优雅!

1.2 为什么选择 Sa-Token

轻量级:  核心代码仅几百 KB,无侵入式集成,对项目依赖极小

简单易用:  API 设计简洁直观,一行代码完成登录认证

功能完整:  覆盖权限认证的各个场景,从简单到复杂应有尽有

扩展性强:  提供多种扩展接口,可自定义底层实现

生态丰富:  支持多种集成方案,包括 RedisJWT、OAuth2.0 等

1.3 适用场景

  • 中小型项目的权限认证
  • 微服务架构的统一鉴权
  • 前后端分离项目的 Token 管理
  • 单点登录(SSO)系统
  • OAuth2.0 授权服务器
  • 需要灵活控制会话的业务系统

二、核心特性

2.1 功能清单

功能模块说明
登录认证单账号登录、多账号登录、同端互斥登录
权限认证角色认证、权限认证、二级认证
Session 会话全端会话、单端会话、会话查询、会话治理
踢人下线根据账号 id 踢人、根据 Token 踢人
账号封禁指定账号封禁、临时封禁、永久封禁
持久层扩展内存、Redis、自定义存储
分布式会话分布式 Session、Redis 集成
微服务鉴权网关统一鉴权、RPC 调用鉴权
单点登录SSO 模式一、SSO 模式二、SSO 模式三
OAuth2.0授权码模式、隐式模式、密码模式、客户端模式
二级认证在已登录的基础上再次认证
Basic 认证Http Basic 认证
独立 Redis使用独立的 Redis,与业务 Redis 分离
Token 风格定制uuid、简单 uuid、雪花算法、JWT
记住我模式延长 Token 有效期
密码加密提供密码加密模块,多种加密算法
会话治理查询所有会话、强制下线、活跃度统计
全局侦听器监听用户登录、注销、被踢下线、被封禁等行为
开箱即用零配置启动框架,所有特性都有默认实现

2.2 技术架构

┌─────────────────────────────────────────────────────┐
│                   应用层(业务代码)                   │
├─────────────────────────────────────────────────────┤
│              Sa-Token API 层(StpUtil)             │
├─────────────────────────────────────────────────────┤
│           Sa-Token 核心层(StpLogic)               │
├─────────────────────────────────────────────────────┤
│         Sa-Token 接口层(StpInterface 等)           │
├─────────────────────────────────────────────────────┤
│    持久化层(SaTokenDao - Redis/内存/自定义)        │
└─────────────────────────────────────────────────────┘

三、快速开始

3.1 环境准备

3.2 Maven 依赖

基础依赖(Spring Boot 2.x)

<!-- Sa-Token 核心依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.44.0</version>
</dependency>

Spring Boot 3.x(Jakarta EE)

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.44.0</version>
</dependency>

Redis 集成(推荐生产环境使用)

<!-- Sa-Token 整合 Redis(使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.44.0</version>
</dependency>

<!-- Redis 连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

3.3 配置文件

在 application.yml 中配置 Sa-Token:

# Sa-Token 配置
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期,单位秒,-1 代表永不过期
  timeout: 2592000
  # 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: false
  # 是否从 cookie 中读取 token
  is-read-cookie: true
  # 是否从 header 中读取 token
  is-read-header: true
  # token 前缀
  token-prefix: Bearer
  # 是否在初始化配置时打印版本字符画
  is-print: true

# Redis 配置(如果使用 Redis 存储)
spring:
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 10s
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

3.4 第一个示例 - 登录认证

3.4.1 创建登录控制器

@RestController
@RequestMapping("/auth")
public class AuthController {

    /**
     * 用户登录
     * @param username 用户名
     * @param password 密码
     * @return 登录结果
     */
    @PostMapping("/login")
    public Result login(@RequestParam String username, @RequestParam String password) {
        // 1. 验证用户名密码(此处仅示例,实际应查询数据库)
        if ("admin".equals(username) && "123456".equals(password)) {
            // 2. 登录成功,为用户创建 Session 并生成 Token
            StpUtil.login(10001);  // 10001 是用户 id

            // 3. 获取 Token 返回给前端
            String token = StpUtil.getTokenValue();

            return Result.success("登录成功", token);
        }

        return Result.error("用户名或密码错误");
    }

    /**
     * 查询登录状态
     */
    @GetMapping("/isLogin")
    public Result isLogin() {
        boolean isLogin = StpUtil.isLogin();
        return Result.success("是否登录:" + isLogin);
    }

    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public Result logout() {
        StpUtil.logout();
        return Result.success("退出成功");
    }

    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/userInfo")
    public Result getUserInfo() {
        // 检查登录状态,未登录会抛出异常
        StpUtil.checkLogin();

        // 获取当前登录用户 id
        long userId = StpUtil.getLoginIdAsLong();

        // 根据 userId 查询用户信息(此处省略)
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("userId", userId);
        userInfo.put("username", "admin");

        return Result.success(userInfo);
    }
}

3.4.2 前端调用示例

// 登录
axios.post('/auth/login', {
    username: 'admin',
    password: '123456'
}).then(res => {
    // 保存 Token 到本地存储
    localStorage.setItem('satoken', res.data.data);
    console.log('登录成功');
});

// 后续请求携带 Token
axios.get('/auth/userInfo', {
    headers: {
        'satoken': localStorage.getItem('satoken')
    }
}).then(res => {
    console.log('用户信息:', res.data);
});

四、核心功能详解

4.1 登录认证

4.1.1 基本登录

// 会话登录,参数可以是任意类型,建议使用用户 id
StpUtil.login(10001);

// 获取当前会话是否已登录
boolean isLogin = StpUtil.isLogin();

// 检查当前会话是否已登录,未登录则抛出异常
StpUtil.checkLogin();

// 获取当前会话登录 id
Object loginId = StpUtil.getLoginId();
long loginIdAsLong = StpUtil.getLoginIdAsLong();
String loginIdAsString = StpUtil.getLoginIdAsString();
int loginIdAsInt = StpUtil.getLoginIdAsInt();

// 获取当前会话的 Token 值
String tokenValue = StpUtil.getTokenValue();

// 获取当前会话的 Token 信息
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();

// 会话注销
StpUtil.logout();

// 指定账号注销(踢人下线)
StpUtil.logout(10001);

// 强制指定账号下线
StpUtil.kickout(10001);

4.1.2 指定设备登录

Sa-Token 支持同一账号在不同设备上登录:

// 指定设备标识登录
StpUtil.login(10001, "PC");      // PC 端登录
StpUtil.login(10001, "APP");     // APP 端登录
StpUtil.login(10001, "WAP");     // H5 端登录

// 查询指定设备是否登录
StpUtil.isLogin("PC");

// 踢出指定设备
StpUtil.logout(10001, "PC");

// 查询指定账号在所有设备的登录情况
List<String> list = StpUtil.getTokenValueListByLoginId(10001);

4.1.3 记住我模式

// 登录时设置记住我,7 天免登录
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));

// 或者使用快捷方法
StpUtil.login(10001, true);  // true 表示开启记住我

4.1.4 自定义 Token 生成策略

// 登录时自定义 Token
StpUtil.login(10001, new SaLoginModel()
    .setToken("自定义Token值")  // 自定义此次登录的 Token 值
    .setTimeout(60 * 60 * 24)   // 设置有效期 24 小时
    .setIsLastingCookie(true)   // 设置为持久化 Cookie
);

4.2 权限认证

Sa-Token 的权限认证模型基于 RBAC(Role-Based Access Control)思想。

4.2.1 实现权限接口

首先需要实现 StpInterface 接口,告诉框架当前用户拥有哪些权限:

@Component
public class StpInterfaceImpl implements StpInterface {

    @Autowired
    private UserService userService;

    /**
     * 返回指定账号所拥有的权限列表
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 根据 loginId 查询用户拥有的权限列表
        List<String> permissions = userService.getPermissionsByUserId(Long.parseLong(loginId.toString()));
        return permissions;
    }

    /**
     * 返回指定账号所拥有的角色列表
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 根据 loginId 查询用户拥有的角色列表
        List<String> roles = userService.getRolesByUserId(Long.parseLong(loginId.toString()));
        return roles;
    }
}

4.2.2 权限认证示例

// 判断当前账号是否含有指定权限
StpUtil.hasPermission("user:add");

// 校验当前账号是否含有指定权限,没有则抛出异常
StpUtil.checkPermission("user:delete");

// 校验当前账号是否含有指定权限(支持多个,只要有一个满足即可)
StpUtil.checkPermissionOr("user:add", "user:update", "user:delete");

// 校验当前账号是否含有指定权限(支持多个,必须全部满足)
StpUtil.checkPermissionAnd("user:add", "user:update");

// 判断当前账号是否含有指定角色
StpUtil.hasRole("admin");

// 校验当前账号是否含有指定角色,没有则抛出异常
StpUtil.checkRole("admin");

// 校验当前账号是否含有指定角色(支持多个,只要有一个满足即可)
StpUtil.checkRoleOr("admin", "super-admin");

// 校验当前账号是否含有指定角色(支持多个,必须全部满足)
StpUtil.checkRoleAnd("admin", "super-admin");

4.2.3 使用注解鉴权

Sa-Token 提供了丰富的注解来简化权限校验:

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 需要登录才能访问
     */
    @SaCheckLogin
    @GetMapping("/info")
    public Result getUserInfo() {
        return Result.success("用户信息");
    }

    /**
     * 需要 user:add 权限才能访问
     */
    @SaCheckPermission("user:add")
    @PostMapping("/add")
    public Result addUser(@RequestBody User user) {
        return Result.success("添加成功");
    }

    /**
     * 需要 user:update 或 user:delete 权限才能访问(满足其一即可)
     */
    @SaCheckPermission(value = {"user:update", "user:delete"}, mode = SaMode.OR)
    @PostMapping("/update")
    public Result updateUser(@RequestBody User user) {
        return Result.success("更新成功");
    }

    /**
     * 需要 admin 角色才能访问
     */
    @SaCheckRole("admin")
    @DeleteMapping("/delete/{id}")
    public Result deleteUser(@PathVariable Long id) {
        return Result.success("删除成功");
    }

    /**
     * 需要 admin 和 super-admin 角色才能访问(必须同时拥有)
     */
    @SaCheckRole(value = {"admin", "super-admin"}, mode = SaMode.AND)
    @PostMapping("/superOperation")
    public Result superOperation() {
        return Result.success("操作成功");
    }
}

4.2.4 权限通配符

Sa-Token 支持权限通配符匹配:

// 用户拥有权限:user:*
// 可以匹配:user:adduser:deleteuser:update// 用户拥有权限:*:delete
// 可以匹配:user:deleteorder:delete、product:delete// 在配置时开启通配符匹配
sa-token:
  check-id-token: true

4.3 Session 会话

4.3.1 User-Session(用户级会话)

// 获取当前登录用户的 Session
SaSession session = StpUtil.getSession();

// 在 Session 中存取数据
session.set("name", "张三");
session.set("age", 18);

String name = session.get("name", "");
int age = session.get("age", 0);

// 使用快捷方法
StpUtil.getSession().set("name", "张三");
String name = StpUtil.getSession().get("name", "");

// 获取指定用户的 Session(无需登录)
SaSession session = StpUtil.getSessionByLoginId(10001);

4.3.2 Token-Session(令牌级会话)

// 获取当前 Token 的 Token-Session
SaSession tokenSession = StpUtil.getTokenSession();

// 存取数据
tokenSession.set("device", "PC");
String device = tokenSession.get("device", "");

4.3.3 Custom-Session(自定义会话)

// 创建自定义 Session
SaSession session = SaSessionCustomUtil.getSessionById("order-" + orderId);
session.set("status", "已支付");

4.4 踢人下线

// 踢掉指定账号(此账号在所有设备上的会话都会被清除)
StpUtil.kickout(10001);

// 踢掉指定账号在指定设备上的登录
StpUtil.kickout(10001, "PC");

// 踢掉指定 Token
StpUtil.kickoutByTokenValue("token值");

4.5 账号封禁

// 封禁指定账号,时长为 86400 秒(1 天)
StpUtil.disable(10001, 86400);

// 永久封禁指定账号
StpUtil.disable(10001, -1);

// 解除封禁
StpUtil.untieDisable(10001);

// 查询指定账号是否被封禁
boolean isDisable = StpUtil.isDisable(10001);

// 获取指定账号剩余封禁时间(秒)
long time = StpUtil.getDisableTime(10001);

// 在登录前检查此账号是否被封禁,被封禁则抛出异常
StpUtil.checkDisable(10001);

4.6 二级认证

某些敏感操作需要在已登录的基础上再次认证:

// 在需要二级认证的接口,进行二级认证检查
StpUtil.checkSafe();

// 开启二级认证(有效期为 120 秒)
StpUtil.openSafe(120);

// 获取二级认证剩余有效时间(秒)
long time = StpUtil.getSafeTime();

// 检查当前会话是否已完成二级认证
boolean isSafe = StpUtil.isSafe();

// 关闭二级认证
StpUtil.closeSafe();

示例场景:

@RestController
@RequestMapping("/account")
public class AccountController {

    /**
     * 修改密码 - 需要二级认证
     */
    @SaCheckSafe
    @PostMapping("/changePassword")
    public Result changePassword(@RequestParam String oldPwd, @RequestParam String newPwd) {
        // 执行修改密码逻辑
        return Result.success("密码修改成功");
    }

    /**
     * 输入密码,开启二级认证
     */
    @PostMapping("/openSafe")
    public Result openSafe(@RequestParam String password) {
        // 验证密码
        if (checkPassword(password)) {
            // 开启二级认证,120 秒有效
            StpUtil.openSafe(120);
            return Result.success("二级认证开启成功");
        }
        return Result.error("密码错误");
    }
}

五、实现原理深度解析

5.1 核心架构设计

5.1.1 整体架构

Sa-Token 采用分层架构设计:

应用层 (Controller/Service)
    ↓
StpUtil (工具类门面层)
    ↓
StpLogic (核心逻辑层)
    ↓
SaTokenDao (数据持久层接口)
    ↓
具体实现 (SaTokenDaoDefaultImpl/SaTokenDaoRedis等)

5.1.2 核心类说明

类名说明
StpUtil工具类门面,提供静态方法供业务层调用
StpLogic核心逻辑实现,所有的鉴权逻辑都在这里
SaTokenDao数据持久层接口,定义了数据存取规范
SaTokenConfig配置类,负责读取和存储配置信息
SaSessionSession 模型,封装会话数据
SaTokenInfoToken 信息封装类
StpInterface权限接口,业务层需实现此接口提供权限数据
SaStrategy策略类,提供各种自定义策略

5.2 登录认证原理

5.2.1 登录流程

// 用户调用登录方法
StpUtil.login(10001);

// 内部执行流程:
public void login(Object id) {
    // 1. 生成 Token
    String tokenValue = createToken(id);

    // 2. 将 Token 与账号 id 的映射关系保存到持久层
    // Key: satoken:login:token:${tokenValue}
    // Value: ${loginId}
    setTokenValue(tokenValue, id);

    // 3. 将账号 id 与 Token 的映射关系保存到持久层(支持多设备登录)
    // Key: satoken:login:session:${loginId}
    // Value: [token1, token2, ...]
    setLoginIdToToken(id, tokenValue);

    // 4. 将 Token 写入到 Cookie/Header 返回给前端
    setTokenToCookie(tokenValue);

    // 5. 触发登录监听器
    SaTokenListener.onLogin(id);
}

5.2.2 Token 生成策略

Sa-Token 支持多种 Token 生成策略:

public interface SaTokenGenerate {
    /**
     * 生成 Token
     */
    String createToken(Object loginId, String loginType);
}

默认实现:

// UUID 风格
public String createToken(Object loginId, String loginType) {
    return UUID.randomUUID().toString();
}

// Simple-UUID 风格(去掉中划线)
public String createToken(Object loginId, String loginType) {
    return UUID.randomUUID().toString().replaceAll("-", "");
}

// 雪花算法
public String createToken(Object loginId, String loginType) {
    return String.valueOf(snowflakeIdWorker.nextId());
}

// JWT 风格
public String createToken(Object loginId, String loginType) {
    return JWTUtil.createToken(loginId, loginType);
}

5.2.3 身份验证流程

// 用户调用验证方法
StpUtil.checkLogin();

// 内部执行流程:
public void checkLogin() {
    // 1. 从请求中获取 Token(优先级:Header > Cookie > Parameter)
    String tokenValue = getTokenValue();

    // 2. 如果 Token 为空,抛出未登录异常
    if(tokenValue == null || tokenValue.equals("")) {
        throw new NotLoginException();
    }

    // 3. 根据 Token 从持久层获取 loginId
    Object loginId = getLoginIdByToken(tokenValue);

    // 4. 如果 loginId 为空,说明 Token 已过期或无效
    if(loginId == null) {
        throw new NotLoginException();
    }

    // 5. 验证通过,刷新 Token 活跃时间
    updateLastActiveTime(tokenValue);

    // 6. 返回 loginId
    return loginId;
}

5.3 权限认证原理

5.3.1 权限数据模型

Sa-Token 的权限模型:

用户 (User)
    ↓ 1:N
角色 (Role)
    ↓ 1:N
权限 (Permission)

5.3.2 权限验证流程

// 用户调用权限验证方法
StpUtil.checkPermission("user:add");

// 内部执行流程:
public void checkPermission(String permission) {
    // 1. 获取当前登录用户 id
    Object loginId = getLoginId();

    // 2. 调用业务层实现的 StpInterface 接口,获取此用户的权限列表
    List<String> permissionList = stpInterface.getPermissionList(loginId, loginType);

    // 3. 判断权限列表中是否包含指定权限
    if(!permissionList.contains(permission)) {
        // 4. 如果不包含,抛出无权限异常
        throw new NotPermissionException(permission);
    }

    // 5. 验证通过
}

5.3.3 角色验证流程

// 用户调用角色验证方法
StpUtil.checkRole("admin");

// 内部执行流程(与权限验证类似)
public void checkRole(String role) {
    Object loginId = getLoginId();
    List<String> roleList = stpInterface.getRoleList(loginId, loginType);

    if(!roleList.contains(role)) {
        throw new NotRoleException(role);
    }
}

5.4 Session 原理

5.4.1 Session 模型

Sa-Token 的 Session 是一个分布式会话模型:

public class SaSession implements Serializable {
    // Session 唯一标识
    private String id;

    // Session 中存储的数据
    private Map<String, Object> dataMap = new ConcurrentHashMap<>();

    // Session 创建时间
    private long createTime;

    // Session 最后活跃时间
    private long lastAccessTime;

    // Session 超时时间(-1 表示永不超时)
    private long timeout = -1;
}

5.4.2 Session 存取流程

// 获取 Session
SaSession session = StpUtil.getSession();

// 内部执行流程:
public SaSession getSession() {
    // 1. 获取当前登录用户 id
    Object loginId = getLoginId();

    // 2. 根据 loginId 生成 Session 的 key
    String sessionKey = splicingSessionKey(loginId);  // 例如:satoken:session:10001

    // 3. 从持久层获取 Session
    SaSession session = saTokenDao.getSession(sessionKey);

    // 4. 如果 Session 不存在,创建新的 Session
    if(session == null) {
        session = new SaSession(sessionKey);
        saTokenDao.setSession(sessionKey, session, timeout);
    }

    // 5. 返回 Session
    return session;
}

// 存储数据
session.set("key", "value");
// 内部会调用 saTokenDao.updateSession() 将更新后的 Session 保存到持久层

// 获取数据
Object value = session.get("key");
// 内部从 Session 的 dataMap 中获取数据

5.4.3 Session 持久化

Session 数据通过 SaTokenDao 接口进行持久化:

public interface SaTokenDao {
    /**
     * 获取 Session
     */
    SaSession getSession(String sessionId);

    /**
     * 保存 Session
     */
    void setSession(String sessionId, SaSession session, long timeout);

    /**
     * 更新 Session
     */
    void updateSession(String sessionId);

    /**
     * 删除 Session
     */
    void deleteSession(String sessionId);
}

在 Redis 中的存储结构:

Key: satoken:session:10001
Value: {
    "id": "satoken:session:10001",
    "dataMap": {
        "name": "张三",
        "age": 18
    },
    "createTime": 1609459200000,
    "lastAccessTime": 1609545600000,
    "timeout": 2592000
}
Expire: 2592000 秒

5.5 持久层原理

5.5.1 SaTokenDao 接口

Sa-Token 通过 SaTokenDao 接口定义数据持久化规范:

public interface SaTokenDao {

    // -------------------- String 数据类型操作 --------------------

    /**
     * 获取 Value
     */
    String get(String key);

    /**
     * 写入 Value,并设定存活时间(单位:秒)
     */
    void set(String key, String value, long timeout);

    /**
     * 更新 Value 的存活时间
     */
    void update(String key, long timeout);

    /**
     * 删除 Value
     */
    void delete(String key);

    /**
     * 获取 Value 的剩余存活时间(秒)
     */
    long getTimeout(String key);

    /**
     * 修改 Value 的剩余存活时间(秒)
     */
    void updateTimeout(String key, long timeout);


    // -------------------- Object 数据类型操作 --------------------

    /**
     * 获取 Object
     */
    Object getObject(String key);

    /**
     * 写入 Object,并设定存活时间(单位:秒)
     */
    void setObject(String key, Object object, long timeout);

    /**
     * 更新 Object 的存活时间
     */
    void updateObject(String key, long timeout);

    /**
     * 删除 Object
     */
    void deleteObject(String key);

    /**
     * 获取 Object 的剩余存活时间(秒)
     */
    long getObjectTimeout(String key);

    /**
     * 修改 Object 的剩余存活时间(秒)
     */
    void updateObjectTimeout(String key, long timeout);


    // -------------------- Session 数据类型操作 --------------------

    /**
     * 获取 Session
     */
    SaSession getSession(String sessionId);

    /**
     * 写入 Session
     */
    void setSession(String sessionId, SaSession session, long timeout);

    /**
     * 更新 Session
     */
    void updateSession(String sessionId);

    /**
     * 删除 Session
     */
    void deleteSession(String sessionId);


    // -------------------- 会话管理 --------------------

    /**
     * 搜索数据
     */
    List<String> searchData(String prefix, String keyword, int start, int size);
}

5.5.2 Redis 实现

Sa-Token 提供了基于 Redis 的持久化实现:

@Component
public class SaTokenDaoRedis implements SaTokenDao {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    @Override
    public void set(String key, String value, long timeout) {
        if(timeout == SaTokenDao.NEVER_EXPIRE) {
            // 永不过期
            stringRedisTemplate.opsForValue().set(key, value);
        } else {
            // 指定过期时间
            stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
        }
    }

    @Override
    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    public long getTimeout(String key) {
        Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;
    }

    // ... 其他方法实现
}

5.5.3 数据存储结构

在 Redis 中的数据存储结构:

1. Token → LoginId 映射
Key: satoken:login:token:${tokenValue}
Value: ${loginId}
Expire: ${timeout}

2. LoginId → Token 映射
Key: satoken:login:session:${loginId}
Value: Set<tokenValue>
Expire: ${timeout}

3. User-Session
Key: satoken:session:${loginId}
Value: SaSession 对象(JSON 序列化)
Expire: ${timeout}

4. Token-Session
Key: satoken:token:${tokenValue}
Value: SaSession 对象(JSON 序列化)
Expire: ${timeout}

5. 账号封禁
Key: satoken:disable:${loginId}
Value: ${disableTime}
Expire: ${disableTime}

5.6 拦截器与过滤器

5.6.1 Sa-Token 路由拦截器

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 路由拦截器
        registry.addInterceptor(new SaInterceptor(handle -> {
            // 指定哪些路由需要拦截
            SaRouter.match("/**")
                .notMatch("/auth/login")      // 排除登录接口
                .notMatch("/auth/register")   // 排除注册接口
                .notMatch("/favicon.ico")     // 排除图标
                .check(r -> StpUtil.checkLogin());  // 检查登录状态

            // 权限认证 - 不同角色不同权限
            SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));

        })).addPathPatterns("/**");
    }
}

5.6.2 全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 未登录异常
     */
    @ExceptionHandler(NotLoginException.class)
    public Result handleNotLoginException(NotLoginException e) {
        String message = "";
        if(e.getType().equals(NotLoginException.NOT_TOKEN)) {
            message = "未提供 Token";
        } else if(e.getType().equals(NotLoginException.INVALID_TOKEN)) {
            message = "Token 无效";
        } else if(e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
            message = "Token 已过期";
        } else if(e.getType().equals(NotLoginException.BE_REPLACED)) {
            message = "Token 已被顶下线";
        } else if(e.getType().equals(NotLoginException.KICK_OUT)) {
            message = "Token 已被踢下线";
        } else {
            message = "当前会话未登录";
        }
        return Result.error(401, message);
    }

    /**
     * 无权限异常
     */
    @ExceptionHandler(NotPermissionException.class)
    public Result handleNotPermissionException(NotPermissionException e) {
        return Result.error(403, "无此权限:" + e.getPermission());
    }

    /**
     * 无角色异常
     */
    @ExceptionHandler(NotRoleException.class)
    public Result handleNotRoleException(NotRoleException e) {
        return Result.error(403, "无此角色:" + e.getRole());
    }

    /**
     * 账号被封禁异常
     */
    @ExceptionHandler(DisableServiceException.class)
    public Result handleDisableServiceException(DisableServiceException e) {
        return Result.error(403, "账号已被封禁,剩余时间:" + e.getDisableTime() + "秒");
    }
}

六、高级特性

6.1 多账号体系认证

Sa-Token 支持在一个项目中同时存在多套账号体系(如:用户账号、管理员账号)

6.1.1 定义多个 StpLogic

// 用户账号体系
@Component
public class StpUserUtil {
    public static final StpLogic stpLogic = new StpLogic("user");

    public static void login(Object id) {
        stpLogic.login(id);
    }

    public static void checkLogin() {
        stpLogic.checkLogin();
    }

    // ... 其他方法
}

// 管理员账号体系
@Component
public class StpAdminUtil {
    public static final StpLogic stpLogic = new StpLogic("admin");

    public static void login(Object id) {
        stpLogic.login(id);
    }

    public static void checkLogin() {
        stpLogic.checkLogin();
    }

    // ... 其他方法
}

6.1.2 使用示例

// 用户登录
StpUserUtil.login(10001);

// 管理员登录
StpAdminUtil.login(20001);

// 用户权限验证
StpUserUtil.checkLogin();

// 管理员权限验证
StpAdminUtil.checkLogin();

6.2 JWT 集成

6.2.1 引入 JWT 依赖

<!-- Sa-Token 整合 JWT -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.44.0</version>
</dependency>

6.2.2 配置 JWT

sa-token:
  # JWT 密钥
  jwt-secret-key: your-secret-key-here
  # Token 风格改为 JWT
  token-style: jwt

6.2.3 JWT 模式

Sa-Token 提供两种 JWT 集成模式:

Simple 模式(推荐):

// 配置
@Configuration
public class SaTokenConfig {
    @Bean
    @Primary
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForSimple();
    }
}

// 使用(与普通模式完全一致)
StpUtil.login(10001);
String token = StpUtil.getTokenValue();

Stateless 模式(无状态):

// 配置
@Configuration
public class SaTokenConfig {
    @Bean
    @Primary
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForStateless();
    }
}

// 使用
StpUtil.login(10001);
String token = StpUtil.getTokenValue();  // JWT Token

6.2.4 自定义 JWT Payload

// 登录时自定义 JWT 载荷
StpUtil.login(10001, SaLoginConfig
    .setExtra("username", "zhangsan")
    .setExtra("email", "zhangsan@example.com")
);

// 获取 JWT 载荷
String username = StpUtil.getExtra("username").toString();

6.3 OAuth2.0 授权

Sa-Token 提供完整的 OAuth2.0 Server 实现。

6.3.1 引入 OAuth2 依赖

<!-- Sa-Token OAuth2.0 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-oauth2</artifactId>
    <version>1.44.0</version>
</dependency>

6.3.2 实现 OAuth2 接口

@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {

    /**
     * 根据 ClientId 获取 Client 信息
     */
    @Override
    public SaClientModel getClientModel(String clientId) {
        // 从数据库查询 Client 信息
        return new SaClientModel()
            .setClientId(clientId)
            .setClientSecret("client-secret")
            .setAllowUrl("*")  // 允许的回调地址
            .setContractScope("userinfo")  // 允许的授权范围
            .setIsAutoMode(true);  // 是否自动授权
    }

    /**
     * 根据 Code 生成 Access Token
     */
    @Override
    public SaTokenInfo generateAccessToken(String code) {
        // 默认实现即可
        return super.generateAccessToken(code);
    }
}

6.3.3 OAuth2 控制器

@RestController
@RequestMapping("/oauth2")
public class OAuth2Controller {

    /**
     * OAuth2 授权入口
     */
    @GetMapping("/authorize")
    public Object authorize(SaRequest req, SaResponse res) {
        return SaOAuth2Util.authorize(req, res);
    }

    /**
     * OAuth2 Token 获取
     */
    @PostMapping("/token")
    public Object token(SaRequest req) {
        return SaOAuth2Util.token(req);
    }

    /**
     * 刷新 Access Token
     */
    @PostMapping("/refresh")
    public Object refresh(SaRequest req) {
        return SaOAuth2Util.refresh(req);
    }

    /**
     * 撤销 Access Token
     */
    @PostMapping("/revoke")
    public Object revoke(SaRequest req) {
        return SaOAuth2Util.revoke(req);
    }

    /**
     * 获取用户信息
     */
    @GetMapping("/userinfo")
    public Object userinfo() {
        // 校验 Access Token
        SaOAuth2Util.checkAccessToken();

        // 获取 loginId
        Object loginId = StpUtil.getLoginId();

        // 返回用户信息
        return Result.success(getUserInfo(loginId));
    }
}

6.4 单点登录(SSO)

Sa-Token 提供三种 SSO 模式。

6.4.1 模式一:共享 Cookie(同域)

适用场景:多个系统部署在同一个主域名下

sa-token:
  # Cookie 的作用域设置为主域名
  cookie-domain: example.com

6.4.2 模式二:URL 重定向(跨域)

适用场景:多个系统部署在不同域名下

认证中心配置:

<!-- Sa-Token SSO 服务端 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-sso-server</artifactId>
    <version>1.44.0</version>
</dependency>
@RestController
public class SsoServerController {

    /**
     * SSO 认证中心首页
     */
    @GetMapping("/sso/auth")
    public Object ssoAuth(SaRequest req, SaResponse res) {
        return SaSsoUtil.ssoAuth(req, res);
    }

    /**
     * SSO 登录页面
     */
    @GetMapping("/sso/doLogin")
    public Object ssoDoLogin(@RequestParam String name, @RequestParam String pwd) {
        // 验证用户名密码
        if("admin".equals(name) && "123456".equals(pwd)) {
            StpUtil.login(10001);
            return SaSsoUtil.ssoDoLogin();
        }
        return "用户名或密码错误";
    }

    /**
     * 校验 Ticket
     */
    @GetMapping("/sso/checkTicket")
    public Object checkTicket(@RequestParam String ticket, @RequestParam String ssoLogoutCall) {
        return SaSsoUtil.checkTicket(ticket, ssoLogoutCall);
    }
}

客户端配置:

<!-- Sa-Token SSO 客户端 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-sso-client</artifactId>
    <version>1.44.0</version>
</dependency>
sa-token:
  sso:
    # SSO 认证中心地址
    auth-url: http://sso.example.com/sso/auth
    # 当前系统的回调地址
    sso-logout-call: http://client1.example.com/sso/logout
@RestController
public class SsoClientController {

    /**
     * SSO 登录回调
     */
    @GetMapping("/sso/login")
    public Object ssoLogin(@RequestParam String ticket) {
        // 使用 Ticket 换取用户信息
        Object loginId = SaSsoUtil.checkTicket(ticket);

        // 登录成功
        StpUtil.login(loginId);

        return "登录成功";
    }

    /**
     * SSO 登出回调
     */
    @GetMapping("/sso/logout")
    public Object ssoLogout(@RequestParam String loginId) {
        // 登出
        StpUtil.logout(loginId);
        return "登出成功";
    }
}

6.4.3 模式三:Http 请求(跨域)

基于 Http 请求获取会话,不使用重定向。

6.5 微服务网关鉴权

6.5.1 Gateway 网关集成

<!-- Sa-Token 整合 Spring Cloud Gateway -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
    <version>1.44.0</version>
</dependency>
@Configuration
public class SaTokenConfig {

    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            // 拦截全部路径
            .addInclude("/**")
            // 排除登录接口
            .addExclude("/auth/login")
            // 鉴权方法
            .setAuth(obj -> {
                // 检查登录状态
                SaRouter.match("/**", r -> StpUtil.checkLogin());

                // 权限认证
                SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
            })
            // 异常处理
            .setError(e -> {
                return SaResult.error(e.getMessage());
            });
    }
}

6.5.2 Dubbo RPC 鉴权

<!-- Sa-Token 整合 Dubbo -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dubbo</artifactId>
    <version>1.44.0</version>
</dependency>
@Component
public class SaTokenDubboFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {
        try {
            // 从上下文获取 Token
            String token = RpcContext.getContext().getAttachment("satoken");

            // 临时设置 Token
            StpUtil.setTokenValue(token);

            // 校验登录
            StpUtil.checkLogin();

            // 执行 RPC 调用
            return invoker.invoke(invocation);

        } finally {
            // 清除临时 Token
            StpUtil.clearTokenValue();
        }
    }
}

6.6 全局监听器

Sa-Token 提供了全局监听器,可以监听用户的各种行为。

@Component
public class SaTokenListener implements SaTokenEventListener {

    /**
     * 每次登录时触发
     */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        System.out.println("用户 " + loginId + " 登录成功,Token:" + tokenValue);
        // 记录登录日志
    }

    /**
     * 每次注销时触发
     */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        System.out.println("用户 " + loginId + " 注销登录,Token:" + tokenValue);
        // 记录注销日志
    }

    /**
     * 每次被踢下线时触发
     */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        System.out.println("用户 " + loginId + " 被踢下线,Token:" + tokenValue);
        // 发送通知
    }

    /**
     * 每次被顶下线时触发
     */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        System.out.println("用户 " + loginId + " 被顶下线,Token:" + tokenValue);
        // 发送通知
    }

    /**
     * 每次被封禁时触发
     */
    @Override
    public void doDisable(String loginType, Object loginId, long disableTime) {
        System.out.println("用户 " + loginId + " 被封禁,封禁时长:" + disableTime + "秒");
        // 发送通知
    }

    /**
     * 每次被解封时触发
     */
    @Override
    public void doUntieDisable(String loginType, Object loginId) {
        System.out.println("用户 " + loginId + " 被解封");
        // 发送通知
    }

    /**
     * 每次创建 Session 时触发
     */
    @Override
    public void doCreateSession(String id) {
        System.out.println("创建 Session:" + id);
    }

    /**
     * 每次注销 Session 时触发
     */
    @Override
    public void doLogoutSession(String id) {
        System.out.println("注销 Session:" + id);
    }
}

七、最佳实践

7.1 前后端分离架构

7.1.1 Token 传递方式

方式一:Header 方式(推荐)

前端:

axios.get('/api/user/info', {
    headers: {
        'satoken': localStorage.getItem('satoken')
    }
});

后端配置:

sa-token:
  # 从 Header 中读取 Token
  is-read-header: true
  # Token 名称
  token-name: satoken

方式二:请求参数方式

前端:

axios.get('/api/user/info?satoken=' + localStorage.getItem('satoken'));

7.1.2 完整示例

前端代码(Vue3):

// api/auth.js
import axios from 'axios';

// 创建 axios 实例
const request = axios.create({
    baseURL: 'http://localhost:8080',
    timeout: 5000
});

// 请求拦截器 - 添加 Token
request.interceptors.request.use(
    config => {
        const token = localStorage.getItem('satoken');
        if (token) {
            config.headers['satoken'] = token;
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

// 响应拦截器 - 处理未登录
request.interceptors.response.use(
    response => {
        return response.data;
    },
    error => {
        if (error.response && error.response.status === 401) {
            // 未登录,跳转到登录页
            localStorage.removeItem('satoken');
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
);

// 登录
export function login(username, password) {
    return request({
        url: '/auth/login',
        method: 'post',
        data: { username, password }
    });
}

// 获取用户信息
export function getUserInfo() {
    return request({
        url: '/auth/userInfo',
        method: 'get'
    });
}

// 退出登录
export function logout() {
    return request({
        url: '/auth/logout',
        method: 'post'
    });
}

登录组件:

<template>
  <div class="login-container">
    <el-form :model="loginForm" :rules="rules" ref="loginFormRef">
      <el-form-item prop="username">
        <el-input v-model="loginForm.username" placeholder="用户名"></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input v-model="loginForm.password" type="password" placeholder="密码"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleLogin">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api/auth';
import { ElMessage } from 'element-plus';

const router = useRouter();
const loginFormRef = ref(null);

const loginForm = ref({
  username: '',
  password: ''
});

const rules = {
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
};

const handleLogin = async () => {
  await loginFormRef.value.validate(async (valid) => {
    if (valid) {
      try {
        const res = await login(loginForm.value.username, loginForm.value.password);
        if (res.code === 200) {
          // 保存 Token
          localStorage.setItem('satoken', res.data);
          ElMessage.success('登录成功');
          // 跳转到首页
          router.push('/');
        } else {
          ElMessage.error(res.message);
        }
      } catch (error) {
        ElMessage.error('登录失败');
      }
    }
  });
};
</script>

后端代码:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public Result login(@RequestBody LoginReq req) {
        // 验证用户名密码
        User user = userService.getUserByUsername(req.getUsername());
        if (user == null) {
            return Result.error("用户不存在");
        }

        // 验证密码(使用 BCrypt)
        if (!BCrypt.checkpw(req.getPassword(), user.getPassword())) {
            return Result.error("密码错误");
        }

        // 检查账号是否被封禁
        StpUtil.checkDisable(user.getId());

        // 登录
        StpUtil.login(user.getId(), new SaLoginModel()
            .setDevice(req.getDevice())  // 设备类型
            .setTimeout(60 * 60 * 24 * 7)  // 7 天有效期
        );

        // 将用户信息存入 Session
        SaSession session = StpUtil.getSession();
        session.set("username", user.getUsername());
        session.set("nickname", user.getNickname());
        session.set("avatar", user.getAvatar());

        // 返回 Token
        return Result.success(StpUtil.getTokenValue());
    }

    @GetMapping("/userInfo")
    public Result getUserInfo() {
        // 检查登录
        StpUtil.checkLogin();

        // 从 Session 获取用户信息
        SaSession session = StpUtil.getSession();
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("userId", StpUtil.getLoginIdAsLong());
        userInfo.put("username", session.get("username"));
        userInfo.put("nickname", session.get("nickname"));
        userInfo.put("avatar", session.get("avatar"));

        return Result.success(userInfo);
    }

    @PostMapping("/logout")
    public Result logout() {
        StpUtil.logout();
        return Result.success("退出成功");
    }
}

7.2 生产环境配置

7.2.1 Redis 集群配置

spring:
  redis:
    # Redis 集群配置
    cluster:
      nodes:
        - 192.168.1.101:6379
        - 192.168.1.102:6379
        - 192.168.1.103:6379
      max-redirects: 3
    password: your-redis-password
    lettuce:
      pool:
        max-active: 200
        max-idle: 50
        min-idle: 10
        max-wait: -1ms

7.2.2 安全配置

sa-token:
  # Token 有效期(7 天)
  timeout: 604800
  # Token 最低活跃时间(1 小时无操作则需要重新登录)
  active-timeout: 3600
  # 不允许同一账号并发登录
  is-concurrent: false
  # 每次登录不共用 Token
  is-share: false
  # 使用安全的 Token 生成策略
  token-style: random-128
  # JWT 密钥(请使用强密码)
  jwt-secret-key: your-very-strong-secret-key-here
  # 开启日志
  is-log: true

7.2.3 性能优化

缓存权限数据:

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 获取用户权限列表(带缓存)
     */
    @Cacheable(value = "user:permissions", key = "#userId")
    public List<String> getPermissionsByUserId(Long userId) {
        return userMapper.selectPermissionsByUserId(userId);
    }

    /**
     * 获取用户角色列表(带缓存)
     */
    @Cacheable(value = "user:roles", key = "#userId")
    public List<String> getRolesByUserId(Long userId) {
        return userMapper.selectRolesByUserId(userId);
    }
}

批量验证:

// 批量踢下线
public void kickoutBatch(List<Long> userIds) {
    for (Long userId : userIds) {
        StpUtil.kickout(userId);
    }
}

// 批量封禁
public void disableBatch(List<Long> userIds, long disableTime) {
    for (Long userId : userIds) {
        StpUtil.disable(userId, disableTime);
    }
}

7.3 安全建议

7.3.1 密码加密

@Service
public class UserService {

    /**
     * 用户注册
     */
    public void register(String username, String password) {
        // 使用 BCrypt 加密密码
        String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));

        // 保存用户
        User user = new User();
        user.setUsername(username);
        user.setPassword(hashedPassword);
        userMapper.insert(user);
    }

    /**
     * 验证密码
     */
    public boolean checkPassword(String rawPassword, String hashedPassword) {
        return BCrypt.checkpw(rawPassword, hashedPassword);
    }
}

7.3.2 防止暴力破解

@Component
public class LoginAttemptService {

    private static final int MAX_ATTEMPT = 5;
    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        attemptsCache = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build(new CacheLoader<String, Integer>() {
                @Override
                public Integer load(String key) {
                    return 0;
                }
            });
    }

    /**
     * 登录成功,清除失败次数
     */
    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    /**
     * 登录失败,增加失败次数
     */
    public void loginFailed(String key) {
        int attempts = attemptsCache.getUnchecked(key);
        attempts++;
        attemptsCache.put(key, attempts);
    }

    /**
     * 检查是否被锁定
     */
    public boolean isBlocked(String key) {
        return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT;
    }
}

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private LoginAttemptService loginAttemptService;

    @PostMapping("/login")
    public Result login(@RequestBody LoginReq req, HttpServletRequest request) {
        String ip = getClientIP(request);

        // 检查是否被锁定
        if (loginAttemptService.isBlocked(ip)) {
            return Result.error("登录失败次数过多,请 1 小时后再试");
        }

        // 验证用户名密码
        User user = userService.getUserByUsername(req.getUsername());
        if (user == null || !userService.checkPassword(req.getPassword(), user.getPassword())) {
            // 记录登录失败
            loginAttemptService.loginFailed(ip);
            return Result.error("用户名或密码错误");
        }

        // 登录成功
        loginAttemptService.loginSucceeded(ip);
        StpUtil.login(user.getId());

        return Result.success(StpUtil.getTokenValue());
    }
}

7.3.3 防止 CSRF 攻击

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    @Bean
    public SaInterceptor getSaInterceptor() {
        return new SaInterceptor(handle -> {
            // 开启 CSRF 防护
            SaRouter.match("/**")
                .notMatch("/auth/login")
                .check(r -> {
                    // 检查 CSRF Token
                    SaCsrfUtil.checkToken();
                });
        });
    }
}

八、总结

8.1 核心优势

  1. 轻量级:核心代码仅几百 KB,依赖少,性能高
  2. 易上手:API 简洁直观,文档详细,学习成本低
  3. 功能全:覆盖权限认证的各个场景
  4. 扩展性强:提供多种扩展点,灵活定制
  5. 生态丰富:支持多种集成方案

8.2 适用场景

  • 中小型项目快速开发
  • 微服务架构的统一鉴权
  • 前后端分离项目
  • 单点登录系统
  • OAuth2.0 授权服务

8.3 学习资源

8.4 社区支持

Sa-Token 拥有活跃的社区,提供:

  • QQ 交流群
  • 微信交流群
  • Issue 提问
  • PR 贡献

附录

A. 常见问题

Q1:Sa-Token 和 JWT 有什么区别?

A:JWT 是一种 Token 生成标准,Sa-Token 是一个权限认证框架。Sa-Token 可以使用 JWT 作为 Token 生成策略,也可以使用其他策略。

Q2:Sa-Token 支持分布式环境吗?

A:支持。通过 Redis 等分布式存储,Sa-Token 可以在分布式环境下使用。

Q3:如何实现单设备登录?

A:设置 is-concurrent: false 即可实现同一账号只能在一个设备登录。

Q4:如何自定义 Token 有效期?

A:在登录时使用 SaLoginModel 指定有效期:

StpUtil.login(10001, new SaLoginModel().setTimeout(3600));

Q5:如何实现记住我功能?

A:设置较长的 Token 有效期,并使用持久化 Cookie:

StpUtil.login(10001, new SaLoginModel()
    .setTimeout(60 * 60 * 24 * 7)  // 7 天
    .setIsLastingCookie(true)  // 持久化 Cookie
);