Sa-Token 是一款轻量级 Java 权限认证框架,核心解决「登录认证、权限授权、单点登录、Token 管理」等问题,相比 Shiro/Spring Security 更简洁易用。

一、核心概念(先理清)

核心功能 作用
登录认证 验证用户身份,生成 Token 并维护会话(支持 Cookie、Header 传参)
权限授权 验证用户是否拥有指定权限/角色(如 admin 角色、user:add 权限)
Token 管理 支持 Token 过期、刷新、踢人下线、注销登录等
路径拦截 拦截指定接口,仅允许认证/授权通过的用户访问
注解鉴权 通过 @SaCheckLogin/@SaCheckRole/@SaCheckPermission 注解鉴权

二、环境准备(Spring Boot 整合)

1. 引入依赖

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

<!-- 如需 Redis 存储 Token(分布式场景),引入以下依赖 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis-jackson</artifactId>
    <version>1.38.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 核心配置(application.yml)

# Sa-Token 配置
sa-token:
  # Token 名称(默认:satoken)
  token-name: satoken
  # Token 有效期(秒),默认30天,-1 永久有效
  timeout: 2592000
  # Token 临时有效期(秒),默认30分钟(用于自动刷新)
  activity-timeout: 1800
  # 是否允许同一账号多地登录(默认false,即单端登录)
  is-concurrent: false
  # 在多人登录同一账号时,是否共用一个Token(默认false)
  is-share: false
  # Token 风格(默认uuid,可选:simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志(默认false)
  is-log: true

# Redis 配置(分布式场景必填)
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 
    database: 0

3. 自定义 Sa-Token 配置类(可选)

如需自定义 Token 生成规则、存储方式等,可编写配置类:

import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.impl.SaTokenDaoRedisJackson;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpLogic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SaTokenConfig {
    // 自定义 StpLogic(如整合 JWT)
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForSimple();
    }

    // 配置 Redis 存储(分布式场景)
    @Bean
    public SaTokenDao saTokenDao() {
        return new SaTokenDaoRedisJackson();
    }
}

三、核心功能实现(认证+授权)

1. 登录认证(核心)

步骤1:用户登录接口
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * 登录认证控制器
 */
@RestController
public class LoginController {

    /**
     * 用户登录接口
     * POST 请求示例:
     * URL: /login
     * Body: {"username":"admin","password":"123456"}
     */
    @PostMapping("/login")
    public SaResult login(@RequestBody Map<String, String> params) {
        // 1. 获取前端参数
        String username = params.get("username");
        String password = params.get("password");

        // 2. 模拟数据库校验用户(真实场景需查数据库)
        if (!"admin".equals(username) || !"123456".equals(password)) {
            return SaResult.error("用户名或密码错误");
        }

        // 3. 登录认证:传入用户ID(唯一标识),生成 Token
        Long userId = 1001L; // 模拟用户ID
        StpUtil.login(userId);

        // 4. 获取 Token 信息
        String token = StpUtil.getTokenValue();
        long timeout = StpUtil.getTokenTimeout();

        // 5. 返回结果
        return SaResult.ok("登录成功")
                .set("token", token)
                .set("userId", userId)
                .set("timeout", timeout);
    }

    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public SaResult logout() {
        // 退出当前用户登录(销毁 Token)
        StpUtil.logout();
        return SaResult.ok("退出登录成功");
    }

    /**
     * 获取当前登录用户信息
     */
    @PostMapping("/getLoginUser")
    public SaResult getLoginUser() {
        // 1. 检查是否登录(未登录会抛出 NotLoginException 异常)
        StpUtil.checkLogin();

        // 2. 获取当前登录用户ID
        Long userId = StpUtil.getLoginIdAsLong();

        // 3. 模拟获取用户信息(真实场景查数据库)
        return SaResult.ok("获取成功")
                .set("userId", userId)
                .set("username", "admin")
                .set("role", "admin")
                .set("permissions", new String[]{"user:add", "user:delete", "user:query"});
    }
}
步骤2:全局异常处理(捕获未登录异常)

Sa-Token 未登录/未授权时会抛出异常,需统一处理:

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * Sa-Token 全局异常处理
 */
@RestControllerAdvice
public class SaTokenExceptionHandler {

    // 处理未登录异常
    @ExceptionHandler(NotLoginException.class)
    public SaResult handleNotLoginException(NotLoginException e) {
        // 返回未登录提示
        return SaResult.error("未登录:" + e.getMessage()).setCode(401);
    }

    // 处理未授权异常(角色)
    @ExceptionHandler(NotRoleException.class)
    public SaResult handleNotRoleException(NotRoleException e) {
        return SaResult.error("无指定角色:" + e.getRole()).setCode(403);
    }

