
参考资料:
Sa-Token介绍
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
一、创建和配置
1.创建SpringBoot项目
创建项目很简单,使用Mybatis-plus框架,可以根据数据库直接生成三层类。
2.添加Sa-Token依赖
1 2 3 4 5 6
| <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.40.0</version> </dependency>
XML
|
3.设置配置文件
有两种配置文件,风格不一样,选择自己喜欢的,新手的话建议选择.properties风格的
核心包所有可配置项,在需要时查看即可,参考sa-token框架配置
下面配置文件的风格是application.yml风格的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| server: port: 8081
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sa-token-db?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF8&useUnicode=true username: root password: root type: com.alibaba.druid.pool.DruidDataSource druid: remove-abandoned: true time-between-eviction-runs-millis: 60000 remove-abandoned-timeout-millis: 1800
sa-token: token-name: satoken timeout: 2592000 active-timeout: -1 is-concurrent: true is-share: true token-style: uuid is-log: true
YML
|
下面的配置文件风格是application.properties风格的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| server.port=8081
sa-token.token-name=satoken
sa-token.timeout=2592000
sa-token.active-timeout=-1
sa-token.is-concurrent=true
sa-token.is-share=true
sa-token.token-style=uuid
sa-token.is-log=true
PROPERTIES
|
4.创建启动类
在项目中新建包 com.xxx 在这个包内创建主类:SaTokenDemoApplication
1 2 3 4 5 6 7
| @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) throws JsonProcessingException { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); } }
JAVA
|
创建好后的项目是这样的,此时可以启动一下,看一下有没有报错

启动成功:

