轻量级权限认证框架:Sa-Token

正如官方文档所说的,Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

引言

在现代 Web 应用和微服务架构中,权限认证 是系统安全的第一道防线。一个优秀的权限认证框架不仅需要提供全面的安全保障,还应该具备简洁易用的 API 设计和良好的扩展性。Sa-Token 作为一款轻量级 Java 权限认证框架,以其 "简单、优雅" 的设计理念,彻底改变了 Java 开发者处理权限认证的方式

本文将深入解析 Sa-Token 中的核心技术,从最基础的登录认证到复杂的微服务网关鉴权,通过大量详实的代码案例,帮助读者全面掌握 Sa-Token 的使用方法和最佳实践。无论你是刚接触权限认证的新手,还是希望提升系统安全性的资深开发者,都能从本文中获得有价值的技术知识。

一、Sa-Token 快速入门

1.1 环境准备

Sa-Token 支持几乎所有主流的 Java 开发环境,包括:

  • JDK 8+(推荐 JDK 17 及以上)
  • Spring Boot 2.x/3.x/4.x
  • Spring MVC、WebFlux、Solon 等 Web 框架

1.2 Spring Boot 集成

Sa-Token 支持在多种环境下进行集成,这里以 SpringBoot 框架为例,其他的框架集成可以看官方文档详情。

在 Spring Boot 项目中集成 Sa-Token 非常简单,只需要在 pom.xml中添加一个依赖:

XML 复制代码
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.45.0</version>
</dependency>

你可以零配置启动项目 ,但同时你也可以在 application.yml 中增加如下配置,定制性使用框架:

bash 复制代码
server:
    port: 8085
    
sa-token: 
    # token 名称(同时也是 cookie 名称)
    token-name: satoken
    # token 有效期(单位:秒) 默认30天,-1 代表永久有效
    timeout: 2592000
    # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
    active-timeout: -1
    # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
    is-concurrent: true
    # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
    is-share: false
    # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
    token-style: uuid
    # 是否输出操作日志 
    is-log: true

1.3 第一个示例:登录认证

让我们通过一个最简单的示例来体验 Sa-Token 的强大之处。创建一个 LoginController,实现基础的登录和登出功能:

java 复制代码
@RestController
@RequestMapping("/auth")
public class LoginController {

    /**
     * 登录接口
     * @param username 用户名
     * @param password 密码
     * @return 登录结果
     */
    @PostMapping("/login")
    public SaResult login(String username, String password) {
        // 实际项目中应从数据库查询用户信息进行校验
        if ("admin".equals(username) && "123456".equals(password)) {
            // 登录成功,将用户ID写入会话
            StpUtil.login(10001);
            // 返回登录信息
            return SaResult.ok("登录成功")
                    .set("token", StpUtil.getTokenValue())
                    .set("userId", StpUtil.getLoginId());
        }
        return SaResult.error("用户名或密码错误");
    }

    /**
     * 登出接口
     * @return 登出结果
     */
    @PostMapping("/logout")
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok("登出成功");
    }

    /**
     * 获取当前登录用户信息
     * @return 用户信息
     */
    @GetMapping("/info")
    public SaResult getUserInfo() {
        // 校验登录状态
        StpUtil.checkLogin();
        // 返回用户信息
        return SaResult.ok()
                .set("userId", StpUtil.getLoginId())
                .set("token", StpUtil.getTokenValue())
                .set("isLogin", StpUtil.isLogin());
    }

    /**
     * 测试是否登录
     * @return 登录状态
     */
    @GetMapping("/is-login")
    public SaResult isLogin() {
        return SaResult.ok()
                .set("isLogin", StpUtil.isLogin());
    }
}

启动项目后,我们可以通过以下步骤测试功能:

  1. 调用 POST /auth/login?username=admin&password=123456,获取登录 Token
  2. 在请求头中携带Token(格式:Authorization: Bearer {token}),调用 GET /auth/info,成功获取用户信息
  3. 调用 POST /auth/logout,登出当前用户
  4. 再次调用 GET /auth/info,会抛出NotLoginException异常

仅仅几行代码,我们就完成了完整的登录认证流程,这就是 Sa-Token 的魅力所在。

二、登录认证核心技术

登录认证是权限系统的基础,Sa-Token 提供了丰富的登录认证功能,满足各种复杂的业务需求。

2.1 基础登录

StpUtil.login() 方法是 Sa-Token 登录认证的核心,它接受一个登录 ID 作为参数,这个 ID 可以是用户的主键、用户名或其他唯一标识。

java 复制代码
// 基础登录
StpUtil.login(10001);

// 登录并指定设备类型(用于同端互斥登录)
StpUtil.login(10001, "PC");
StpUtil.login(10001, "APP");
StpUtil.login(10001, "MINI_PROGRAM");