    // 处理未授权异常(权限)
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handleNotPermissionException(NotPermissionException e) {
        return SaResult.error("无指定权限:" + e.getPermission()).setCode(403);
    }
}

2. 权限授权(两种方式:注解+拦截器)

方式1:注解鉴权(推荐,灵活)

通过 @SaCheckLogin/@SaCheckRole/@SaCheckPermission 注解实现细粒度鉴权:

import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 权限授权控制器(注解式)
 */
@RestController
public class PermissionController {

    /**
     * 示例1:仅登录用户可访问
     */
    @SaCheckLogin
    @GetMapping("/user/info")
    public SaResult userInfo() {
        return SaResult.ok("获取用户信息成功").set("data", "用户信息");
    }

    /**
     * 示例2:仅拥有 admin 角色的用户可访问
     * orRole = true:多个角色满足其一即可(默认false,需全部满足)
     */
    @SaCheckRole(value = {"admin", "super_admin"}, orRole = true)
    @GetMapping("/admin/dashboard")
    public SaResult adminDashboard() {
        return SaResult.ok("管理员控制台").set("data", "管理员数据");
    }

    /**
     * 示例3:仅拥有 user:add 权限的用户可访问
     * orPermission = true:多个权限满足其一即可
     */
    @SaCheckPermission(value = {"user:add", "user:edit"}, orPermission = true)
    @PostMapping("/user/add")
    public SaResult addUser() {
        return SaResult.ok("添加用户成功");
    }

    /**
     * 示例4:登录且拥有指定角色+权限
     */
    @SaCheckLogin
    @SaCheckRole("admin")
    @SaCheckPermission("user:delete")
    @PostMapping("/user/delete")
    public SaResult deleteUser() {
        return SaResult.ok("删除用户成功");
    }
}
方式2:拦截器鉴权(全局路径控制)

适合对整站路径进行统一鉴权(如 /admin/** 仅管理员可访问):

步骤1:编写 Sa-Token 拦截器
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Sa-Token 拦截器(路径鉴权)
 */
public class SaTokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 配置不需要鉴权的路径
        String path = request.getRequestURI();
        if (path.contains("/login") || path.contains("/register") || path.contains("/static")) {
            return true; // 放行
        }

        // 2. 登录校验:拦截所有请求,必须登录后才能访问
        StpUtil.checkLogin();

        // 3. 角色/权限校验:/admin/** 路径需 admin 角色
        if (path.startsWith("/admin/")) {
            StpUtil.checkRole("admin");
        }

        // 4. /user/** 路径需 user:query 权限
        if (path.startsWith("/user/")) {
            StpUtil.checkPermission("user:query");
        }

        // 放行
        return true;
    }
}
步骤2:注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web 配置(注册拦截器)
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器
        registry.addInterceptor(new SaTokenInterceptor())
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/login", "/register", "/static/**"); // 排除无需鉴权的路径
    }
}

3. 自定义权限加载(从数据库获取角色/权限)

真实项目中,角色和权限需从数据库加载,需实现 StpInterface 接口:

步骤1:实现 StpInterface 接口
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义权限加载接口(从数据库获取用户角色/权限)
 */
@Component
public class MyStpInterface implements StpInterface {

    /**
     * 返回指定用户拥有的角色标识集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 真实场景:根据 loginId(用户ID)查数据库获取角色
        List<String> roleList = new ArrayList<>();
        // 模拟:admin 用户拥有 admin 角色,普通用户拥有 user 角色
        if ("1001".equals(loginId.toString())) {
            roleList.add("admin");
        } else {
            roleList.add("user");
        }
        return roleList;
    }

    /**
     * 返回指定用户拥有的权限标识集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 真实场景:根据 loginId 查数据库获取权限
        List<String> permissionList = new ArrayList<>();
        // 模拟:admin 用户拥有所有权限,普通用户仅拥有查询权限
        if ("1001".equals(loginId.toString())) {
            permissionList.add("user:add");
            permissionList.add("user:delete");
            permissionList.add("user:edit");
            permissionList.add("user:query");
        } else {
            permissionList.add("user:query");
        }
        return permissionList;
    }
}
步骤2:使用自定义权限

实现 StpInterface 后,@SaCheckRole/@SaCheckPermission 会自动从该接口获取角色/权限,无需手动绑定。

4. 高级功能:Token 管理

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Token 管理控制器
 */
@RestController
public class TokenController {

    /**
     * 刷新 Token(延长有效期)
     */
    @PostMapping("/refreshToken")
    public SaResult refreshToken() {
        StpUtil.refreshToken();
        return SaResult.ok("Token 刷新成功").set("token", StpUtil.getTokenValue());
    }

