Sa-Token
SpringBoot集成初体验
依赖引入
SpringBoot 3.x版本引入
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
3版本以下引入
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
yml配置
在application.yml
中加入以下配置
server:
# 端口
port: 8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
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: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
controller测试
package com.wl.satoken.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author NoDreamJava
* @Date 2023-08-02 16:43
* @Version 1.0
*/
@RestController
@RequestMapping("/auth")
public class UserController {
@RequestMapping("/login")
public String login(String username, String password) {
if (username.equals("admin") && password.equals("123456")) {
StpUtil.login(10001);
return "登录成功";
} else {
return "登录失败";
}
}
// 查询登陆状态
@RequestMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
}
访问地址:http://localhost:8081/auth/login?username=admin&password=123456
检查是否登陆:http://localhost:8081/auth/isLogin
全局异常处理
创建一个全局异常拦截器,统一返回给前端的格式
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
登录认证
在完成登陆后会发现,token被自动写入到浏览器中了
这是因为 sa-Token用了 Cookie 自动注入的特性,省略了手写返回 token 的代码
登录和注销
// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
会话查询
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();
// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型
// ---------- 指定未登录情形下返回的默认值 ----------
// 获取当前会话账号id, 如果未登录,则返回 null
StpUtil.getLoginIdDefaultNull();
// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);
token查询
// 获取当前会话的 token 值
StpUtil.getTokenValue();
// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();
// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();
// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
测试
加入两个接口测试
// 查询token信息
@RequestMapping("/getTokenInfo")
public SaResult getTokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
// 注销
@RequestMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
访问:http://localhost:8081/auth/getTokenInfo查询token信息
访问:http://localhost:8081/auth/logout注销当前用户
权限认证
权限认证本质就是来控制一个账号是否拥有操作权限,深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。
问题的核心就是两个:
- 如何获取一个账号所拥有的权限码集合?
- 本次操作需要验证的权限码是哪个?
获取当前账号权限码集合
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
校验权限
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();
// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user.add");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
角色校验
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();
// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
权限通配符
sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*
的权限时,art.add
、art.delete
、art.update
都将匹配通过
// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add"); // true
StpUtil.hasPermission("art.update"); // true
StpUtil.hasPermission("goods.add"); // false
// 当拥有 *.delete 权限时
StpUtil.hasPermission("art.delete"); // true
StpUtil.hasPermission("user.delete"); // true
StpUtil.hasPermission("user.update"); // false
// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js"); // true
StpUtil.hasPermission("index.css"); // false
StpUtil.hasPermission("index.html"); // false
测试
建一张用户表
CREATE TABLE `user` (
`id` int NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`permissions` varchar(255) DEFAULT NULL,
`role` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
插入一条数据
INSERT INTO `sys`.`user`(`id`, `user_name`, `password`, `permissions`, `role`) VALUES (1001, 'wl', '123456', 'user.add,user.update', 'leader');
实现获取账号权限码集合接口
package com.wl.sa.auth;
/**
* @Author NoDreamJava
* @Date 2023-08-15 10:13
* @Version 1.0
*/
import cn.dev33.satoken.stp.StpInterface;
import com.wl.sa.mapper.UserMapper;
import com.wl.sa.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
@Autowired
private UserMapper userMapper;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
User user = userMapper.selectById(Long.parseLong(loginId.toString()));
return Arrays.stream(user.getPermissions().split(",")).collect(Collectors.toList());
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
User user = userMapper.selectById(Long.parseLong(loginId.toString()));
return Arrays.stream(user.getRole().split(",")).collect(Collectors.toList());
}
}
添加测试接口
@RequestMapping("/getPermission")
public SaResult getPermission() {
// 查询权限信息 ,如果当前会话未登录,会返回一个空集合
List<String> permissionList = StpUtil.getPermissionList();
System.out.println("当前登录账号拥有的所有权限:" + permissionList);
return SaResult.ok()
.set("permissionList", permissionList);
}
访问地址:http://localhost:8081/auth/getPermission验证
踢人下线
踢人下线,核心操作就是找到指定 loginId
对应的 Token
,并设置其失效。
强制注销
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
踢人下线
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销 和 踢人下线 的区别在于:
- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
小案例测试
添加查询角色接口,只有登录才可以访问
// 登录校验:只有登录之后才能进入该方法
@SaCheckLogin
@RequestMapping("/getUserInfo")
public SaResult getUserInfo() {
List<User> userList = userMapper.selectList(null);
return SaResult.ok()
.set("userList", userList);
}
// 根据id踢人下线
@RequestMapping("/kickById")
public SaResult kickById(String loginId) {
StpUtil.kickout(loginId);
return SaResult.ok();
}
先访问接口 /kickById
踢人下线,然后访问 /getUserInfo
,发现提示已经被提下线
注解鉴权
注解鉴权 —— 优雅的将鉴权与业务代码分离!
@SaCheckLogin
: 登录校验 —— 只有登录之后才能进入该方法。@SaCheckRole("admin")
: 角色校验 —— 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add")
: 权限校验 —— 必须具有指定权限才能进入该方法。@SaCheckSafe
: 二级认证校验 —— 必须二级认证之后才能进入该方法。@SaCheckBasic
: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。@SaIgnore
:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckDisable("comment")
:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
注册拦截器
package com.wl.sa.interceptor;
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 NoDreamJava
* @Date 2023-08-15 15:17
* @Version 1.0
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
使用注解鉴权
// 权限校验:必须具有指定权限才能进入该方法
@SaCheckPermission("user.add")
@RequestMapping("/addByPermission")
public String add() {
return "用户增加";
}
// 角色校验:必须具有指定角色才能进入该方法
@SaCheckRole("admin")
@RequestMapping("/addByRole")
public String addByRole() {
return "用户增加";
}
目前数据库表中的 权限列表为 user.add,user.update 角色列表为leader
访问权限接口:http://localhost:8081/auth/addByPermission
访问角色接口:http://localhost:8081/auth/addByRole
设定校验模式
@SaCheckRole
与@SaCheckPermission
注解可设置校验模式
// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("/atJurOr")
@SaCheckPermission(value = {"user.add", "user.all", "user.delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
访问接口:http://localhost:8081/auth/atJurOr
mode有两种取值:
SaMode.AND
,标注一组权限,会话必须全部具有才可通过校验。SaMode.OR
,标注一组权限,会话只要具有其一即可通过校验。
角色权限双重“or校验”
一个接口在具有权限 user.add
或角色 admin
时可以调通。怎么写?
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
访问:http://localhost:8081/auth/userAdd
orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:
- 写法一:
orRole = "admin"
,代表需要拥有角色 admin 。 - 写法二:
orRole = {"admin", "manager", "staff"}
,代表具有三个角色其一即可。 - 写法三:
orRole = {"admin, manager, staff"}
,代表必须同时具有三个角色。
忽略认证
使用 @SaIgnore
可表示一个接口忽略认证
- @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
- @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
- @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权
批量注解鉴权
使用 @SaCheckOr
表示批量注解鉴权
// 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
@SaCheckOr(
login = @SaCheckLogin,
role = @SaCheckRole("admin"),
permission = @SaCheckPermission("user.add"),
safe = @SaCheckSafe("update-password"),
basic = @SaCheckBasic(account = "sa:123456"),
disable = @SaCheckDisable("submit-orders")
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
路由拦截鉴权
简单鉴权配置
注册了一个基于 StpUtil.checkLogin()
的登录校验拦截器,并且排除了/auth/login
接口用来开放登录(除了/auth/login
以外的所有接口都需要登录才能访问)。
/**
* @Author NoDreamJava
* @Date 2023-08-15 15:17
* @Version 1.0
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler-> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/auth/login");
}
}
自定义校验规则
自定义认证规则:new SaInterceptor(handle -> StpUtil.checkLogin())
是最简单的写法,代表只进行登录校验功能。
可以使用自定义校验函数来实现复杂校验
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,定义详细认证规则
registry.addInterceptor(new SaInterceptor(handler -> {
// 指定一条 match 规则
SaRouter
.match("/**") // 拦截的 path 列表,可以写多个 */
.notMatch("/user/doLogin") // 排除掉的 path 列表,可以写多个
.check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式
// 根据路由划分模块,不同模块不同鉴权
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
})).addPathPatterns("/**");
}
}
忽略路由拦截认证
我们可以使用 @SaIgnore
注解,忽略掉路由拦截认证
@SaIgnore
@RequestMapping("/user/getList")
public SaResult getList() {
System.out.println("------------ 访问进来方法");
return SaResult.ok();
}
Session会话
在 Sa-Token 中,Session 分为三种,分别是:
Account-Session
: 指的是框架为每个 账号id 分配的 SessionToken-Session
: 指的是框架为每个 token 分配的 SessionCustom-Session
: 指的是以一个 特定的值 作为SessionId,来分配的 Session
有关 Account-Session 与 Token-Session 的详细区别,可参考:Session模型详解
Account-Session
// 获取当前账号 id 的 Account-Session (必须是登录后才能调用)
StpUtil.getSession();
// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSession(true);
// 获取账号 id 为 10001 的 Account-Session
StpUtil.getSessionByLoginId(10001);
// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSessionByLoginId(10001, true);
// 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null
StpUtil.getSessionBySessionId("xxxx-xxxx");
Token-Session
// 获取当前 Token 的 Token-Session 对象
StpUtil.getTokenSession();
// 获取指定 Token 的 Token-Session 对象
StpUtil.getTokenSessionByToken(token);
Custom-Session
// 查询指定key的Session是否存在
SaSessionCustomUtil.isExists("goods-10001");
// 获取指定key的Session,如果没有,则新建并返回
SaSessionCustomUtil.getSessionById("goods-10001");
// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回
SaSessionCustomUtil.getSessionById("goods-10001", false);
// 删除指定key的Session
SaSessionCustomUtil.deleteSessionById("goods-10001");
集成Redis
sa-Token 默认将数据保存在内存中,此模式读写速度最快,但是也有以下缺点:
- 重启后数据会丢失
- 无法在分布式环境中共享数据
以下是框架提供的 Redis 集成包:
jdk默认序列化方式(二选一)
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.35.0.RC</version>
</dependency>
jackjson序列化方式(二选一)
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.35.0.RC</version>
</dependency>
连接池
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
yml配置redis
spring:
# redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
集成redis后,token信息无需自己写入redis,框架自动保存
前后端分离
在前后端分离模式中,不能后端控制写入了,需要前端写入,后端只需返回token信息即可。
修改一下接口
// 登陆
@RequestMapping("/login")
public SaResult login(String username, String password) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", username)
.eq("password", password);
User user = userMapper.selectOne(wrapper);
StpUtil.login(user.getId());
// 获取token信息
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return SaResult.data(tokenInfo);
}
自定义token风格
内置风格
Sa-Token 默认的 token 生成策略是 uuid 风格
// 1. token-style=uuid —— uuid风格 (默认风格)
"623368f0-ae5e-4475-a53f-93e4225f16ae"
// 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线
"6fd4221395024b5f87edd34bc3258ee8"
// 3. token-style=random-32 —— 随机32位字符串
"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W"
// 4. token-style=random-64 —— 随机64位字符串
"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc"
// 5. token-style=random-128 —— 随机128位字符串
"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj"
// 6. token-style=tik —— tik风格
"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
自定义Token生成策略
只需要重写 SaStrategy
策略类的 createToken
算法即可
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenConfigure {
/**
* 重写 Sa-Token 框架内部算法策略
*/
@Autowired
public void rewriteSaStrategy() {
// 重写 Token 生成策略
SaStrategy.instance.createToken = (loginId, loginType) -> {
return SaFoxUtil.getRandomString(150); // 随机150位长度字符串
};
}
}
自定义Token前缀
在某些系统中,前端提交token时会在前面加个固定的前缀
{
"satoken": "Bearer xxxx-xxxx-xxxx-xxxx"
}
此时后端如果不做任何特殊处理,框架将会把Bearer
视为token的一部分,无法正常读取token信息,导致鉴权失败。
这时只需要在yml配置前缀即可
sa-token:
# token前缀
token-prefix: Bearer
记住我模式
sa-Token默认是记住我模式,在浏览器写入一个持久Cookie
储存 Token,此时用户即使重启浏览器 Token 依然有效
如果想取消记住我则在登陆成功后,StpUtil.login()方法第二个参数置为false即可
StpUtil.login(10001, false);
前后端分离场景下,需要前端进行记住我操作
模拟他人&身份切换
操作其他账号
// 获取指定账号10001的`tokenValue`值
StpUtil.getTokenValueByLoginId(10001);
// 将账号10001的会话注销登录
StpUtil.logout(10001);
// 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回
StpUtil.getSessionByLoginId(10001);
// 获取账号10001的Session对象, 如果session尚未创建, 则返回null
StpUtil.getSessionByLoginId(10001, false);
// 获取账号10001是否含有指定角色标识
StpUtil.hasRole(10001, "super-admin");
// 获取账号10001是否含有指定权限码
StpUtil.hasPermission(10001, "user:add");
临时身份切换
// 将当前会话[身份临时切换]为其它账号(本次请求内有效)
StpUtil.switchTo(10044);
// 此时再调用此方法会返回 10044 (我们临时切换到的账号id)
StpUtil.getLoginId();
// 结束 [身份临时切换]
StpUtil.endSwitch();
案例测试
往数据库插入一条数据
INSERT INTO `sys`.`user`(`id`, `user_name`, `password`, `permissions`, `role`) VALUES (1002, 'admin', '123456', 'user.add,user.update,user.delete', 'admin');
接口代码
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
// 查看其他账号的token值 保证只有admin角色才有权限查看
@RequestMapping("/getTokenByLoginId")
@SaCheckRole("admin")
public SaResult getTokenByLoginId(String loginId) {
return SaResult.data(StpUtil.getTokenValueByLoginId(loginId));
}
// 切换角色
@RequestMapping("/switchRole")
public SaResult switchRole(String loginId) {
StpUtil.switchTo(loginId);
return SaResult.ok();
}
// 获取当前登陆id
@RequestMapping("/getLoginId")
public SaResult getLoginId() {
return SaResult.data(StpUtil.getLoginId());
}
先调用登录接口:http://localhost:8081/auth/login?username=admin&password=123456
再获取其他账号信息的接口:http://localhost:8081/auth/getTokenByLoginId?loginId=1001
切换账号:http://localhost:8081/auth/switchRole?loginId=1001
再去访问获取token信息的接口:http://localhost:8081/auth/getTokenByLoginId?loginId=1001
获取当前账号id:http://localhost:8081/auth/switchRole?loginId=1001
同端互斥
同端互斥就是在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。
在sa-Token中如何做到同端互斥?
指定设备类型登陆
首先在配置文件中,将 isConcurrent
配置为false,然后调用登录等相关接口时声明设备类型即可:
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");
指定设备类型强制注销
// 指定`账号id`和`设备类型`进行强制注销
StpUtil.logout(10001, "PC");
查询当前设备类型
// 返回当前token的登录设备类型
StpUtil.getLoginDevice();
id反查token
// 获取指定loginId指定设备类型端的tokenValue
StpUtil.getTokenValueByLoginId(10001, "APP");
案例测试
接口代码
// 登陆
@RequestMapping("/loginDevice")
public SaResult loginDevice(String username, String password) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", username)
.eq("password", password);
User user = userMapper.selectOne(wrapper);
StpUtil.login(user.getId(),"PC");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return SaResult.data(tokenInfo);
}
访问:http://localhost:8081/auth/loginDevice?username=admin&password=123456
再访问:http://localhost:8081/auth/loginDevice?username=admin&password=123456
发现token值被更新了,再看控制台打印
账号封禁
账号封禁
在部分场景下,我们还需要将其 账号封禁,以防止其再次登录。
// 封禁指定账号
StpUtil.disable(10001, 86400);
对于正在登陆的账号,封禁并不会立即使他掉线,需要先踢下线再封禁的策略
// 先踢下线
StpUtil.kickout(10001);
// 再封禁账号
StpUtil.disable(10001, 86400);
待到下次登录时,我们先校验一下这个账号是否已被封禁:
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 通过校验后,再进行登录:
StpUtil.login(10001);
// 封禁指定账号
StpUtil.disable(10001, 86400);
// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001);
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2
StpUtil.getDisableTime(10001);
// 解除封禁
StpUtil.untieDisable(10001);
分类封禁
有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务
有关分类封禁的所有方法
// 封禁:指定账号的指定服务
StpUtil.disable(10001, "<业务标识>", 86400);
// 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001, "<业务标识>");
// 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001, "<业务标识>");
// 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁)
StpUtil.getDisableTime(10001, "<业务标识>");
// 解封:指定账号的指定服务
StpUtil.untieDisable(10001, "<业务标识>");
阶梯封禁
对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 “阶梯” 一般有两种形式:
- 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延……
- 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等……
相关接口
// 阶梯封禁,参数:封禁账号、封禁级别、封禁时间
StpUtil.disableLevel(10001, 3, 10000);
// 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2)
StpUtil.getDisableLevel(10001);
// 判断:指定账号是否已被封禁到指定级别,返回 true 或 false
StpUtil.isDisableLevel(10001, 3);
// 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 `DisableServiceException`
StpUtil.checkDisableLevel(10001, 2);
// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间
StpUtil.disableLevel(10001, "comment", 3, 10000);
// 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2)
StpUtil.getDisableLevel(10001, "comment");
// 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false
StpUtil.isDisableLevel(10001, "comment", 3);
// 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常
StpUtil.checkDisableLevel(10001, "comment", 2);
使用注解完成封禁校验
@SaCheckDisable // 校验当前账号是否被封禁
@SaCheckDisable("comment") // 校验当前账号是否被封禁 comment 服务
@SaCheckDisable({"comment", "place-order", "open-shop"}) // 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值
@SaCheckDisable(level = 5) // 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常
@SaCheckDisable(value = "comment", level = 5) // 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级
案例测试
// 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法
@SaCheckDisable
@RequestMapping("/send")
public SaResult send() {
return SaResult.ok();
}
// 封禁账号接口
@RequestMapping("/disable/{loginId}")
public SaResult disableById(@PathVariable String loginId) {
StpUtil.disable(loginId, 120);
return SaResult.ok();
}
先登陆账号1002,封禁账号1001,访问:http://localhost:8081/auth/disable/1001
再登陆账号1001,访问接口:http://localhost:8081/auth/send
密码加密
摘要加密
md5、sha1、sha256
// md5加密
SaSecureUtil.md5("123456");
// sha1加密
SaSecureUtil.sha1("123456");
// sha256加密
SaSecureUtil.sha256("123456");
对称加密
AES加密
// 定义秘钥和明文
String key = "123456";
String text = "Sa-Token 一个轻量级java权限认证框架";
// 加密
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);
// 解密
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);
非对称加密
RSA加密
// 定义私钥和公钥
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";
// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);
// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2);
调用以下方法生成公钥和私钥
// 生成一对公钥和私钥,其中Map对象 (private=私钥, public=公钥)
System.out.println(SaSecureUtil.rsaGenerateKeyPair());
Bcrypt加密
由它加密的文件可在所有支持的操作系统和处理器上进行转移
它的口令必须是8至56个字符,并将在内部被转化为448位的密钥
// 使用方法
String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
// 使用checkpw方法检查被加密的字符串是否与原始字符串匹配:
BCrypt.checkpw(candidate_password, stored_hash);
// gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度:
String strong_salt = BCrypt.gensalt(10);
String stronger_salt = BCrypt.gensalt(12);
案例测试
新增注册接口
// 注册接口
@RequestMapping("/register")
public SaResult register(String username, String password) {
// 使用 BCrypt加密
String salt = BCrypt.gensalt();
User user = new User();
user.setUserName(username);
user.setPassword(BCrypt.hashpw(password, salt));
userMapper.insert(user);
return SaResult.ok();
}
// 登陆
@RequestMapping("/loginEncodePassword")
public SaResult loginEncodePassword(String username, String password) {
// 根据用户名查询用户信息
User user = userMapper.selectOne(new QueryWrapper<User>().eq("user_name", username));
if (user == null || !BCrypt.checkpw(password, user.getPassword())) {
throw new NullPointerException("用户名或密码错误");
}
StpUtil.login(user.getId(), "PC");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return SaResult.data(tokenInfo);
}
访问注册接口:http://localhost:8081/auth/register?username=super-admin&password=123456
访问登录接口:http://localhost:8081/auth/loginEncodePassword?username=super-admin&password=123456
会话查询
具体api
// 查询所有已登录的 Token
StpUtil.searchTokenValue(String keyword, int start, int size, boolean sortType);
// 查询所有账号 Session 会话
StpUtil.searchSessionId(String keyword, int start, int size, boolean sortType);
// 查询所有令牌 Session 会话
StpUtil.searchTokenSessionId(String keyword, int start, int size, boolean sortType);
keyword
: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。start
: 数据开始处索引。size
: 要获取的数据条数 (值为-1代表一直获取到末尾)。sortType
: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。
简单例子
// 查询 value 包括 1000 的所有 token,结果集从第 0 条开始,返回 10 条
List<String> tokenList = StpUtil.searchTokenValue("1000", 0, 10, true);
for (String token : tokenList) {
System.out.println(token);
}
Http Basic 认证
Http Basic 是 http 协议中最基础的认证方式。
启用认证
// http Basic 认证
@RequestMapping("/basicAuth")
public SaResult basicAuth() {
SaBasicUtil.check("admin:123456");
return SaResult.ok();
}
在访问接口:http://localhost:8081/auth/basicAuth后会弹出一个对话框
其他启用方式
// 对当前会话进行 Basic 校验,账号密码为 yml 配置的值(例如:sa-token.basic=sa:123456)
SaBasicUtil.check();
// 对当前会话进行 Basic 校验,账号密码为:`sa / 123456`
SaBasicUtil.check("sa:123456");
// 以注解方式启用 Basic 校验
@SaCheckBasic(account = "sa:123456")
@RequestMapping("test3")
public SaResult test3() {
return SaResult.ok();
}
// 在全局拦截器 或 过滤器中启用 Basic 认证
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**").addExclude("/favicon.ico")
.setAuth(obj -> {
SaRouter.match("/test/**", () -> SaBasicUtil.check("sa:123456"));
});
}
全局侦听器
Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。
开启日志打印
在yml文件中配置开启日志打印
sa-token:
# 是否输出操作日志
is-log: true
自定义侦听器实现
新建实现类
package com.wl.sa.listener;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 自定义侦听器的实现
*/
@Component
public class MySaTokenListener implements SaTokenListener {
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(loginId + " - " +nowTime+"登陆成功,token为:" + tokenValue);
}
/** 每次注销时触发 */
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doLogout");
}
/** 每次被踢下线时触发 */
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doKickout");
}
/** 每次被顶下线时触发 */
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doReplaced");
}
/** 每次被封禁时触发 */
@Override
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
System.out.println("---------- 自定义侦听器实现 doDisable");
}
/** 每次被解封时触发 */
@Override
public void doUntieDisable(String loginType, Object loginId, String service) {
System.out.println("---------- 自定义侦听器实现 doUntieDisable");
}
/** 每次二级认证时触发 */
@Override
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
System.out.println("---------- 自定义侦听器实现 doOpenSafe");
}
/** 每次退出二级认证时触发 */
@Override
public void doCloseSafe(String loginType, String tokenValue, String service) {
System.out.println("---------- 自定义侦听器实现 doCloseSafe");
}
/** 每次创建Session时触发 */
@Override
public void doCreateSession(String id) {
System.out.println("---------- 自定义侦听器实现 doCreateSession");
}
/** 每次注销Session时触发 */
@Override
public void doLogoutSession(String id) {
System.out.println("---------- 自定义侦听器实现 doLogoutSession");
}
/** 每次Token续期时触发 */
@Override
public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
System.out.println("---------- 自定义侦听器实现 doRenewTimeout");
}
}
全局过滤器
Sa-Token全局过滤器默认处于关闭状态,开启需要手动开启
/**
* [Sa-Token 权限认证] 配置类
*/
@Configuration
public class SaTokenConfigure {
/**
* 注册 [Sa-Token全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 拦截路由 与 放行路由
.addInclude("/**").addExclude("/favicon.ico") /* 排除掉 /favicon.ico */
// 认证函数: 每次请求执行
.setAuth(obj -> {
System.out.println("---------- 进入Sa-Token全局认证 -----------");
// 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());
// 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
System.out.println("---------- 进入Sa-Token异常处理 -----------");
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
.setBeforeAuth(r -> {
// ---------- 设置一些安全响应头 ----------
SaHolder.getResponse()
// 服务器名称
.setServer("sa-server")
// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
.setHeader("X-Frame-Options", "SAMEORIGIN")
// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
.setHeader("X-XSS-Protection", "1; mode=block")
// 禁用浏览器内容嗅探
.setHeader("X-Content-Type-Options", "nosniff")
;
})
;
}
}
多账号认证
有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 user表
和 admin表
, 在这种场景下,如果两套账号我们都使用 StpUtil
类的API进行登录鉴权,那么势必会发生逻辑冲突。
比如说,对于原生StpUtil
类,我们只做admin账号
权限认证,而对于user账号
,我们则:
- 新建一个新的权限认证类,比如:
StpUserUtil.java
。 - 将
StpUtil.java
类的全部代码复制粘贴到StpUserUtil.java
里。 - 更改一下其
LoginType
, 比如:
public class StpUserUtil {
/**
* 账号体系标识
*/
public static final String TYPE = "user"; // 将 LoginType 从`login`改为`user`
// 其它代码 ...
}复制到剪贴板错误复制成功12345678910
- 接下来就可以像调用
StpUtil.java
一样调用StpUserUtil.java
了,这两套账号认证的逻辑是完全隔离的。
单点登陆
单点登陆简介
参考:单点登陆简介
搭建统一认证中心
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.35.0.RC</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.26</version>
</dependency>
开发认证接口
新建 SsoServerController
,用于对外开放接口:
package com.example.sasso.controller;
import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.sso.SaSsoProcessor;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.dtflys.forest.Forest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Sa-Token-SSO Server端 Controller
*/
@RestController
public class SsoServerController {
/*
* SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口)
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.serverDister();
}
/**
* 配置SSO相关参数
*/
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置:未登录时返回的View
sso.setNotLoginView(() -> {
String msg = "当前会话在SSO-Server端尚未登录,请先访问"
+ "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+ "进行登录之后,刷新页面开始授权";
return msg;
});
// 配置:登录处理函数
sso.setDoLoginHandle((name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据进行登录
if("sa".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
});
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
sso.setSendHttp(url -> {
try {
// 发起 http 请求
System.out.println("------ 发起请求:" + url);
return Forest.get(url).executeAsString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
});
}
}
yml配置
# 端口
server:
port: 9000
# Sa-Token 配置
sa-token:
# ------- SSO-模式一相关配置 (非模式一不需要配置)
# cookie:
# 配置 Cookie 作用域
# domain: stp.com
# ------- SSO-模式二相关配置
sso:
# Ticket有效期 (单位: 秒),默认五分钟
ticket-timeout: 300
# 所有允许的授权回调地址
allow-url: "*"
# ------- SSO-模式三相关配置 (下面的配置在使用SSO模式三时打开)
# 是否打开模式三
is-http: true
sign:
# API 接口调用秘钥
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明)
spring:
# Redis配置 (SSO模式一和模式二使用Redis来同步会话)
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
forest:
# 关闭 forest 请求日志打印
log-enabled: false
测试
访问:http://localhost:9000/sso/auth
默认账号密码为:sa / 123456
SSO模式一共享Cookie同步会话
设计思路
如果我们的多个系统可以做到:前端同域、后端同Redis,那么便可以使用 [共享Cookie同步会话]
的方式做到单点登录。
首先我们分析一下多个系统之间,为什么无法同步登录状态?
- 前端的
Token
无法在多个系统下共享。 - 后端的
Session
无法在多个系统间共享。
所以单点登录第一招,就是对症下药:
- 使用
共享Cookie
来解决 Token 共享问题。 - 使用
Redis
来解决 Session 共享问题。
所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名stp.com
下的Cookie,在s1.stp.com
、s2.stp.com
等子域名都是可以共享访问的。
而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 [权限缓存与业务缓存分离] 的解决方案,详情戳:Alone独立Redis插件。
准备工作
在hosts文件里添加以下内容
127.0.0.1 sso.stp.com
127.0.0.1 s1.stp.com
127.0.0.1 s2.stp.com
127.0.0.1 s3.stp.com
指定Cookie作用域
在sso.stp.com
访问服务器,其Cookie也只能写入到sso.stp.com
下,为了将Cookie写入到其父级域名stp.com
下,我们需要更改 SSO-Server 端的 yml 配置
sa-token:
cookie:
# 配置 Cookie 作用域
domain: stp.com
这个时候 server认证中心yml
配置应该是
# 端口
server:
port: 9000
# Sa-Token 配置
sa-token:
# ------- SSO-模式一相关配置 (非模式一不需要配置)
cookie:
# 配置 Cookie 作用域
domain: stp.com
spring:
# Redis配置 (SSO模式一和模式二使用Redis来同步会话)
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
forest:
# 关闭 forest 请求日志打印
log-enabled: false
搭建Client端项目
添加依赖
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.35.0.RC</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.35.0.RC</version>
</dependency>
新建controller
package com.wl.sassoclient.controller;
import cn.dev33.satoken.sso.SaSsoManager;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SsoClientController {
// SSO-Client端:首页
@RequestMapping("/")
public String index() {
String authUrl = SaSsoManager.getConfig().splicingAuthUrl();
String solUrl = SaSsoManager.getConfig().splicingSloUrl();
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='" + authUrl + "?mode=simple&redirect=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
return str;
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
yml配置
# 端口
server:
port: 9001
# Sa-Token 配置
sa-token:
# SSO-相关配置
sso:
# SSO-Server端-单点登录授权地址
auth-url: http://sso.stp.com:9000/sso/auth
# SSO-Server端-单点注销地址
slo-url: http://sso.stp.com:9000/sso/signout
# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
alone-redis:
# Redis数据库索引
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
访问测试
启动项目访问三个应用端:
都返回
点击登录,被重定向至SSO认证中心
点击登陆,刷线页面,另外两个Client端也登陆了
SSO模式二URL重定向传播会话
如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 [URL重定向传播会话]
的方式做到单点登录。
首先我们再次复习一下,多个系统之间为什么无法同步登录状态?
- 前端的
Token
无法在多个系统下共享。 - 后端的
Session
无法在多个系统间共享。
关于第二点,我们已在 "SSO模式一" 章节中阐述,使用 Alone独立Redis插件 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。
而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。
-
用户在 子系统 点击
[登录]
按钮。 -
用户跳转到子系统登录接口
/sso/login
,并携带back参数
记录初始页面URL。
- 形如:
http://{sso-client}/sso/login?back=xxx
- 形如:
-
子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带
redirect参数
记录子系统的登录页URL。- 形如:
http://{sso-server}/sso/auth?redirect=xxx?back=xxx
- 形如:
-
用户进入了 SSO认证中心 的登录页面,开始登录。
-
用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口
/sso/login
,并携带
ticket码
参数。- 形如:
http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx
- 形如:
-
子系统根据
ticket码
从SSO-Redis
中获取账号id,并在子系统登录此账号会话。 -
子系统将用户再次重定向至最初始的
back
页面。
整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击[登录]
按钮,由于此用户在SSO认证中心已有会话存在, 所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。
1.修改hosts文件
添加以下内容
127.0.0.1 sa-sso-server.com
127.0.0.1 sa-sso-client1.com
127.0.0.1 sa-sso-client2.com
127.0.0.1 sa-sso-client3.com
2.注释配置认证中心 server 的yml以下配置
sa-token:
cookie:
# 配置 Cookie 作用域
domain: stp.com
这个时候server的yml
应该为
# 端口
server:
port: 9000
# Sa-Token 配置
sa-token:
# ------- SSO-模式二相关配置
sso:
# Ticket有效期 (单位: 秒),默认五分钟
ticket-timeout: 300
# 所有允许的授权回调地址
allow-url: "*"
spring:
# Redis配置 (SSO模式一和模式二使用Redis来同步会话)
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
forest:
# 关闭 forest 请求日志打印
log-enabled: false
3.引入依赖
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.35.0.RC</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.35.0.RC</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.35.0.RC</version>
</dependency>
创建sso-client端认证接口
/**
* Sa-Token-SSO Client端 Controller
*/
@RestController
public class SsoClientController {
// 首页
@RequestMapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client 应用端</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a> " +
"<a href='/sso/logout?back=self'>注销</a></p>";
return str;
}
/*
* SSO-Client端:处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址,接受参数:back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
}
yml配置
# 端口
server:
port: 9001
# sa-token配置
sa-token:
# SSO-相关配置
sso:
# SSO-Server端 统一认证地址
auth-url: http://sa-sso-server.com:9000/sso/auth
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
alone-redis:
# Redis数据库索引 (默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
测试
启动项目,访问:http://sa-sso-client1.com:9001/
首次打开,提示当前未登录,我们点击登录
按钮,页面会被重定向到登录中心
点击登录,刷新页面
再访问:http://sa-sso-client1.com:9001/,发现还是未登陆状态,点击登录,会直接提示登录成功
这样就完成了不同域, 同redis的单点登陆。