// 登录并指定是否记住我
StpUtil.login(10001, true); // 记住我,有效期7天
StpUtil.login(10001, false); // 不记住我,浏览器关闭即失效

// 登录并指定设备类型和是否记住我
StpUtil.login(10001, "PC", true);

2.2 登录状态校验

Sa-Token 提供了多种方法来校验登录状态:

java 复制代码
// 校验当前客户端是否已登录,未登录则抛出NotLoginException异常
StpUtil.checkLogin();

// 判断当前客户端是否已登录,返回boolean值
boolean isLogin = StpUtil.isLogin();

// 获取当前登录用户的ID,如果未登录则抛出异常
Object loginId = StpUtil.getLoginId();

// 获取当前登录用户的ID,转换为String类型
String loginIdStr = StpUtil.getLoginIdAsString();

// 获取当前登录用户的ID,转换为int类型
int loginIdInt = StpUtil.getLoginIdAsInt();

// 获取当前登录用户的ID,转换为long类型
long loginIdLong = StpUtil.getLoginIdAsLong();

// 获取当前登录用户的ID,如果未登录则返回默认值
Object loginIdDefault = StpUtil.getLoginIdDefault(null);

2.3 登出操作

Sa-Token 提供了多种登出方式:

java 复制代码
// 登出当前客户端
StpUtil.logout();

// 登出指定用户的所有客户端
StpUtil.logout(10001);

// 登出指定用户的指定设备类型
StpUtil.logout(10001, "PC");

// 登出指定Token对应的客户端
StpUtil.logoutByTokenValue("token-xxxxxx");

2.4 Token 操作

Token 是 Sa-Token 身份认证的核心,Sa-Token 提供了丰富的 Token 操作方法:

java 复制代码
// 获取当前登录用户的Token
String token = StpUtil.getTokenValue();

// 获取当前登录用户的Token信息(包含Token值、有效期等)
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println("Token值:" + tokenInfo.getTokenValue());
System.out.println("Token有效期:" + tokenInfo.getTokenTimeout());
System.out.println("Token创建时间:" + tokenInfo.getTokenCreateTime());
System.out.println("用户ID:" + tokenInfo.getLoginId());
System.out.println("设备类型:" + tokenInfo.getDeviceType());

// 判断指定Token是否有效
boolean isValid = StpUtil.isValidToken("token-xxxxxx");

// 刷新Token的有效期
StpUtil.renewTimeout();

// 获取指定用户的Token列表
List<String> tokenList = StpUtil.getTokenListByLoginId(10001);

2.5 同端互斥登录

同端互斥登录是指同一账号在同一设备类型上只能登录一个客户端,例如一个账号不能同时在两个手机上登录。Sa-Token 提供了非常简单的实现方式:

bash 复制代码
// 登录时指定设备类型
StpUtil.login(10001, "APP");

// 配置文件中开启同端互斥登录
// application.yml
sa-token:
  is-concurrent: false # 禁止同一账号多端登录
  is-share: false # 不同设备类型不共享Token

当同一账号在同一设备类型上再次登录时,之前登录的客户端会被自动踢下线。

2.6 记住我模式

记住我模式允许用户在关闭浏览器后再次打开网站时无需重新登录。Sa-Token 默认支持记住我模式,有效期为 7 天:

bash 复制代码
// 登录时开启记住我模式
StpUtil.login(10001, true);

// 配置文件中修改记住我有效期
// application.yml
sa-token:
  timeout: 2592000 # Token默认有效期,单位秒(30天)
  remember-me-timeout: 604800 # 记住我模式有效期,单位秒(7天)

2.7 自动续签

Sa-Token 提供了两种 Token 过期策略,并支持自动续签:

bash 复制代码
// 配置文件中开启自动续签
// application.yml
sa-token:
  auto-renew: true # 开启自动续签
  timeout: 1800 # Token有效期,单位秒(30分钟)
  activity-timeout: 1800 # 活跃超时时间,单位秒(30分钟)

当 auto-renew 设置为 true 时,每次用户操作都会自动刷新 Token 的有效期。如果用户在activity-timeout 时间内没有任何操作,Token 会自动失效。

三、权限认证核心技术

权限认证是在登录认证的基础上,进一步控制用户对系统资源的访问权限。Sa-Token 提供了多种权限认证方式,包括代码鉴权注解鉴权路由拦截鉴权

3.1 权限认证基础

Sa-Token 的权限认证基于 "用户 - 角色 - 权限" 模型(RBAC 模型),每个用户可以拥有多个角色,每个角色可以拥有多个权限。