5.创建数据库文件
1 2 3 4 5 6 7 8 9 10 11 12
| CREATE TABLE `users` ( `user_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', `password` varchar(125) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码', `user_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名', `permission_level` int(11) NOT NULL DEFAULT 0 COMMENT '权限等级', `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否被删除', `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', `cellphone_number` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号', `user_type` int(10) NULL DEFAULT NULL COMMENT '用户类型:1超级管理员,2管理员,3运维人员,4工人,5游客', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SQL
|
创建数据库,执行SQL语句

6.引入依赖
引入本次教程所需要的所有依赖,除了上面已经引入过的Sa-Token依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1.tmp</version> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.22</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1.tmp</version> </dependency>
<dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.2</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency>
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.12</version> </dependency>
XML
|
7.创建数据库文件自动生成类
因为我们要使用mybatis-plus框架,而这个框架可以自动根据数据库表生成三层类,所以我们要有一个自动生成代码文件的类
7.1 生成代码类
需要修改部分代码,下面注释有:**TODO:(需要修改)**的地方都要修改成自己的
启动这个类之前需要先启动主类,然后才可以运行这个生成代码类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.PackageConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class TestAutoGenerate { public static void main(String[] args) {
AutoGenerator mpg = new AutoGenerator();
GlobalConfig gc = new GlobalConfig(); String projectPath = "D:\\code\\yuxing_film\\sa-token-demo-springboot"; gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("zcj"); gc.setOpen(false); gc.setFileOverride(false); gc.setIdType(IdType.ASSIGN_ID); gc.setDateType(DateType.ONLY_DATE); gc.setServiceName("%sService"); mpg.setGlobalConfig(gc);
DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/sa-token-db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("root"); mpg.setDataSource(dsc);
PackageConfig pc = new PackageConfig(); pc.setParent("com"); pc.setModuleName("pj"); pc.setEntity("entity"); pc.setMapper("mapper"); pc.setService("service"); pc.setController("controller"); mpg.setPackageInfo(pc);
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("users");
strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy);
mpg.execute(); } }
JAVA
|
生成完成,在com.pj包下自动生成了下列文件,需要把mapper/xml/UsersMapper.xml文件放到resources/mapper文件夹内,没有这个文件夹的话,就创建一个

最终的目录是这样的

二、登录认证
OK,准备工作已经做完了,现在需要进行登录认证,登陆之前我们肯定要有用户,所以我们先创建一个用户,创建一个新增用户的接口,供后续登录使用
解决启动类报错问题
引入了mybatis-plus框架之后,需要在启动类上加个注解**@MapperScan(“com.pj.mapper”)**括号内是自己的mapper包,该包要被spring扫描到,否则启动会报错.
SaTokenDemoApplication.java类
1 2 3 4 5 6 7 8
| @SpringBootApplication @MapperScan("com.pj.mapper") public class SaTokenDemoApplication { public static void main(String[] args) throws JsonProcessingException { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); } }
JAVA
|
1.创建新增用户接口
UsersController.java类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @RestController @RequestMapping("/user") public class UsersController { @Autowired private UsersService usersService;
@PostMapping("/savaUser") public R<String> savaUser(@RequestBody Users user) { return usersService.savaUser(user); }
@GetMapping("/selectAllUser") public List<Users> queryAllUser() { return usersService.list(); } }
JAVA
|
UsersServiceImpl.java类
该类是业务层,实现了UsersService接口,在此省略此接口的内容,
该类中有用到PasswordUtil.java工具类,这个工具类的作用是对明文密码进行加密,对加密密码进行解密并对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements UsersService {
@Autowired private UsersMapper usersMapper;
@Override public R<String> savaUser(Users user) { if (usersMapper.selectById(user.getUserId()) != null){ return R.failed("新增用户失败,用户已存在"); } Users users = new Users(); users.setUserId(user.getUserId()); users.setUserName(user.getUserName()); String hashedPassword = PasswordUtil.encryptPassword(user.getPassword()); users.setPassword(hashedPassword); users.setUserType(user.getUserType()); int insert = usersMapper.insert(users); if (insert <= 0){ return R.failed("新增用户失败"); } return R.ok("新增用户成功"); } }
JAVA
|
PasswordUtil.java工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class PasswordUtil {
public static String encryptPassword(String plainTextPassword) { return BCrypt.hashpw(plainTextPassword, BCrypt.gensalt()); }
public static boolean verifyPassword(String plainTextPassword, String hashedPassword) { return BCrypt.checkpw(plainTextPassword, hashedPassword); } }
JAVA
|
2.测试新增用户接口
接下来让我们重启一下项目,使用测试工具测试一下接口是否可用
调用该接口是成功的

数据库中也有这条数据,userType属性,后续使用权限校验会用到

接下来,我们就可以测试Sa-Token自带的登录认证了
3.配置拦截器
在com.pj包下创建一个config包,在这个config包内创建SaTokenConfig.java配置类,配置我们的拦截器
配置sa-token拦截器,会自动拦截登录接口之外的所有请求,检查有没有携带正确的Cookie,正确则放行,错误则爆出异常,异常码500,前端可以根据这个状态码来做一些操作.
只有登录过后,才能请求其他接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class SaTokenConfig implements WebMvcConfigurer {
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) .addPathPatterns("/**") .excludePathPatterns("/user/login"); } }
JAVA
|
4.创建login登录接口
登录接口最好是重新创建一个controller类,service接口,以及接口的Impl实现类,我们为了节约时间,就直接在UsersController层编写了
首先创建一个LoginService接口和接口的实现类LoginServiceImpl
在这两个类里面实现登录认证
UsersController.java类
在这个类后面追加一个登录接口
1 2 3 4 5 6 7
|
@PostMapping("/login") public R login(@RequestBody LoginBody loginBody){ return usersService.login(loginBody); }
JAVA
|
UsersServiceImpl接口
因为controller层调用的是usersService接口,所以我们在这里面编写具体的登录代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Override public R login(LoginBody loginBody) { if (loginBody == null || loginBody.getUserName().isEmpty() || loginBody.getPassword().isEmpty()){ return R.failed("用户名或密码为空"); } QueryWrapper<Users> usersQueryWrapper = new QueryWrapper<>(); usersQueryWrapper.eq("user_name",loginBody.getUserName()); Users user = usersMapper.selectOne(usersQueryWrapper); String password = user.getPassword(); if (!PasswordUtil.verifyPassword(loginBody.getPassword(),password)){ return R.failed("密码错误"); } StpUtil.login(loginBody.getUserName()); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); SaResult data = SaResult.data(tokenInfo); return R.ok(data); }
JAVA
|
5.测试登录接口
测试登录接口,登录成功

因为我们配置了拦截器,只有先登录之后,才可以访问查询接口

6.测试登录认证功能
此时我们重启项目,直接访问查询接口
这是没有登录的情况,直接报错了,项目每次重新都要先登录,否则无法访问其他的接口
由此可见,我们配置的sa-token登录认证功能和配置的拦截器是生效的

三、权限校验
1.改造登录代码,授予用户权限
简单介绍:
权限校验是针对不同的用户来说的,管理员用户拥有所有的接口访问权限,包括增删改查.但是对于别的用户,比如访客来说,就只能查看数据,不能进行增删改,所以我们需要针对每个用户的不同,赋予每个用户不同的权限,然后在访问改接口的时候校验权限,如果有该权限的话,就允许访问该接口,否则不行!
接下来我们对登录业务层代码进行改造,针对当前登录用户,给予不同的权限
UsersServiceImpl.java类
上帝权限
当一个账号拥有 “*
“ 权限时,他可以验证通过任何权限码 (角色认证同理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
@Override public R login(LoginBody loginBody) {
if (loginBody == null || loginBody.getUserName().isEmpty() || loginBody.getPassword().isEmpty()){ return R.failed("用户名或密码为空"); }
QueryWrapper<Users> usersQueryWrapper = new QueryWrapper<>(); usersQueryWrapper.eq("user_name",loginBody.getUserName()); Users user = usersMapper.selectOne(usersQueryWrapper); String password = user.getPassword(); if (!PasswordUtil.verifyPassword(loginBody.getPassword(),password)){ return R.failed("密码错误"); }
StpUtil.login(loginBody.getUserName());
List<String> permissionListResult = setUsernamePermission(user.getUserType()); StpUtil.getSession().set("permissionList", permissionListResult);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); SaResult data = SaResult.data(tokenInfo); return R.ok(data); }
public List<String> setUsernamePermission(Integer userType){ ArrayList<String> permissionList = new ArrayList<>(); switch (userType){ case 1: permissionList.add("*"); return permissionList; case 2: Collections.addAll(permissionList, "/select"); return permissionList; default: return null; } }
JAVA
|
2.新建一个类,实现 StpInterface接口
创建interceptor包,把这个类放到这个包里面
该类实现了StpInterface接口,Sa-Token框架在这个接口内做了一下权限校验的操作,只需要获取权限列表就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Component public class StpInterfaceImpl implements StpInterface {
@Override public List<String> getPermissionList(Object loginId, String loginType) { return (List<String>) StpUtil.getSession().get("permissionList"); }
@Override public List<String> getRoleList(Object loginId, String loginType) { List<String> list = new ArrayList<String>(); list.add("admin"); list.add("super-admin"); return list; } }
JAVA
|
3.在控制层方法内设置访问权限
UsersController.java
只需要添加一行代码即可(也可以改良为自定义注解形式去拦截接口的请求权限,sa-token也提供了注解鉴权)
更具体的权限认证操作可以访问Sa-Token官网: 权限认证 (sa-token.cc)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@PostMapping("/savaUser") public R<String> savaUser(@RequestBody Users user) { StpUtil.checkPermission("/insert"); return usersService.savaUser(user); }
@GetMapping("/selectAllUser") public List<Users> queryAllUser() { StpUtil.checkPermission("/select"); return usersService.list(); }
JAVA
|
4.测试权限校验
我们针对两个用户分别测试,管理员和访客,所以需要增加一个测试用户,用户类型设置为访客,也就是2
此时我们新增用户,却发现,报错了,500,也就是未登录,Sa-Token框架检测到我们没有登录就想要访问新增接口,这是不允许的,所以我们需要先登录

登录过后,在新增用户.成功

4.1 测试管理员权限
此时我们有了两个用户,我们先测试一下管理员的权限,测试方法很简单,用管理员的账号登录,然后访问新增和查询接口,看看是不是都能访问成功


可以看到,test2用户是新增成功的,
管理员拥有*上帝权限,可以访问所有的接口,以 /selectAllUser 为例:

接下来,让我们测试访客权限
4.2 测试访客权限
测试方法也很简单,用访客的账号登录,然后访问新增和查询接口,看看是不是都能访问成功
登录成功

没有权限,添加失败

此时我们再访问一下查询接口
是访问成功的,和管理员权限访问的结果一样

可以看到,我们设置的权限校验功能是成功的。
上述内容只是做简单的验证,实际情况根据业务所需去设计和使用sa-token。
比如参考如下方案,做统一认证中心,从源头上解决单点登录的场景需求。
附:SSO单点登录统一认证中心
Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题:
系统架构 |
采用模式 |
简介 |
文档链接 |
前端同域 + 后端同 Redis |
模式1 |
共享 Cookie 同步会话 |
文档、示例 |
前端不同域 + 后端同 Redis |
模式2 |
URL重定向传播会话 |
文档、示例 |
前端不同域 + 后端不同 Redis |
模式3 |
Http请求获取会话 |
文档、示例 |
- 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:
c1.domain.com
、c2.domain.com
、c3.domain.com
。
- 后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了
[权限缓存与业务缓存分离]
的解决方案,详情: Alone独立Redis插件。
- 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)。
模式1:前端同域、后端同Redis

模式2:前端不同域、后端同Redis

模式3:前端不同域、后端不同Redis
在模式2中我们只需要将需要同步的资料放到 SaSession 即可,但是在模式3中两端不再连接同一个 Redis,这时候我们需要通过 http 接口来同步信息。
无刷单点注销

所谓单点登录,其本质就是多个系统之间的会话共享
。
当我们理解这一点之后,三种模式的工作原理也浮出水面:
- 模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。
- 模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。
- 模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。
前后端分离架构下的整合
https://sa-token.cc/doc.html#/sso/sso-h5