    /**
     * 踢人下线(强制注销指定用户)
     */
    @PostMapping("/kickout")
    public SaResult kickout(Long userId) {
        StpUtil.kickout(userId);
        return SaResult.ok("已强制用户 " + userId + " 下线");
    }

    /**
     * 检查 Token 是否有效
     */
    @PostMapping("/checkToken")
    public SaResult checkToken(String token) {
        boolean isValid = StpUtil.checkToken(token);
        return SaResult.ok("Token 有效性:" + isValid);
    }

    /**
     * 设置 Token 有效期(单独指定某个用户的 Token 有效期)
     */
    @PostMapping("/setTokenTimeout")
    public SaResult setTokenTimeout(Long userId, long timeout) {
        StpUtil.setTokenTimeout(userId, timeout);
        return SaResult.ok("设置 Token 有效期成功");
    }
}

四、实战场景整合(前后端分离)

1. 前端传递 Token 方式

前后端分离场景下,前端需在请求头中携带 Token(默认名称:satoken):

// Axios 请求示例
axios({
  url: '/user/info',
  method: 'get',
  headers: {
    'satoken': localStorage.getItem('satoken') // 从本地存储获取 Token
  }
}).then(res => {
  console.log(res.data);
});

2. 跨域配置(解决前端跨域携带 Token)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 跨域配置
 */
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许所有域名跨域(生产环境需指定具体域名)
        config.addAllowedOriginPattern("*");
        // 允许携带 Cookie/Token
        config.setAllowCredentials(true);
        // 允许所有请求头
        config.addAllowedHeader("*");
        // 允许所有请求方法
        config.addAllowedMethod("*");
        // 暴露响应头中的 satoken
        config.addExposedHeader("satoken");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

3. 常见场景:登录状态保持

  • Sa-Token 会自动维护 Token 有效期,当用户操作时(如访问接口),Token 临时有效期(activity-timeout)会自动刷新;
  • 若 Token 即将过期,前端可调用 /refreshToken 接口刷新 Token,避免用户重新登录。

五、避坑要点(高频问题)

1. 未登录异常:NotLoginException

  • 现象:访问需登录的接口时抛出该异常;
  • 原因:
    1. 未携带 Token;
    2. Token 已过期;
    3. Token 无效/被注销;
  • 解决:
    1. 前端检查 Token 是否正确携带;
    2. 后端检查 Token 有效期配置;
    3. 重新登录生成新 Token。

2. 跨域时 Token 无法携带

  • 现象:前端携带 Token 但后端获取不到;
  • 原因:跨域配置未开启 AllowCredentials
  • 解决:跨域配置中设置 config.setAllowCredentials(true)

3. 分布式场景 Token 不共享

  • 现象:多服务部署时,登录后访问另一服务提示未登录;
  • 原因:未配置 Redis 存储 Token;
  • 解决:引入 Redis 依赖,配置 Redis 连接信息,让 Token 存储在 Redis 中。

4. 注解鉴权不生效

  • 现象:加了 @SaCheckRole 但无权限用户仍可访问;
  • 原因:
    1. 未实现 StpInterface 接口,角色/权限加载失败;
    2. 注解参数配置错误(如 orRole=false 但用户仅满足一个角色);
  • 解决:
    1. 检查 StpInterface 实现类是否加 @Component
    2. 核对注解参数(如 orRole=true 表示满足其一即可)。

5. Token 重复登录被挤下线

  • 现象:同一账号登录多个端,后登录的挤掉先登录的;
  • 原因:sa-token.is-concurrent=false(禁止同一账号多地登录);
  • 解决:
    1. 设置 is-concurrent=true 允许多地登录;
    2. 或设置 is-share=true 共用一个 Token。

六、核心总结

  1. 认证核心
    • 登录:StpUtil.login(userId) 生成 Token;
    • 校验登录:StpUtil.checkLogin()@SaCheckLogin
    • 退出:StpUtil.logout() 销毁 Token;
  2. 授权核心
    • 角色鉴权:@SaCheckRole("admin")
    • 权限鉴权:@SaCheckPermission("user:add")
    • 自定义权限:实现 StpInterface 从数据库加载角色/权限;
  3. 分布式场景
    • 引入 Redis 依赖,配置 Redis 连接,实现 Token 跨服务共享;
  4. 避坑关键
    • 跨域配置需开启 AllowCredentials,否则 Token 无法携带;
    • 注解鉴权需确保 StpInterface 实现类被 Spring 扫描;
    • 未登录/未授权异常需全局捕获,统一返回格式。

Sa-Token 整合用户认证授权的核心是「简单、轻量、无侵入」,通过少量代码即可实现企业级的权限控制,相比传统框架大幅降低开发成本,适合中小项目、前后端分离项目快速集成。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