首先,我们需要实现 StpInterface 接口,告诉 Sa-Token 某个用户拥有哪些权限和角色:

java 复制代码
@Component
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回指定用户拥有的权限列表
     * @param loginId 登录ID
     * @param loginType 登录类型
     * @return 权限列表
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 实际项目中应从数据库查询用户的权限
        List<String> permissionList = new ArrayList<>();
        
        // 假设用户ID为10001的是管理员,拥有所有权限
        if (loginId.equals(10001)) {
            permissionList.add("user:add");
            permissionList.add("user:delete");
            permissionList.add("user:update");
            permissionList.add("user:query");
            permissionList.add("admin:system");
        } 
        // 假设用户ID为10002的是普通用户,只有查询权限
        else if (loginId.equals(10002)) {
            permissionList.add("user:query");
        }
        
        return permissionList;
    }

    /**
     * 返回指定用户拥有的角色列表
     * @param loginId 登录ID
     * @param loginType 登录类型
     * @return 角色列表
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 实际项目中应从数据库查询用户的角色
        List<String> roleList = new ArrayList<>();
        
        if (loginId.equals(10001)) {
            roleList.add("admin");
            roleList.add("super-admin");
        } 
        else if (loginId.equals(10002)) {
            roleList.add("user");
        }
        
        return roleList;
    }
}

3.2 代码鉴权

代码鉴权是最基础的权限认证方式,通过调用 StpUtil的静态方法来校验权限:

java 复制代码
// 校验当前用户是否拥有指定权限,没有则抛出NotPermissionException异常
StpUtil.checkPermission("user:add");

// 校验当前用户是否拥有指定权限,返回boolean值
boolean hasPermission = StpUtil.hasPermission("user:add");

// 校验当前用户是否拥有指定权限中的任意一个
StpUtil.checkPermissionOr("user:add", "user:update");

// 校验当前用户是否拥有指定权限中的所有
StpUtil.checkPermissionAnd("user:add", "user:delete");

// 校验当前用户是否拥有指定角色,没有则抛出NotRoleException异常
StpUtil.checkRole("admin");

// 校验当前用户是否拥有指定角色,返回boolean值
boolean hasRole = StpUtil.hasRole("admin");

// 校验当前用户是否拥有指定角色中的任意一个
StpUtil.checkRoleOr("admin", "super-admin");

// 校验当前用户是否拥有指定角色中的所有
StpUtil.checkRoleAnd("admin", "user");

3.3 注解鉴权

注解鉴权是最优雅的权限认证方式,通过在方法上添加注解来控制访问权限:

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 只有登录后才能访问
     */
    @SaCheckLogin
    @GetMapping("/list")
    public SaResult getUserList() {
        return SaResult.ok("获取用户列表成功");
    }

    /**
     * 只有拥有"user:add"权限的用户才能访问
     */
    @SaCheckPermission("user:add")
    @GetMapping("/add")
    public SaResult addUser() {
        return SaResult.ok("添加用户成功");
    }

    /**
     * 只有拥有"user:delete"权限的用户才能访问
     */
    @SaCheckPermission("user:delete")
    @GetMapping("/delete")
    public SaResult deleteUser() {
        return SaResult.ok("删除用户成功");
    }

    /**
     * 只有拥有"admin"角色的用户才能访问
     */
    @SaCheckRole("admin")
    @GetMapping("/admin")
    public SaResult adminPage() {
        return SaResult.ok("管理员页面");
    }

    /**
     * 拥有"admin"或"super-admin"角色的用户都能访问
     */
    @SaCheckRole(or = {"admin", "super-admin"})
    @GetMapping("/super-admin")
    public SaResult superAdminPage() {
        return SaResult.ok("超级管理员页面");
    }

    /**
     * 必须同时拥有"user:add"和"user:delete"权限才能访问
     */
    @SaCheckPermission(and = {"user:add", "user:delete"})
    @GetMapping("/manage")
    public SaResult userManage() {
        return SaResult.ok("用户管理页面");
    }
}

3.4 路由拦截鉴权

路由拦截鉴权是通过拦截器来统一控制路由的访问权限,适合对大量路由进行批量权限控制:

