Spring Security、Apache Shiro、Sa-Token是Java Web开发中常用的主流权限认证框架。
三种框架的上手难度依次递减:Spring Security > Apache Shiro > Sa-Token
如果你使用过Apache Shiro,你会发现:
- Sa-Token和Apache Shiro有许多相似点
- 通配符权限:分隔符不同
- Sa-Token:user.*、user.login
- Apache Shiro:user:*、user:login
- 通配符权限:分隔符不同
- Apache Shiro对于前后端分离项目不友好
本篇文章将结合博主的Sa-Token框架使用经验和官方文档,通过一整篇文章详细介绍在Java开发神器(Spring Boot)中使用Sa-Token来完成权限认证功能。
创建项目
在Intellij IDEA中创建一个Maven项目,名字就叫sa-token-study。

在这里就不直接通过Spring Initializer创建Spring Boot项目了,因为默认会使用较新的Spring Boot版本,所以会触发Maven依赖包的自动下载。
添加依赖
在Maven的配置文件pom.xml中,添加Sa-Token的必要依赖。
同时,设置当前Maven继承spring-boot-starter-parent,添加Spring Boot的启动器依赖。
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<version>2.3.4.RELEASE</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
<version>20260531</version>
<groupId>cn.edu.sgu.www</groupId>
<artifactId>sa-token-study</artifactId>
<properties>
<sa-token.version>1.45.0</sa-token.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
</dependencies>
</project>
修改配置
创建配置文件
因为这是一个Maven项目,Spring Boot的配置文件需要手动创建。
在src/main/resources目录下创建一个application.yml文件。

添加框架配置
直接从Sa-Token官方文档复制配置文件内容,把配置sa-token.token-name的值改成了项目名。
XML
server:
# 端口
port: 8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: sa-token-study
# 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
补全项目
将当前Maven项目完全变成Spring Boot项目。
创建项目根包
在src/main/java包下创建多级包cn.edu.sgu.www.study

创建启动类
在项目根包cn.edu.sgu.www.study下创建Spring Boot启动类。
java
package cn.edu.sgu.www.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@SpringBootApplication
public class SaTokenStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenStudyApplication.class, args);
}
}
登录认证
Sa-Token中登录认证只需要一句代码,调用StpUtil工具类的login()方法,把账号ID作为参数。
XML
StpUtil.login(Object userId);
案例代码:登录ID为10086的账号
java
StpUtil.login(10086);
保存用户信息
通常,我们希望像HttpSession一样,将用户的信息保存到当前会话中,以便在其他地方使用。
java
SaSession session = StpUtil.getSession();
User user = new User();
session.set("user", user);
获取用户信息
java
SaSession session = StpUtil.getSession();
Object obj = session.get("user");
User user = (User) obj;
// todo:访问user
登录认证案例
创建控制器类
在项目根包cn.edu.sgu.www.study下创建controller.LoginController类。
定义一个登录方法login(),该方法接收一个userId参数。
java
package cn.edu.sgu.www.study.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@RestController
public class LoginController {
@GetMapping("/login")
public void login(String userId) {
StpUtil.login(userId);
System.out.println("登录成功");
}
}
测试登录接口
运行启动类SaTokenStudyApplication的main()方法,启动当前Spring Boot项目。
在浏览器地址栏输入以下网址访问登录接口
java
http://localhost:8081/login?userId=10086
在Intellij IDEA的控制台,可以看到方法里打印的登录成功信息和Sa-Token登录产生的会话信息。

权限认证
权限校验
可以使用以下方法来完成权限的校验。
java
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();
// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.insert");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user.insert");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.insert", "user.delete", "user.update");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.insert", "user.delete", "user.update");
角色校验
可以使用以下方法来完成角色的校验。
java
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();
// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("sys-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("sys-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("sys-admin", "sys-user");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("sys-admin", "sys-user");
提供权限
可以通过提供一个StpInterface类型的Bean来根据账号ID 和登录类型为对应账号设置权限和角色。
java
package cn.edu.sgu.www.study.support;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 权限数据源加载类
* @author 沐雨橙风ιε
* @version 1.0
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<String> list = new ArrayList<>();
list.add("user.insert");
list.add("user.delete");
list.add("user.update");
return list;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
List<String> list = new ArrayList<>();
list.add("sys-admin");
list.add("sys-user");
return list;
}
}
鉴权方式
注解鉴权
Sa-Token框架提供了以下注解用于鉴权,只需要在对应的控制器接口方法上使用这些注解。
常用的注解
@SaIgnore:忽略校验 ------ 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckLogin:登录校验 ------ 只有登录之后才能进入该方法。@SaCheckRole("admin"):角色校验 ------ 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add"):权限校验 ------ 必须具有指定权限才能进入该方法。
不常用的注解
@SaCheckSafe:二级认证校验 ------ 必须二级认证之后才能进入该方法。@SaCheckHttpBasic:HttpBasic校验 ------ 只有通过 HttpBasic 认证后才能进入该方法。@SaCheckHttpDigest:HttpDigest校验 ------ 只有通过 HttpDigest 认证后才能进入该方法。@SaCheckDisable("comment"):账号服务封禁校验 ------ 校验当前账号指定服务是否被封禁。@SaCheckSign:API 签名校验 ------ 用于跨系统的 API 签名参数校验。
创建控制器
在项目根包cn.edu.sgu.www.study.controller下创建UserController类。
增加3个HTTP接口:/user/insert、/user/delete、/user/update
为了能够直接通过浏览器地址栏访问,将接口的HTTP请求方法都设置为GET。
java
package cn.edu.sgu.www.study.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/insert")
public void insert() {
System.out.println("添加用户");
}
@GetMapping("/delete")
public void delete() {
System.out.println("删除用户");
}
@GetMapping("/update")
public void update() {
System.out.println("修改用户");
}
}
注册拦截器
注解鉴权是基于AOP(拦截器)完成的,需要添加Sa-Token的拦截器到Spring MVC中。
java
package cn.edu.sgu.www.study.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
使用鉴权注解
在三个接口上使用@SaCheckPermission注解,配置需要指定的权限才能访问对应的方法。
java
package cn.edu.sgu.www.study.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@SaCheckPermission("user.insert")
@GetMapping("/insert")
public void insert() {
System.out.println("添加用户");
}
@SaCheckPermission("user.delete")
@GetMapping("/delete")
public void delete() {
System.out.println("删除用户");
}
@SaCheckPermission("user.update")
@GetMapping("/update")
public void update() {
System.out.println("修改用户");
}
}
测试注解鉴权
启动当前Spring Boot项目,在浏览器地址栏输入以下网址分别访问三个接口。
java
http://localhost:8081/user/insert
http://localhost:8081/user/delete
http://localhost:8081/user/update
访问失败
访问3个接口都触发了服务器异常。