java 复制代码
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    /**
     * 注册Sa-Token拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor(handler -> {
            // 所有请求都需要登录认证
            SaRouter.match("/**")
                    .notMatch("/auth/login", "/auth/register", "/doc.html", "/webjars/**", "/v3/api-docs/**")
                    .check(r -> StpUtil.checkLogin());

            // 用户模块权限控制
            SaRouter.match("/user/add", "/user/delete", "/user/update")
                    .check(r -> StpUtil.checkPermission("user:manage"));

            // 管理员模块权限控制
            SaRouter.match("/admin/**")
                    .check(r -> StpUtil.checkRole("admin"));

            // 超级管理员模块权限控制
            SaRouter.match("/super-admin/**")
                    .check(r -> StpUtil.checkRole("super-admin"));

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

3.5 权限通配符

Sa-Token 支持权限通配符,可以更灵活地定义权限:

java 复制代码
// 配置用户拥有"user:*"权限,表示拥有user模块的所有权限
permissionList.add("user:*");

// 校验是否拥有"user:add"权限,会匹配"user:*"
StpUtil.checkPermission("user:add"); // 通过

// 配置用户拥有"*:query"权限,表示拥有所有模块的查询权限
permissionList.add("*:query");

// 校验是否拥有"user:query"权限,会匹配"*:query"
StpUtil.checkPermission("user:query"); // 通过

// 配置用户拥有"*"权限,表示拥有所有权限(上帝权限)
permissionList.add("*");

// 校验任何权限都会通过
StpUtil.checkPermission("any:permission"); // 通过

3.6 二级认证

二级认证是在已登录的基础上,再次要求用户进行身份验证,以提高系统的安全性。例如,用户在修改密码、转账等敏感操作时,需要再次输入密码进行验证。

java 复制代码
// 开启二级认证,有效期120秒
StpUtil.openSafe(120);

// 校验是否已经通过二级认证
StpUtil.checkSafe();

// 判断是否已经通过二级认证
boolean isSafe = StpUtil.isSafe();

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

// 获取二级认证剩余有效期
long timeout = StpUtil.getSafeTime();

在实际项目中,我们可以这样使用二级认证:

java 复制代码
/**
 * 修改密码接口
 */
@PostMapping("/update-password")
public SaResult updatePassword(String oldPassword, String newPassword) {
    // 校验登录状态
    StpUtil.checkLogin();
    
    // 校验是否已经通过二级认证
    if (!StpUtil.isSafe()) {
        return SaResult.error("请先进行二级认证");
    }
    
    // 校验旧密码
    // ...
    
    // 修改密码
    // ...
    
    return SaResult.ok("密码修改成功");
}

/**
 * 二级认证接口
 */
@PostMapping("/safe-auth")
public SaResult safeAuth(String password) {
    // 校验登录状态
    StpUtil.checkLogin();
    
    // 校验密码
    // ...
    
    // 开启二级认证,有效期5分钟
    StpUtil.openSafe(300);
    
    return SaResult.ok("二级认证成功");
}

四、会话管理核心技术

会话管理是权限系统的重要组成部分,Sa-Token 提供了强大的会话管理功能,支持 全端共享 Session单端独享 Session自定义 Session等多种模式。

4.1 Session 基础操作

Sa-Token 的 Session 分为两种:User-SessionToken-SessionUser-Session 是与用户 ID 关联的会话Token-Session 是与 Token 关联的会话

java 复制代码
// 获取当前用户的User-Session
SaSession userSession = StpUtil.getSession();

// 获取指定用户的User-Session
SaSession userSessionById = StpUtil.getSessionByLoginId(10001);

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

// 获取指定Token的Token-Session
SaSession tokenSessionByToken = StpUtil.getTokenSessionByToken("token-xxxxxx");

// 在Session中存储数据
userSession.set("username", "yonghu");
userSession.set("loginTime", System.currentTimeMillis());

// 从Session中获取数据
String username = (String) userSession.get("username");
Long loginTime = (Long) userSession.get("loginTime");

// 从Session中获取数据,如果不存在则返回默认值
String email = (String) userSession.get("email", "default@example.com");

// 删除Session中的数据
userSession.delete("username");

// 清空Session中的所有数据
userSession.clear();

// 获取Session的ID
String sessionId = userSession.getId();

// 获取Session的创建时间
long createTime = userSession.getCreateTime();

// 获取Session的最后访问时间
long lastAccessTime = userSession.getLastAccessTime();

// 获取Session的剩余有效期
long timeout = userSession.getTimeout();

// 设置Session的有效期
userSession.setTimeout(3600); // 1小时

4.2 自定义 Session

Sa-Token 支持自定义 Session,你可以根据业务需求扩展 Session 的功能:

java 复制代码
@Component
public class MySaSessionCustomizer implements SaSessionCustomizer {

    @Override
    public void customize(SaSession session) {
        // 自定义Session的初始化逻辑
        System.out.println("Session创建:" + session.getId());
        
        // 可以在这里添加一些默认数据
        session.set("createTime", System.currentTimeMillis());
    }
}

4.3 会话查询

Sa-Token 提供了方便的会话查询接口,可以查询当前在线用户、指定用户的登录状态等信息:

java 复制代码
// 查询当前在线用户的Token数量 
int onlineCount = StpUtil.searchTokenValue("", 0, -1, false).size();

// 查询指定用户是否在线
boolean isOnline = StpUtil.isLogin(10001);

// 查询指定用户的所有登录设备
List<String> deviceList = StpUtil.getLoginDeviceList(10001);

// 查询指定用户的所有Token
List<String> tokenList = StpUtil.getTokenListByLoginId(10001);

// 分页查询所有在线Token
List<String> onlineTokens = StpUtil.searchTokenValue("", 0, 10, false);

4.4 踢人下线

踢人下线是会话管理的重要功能,Sa-Token 提供了多种踢人下线的方式:

java 复制代码
// 将指定用户的所有客户端踢下线
StpUtil.kickout(10001);

// 将指定用户的指定设备类型踢下线
StpUtil.kickout(10001, "PC");

// 将指定Token对应的客户端踢下线
StpUtil.kickoutByTokenValue("token-xxxxxx");

// 将指定用户的所有客户端踢下线,并给出提示信息
StpUtil.kickout(10001, "您的账号在其他设备登录");

当用户被踢下线后,再次访问系统时会抛出 NotLoginException 异常,异常信息中包含踢下线的原因。

五、Token 定制与安全

5.1 Token 风格定制

Sa-Token 内置了六种 Token 风格,你可以根据业务需求选择合适的 Token 风格:

bash 复制代码
// 配置文件中设置Token风格
// application.yml
sa-token:
  token-style: uuid # Token风格,可选值:uuid、simple-uuid、random-32、random-64、random-128、tik

各种 Token 风格的说明:

  • uuid:标准 UUID 格式,例如:550e8400-e29b-41d4-a716-446655440000
  • simple-uuid:去掉横线的 UUID 格式,例如:550e8400e29b41d4a716446655440000
  • random-32:32 位随机字符串
  • random-64:64 位随机字符串
  • random-128:128 位随机字符串
  • tik:自定义风格,例如:t_xxxxxx

你也可以自定义 Token 生成策略:

java 复制代码
@Configuration
public class SaTokenStrategyConfig {

    @Bean
    public SaStrategy saStrategy() {
        SaStrategy strategy = new SaStrategy();
        
        // 自定义Token生成策略
        strategy.createToken = (loginId, loginType, deviceType) -> {
            // 生成自定义格式的Token
            return "my-token-" + loginId + "-" + System.currentTimeMillis();
        };
        
        return strategy;
    }
}

5.2 Token 前缀

Sa-Token 支持为 Token 添加前缀,例如常见的Bearer 前缀:

bash 复制代码
// 配置文件中设置Token前缀
// application.yml
sa-token:
  token-prefix: Bearer # Token前缀

配置后,客户端需要在请求头中这样携带 Token:

bash 复制代码
Authorization: Bearer token-xxxxxx

5.3 密码加密

Sa-Token 提供了基础的密码加密工具类,可以快速实现 MD5、SHA1、SHA256、AES 等加密算法:

java 复制代码
import cn.dev33.satoken.secure.SaSecureUtil;

// MD5加密
String md5 = SaSecureUtil.md5("123456");

// SHA1加密
String sha1 = SaSecureUtil.sha1("123456");

// SHA256加密
String sha256 = SaSecureUtil.sha256("123456");

// AES加密
String key = "1234567890123456"; // 16位密钥
String aesEncrypt = SaSecureUtil.aesEncrypt(key, "123456");
String aesDecrypt = SaSecureUtil.aesDecrypt(key, aesEncrypt);

// BCrypt加密(推荐用于密码存储)
String bcryptHash = SaSecureUtil.bcryptHash("123456");
boolean bcryptCheck = SaSecureUtil.bcryptCheck("123456", bcryptHash);

5.4 账号封禁

Sa-Token 提供了账号封禁功能,可以临时或永久封禁用户账号:

java 复制代码
// 封禁指定用户,有效期为1天(单位:秒)
StpUtil.disable(10001, 86400);

// 封禁指定用户的指定服务,有效期为1小时
StpUtil.disable(10001, "comment", 3600);

// 解封指定用户
StpUtil.untieDisable(10001);

// 解封指定用户的指定服务
StpUtil.untieDisable(10001, "comment");

// 检查指定用户是否被封禁
boolean isDisabled = StpUtil.isDisable(10001);

// 检查指定用户的指定服务是否被封禁
boolean isCommentDisabled = StpUtil.isDisable(10001, "comment");

// 获取封禁剩余时间
long disableTime = StpUtil.getDisableTime(10001);

在实际项目中,我们可以在拦截器中添加账号封禁检查:

java 复制代码
registry.addInterceptor(new SaInterceptor(handler -> {
    // 所有请求都需要登录认证
    SaRouter.match("/**")
            .notMatch("/auth/login", "/auth/register")
            .check(r -> {
                StpUtil.checkLogin();
                // 检查账号是否被封禁
                StpUtil.checkDisable(StpUtil.getLoginId());
            });
})).addPathPatterns("/**");

5.5 全局安全响应头

Sa-Token 提供了全局过滤器,可以方便地设置安全响应头,提高系统的安全性:

java 复制代码
@Configuration
public class SaTokenFilterConfig {

    @Bean
    public SaServletFilter saServletFilter() {
        return new SaServletFilter()
                // 拦截所有请求
                .addInclude("/**")
                // 排除静态资源
                .addExclude("/static/**", "/favicon.ico")
                // 前置处理
                .setBeforeAuth(req -> {
                    // 设置安全响应头
                    SaServletFilter.setResponseHeader("X-Frame-Options", "DENY");
                    SaServletFilter.setResponseHeader("X-XSS-Protection", "1; mode=block");
                    SaServletFilter.setResponseHeader("X-Content-Type-Options", "nosniff");
                    SaServletFilter.setResponseHeader("Content-Security-Policy", "default-src 'self'");
                })
                // 异常处理
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                });
    }
}

六、高级特性

6.1 模拟他人账号

模拟他人账号功能允许管理员临时切换到其他用户的身份,方便排查问题:

java 复制代码
// 切换到用户ID为10002的身份
StpUtil.switchTo(10002);

// 此时获取的登录ID是10002
System.out.println(StpUtil.getLoginId()); // 输出:10002

// 结束模拟,切换回原来的身份
StpUtil.endSwitch();

// 此时获取的登录ID是原来的用户ID
System.out.println(StpUtil.getLoginId()); // 输出:10001

// 判断是否正在模拟他人
boolean isSwitch = StpUtil.isSwitch();

6.2 临时身份切换

临时身份切换功能允许用户临时切换到另一个身份,执行特定操作后再切换回来:

java 复制代码
// 临时切换到用户ID为10002的身份,执行操作
StpUtil.runAs(10002, () -> {
    // 这里的代码会以10002的身份执行
    System.out.println("当前登录ID:" + StpUtil.getLoginId()); // 输出:10002
});

// 执行完后自动切换回原来的身份
System.out.println("当前登录ID:" + StpUtil.getLoginId()); // 输出:10001

6.3 Http Basic 认证

Sa-Token 支持一行代码接入 Http Basic 认证:

java 复制代码
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.util.SaResult;
@RestController
@RequestMapping("/basic")
public class HttpBasicController {

    @GetMapping("/test")
    public SaResult testBasicAuth() {
        // 校验Http Basic认证,用户名:admin,密码:123456
        SaHttpBasicUtil.check("admin", "123456");
        
        return SaResult.ok("Http Basic认证成功");
    }
}

6.4 全局侦听器

Sa-Token 提供了全局侦听器,可以在用户登录、注销、被踢下线等关键操作时执行自定义逻辑:

java 复制代码
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import org.springframework.stereotype.Component;

@Component
public class MySaTokenListener implements SaTokenListener {

    /**
     * 用户登录时触发
     */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        System.out.println("用户登录:" + loginId + ",Token:" + tokenValue + ",设备类型:" + loginModel.getDevice());
        // 可以在这里记录登录日志
    }

    /**
     * 用户注销时触发
     */
    @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 doDisable(String loginType, Object loginId, String service, long time) {
        System.out.println("用户被封禁:" + loginId + ",服务:" + service + ",时长:" + time + "秒");
        // 可以在这里记录封禁日志
    }

    /**
     * 用户被解封时触发
     */
    @Override
    public void doUntieDisable(String loginType, Object loginId, String service) {
        System.out.println("用户被解封:" + loginId + ",服务:" + service);
        // 可以在这里记录解封日志
    }

    /**
     * Token续期时触发
     */
    @Override
    public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
        System.out.println("Token续期:" + tokenValue + ",剩余有效期:" + timeout + "秒");
    }
}

6.5 多账号体系认证

Sa-Token 支持一个系统多套账号分开鉴权,例如商城系统中的用户账号和管理员账号:

java 复制代码
// 定义用户账号的StpUtil
public class StpUserUtil {
    public static final String TYPE = "user";
    public static final StpLogic stpLogic = new StpLogic(TYPE);
}

// 定义管理员账号的StpUtil
public class StpAdminUtil {
    public static final String TYPE = "admin";
    public static final StpLogic stpLogic = new StpLogic(TYPE);
}

使用方式和普通的 StpUtil 完全一样:

java 复制代码
// 用户登录
StpUserUtil.stpLogic.login(10001);

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