Intellij IDEA的控制台打印了3个一样的异常的栈信息,这是因为没有登录,之前登录的token已经在项目重启之后失效了(存在了内存中,被删除了)。

完成登录
为了解决这个问题,需要在访问这些接口之前,通过登录接口登录。
java
http://localhost:8081/login?userId=10086
鉴权成功
登录完成后,在浏览器地址栏输入以下网址分别访问三个接口。
java
http://localhost:8081/user/insert
http://localhost:8081/user/delete
http://localhost:8081/user/update
在Intellij IDEA的控制台,可以看到3个方法里打印的信息。

鉴权失败
注释掉设置的权限,然后重启项目,再次访问三个接口。

从控制台打印的异常栈信息,可以看到没有对应的user.insert权限。



可以用相似的方式测试@SaCheckRole、@SaCheckLogin等其他注解。
路由拦截鉴权
注解鉴权固然方便,但是不利于维护,更新时需要到每个控制器类中去修改。
于是,Sa-Token的另外一种鉴权方式更适合大多数Web应用:路由拦截鉴权。
可以在过滤器、拦截器中动态配置访问某个(某些)url需要哪个(哪些)权限。
为什么使用路由拦截鉴权?
- 路由拦截鉴权的最大特点就是动态,可以动态配置路由权限。
- 比起直接在代码里写死,这种方式无疑更加灵活,也更易于理解。
拦截器鉴权
由于刚学完基于拦截器的注解鉴权,就先介绍基于拦截器的路由拦截鉴权。
使用基于拦截器的路由拦截鉴权,只需要在拦截器中配置路由和需要的对应权限,并且关闭注解鉴权,避免一个url被鉴权两次的bug。
java
package cn.edu.sgu.www.study.config;
import cn.dev33.satoken.fun.SaParamFunction;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.router.SaRouterStaff;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token拦截器
registry.addInterceptor(saInterceptor()).addPathPatterns("/**");
}
private SaInterceptor saInterceptor() {
return new SaInterceptor(new SaParamFunction<Object>() {
@Override
public void run(Object handler) {
SaRouter.match("/user/insert").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.insert");
}
});
SaRouter.match("/user/delete").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.delete");
}
});
SaRouter.match("/user/update").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.update");
}
});
}
}).isAnnotation(false);
}
}
过滤器鉴权
路由拦截鉴权除了基于拦截器的鉴权方式以外,还可以通过配置过滤器来完成鉴权操作。
java
package cn.edu.sgu.www.study.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.filter.SaFilterAuthStrategy;
import cn.dev33.satoken.filter.SaFilterErrorStrategy;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.fun.SaParamFunction;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.router.SaRouterStaff;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 沐雨橙风ιε
* @version 1.0
*/
@Configuration
public class SaTokenConfig {
/**
* 注册Sa-Token全局过滤器
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter().setAuth(new SaFilterAuthStrategy() {
@Override
public void run(Object obj) {
SaRouter.match("/user/insert").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.insert");
}
});
SaRouter.match("/user/delete").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.delete");
}
});
SaRouter.match("/user/update").check(new SaParamFunction<SaRouterStaff>() {
@Override
public void run(SaRouterStaff r) {
StpUtil.checkPermission("user.update");
}
});
}
}).setError(new SaFilterErrorStrategy() {
@Override
public Object run(Throwable throwable) {
String message = null;
int code = 200;
// 不同类型的异常区别处理
if (throwable instanceof NotLoginException) {
message = "请登录后访问!";
code = 403;
} else if (throwable instanceof NotRoleException || throwable instanceof NotPermissionException) {
message = "正在访问未授权的资源!";
code = 401;
}
SaResponse response = SaHolder.getResponse();
response.setStatus(code);
return new SaResult(code, message, null);
}
}).addInclude("/**");
}
}
使用全局过滤器之后,就不需要注册Sa-Token的拦截器了。
会话共享
Sa-Token默认将登录会话信息保存在内存中(java.util.Map),所以重启应用将导致会话丢失。
对此,Sa-Token实现了Redis的无缝集成,只需要添加一个依赖,就可以将会话存储在Redis中,实现会话的共享。
XML
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.45.0</version>
</dependency>
完整的代码已经保存到码云,需要的自行拉取~
Spring Boot整合Sa-Token权限认证框架
https://gitee.com/muyu-chengfeng/sa-token-study.git