// 校验用户是否登录
StpUserUtil.stpLogic.checkLogin();

// 校验管理员是否登录
StpAdminUtil.stpLogic.checkLogin();

6.6 JWT 集成

Sa-Token 提供了三种模式的 JWT 集成方案:

模式一:简单模式

将 JWT 作为 Token 的生成策略:

XML 复制代码
<!-- 引入JWT依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.45.0</version>
</dependency>
bash 复制代码
// 配置文件中开启JWT
// application.yml
sa-token:
  jwt-secret-key: your-secret-key # JWT密钥
  is-jwt: true # 开启JWT模式

模式二:混入模式

在 Sa-Token 的 Token 中混入 JWT 信息:

java 复制代码
// 登录时添加JWT额外信息
 StpUtil.login(10001, SaLoginModel.create()
        .setExtra("username", "yonghu")
        .setExtra("email", "admin@example.com"));

// 获取JWT中的额外信息
String username = (String) StpUtil.getExtra("username");
String email = (String) StpUtil.getExtra("email");

模式三:独立模式

完全使用 JWT 作为认证方式,不依赖 Sa-Token 的 Token 存储:

java 复制代码
import cn.dev33.satoken.jwt.SaJwtUtil;
import cn.dev33.satoken.util.SaResult;
import io.jsonwebtoken.Claims;

// 生成JWT
String jwt = SaJwtUtil.createToken(10001);

// 解析JWT
Claims claims = SaJwtUtil.parseToken(jwt);
Object loginId = claims.getSubject();

// 校验JWT
SaJwtUtil.checkToken(jwt);

七、持久化与分布式

7.1 Redis 集成

默认情况下,Sa-Token 将数据存储在内存中,重启服务后数据会丢失。在生产环境中,我们需要将数据持久化到 Redis 中:

XML 复制代码
<!-- 引入Redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 引入Sa-Token Redis集成依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis</artifactId>
    <version>1.45.0</version>
</dependency>
bash 复制代码
// 配置文件中配置Redis
// application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: your-password
    database: 0

sa-token:
  # 使用Redis存储数据
  dao: cn.dev33.satoken.dao.SaTokenDaoRedisJackson

7.2 独立 Redis

Sa-Token 支持将权限缓存与业务缓存分离,使用独立的 Redis 实例:

XML 复制代码
<!-- 引入独立Redis插件依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-alone-redis</artifactId>
    <version>1.45.0</version>
</dependency>
bash 复制代码
// 配置文件中配置独立Redis
// application.yml
sa-token:
  alone-redis:
    host: localhost
    port: 6379
    password: your-password
    database: 1

7.3 分布式会话

通过 Redis 集成,Sa-Token 天然支持分布式会话。在微服务架构中,所有服务共享同一个 Redis 实例,就可以实现跨服务的会话共享。

7.4 微服务网关鉴权

在微服务架构中,我们通常将鉴权逻辑放在网关层统一处理。Sa-Token 提供了与 Spring Cloud Gateway 的集成方案:

XML 复制代码
<!-- 引入Gateway集成依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.45.0</version>
</dependency>
java 复制代码
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SaTokenGatewayConfig {

    @Bean
    public SaReactorFilter saReactorFilter() {
        return new SaReactorFilter()
                // 拦截所有请求
                .addInclude("/**")
                // 排除登录接口和静态资源
                .addExclude("/auth/login", "/auth/register", "/doc.html", "/webjars/**", "/v3/api-docs/**")
                // 鉴权逻辑
                .setAuth(obj -> {
                    // 所有请求都需要登录认证
                    SaRouter.match("/**").check(r -> StpUtil.checkLogin());
                    
                    // 用户模块权限控制
                    SaRouter.match("/user/**").check(r -> StpUtil.checkPermission("user"));
                    
                    // 管理员模块权限控制
                    SaRouter.match("/admin/**").check(r -> StpUtil.checkRole("admin"));
                })
                // 异常处理
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                });
    }
}

7.5 RPC 调用鉴权

在微服务架构中,服务之间的 RPC 调用也需要进行鉴权。Sa-Token 提供了 Dubbo 和 gRPC 的集成方案:

XML 复制代码
<!-- 引入Dubbo集成依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dubbo</artifactId>
    <version>1.45.0</version>
</dependency>
java 复制代码
// 服务提供者端添加鉴权注解
@Service
public class UserServiceImpl implements UserService {

    @SaCheckPermission("user:query")
    @Override
    public User getUserById(Long id) {
        // ...
    }
}

// 服务消费者端传递Token
StpUtil.setToken("token-xxxxxx");
User user = userService.getUserById(1L);

八、最佳实践

8.1 统一异常处理

在实际项目中,我们需要统一处理 Sa-Token 抛出的各种异常:

java 复制代码
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理未登录异常
     */
    @ExceptionHandler(NotLoginException.class)
    public SaResult handleNotLoginException(NotLoginException e) {
        return SaResult.error("未登录或登录已过期").setCode(401);
    }

    /**
     * 处理权限不足异常
     */
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handleNotPermissionException(NotPermissionException e) {
        return SaResult.error("权限不足:" + e.getPermission()).setCode(403);
    }

    /**
     * 处理角色不足异常
     */
    @ExceptionHandler(NotRoleException.class)
    public SaResult handleNotRoleException(NotRoleException e) {
        return SaResult.error("角色不足:" + e.getRole()).setCode(403);
    }

    /**
     * 处理Sa-Token其他异常
     */
    @ExceptionHandler(SaTokenException.class)
    public SaResult handleSaTokenException(SaTokenException e) {
        return SaResult.error(e.getMessage()).setCode(500);
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public SaResult handleException(Exception e) {
        e.printStackTrace();
        return SaResult.error("系统异常:" + e.getMessage()).setCode(500);
    }
}

8.2 前后端分离项目最佳实践

在前后端分离项目中,前端通常将 Token 存储在 localStorage 中,每次请求时在请求头中携带 Token:

javascript 复制代码
// 登录成功后保存Token
localStorage.setItem('token', res.data.token);

// 请求拦截器中添加Token
axios.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = 'Bearer ' + token;
    }
    return config;
});

// 响应拦截器中处理未登录异常
axios.interceptors.response.use(
    response => response,
    error => {
        if (error.response && error.response.status === 401) {
            // 清除Token并跳转到登录页
            localStorage.removeItem('token');
            router.push('/login');
        }
        return Promise.reject(error);
    }
);

8.3 安全最佳实践

  1. 使用 HTTPS:所有请求都应该使用 HTTPS 协议,防止 Token 被窃取
  2. 设置合理的 Token 有效期:不要将 Token 有效期设置得过长,建议设置为 30 分钟到 2 小时
  3. 开启自动续签:在用户活跃时自动续签 Token,不活跃时自动失效
  4. 使用 BCrypt 加密密码:不要使用 MD5 等弱加密算法存储密码
  5. 开启账号封禁功能:对异常登录的账号进行临时封禁
  6. 设置安全响应头:防止 XSS(跨站脚本攻击)、CSRF(跨站请求伪造) 等常见攻击
  7. 定期备份数据:定期备份 Redis 中的会话数据,防止数据丢失

总结

Sa-Token 作为一款轻量级 Java 权限认证框架,以其简洁的 API 设计、全面的功能覆盖和良好的扩展性,为 Java 开发者提供了一个优秀的权限认证解决方案。本文详细介绍了 Sa-Token 的核心技术,包括登录认证、权限认证、会话管理、Token 定制、持久化与分布式等方面,并提供了大量详实的代码案例。

Sa-Token 的优势不仅在于其功能的全面性,更在于其设计理念的先进性。它将复杂的权限认证逻辑封装成简单易用的静态方法,让开发者能够专注于业务逻辑,而非权限配置的细枝末节。随着微服务架构的普及,Sa-Token 的分布式会话和网关鉴权功能将发挥越来越重要的作用。

相关推荐
步步为营DotNet15 小时前
探索.NET 11:.NET Aspire 在云原生微服务治理中的创新实践
微服务·云原生·.net
梵得儿SHI16 小时前
SpringCloud 进阶拓展:Spring Security OAuth2+JWT 微服务统一认证授权全实战|生产级方案 + 源码解析 + 踩坑实录
spring·spring cloud·微服务·spring security·jwt·oauth2·统一认证授权
未若君雅裁17 小时前
RabbitMQ 消息可靠性:生产者确认、持久化、消费者ACK与幂等消费
分布式·微服务·rabbitmq
huipeng9261 天前
企业级微服务开发实战(一):项目启动与工程化设计
java·开发语言·spring boot·spring cloud·微服务·云原生·架构
一切顺势而行2 天前
easysearch 安装
spring·spring cloud·微服务
程序员老邢2 天前
《技术底稿 41》从三机混跑到四机隔离:微服务集群环境拆分实战复盘
微服务·云原生·架构·devops·服务器运维·技术底稿·环境隔离
未若君雅裁2 天前
分布式接口幂等性设计:唯一索引、Token 与分布式锁
分布式·微服务
万里侯2 天前
Kubernetes Operator模式:自动化运维的高级实践
微服务·容器·k8s
未若君雅裁2 天前
微服务监控与 SkyWalking 链路追踪
微服务·架构·skywalking