尼恩说在前面
用户权限认证 如何设计? 用户SSO 单点登录 如何设计? 你们的 Token 和 Session 怎么设计的? Token 和 Session 是什么关系? Token 如何和 OAuth2.0 结合? 前后端分离架构下, SSO 单点登录如何设计?
SpringSecurity& Auth2.0 学习圣经:从入门到精通 SpringSecurity& Auth2.0 史上最牛的 权限系统,如何设计?来了一个 Sa-Token学习圣经
本文目录
- 尼恩说在前面
- 1 基本概念
- 1.1 认证
- 1.2 授权(鉴权)
- 2 Sa-Token简介
- 2.1 介绍
- 2.2 功能一览
- 3 Sa-Token认证
- 3.1 登录认证
- 3.2 踢人下线
- 3.3 全局异常处理
- 3.4 二级认证
- 3.5 同端互斥登录
- 3.6 Http Basic/Digest 认证
-3.6.1 HttpBasic认证
-3.6.2 Http Digest 认证
- 4 Sa-Token授权(鉴权)
- 4.1 权限认证
-4.1.1 获取当前账号权限码集合
-4.1.2 权限校验
-4.1.3 角色校验
-4.1.4 拦截全局异常
-4.1.5 权限通配符
-4.1.6 如何把权限精确到按钮级?
- 4.2 注解鉴权
-4.2.1 注册拦截器
-4.2.2 使用注解鉴权
-4.2.3 设定校验模式
-4.2.4 角色权限双重 “or校验”
-4.2.5 忽略认证
-4.2.6 批量注解鉴权
- 4.3路由拦截鉴权
-4.3.1 注册 Sa-Token 路由拦截器
-4.3.2 校验函数详解
-4.3.3 匹配特征详解
-4.3.4 提前退出匹配链
-4.3.5 使用free打开一个独立的作用域
-4.3.6 使用注解忽略掉路由拦截校验
-4.3.7 关闭注解校验
- 5 Sa-Token 进阶
- 5.1 Session会话
-5.1.1 Session模型结构图
-5.1.2 Account-Session
-5.1.3 Token-Session
-5.1.4 Custom-Session
-5.1.5 未登录场景下获取 Token-Session
- 5.2 身份切换
- 5.3 [记住我] 模式
- 5.4 账号封禁
- 5.5 密码加密
- 5.6 全局侦听器
-5.6.1 工作原理
-5.6.2 自定义侦听器实现
-5.6.3 其它注意点
- 6 微服务架构下安全认证
- 6.1 微服务分布式Session认证架构方案
- 6.2 集成Redis
- 6.3 集成jwt
- 6.4 前后端分离(无Cookie模式)
- 6.5 内部服务外网隔离
-6.5.1 需求场景
-6.5.2 网关转发鉴权
-6.5.3 服务间内部调用鉴权
-6.5.4 Same-Token 模块详解
- 7 单点登录(SSO)
- 7.1 单点登录架构选型
- 7.2 认证中心 SSO-Server
- 7.3 SSO接口详解
-7.3.1 SSO-Server 认证中心接口
-7.3.2 SSO-Client 接口详解
- 7.4 模式一 共享Cookie同步会话
-7.4.1 设计思路
-7.4.2 SSO-Server
-7.4.3 SSO-Client
-7.4.4 访问测试
- 7.5 模式二 URL重定向传播会话
-7.5.1 设计思路
-7.5.2 SSO-Server
-7.5.3 SSO-Client
-7.5.4 测试访问
- 7.6 模式三 Http请求获取会话
-7.6.1 问题分析
-7.6.2 在Client 端更改 Ticket 校验方式
-7.6.3 获取 UserInfo
-7.6.4 单点注销
-7.6.5 总结
- 7.7 前后端分离架构下SSO
-7.7.1 SSO-Client后端
-7.7.2 SSO-Client前端
-7.7.3 SSO-Server后端
-7.7.4 SSO-Server前端
- 7.8 Sa-Token-OAuth2.0 模块
-7.8.1 简介
-7.8.2 实战案例
-7.8.3 OAuth2开放接口详解
-7.8.3.1 模式一:授权码(Authorization Code)
-7.8.3.2 模式二:隐藏式(Implicit)
-7.8.3.3 模式三:密码式(Password)
-7.8.3.4 模式四:凭证式(Client Credentials)
- 说在最后:有问题找老架构取经
1 基本概念
认证(Authentication) 授权(Authorization)
1.1 认证
如果校验通过,则:正常返回数据。 如果校验未通过,则:抛出异常,告知其需要先进行登录。
用户提交 name
+password
参数,调用登录接口。登录成功,返回这个用户的 Token 会话凭证。 用户后续的每次请求,都携带上这个 Token。 服务器根据 Token 判断此会话是否登录成功。
1.2 授权(鉴权)
有,就让你通过。 没有?那么禁止访问!
["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。2 Sa-Token简介
2.1 介绍
// 会话登录,参数填登录人的账号id
StpUtil.login(10001);
// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常
StpUtil.checkLogin();
// 将账号id为 10077 的会话踢下线
StpUtil.kickout(10077);
// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法
@SaCheckPermission("user:add")
public String insert(SysUser user) {
// ...
return "用户增加";
}
// 根据路由划分模块,不同模块不同鉴权
registry.addInterceptor(new SaInterceptor(handler -> {
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"));
// 更多模块...
})).addPathPatterns("/**");
2.2 功能一览
登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录。 权限认证 —— 权限认证、角色认证、会话二级认证。 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线。 注解式鉴权 —— 优雅的将鉴权与业务代码分离。 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配 restful 模式。 Session会话 —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。 持久层扩展 —— 可集成 Redis,重启数据不丢失。 前后台分离 —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。 Token风格定制 —— 内置六种 Token 风格,还可:自定义 Token 生成策略。 记住我模式 —— 适配 [记住我] 模式,重启浏览器免验证。 二级认证 —— 在已登录的基础上再次认证,保证安全性。 模拟他人账号 —— 实时操作任意用户状态数据。 临时身份切换 —— 将会话身份临时切换为其它账号。 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。 账号封禁 —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。 密码加密 —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。 会话查询 —— 提供方便灵活的会话查询接口。 Http Basic认证 —— 一行代码接入 Http Basic、Digest 认证。 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。 全局过滤器 —— 方便的处理跨域,全局设置安全响应头等操作。 多账号体系认证 —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表) 单点登录 —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。 单点注销 —— 任意子系统内发起注销,即可全端下线。 OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 。 分布式会话 —— 提供共享数据中心分布式会话方案。 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。 RPC调用鉴权 —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔 临时Token认证 —— 解决短时间的 Token 授权问题。 独立Redis —— 将权限缓存与业务缓存分离。 Quick快速登录认证 —— 为项目零代码注入一个登录页面。 标签方言 —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。 jwt集成 —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。 RPC调用状态传递 —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。 参数签名 —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。 开箱即用 —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。 最新技术栈 —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。
3 Sa-Token认证
添加依赖
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
配置文件
# 端口
server:
port: 8081
# sa-token 配置
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
3.1 登录认证
登录与注销
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);
检查此账号是否之前已有登录; 为账号生成 Token
凭证与Session
会话;记录 Token 活跃时间; 通知全局侦听器,xx 账号登录成功; 将 Token
注入到请求上下文;等等其它工作……
Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
。所以一般情况下,我们的登录接口代码,会大致类似如下:// 会话登录接口
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 第一步:比对前端提交的账号名称、密码
if("zhang".equals(name) && "123456".equals(pwd)) {
// 第二步:根据账号id,进行登录
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
StpUtil.login(id)
方法利用了 Cookie 自动注入的特性,省略了你手写返回 token 的代码。Cookie最基本的两点:
Cookie 可以从后端控制往浏览器中写入 token 值。 Cookie 会在前端每次发起请求时自动提交 token 值。
StpUtil.login(id)
一句代码就完成登录认证。// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
NotLoginException
代表当前会话暂未登录,可能的原因有很多:前端没有提交 token、前端提交的 token 是无效的、前端提交的 token 已经过期 …… 等等,可参照此篇:未登录场景值,了解如何获取未登录的场景值。登录账号查询
// 获取当前会话账号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();
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // Account-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
"tokenActiveTimeout": -1, // token 距离被冻结还剩的时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备类型
},
}
测试案例
LoginController
,复制或手动敲出以下代码/**
* 登录测试
*/
@RestController
@RequestMapping("/acc/")
public class LoginController {
// 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
// 查询登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
// 测试注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
3.2 踢人下线
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已被踢下线。
3.3 全局异常处理
NotLoginException
异常的场景值,来定制化处理未登录的逻辑 应用场景举例:未登录、被顶下线、被踢下线等场景需要不同方式来处理 在会话未登录的情况下尝试获取loginId
会使框架抛出NotLoginException
异常,而同为未登录异常却有五种抛出场景的区分场景值 | 对应常量 | 含义说明 |
// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)
throws Exception {
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效 token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token 无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token 已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token 已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token 已被踢下线";
}
else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
message = "token 已被冻结";
}
else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
message = "未按照指定前缀提交 token";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return SaResult.error(message);
}
3.4 二级认证
保证操作者是当前账号本人。 增加操作步骤,防止误删除重要数据。
具体API
Sa-Token
中进行二级认证非常简单,只需要使用以下API:// 在当前会话 开启二级认证,时间为120秒
StpUtil.openSafe(120);
// 获取:当前会话是否处于二级认证时间内
StpUtil.isSafe();
// 检查当前会话是否已通过二级认证,如未通过则抛出异常
StpUtil.checkSafe();
// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime();
// 在当前会话 结束二级认证
StpUtil.closeSafe();
一个小示例
// 删除仓库
@RequestMapping("deleteProject")
public SaResult deleteProject(String projectId) {
// 第1步,先检查当前会话是否已完成二级认证
if(!StpUtil.isSafe()) {
return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口");
}
// 第2步,如果已完成二级认证,则开始执行业务逻辑
// ...
// 第3步,返回结果
return SaResult.ok("仓库删除成功");
}
// 提供密码进行二级认证
@RequestMapping("openSafe")
public SaResult openSafe(String password) {
// 比对密码(此处只是举例,真实项目时可拿其它参数进行校验)
if("123456".equals(password)) {
// 比对成功,为当前会话打开二级认证,有效期为120秒
StpUtil.openSafe(120);
return SaResult.ok("二级认证成功");
}
// 如果密码校验失败,则二级认证也会失败
return SaResult.error("二级认证失败");
}
前端调用 deleteProject
接口,尝试删除仓库。后端校验会话尚未完成二级认证,返回: 仓库删除失败,请完成二级认证后再次访问接口
。前端将信息提示给用户,用户输入密码,调用 openSafe
接口。后端比对用户输入的密码,完成二级认证,有效期为:120秒。 前端在 120 秒内再次调用 deleteProject
接口,尝试删除仓库。后端校验会话已完成二级认证,返回: 仓库删除成功
。
指定业务标识进行二级认证
StpUtil.openSafe()
无法提供细粒度的认证操作, 此时我们可以指定一个业务标识来分辨不同的业务线:// 在当前会话 开启二级认证,业务标识为client,时间为600秒
StpUtil.openSafe("client", 600);
// 获取:当前会话是否已完成指定业务的二级认证
StpUtil.isSafe("client");
// 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常
StpUtil.checkSafe("client");
// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime("client");
// 在当前会话 结束指定业务标识的二级认证
StpUtil.closeSafe("client");
// 打开了业务标识为 client 的二级认证
StpUtil.openSafe("client");
// 判断是否处于 shop 的二级认证,会返回 false
StpUtil.isSafe("shop"); // 返回 false
// 也不会通过校验,会抛出异常
StpUtil.checkSafe("shop");
使用注解进行二级认证
@SaCheckSafe
注解,可以在代码进入此方法之前进行一次二级认证校验// 二级认证:必须二级认证之后才能进入该方法
@SaCheckSafe
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 指定业务类型,进行二级认证校验
@SaCheckSafe("art")
@RequestMapping("add2")
public String add2() {
return "文章增加";
}
3.5 同端互斥登录
具体API
isConcurrent
配置为false,然后调用登录等相关接口时声明设备类型即可:指定设备类型登录
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");
NotLoginException
异常,场景值=-4
指定设备类型强制注销
// 指定`账号id`和`设备类型`进行强制注销
StpUtil.logout(10001, "PC");
NotLoginException
异常,场景值=-2
查询当前登录的设备类型
// 返回当前token的登录设备类型
StpUtil.getLoginDevice();
Id 反查 Token
// 获取指定loginId指定设备类型端的tokenValue
StpUtil.getTokenValueByLoginId(10001, "APP");
3.6 Http Basic/Digest 认证
3.6.1 HttpBasic认证
简单、易集成。 功能支持度低。
启用 Http Basic 认证
@RequestMapping("test3")
public SaResult test3() {
SaHttpBasicUtil.check("sa:123456");
// ... 其它代码
return SaResult.ok();
}
(sa / 123456)
,才可以继续访问数据:其它启用方式
// 对当前会话进行 Http Basic 校验,账号密码为 yml 配置的值(例如:sa-token.http-basic=sa:123456)
SaHttpBasicUtil.check();
// 对当前会话进行 Http Basic 校验,账号密码为:`sa / 123456`
SaHttpBasicUtil.check("sa:123456");
// 以注解方式启用 Http Basic 校验
@SaCheckHttpBasic(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/**", () -> SaHttpBasicUtil.check("sa:123456"));
});
}
URL 认证
http://sa:123456@127.0.0.1:8081/test/test3
3.6.2 Http Digest 认证
// 测试 Http Digest 认证 浏览器访问:http://localhost:8081/test/testDigest
@RequestMapping("testDigest")
public SaResult testDigest() {
SaHttpDigestUtil.check("sa", "123456");
return SaResult.ok();
}
// 使用注解方式开启 Http Digest 认证
@SaCheckHttpDigest("sa:123456")
@RequestMapping("testDigest2")
public SaResult testDigest() {
return SaResult.ok();
}
// 对当前会话进行 Http Digest 校验,账号密码为 yml 配置的值(例如:sa-token.http-digest=sa:123456)
SaHttpDigestUtil.check();
http://sa:123456@127.0.0.1:8081/test/testDigest
4 Sa-Token授权(鉴权)
4.1 权限认证
有,就让你通过。 没有?那么禁止访问!
["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。如何获取一个账号所拥有的权限码集合? 本次操作需要验证的权限码是哪个?
4.1.1 获取当前账号权限码集合
StpInterface
类似Spring Security的UserDetailService 你需要做的就是新建一个类,实现 StpInterface
接口,例如以下代码:/**
* 自定义权限加载接口实现类
*/
@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;
}
}
loginId:账号id,即你在调用 StpUtil.login(id)
时写入的标识值。loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。
com.pj.satoken.StpInterfaceImpl
有同学会产生疑问:我实现了此接口,但是程序启动时好像并没有执行,是不是我写错了? 答:不执行是正常现象,程序启动时不会执行这个接口的方法,在每次调用鉴权代码时,才会执行到此。4.1.2 权限校验
// 获取:当前账号所拥有的权限集合
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");
NotPermissionException
对象可通过 getLoginType()
方法获取具体是哪个 StpLogic
抛出的异常4.1.3 角色校验
// 获取:当前账号所拥有的角色集合
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");
NotRoleException
对象可通过 getLoginType()
方法获取具体是哪个 StpLogic
抛出的异常4.1.4 拦截全局异常
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
4.1.5 权限通配符
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
"*"
权限时,他可以验证通过任何权限码 (角色认证同理)4.1.6 如何把权限精确到按钮级?
在登录时,把当前账号拥有的所有权限码一次性返回给前端。 前端将权限码集合保存在 localStorage
或其它全局状态管理对象中。在需要权限控制的按钮上,使用 js 进行逻辑判断,例如在 Vue
框架中我们可以使用如下写法:
// `arr`是当前用户拥有的权限码数组
// `user.delete`是显示按钮需要拥有的权限码
// `删除按钮`是用户拥有权限码才可以看到的内容。
<button v-if="arr.indexOf('user.delete') > -1">删除按钮</button>
前端有了鉴权后端还需要鉴权吗? 需要! 前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全:无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!
4.2 注解鉴权
@SaCheckLogin
: 登录校验 —— 只有登录之后才能进入该方法。@SaCheckRole("admin")
: 角色校验 —— 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add")
: 权限校验 —— 必须具有指定权限才能进入该方法。@SaCheckSafe
: 二级认证校验 —— 必须二级认证之后才能进入该方法。@SaCheckHttpBasic
: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。@SaCheckHttpDigest
: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。@SaIgnore
:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckDisable("comment")
:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
4.2.1 注册拦截器
SpringBoot2.0
为例,新建配置类SaTokenConfigure.java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
springboot
启动类扫描到即可4.2.2 使用注解鉴权
// 登录校验:只有登录之后才能进入该方法
@SaCheckLogin
@RequestMapping("info")
public String info() {
return "查询用户信息";
}
// 角色校验:必须具有指定角色才能进入该方法
@SaCheckRole("super-admin")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 权限校验:必须具有指定权限才能进入该方法
@SaCheckPermission("user-add")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 二级认证校验:必须二级认证之后才能进入该方法
@SaCheckSafe()
@RequestMapping("add")
public String add() {
return "用户增加";
}
// Http Basic 校验:只有通过 Http Basic 认证后才能进入该方法
@SaCheckHttpBasic(account = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// Http Digest 校验:只有通过 Http Digest 认证后才能进入该方法
@SaCheckHttpDigest(value = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
@SaCheckDisable("comment")
@RequestMapping("send")
public String send() {
return "查询用户信息";
}
4.2.3 设定校验模式
@SaCheckRole
与@SaCheckPermission
注解可设置校验模式,例如:// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
SaMode.AND
,标注一组权限,会话必须全部具有才可通过校验。SaMode.OR
,标注一组权限,会话只要具有其一即可通过校验。
4.2.4 角色权限双重 “or校验”
user.add
或角色 admin
时可以调通。怎么写?// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
写法一: orRole = "admin"
,代表需要拥有角色 admin 。写法二: orRole = {"admin", "manager", "staff"}
,代表具有三个角色其一即可。写法三: orRole = {"admin, manager, staff"}
,代表必须同时具有三个角色。
4.2.5 忽略认证
@SaIgnore
可表示一个接口忽略认证:@SaCheckLogin
@RestController
public class TestController {
// ... 其它方法
// 此接口加上了 @SaIgnore 可以游客访问
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}
}
TestController
中的所有方法都需要登录后才可以访问,但是 getList
接口可以匿名游客访问。@SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。 @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。 @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权,在下面的 [路由拦截鉴权] 章节中我们会讲到。
4.2.6 批量注解鉴权
@SaCheckOr
表示批量注解鉴权:// 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
@SaCheckOr(
login = @SaCheckLogin,
role = @SaCheckRole("admin"),
permission = @SaCheckPermission("user.add"),
safe = @SaCheckSafe("update-password"),
httpBasic = @SaCheckHttpBasic(account = "sa:123456"),
disable = @SaCheckDisable("submit-orders")
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
// 当前客户端只要有 [ login 账号登录] 或者 [user 账号登录] 其一,就可以通过验证进入方法。
// 注意:`type = "login"` 和 `type = "user"` 是多账号模式章节的扩展属性,此处你可以先略过这个知识点。
@SaCheckOr(
login = { @SaCheckLogin(type = "login"), @SaCheckLogin(type = "user") }
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
@SaCheckOr
,为什么没有与之对应的 @SaCheckAnd
呢?因为当你写多个注解时,其天然就是 and
校验关系,例如:// 当你在一个方法上写多个注解鉴权时,其默认就是要满足所有注解规则后,才可以进入方法,只要有一个不满足,就会抛出异常
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("user.add")
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
4.3路由拦截鉴权
4.3.1 注册 Sa-Token 路由拦截器
SpringBoot2.0
为例,新建配置类SaTokenConfigure.java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/user/doLogin");
}
}
StpUtil.checkLogin()
的登录校验拦截器,并且排除了/user/doLogin
接口用来开放登录(除了/user/doLogin
以外的所有接口都需要登录才能访问)。4.3.2 校验函数详解
new SaInterceptor(handle -> StpUtil.checkLogin())
是最简单的写法,代表只进行登录校验功能。我们可以往构造函数塞一个完整的 lambda 表达式,来定义详细的校验规则,例如:@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("/**");
}
}
参数一:要匹配的path路由。 参数二:要执行的校验函数。
StpUtil.checkPermission("xxx")
进行权限校验,你还可以写任意代码,例如:@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义认证规则
registry.addInterceptor(new SaInterceptor(handler -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
// 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
// 权限校验 -- 不同模块校验不同权限
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"));
// 甚至你可以随意的写一个打印语句
SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));
// 连缀写法
SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----"));
})).addPathPatterns("/**");
}
}
4.3.3 匹配特征详解
// 基础写法样例:匹配一个path,执行一个校验函数
SaRouter.match("/user/**").check(r -> StpUtil.checkLogin());
// 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由
// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法
SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ );
// 根据 path 路由排除匹配
// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法
SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ );
// 根据请求类型匹配
SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ );
// 根据一个 boolean 条件进行匹配
SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 根据一个返回 boolean 结果的lambda表达式匹配
SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 多个条件一起使用
// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头
SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ );
// 可以无限连缀下去
// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css
SaRouter
.match(SaHttpMethod.GET)
.match("/admin/**")
.match("/**/send/**")
.notMatch("/**/*.js")
.notMatch("/**/*.css")
// ....
.check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ );
4.3.4 提前退出匹配链
SaRouter.stop()
可以提前退出匹配链,例:registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**").check(r -> System.out.println("进入1"));
SaRouter.match("/**").check(r -> System.out.println("进入2")).stop();
SaRouter.match("/**").check(r -> System.out.println("进入3"));
SaRouter.match("/**").check(r -> System.out.println("进入4"));
SaRouter.match("/**").check(r -> System.out.println("进入5"));
})).addPathPatterns("/**");
stop()
函数,SaRouter
还提供了 back()
函数,用于:停止匹配,结束执行,直接向前端返回结果// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端
SaRouter.match("/user/back").back("要返回到前端的内容");复制到剪贴板错误复制成功
1
2
SaRouter.stop()
会停止匹配,进入Controller。SaRouter.back()
会停止匹配,直接返回结果到前端。
4.3.5 使用free打开一个独立的作用域
// 进入 free 独立作用域
SaRouter.match("/**").free(r -> {
SaRouter.match("/a/**").check(/* --- */);
SaRouter.match("/b/**").check(/* --- */).stop();
SaRouter.match("/c/**").check(/* --- */);
});
// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配
SaRouter.match("/**").check(/* --- */);
4.3.6 使用注解忽略掉路由拦截校验
@SaIgnore
注解,忽略掉路由拦截认证:1、先配置好了拦截规则:@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 根据路由划分模块,不同模块不同鉴权
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// ...
})).addPathPatterns("/**");
}
Controller
里又添加了忽略校验的注解@SaIgnore
@RequestMapping("/user/getList")
public SaResult getList() {
System.out.println("------------ 访问进来方法");
return SaResult.ok();
}
注解 @SaIgnore
的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。
4.3.7 关闭注解校验
SaInterceptor
只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要:@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new SaInterceptor(handle -> {
SaRouter.match("/**").check(r -> StpUtil.checkLogin());
}).isAnnotation(false) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了
).addPathPatterns("/**");
}
拦截器和过滤器鉴权:首先我们先梳理清楚一个问题,既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之:
相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。 过滤器可以拦截静态资源,方便我们做一些权限控制。 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。
由于太过底层,导致无法率先拿到 HandlerMethod
对象,无法据此添加一些额外功能。由于拦截的太全面了,导致我们需要对很多特殊路由(如 /favicon.ico
)做一些额外处理。在Spring中,过滤器中抛出的异常无法进入全局 @ExceptionHandler
,我们必须额外编写代码进行异常处理。
5 Sa-Token 进阶
5.1 Session会话
[唯一id]
作为身份标识,注入到 Cookie 之中, 之后每次发起请求时,客户端都要将它提交到后台,服务器根据 [唯一id]
找到每个请求专属的Session对象,维持会话 这种机制简单粗暴,却有N多明显的缺点:同一账号分别在PC、APP登录,会被识别为两个不相干的会话 一个设备难以同时登录两个账号 每次一个新的客户端访问服务器时,都会产生一个新的Session对象,即使这个客户端只访问了一次页面 在不支持Cookie的客户端下,这种机制会失效
Sa-Token只在调用 StpUtil.login(id)
登录会话时才会产生Session,不会为每个陌生会话都产生Session,节省性能在登录时产生的Session,是分配给账号id的,而不是分配给指定客户端的,也就是说在PC、APP上登录的同一账号所得到的Session也是同一个,所以两端可以非常轻松的同步数据 Sa-Token支持Cookie、Header、body三个途径提交Token,而不是仅限于Cookie 由于不强依赖Cookie,所以只要将Token存储到不同的地方,便可以做到一个客户端同时登录多个账号
5.1.1 Session模型结构图
Account-Session
: 指的是框架为每个 账号id 分配的 SessionToken-Session
: 指的是框架为每个 token 分配的 SessionCustom-Session
: 指的是以一个 特定的值 作为SessionId,来分配的 Session
Account-Session
以账号 id 为主,只要 token 指向的账号 id 一致,那么对应的Session对象就一致Token-Session
以token为主,只要token不同,那么对应的Session对象就不同Custom-Session
以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对
5.1.2 Account-Session
Account-Session
,你可以通过如下方式操作它:// 获取当前会话的 Account-Session
SaSession session = StpUtil.getSession();
// 从 Account-Session 中读取、写入数据
session.get("name");
session.set("name", "张三");
Account-Session
在不同端同步数据是非常方便的,因为只要 PC 和 APP 登录的账号id一致,它们对应的都是同一个Session, 举个应用场景:在PC端点赞的帖子列表,在APP端的点赞记录里也要同步显示出来5.1.3 Token-Session
Account-Session
,同时还为每个token分配了不同的Token-Session
不同的设备端,哪怕登录了同一账号,只要它们得到的token不一致,它们对应的 Token-Session
就不一致,这就为我们不同端的独立数据读写提供了支持:// 获取当前会话的 Token-Session
SaSession session = StpUtil.getTokenSession();
// 从 Token-Session 中读取、写入数据
session.get("name");
session.set("name", "张三");
5.1.4 Custom-Session
Custom-Session
,你可以将其理解为:自定义Session Custom-Session不依赖特定的 账号id 或者 token,而是依赖于你提供的SessionId:// 获取指定key的 Custom-Session
SaSession session = SaSessionCustomUtil.getSessionById("goods-10001");
// 从 Custom-Session 中读取、写入数据
session.get("name");
session.set("name", "张三");
SaManager.getConfig().getTimeout()
, 如果需要修改会话有效期, 可以在创建之后, 使用对象方法修改session.updateTimeout(1000); // 参数说明和全局有效期保持一致复制到剪贴板错误复制成功
1
5.1.5 未登录场景下获取 Token-Session
StpUtil.getTokenSession()
获取 Token-Session
。如果想要在未登录场景下获取 Token-Session ,有两种方法:方法一:将全局配置项 tokenSessionCheckLogin
改为 false,详见:框架配置方法二:使用匿名 Token-Session
// 获取当前 Token 的匿名 Token-Session (可在未登录情况下使用的 Token-Session)
StpUtil.getAnonTokenSession();
Token-Session
对象, 而是随机一个新的 Token 值来创建 Token-Session
对象,此 Token 值可以通过 StpUtil.getTokenValue()
获取到。5.2 身份切换
Account-Session
,等等... Sa-Token 在 API 设计时充分考虑了这一点,暴露出多个api进行此类操作:有关操作其它账号的api
// 获取指定账号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();
StpUtil.endSwitch()
关闭身份切换)System.out.println("------- [身份临时切换]调用开始...");
StpUtil.switchTo(10044, () -> {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true
System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044
});
System.out.println("------- [身份临时切换]调用结束...");
5.3 [记住我] 模式
[记住我]
按钮,当你勾选它登录后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码: 那么在Sa-Token中,如何做到 [ 记住我 ] 功能呢?在 Sa-Token 中实现记住我功能
[记住我]
模式,为了实现[非记住我]
模式,你需要在登录时如下设置:// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录
StpUtil.login(10001, false);
[记住我]
的具体原理是?实现原理
临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。
勾选 [记住我] 按钮时:调用 StpUtil.login(10001, true)
,在浏览器写入一个持久Cookie
储存 Token,此时用户即使重启浏览器 Token 依然有效。不勾选 [记住我] 按钮时:调用 StpUtil.login(10001, false)
,在浏览器写入一个临时Cookie
储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。
前后端分离模式下如何实现[记住我]?
// 使用本地存储保存token,达到 [持久Cookie] 的效果
uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用globalData保存token,达到 [临时Cookie] 的效果
getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx";
// 使用 localStorage 保存token,达到 [持久Cookie] 的效果
localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用 sessionStorage 保存token,达到 [临时Cookie] 的效果
sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
登录时指定 Token 有效期
[记住我]
模式,还可以指定一个特定的时间作为 Token 有效时长,如下示例:// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
// ----------------------- 示例2:所有参数
// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginModel()
.setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型
.setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
.setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值)
.setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token
.setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头
);
5.4 账号封禁
账号封禁
// 封禁指定账号
StpUtil.disable(10001, 86400);
参数1:要封禁的账号id。 参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。
// 先踢下线
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);
分类封禁
1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。 2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。 3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。
// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);
参数1:要封禁的账号id。 参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。 参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。
/*
* 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识
*/
// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);
// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment`
StpUtil.checkDisable(10001, "comment");
// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能
StpUtil.checkDisable(10001, "place-order");
// 现在我们再将其下单能力封禁一下,期限为 7天
StpUtil.disable(10001, "place-order", 86400 * 7);
// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常)
StpUtil.checkDisable(10001, "place-order");
// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码)
StpUtil.checkDisable(10001, "open-shop");
业务封禁 -> 业务校验
的处理步骤。有关分类封禁的所有方法:// 封禁:指定账号的指定服务
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.disable(10001, 86400)
传入不同的封禁时间即可,下面我们着重探讨一下基于处罚力度的阶梯形式。假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度:1、轻度违规:封禁其发帖、评论能力,但允许其点赞、关注等操作。 2、中度违规:封禁其发帖、评论、点赞、关注等一切与别人互动的能力,但允许其浏览帖子、浏览评论。 3、重度违规:封禁其登录功能,限制一切能力。
轻度
、中度
、重度
3 个力度, 我们将其量化为一级封禁
、二级封禁
、三级封禁
3个等级,数字越大代表封禁力度越高。然后我们就可以使用阶梯封禁的API,进行鉴权了:// 阶梯封禁,参数:封禁账号、封禁级别、封禁时间
StpUtil.disableLevel(10001, 3, 10000);
// 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2)
StpUtil.getDisableLevel(10001);
// 判断:指定账号是否已被封禁到指定级别,返回 true 或 false
StpUtil.isDisableLevel(10001, 3);
// 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 `DisableServiceException`
StpUtil.checkDisableLevel(10001, 2);
DisableServiceException
异常代表当前账号未通过封禁校验,可以:通过 e.getLevel()
获取这个账号实际被封禁的等级。通过 e.getLimitLevel()
获取这个账号在校验时要求低于的等级。当Level >= LimitLevel
时,框架就会抛出异常。
// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间
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
@PostMapping("send")
public SaResult send() {
// ...
return SaResult.ok();
}
// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
@SaCheckDisable("comment")
@PostMapping("send")
public SaResult send() {
// ...
return SaResult.ok();
}
// 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法
@SaCheckDisable({"comment", "place-order", "open-shop"})
@PostMapping("send")
public SaResult send() {
// ...
return SaResult.ok();
}
// 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常
@SaCheckDisable(level = 5)
@PostMapping("send")
public SaResult send() {
// ...
return SaResult.ok();
}
// 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常
@SaCheckDisable(value = "comment", level = 5)
@PostMapping("send")
public SaResult send() {
// ...
return SaResult.ok();
}
5.5 密码加密
Sa-Token
在 v1.14 版本添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法。摘要加密
// md5加密
SaSecureUtil.md5("123456");
// sha1加密
SaSecureUtil.sha1("123456");
// sha256加密
SaSecureUtil.sha256("123456");
对称加密
// 定义秘钥和明文
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);
非对称加密
// 定义私钥和公钥
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());
Base64编码与解码
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";
// 使用Base64编码
String base64Text = SaBase64Util.encode(text);
System.out.println("Base64编码后:" + base64Text);
// 使用Base64解码
String text2 = SaBase64Util.decode(base64Text);
System.out.println("Base64解码后:" + text2);
BCrypt加密
// 使用方法
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);
5.6 全局侦听器
5.6.1 工作原理
SaTokenListenerForLog
实现:代码参考 ,功能是控制台 log 打印输出,你可以通过配置sa-token.is-log=true
开启。要注册自定义的侦听器也非常简单:新建类实现 SaTokenListener
接口。将实现类注册到 SaTokenEventCenter
事件发布中心。
5.6.2 自定义侦听器实现
新建实现类:
MySaTokenListener.java
,实现SaTokenListener
接口,并添加上注解@Component
,保证此类被SpringBoot
扫描到:/**
* 自定义侦听器的实现
*/
@Component
public class MySaTokenListener implements SaTokenListener {
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------- 自定义侦听器实现 doLogin");
}
/** 每次注销时触发 */
@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");
}
}
将侦听器注册到事件中心:
@Component
注解,会被 SpringBoot 扫描并自动注册到事件中心,此时我们无需手动注册。如果我们没有添加 @Component
注解或者项目属于非 IOC 自动注入环境,则需要我们手动将这个侦听器注册到事件中心:// 将侦听器注册到事件发布中心
SaTokenEventCenter.registerListener(new MySaTokenListener());
// 获取已注册的所有侦听器
SaTokenEventCenter.getListenerList();
// 重置侦听器集合
SaTokenEventCenter.setListenerList(listenerList);
// 注册一个侦听器
SaTokenEventCenter.registerListener(listener);
// 注册一组侦听器
SaTokenEventCenter.registerListenerList(listenerList);
// 移除一个侦听器
SaTokenEventCenter.removeListener(listener);
// 移除指定类型的所有侦听器
SaTokenEventCenter.removeListener(cls);
// 清空所有已注册的侦听器
SaTokenEventCenter.clearListener();
// 判断是否已经注册了指定侦听器
SaTokenEventCenter.hasListener(listener);
// 判断是否已经注册了指定类型的侦听器
SaTokenEventCenter.hasListener(cls);
启动测试:
TestController
中添加登录测试代码:// 测试登录接口
@RequestMapping("login")
public SaResult login() {
System.out.println("登录前");
StpUtil.login(10001);
System.out.println("登录后");
return SaResult.ok();
}
5.6.3 其它注意点
你可以通过继承SaTokenListenerForSimple快速实现一个侦听器:
@Component
public class MySaTokenListener extends SaTokenListenerForSimple {
/*
* SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。
*/
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------- 自定义侦听器实现 doLogin");
}
}复制到剪贴板错误复制成功
1
2
3
4
5
6
7
8
9
10
11
使用匿名内部类的方式注册:
// 登录时触发
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------------- doLogin");
}
});
使用 try-catch 包裹不安全的代码:
try-catch
包裹代码,以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。// 登录时触发
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
try {
// 不安全代码需要写在 try-catch 里
// ......
} catch (Exception e) {
e.printStackTrace();
}
}
});
疑问:一个项目可以注册多个侦听器吗?
6 微服务架构下安全认证
6.1 微服务分布式Session认证架构方案
微服务架构下安全认证面临的挑战:
Session
在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。首先我们要明白,分布式环境下为什么Session
会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, 这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。解决方案
Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点 Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务 建立会话中心:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如: Redis
颁发无状态token:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如: jwt
方案选择
方案一:性能消耗太大,不太考虑 方案二:需要从网关处动手,与框架无关 方案三:Sa-Token 整合 Redis
非常简单,详见章节:集成 Redis方案四:详见官方仓库中 Sa-Token 整合 jwt
的示例
jwt
模式不在服务端存储数据,对于比较复杂的业务可能会功能受限,因此更加推荐使用方案三6.2 集成Redis
重启后数据会丢失。 无法在分布式环境中共享数据。
引入依赖
<!-- Sa-Token 整合Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<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>${sa-token.version}</version>
</dependency>
Sa-Token 整合 Redis (使用 jdk 默认序列化方式)
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.38.0</version>
</dependency>
Sa-Token 整合 Redis(使用 jackson 序列化方式)
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
集成 Redis 请注意:
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Sa-Token-Alone-Redis 独立Redis插件
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.38.0</version>
</dependency>
配置文件
# 端口
server:
port: 8081
# Sa-Token配置
sa-token:
# Token名称 (同时也是cookie名称)
token-name: satoken
# Token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# Token风格
token-style: uuid
# 配置Sa-Token单独使用的Redis连接
alone-redis:
# Redis模式(默认单体)
# pattern: single
# Redis数据库索引(默认为0)
database: 2
# 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
spring:
# 配置业务使用的Redis连接
redis:
# Redis数据库索引(默认为0)
database: 0
# 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
6.3 集成jwt
引入依赖
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.38.0</version>
</dependency>
版本兼容性
注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,保险起见:你的项目中要么不引入 hutool,要么引入版本 >= 5.7.14 的 hutool 版本。 hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题。
配置秘钥
application.yml
配置文件中配置 jwt 生成秘钥:yaml 风格sa-token:
# jwt秘钥
jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
注入jwt实现
Simple 简单模式 Mixin 混入模式 Stateless 无状态模式
@Configuration
public class SaTokenConfigure {
// Sa-Token 整合 jwt (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
//return new StpLogicJwtForMixin();
//return new StpLogicJwtForStateless();
}
}
开始使用
/**
* 登录测试
*/
@RestController
@RequestMapping("/acc/")
public class LoginController {
// 测试登录
@RequestMapping("login")
public SaResult login() {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
// 查询登录状态
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
// 测试注销
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ
不同模式策略对比
功能点 | Simple 简单模式 | Mixin 混入模式 | Stateless 无状态模式 |
扩展参数
// 登录10001账号,并为生成的 Token 追加扩展参数name
StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan"));
// 连缀写法追加多个
StpUtil.login(10001, SaLoginConfig
.setExtra("name", "zhangsan")
.setExtra("age", 18)
.setExtra("role", "超级管理员"));
// 获取扩展参数
String name = StpUtil.getExtra("name");
// 获取任意 Token 的扩展参数
String name = StpUtil.getExtra("tokenValue", "name");
几个注意点 使用 jwt-simple 模式后,is-share=false 恒等于 false。
is-share=true
的意思是每次登录都产生一样的 token,这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的, 为保证正确设定 Extra 数据,当使用 jwt-simple
模式后,is-share
配置项 恒等于 false
。使用 jwt-mixin 模式后,is-concurrent 必须为 true。
is-concurrent=false
代表每次登录都把旧登录顶下线,但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中, 技术上来讲无法将其踢下线,所以此时顶人下线和踢人下线等 API 都属于不可用状态,所以此时 is-concurrent
配置项必须配置为 true
。使用 jwt-mixin 模式后,max-try-times 恒等于 -1。
max-try-times
恒等于 -1。6.4前后端分离(无Cookie模式)
何为无 Cookie 模式?
Cookie模式
完成,而 Cookie 有两个特性:可由后端控制写入。 每次请求自动提交。
不能后端控制写入了,就前端自己写入。(难点在后端如何将 Token 传递到前端) 每次请求不能自动提交了,那就手动提交。(难点在前端如何将 Token 传递到后端,同时后端将其读取出来)
后端将 token 返回到前端
首先调用 StpUtil.login(id)
进行登录。调用 StpUtil.getTokenInfo()
返回当前会话的 token 详细参数。此方法返回一个对象,其有两个关键属性: tokenName
和tokenValue
(token 的名称和 token 的值)。将此对象传递到前台,让前端人员将这两个值保存到本地。
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
// 第1步,先登录上
StpUtil.login(10001);
// 第2步,获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 第3步,返回给前端
return SaResult.data(tokenInfo);
}
前端将 token 提交到后端
无论是app还是小程序,其传递方式都大同小异。 那就是,将 token 塞到请求 header
里 ,格式为:{tokenName: tokenValue}
。以经典跨端框架 uni-app 为例:
// 1、首先在登录时,将 tokenValue 存储在本地,例如:
uni.setStorageSync('tokenValue', tokenValue);
// 2、在发起ajax请求的地方,获取这个值,并塞到header里
uni.request({
url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
header: {
"content-type": "application/x-www-form-urlencoded",
"satoken": uni.getStorageSync('tokenValue') // 关键代码, 注意参数名字是 satoken
},
success: (res) => {
console.log(res.data);
}
});
// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如:
uni.setStorageSync('tokenName', tokenName);
uni.setStorageSync('tokenValue', tokenValue);
// 2、在发起ajax的地方,获取这两个值, 并组织到head里
var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值
var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值
var header = {
"content-type": "application/x-www-form-urlencoded"
};
if (tokenName != undefined && tokenName != '') {
header[tokenName] = tokenValue;
}
// 3、后续在发起请求时将 header 对象塞到请求头部
uni.request({
url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
header: header,
success: (res) => {
console.log(res.data);
}
});
只要按照如此方法将 token
值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。你可能会有疑问,难道我每个 ajax
都要写这么一坨?岂不是麻烦死了?你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。
后端尝试从header中读取token
# # 是否尝试从header里读取token
# is-read-header: true
# # 是否尝试从cookie里读取token
# is-read-cookie: true
6.5 内部服务外网隔离
6.5.1 需求场景
物理隔离:子服务部署在指定的内网环境中,只有网关对外网开放 逻辑隔离:子服务与网关同时暴露在外网,但是子服务会有一个权限拦截层保证只接受网关发送来的请求,绕过网关直接访问子服务会被提示:无效请求
网关转发鉴权 、 服务间内部调用鉴权
使用 OAuth2.0 模式的凭证式,将 Client-Token 用作各个服务的身份凭证进行权限校验 使用 Same-Token 模块提供的身份校验能力,完成服务间的权限认证
Same-Token
模块的整合步骤,其鉴权流程与 OAuth2.0 类似,不过使用方式上更加简洁(使用方案一的同学可参考Sa-OAuth2模块,此处不再赘述)6.5.2 网关转发鉴权
引入依赖
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
网关处添加Same-Token
/**
* 全局过滤器,为请求添加 Same-Token
*/
@Component
public class ForwardAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
// 为请求追加 Same-Token 参数
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
}
Same-Token
参数,这个参数会被转发到子服务在子服务里校验参数
/**
* Sa-Token 权限认证 配置类
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 全局过滤器
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
// 校验 Same-Token 身份凭证 —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken();
String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
SaSameUtil.checkToken(token);
})
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}
无效Same-Token:xxx
6.5.3 服务间内部调用鉴权
Same-Token
作为身份凭证的 在服务里添加 Same-Token 流程与网关类似,我们以RPC框架 Feign
为例:首先在调用方添加 FeignInterceptor
/**
* feign拦截器, 在feign请求发出之前,加入一些操作
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
// 为 Feign 的 RCP调用 添加请求头Same-Token
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
// 如果希望被调用方有会话状态,此处就还需要将 satoken 添加到请求头中
// requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());
}
}
在调用接口里使用此 Interceptor
/**
* 服务调用
*/
@FeignClient(
name = "sp-home", // 服务名称
configuration = FeignInterceptor.class, // 请求拦截器 (关键代码)
fallbackFactory = SpCfgInterfaceFallback.class // 服务降级处理
)
public interface SpCfgInterface {
// 获取server端指定配置信息
@RequestMapping("/SpConfig/getConfig")
public String getConfig(@RequestParam("key")String key);
}
6.5.4 Same-Token 模块详解
服务调用方获取Token, 提交到请求中,被调用方取出Token进行校验, Token一致则校验通过,否则拒绝服务
// 获取当前Same-Token
SaSameUtil.getToken();
// 判断一个Same-Token是否有效
SaSameUtil.isValid(token);
// 校验一个Same-Token是否有效 (如果无效则抛出异常)
SaSameUtil.checkToken(token);
// 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常)
SaSameUtil.checkCurrentRequestToken();
// 刷新一次Same-Token (注意集群环境中不要多个服务重复调用)
SaSameUtil.refreshToken();
// 在 Request 上储存 Same-Token 时建议使用的key
SaSameUtil.SAME_TOKEN;
疑问:这个Token保存在什么地方?有没有泄露的风险?Token为永久有效还是临时有效?
如何主动刷新Same-Token,例如:五分钟、两小时刷新一次?
需要注意的一点是:Same-Token默认的自刷新机制,并不能做到高并发可用,多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效,其只能适用于 项目开发阶段 或 低并发业务场景
/**
* Same-Token,定时刷新
*/
@Configuration
public class SaSameTokenRefreshTask {
// 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token
@Scheduled(cron = "0 0/5 * * * ? ")
public void refreshToken(){
SaSameUtil.refreshToken();
}
}
五分钟
、十分钟
或 两小时
,只要低于Same-Token的有效期(默认为一天)即可。如果网关携带token转发的请求在落到子服务的节点上时,恰好刷新了token,导致鉴权未通过怎么办?
7 单点登录(SSO)
7.1 单点登录架构选型
什么是单点登录?解决什么问题?
在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
架构选型
系统架构 | 采用模式 | 简介 |
前端同域:就是指多个系统可以部署在同一个主域名之下,比如: c1.domain.com
、c2.domain.com
、c3.domain.com
。后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 [权限缓存与业务缓存分离]
的解决方案,详情: Alone独立Redis插件。如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)。
Sa-Token-SSO 特性 API 简单易用,文档介绍详细,且提供直接可用的集成示例。 支持三种模式,不论是否跨域、是否共享Redis、是否前后端分离,都可以完美解决。 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝 Ticket劫持
、Token窃取
等常见攻击手段(文档讲述攻击原理和防御手段)。不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是: http://a.com?id=1&name=2
,登录成功之后就变成了:http://a.com?id=1
,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面。无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决 权限认证
+单点登录
问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合……高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发。
7.2认证中心 SSO-Server
添加依赖
sa-token-demo-sso-server
,引入依赖:Maven 方式<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</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>
除了 sa-token-spring-boot-starter
和sa-token-sso
以外,其它包都是可选的:
在 SSO 模式三时 Redis 相关包是可选的 在前后端分离模式下可以删除 thymeleaf 相关包 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包
开放认证接口
SsoServerController
,用于对外开放接口:/**
* Sa-Token-SSO Server端 Controller
*/
@RestController
public class SsoServerController {
/**
* SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口)
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoServerProcessor.instance.dister();
}
/**
* 配置SSO相关参数
*/
@Autowired
private void configSso(SaSsoServerConfig ssoServer) {
// 配置:未登录时返回的View
ssoServer.notLoginView = () -> {
String msg = "当前会话在SSO-Server端尚未登录,请先访问"
+ "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+ "进行登录之后,刷新页面开始授权";
return msg;
};
// 配置:登录处理函数
ssoServer.doLoginHandle = (name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据进行登录
if("sa".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
};
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
ssoServer.sendHttp = url -> {
try {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
} catch (Exception e) {
e.printStackTrace();
return null;
}
};
}
}
注意:
在 doLoginHandle
函数里如果要获取name, pwd以外的参数,可通过SaHolder.getRequest().getParam("xxx")
来获取在 sendHttp
函数中,使用try-catch
是为了提高整个注销流程的容错性,避免在一些极端情况下注销失败(例如:某个 Client 端上线之后又下线,导致 http 请求无法调用成功,从而阻断了整个注销流程)
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
application.yml配置
# 端口
server:
port: 9000
# Sa-Token 配置
sa-token:
# ------- SSO-模式一相关配置 (非模式一不需要配置)
# cookie:
# 配置 Cookie 作用域
# domain: stp.com
# ------- SSO-模式二相关配置
sso-server:
# 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
注意点: sa-token.sso-server.allow-url
为了方便测试配置为*
,线上生产环境一定要配置为详细URL地址,否则会有被 Ticket 劫持的风险,比如 http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/ 借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站 推荐配置:allow-url: [http://sa-sso-client1.com:9001/sso/login](http://sa-sso-client1.com:9001/sso/login)
创建启动类
@SpringBootApplication
public class SaSsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SaSsoServerApplication.class, args);
System.out.println();
System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------");
System.out.println("配置信息:" + SaSsoManager.getServerConfig());
System.out.println();
}
}
http://localhost:9000/sso/auth
/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/
,里面有制作好的登录页面:sa / 123456
,先别着急点击登录,因为我们还没有搭建对应的 Client 端项目, 真实项目中我们是不会直接从浏览器访问 /sso/auth
授权地址的,我们需要在 Client 端点击登录按钮重定向而来。/sso/auth
统一授权地址,这个 SSO-Server 认证中心还开放了哪些API:SSO-Server 认证中心开放接口7.3 SSO接口详解
7.3.1 SSO-Server 认证中心接口
单点登录授权地址
http://{host}:{port}/sso/auth
参数 | 是否必填 | 说明 |
情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 redirect
地址,并携带ticket
参数。
RestAPI 登录接口
http://{host}:{port}/sso/doLogin
参数 | 是否必填 | 说明 |
此接口属于 RestAPI (使用ajax访问),会进入后端配置的 ssoServer.doLoginHandle
函数中,此函数的返回值即是此接口的响应值。另外需要注意:此接口并非只能携带 name、pwd 参数,因为你可以在方法里通过 SaHolder.getRequest().getParam("xxx")
来获取前端提交的其它参数。
Ticket 校验接口
(isHttp=true)
时打开http://{host}:{port}/sso/checkTicket
参数 | 是否必填 | 说明 |
/sso/auth | ||
md5( [client={client值}&]nonce={随机字符串}&[ssoLogoutCall={单点注销回调地址}&]ticket={ticket值}×tamp={13位时间戳}&key={secretkey秘钥} ) | ||
校验成功时:
{
"code": 200,
"msg": "ok",
"data": "10001", // 此 ticket 指向的 loginId
"remainSessionTimeout": 7200, // 此账号在 sso-server 端的会话剩余有效期(单位:s)
}
校验失败时:
{
"code": 500,
"msg": "无效ticket:vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq",
"data": null
}
单点注销接口
http://{host}:{port}/sso/signout
http://{host}:{port}/sso/signout?back=xxx
参数 | 是否必填 | 说明 |
md5( loginId={账号id}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} ) | ||
http://{host}:{port}/sso/signout?loginId={value}×tamp={value}&nonce={value}&sign={value}
{
"code": 200, // 200表示请求成功,非200标识请求失败
"msg": "单点注销成功",
"data": null
}
{
"code": 500, // 200表示请求成功,非200标识请求失败
"msg": "签名无效:xxx", // 失败原因
"data": null
}
7.3.2 SSO-Client 接口详解
登录地址
http://{host}:{port}/sso/login
参数 | 是否必填 | 说明 |
方式一:我们需要登录操作,所以带着 back 参数主动访问此接口,框架会拼接好参数后再次将用户重定向至认证中心。 方式二:用户在认证中心登录成功后,带着 ticket 参数重定向而来,此为框架自动处理的逻辑,开发者无需关心。
注销地址
http://{host}:{port}/sso/logout
参数 | 是否必填 | 说明 |
方式一:直接 location.href
网页跳转,此时可携带 back 参数。方式二:使用 Ajax 异步调用(此方式不可携带 back 参数,但是需要提交会话 Token ),注销成功将返回以下内容:
{
"code": 200, // 200表示请求成功,非200标识请求失败
"msg": "单点注销成功",
"data": null
}
单点注销回调接口
(isHttp=true)
时打开,且为框架回调,开发者无需关心http://{host}:{port}/sso/logoutCall
参数 | 是否必填 | 说明 |
md5( loginId={账号id}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} ) | ||
{
"code": 200, // 200表示请求成功,非200标识请求失败
"msg": "单点注销回调成功",
"data": null
}
7.4 模式一 共享Cookie同步会话
sa-token-demo-sso-server sa-token-demo-sso1-client
**[共享Cookie同步会话]**
的方式做到单点登录。7.4.1 设计思路
前端的 Token
无法在多个系统下共享。后端的 Session
无法在多个系统间共享。
使用 共享Cookie
来解决 Token 共享问题。使用 Redis
来解决 Session 共享问题。
stp.com
下的Cookie,在s1.stp.com
、s2.stp.com
等子域名都是可以共享访问的。而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中7.4.2 SSO-Server
准备工作
(C:\windows\system32\drivers\etc\hosts)
,添加以下IP映射,方便我们进行测试: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
sso.stp.com
为统一认证中心地址,当用户在其它 Client 端发起登录请求时,均将其重定向至认证中心,待到登录成功之后再原路返回到 Client 端。指定Cookie的作用域
sso.stp.com
访问服务器,其Cookie也只能写入到sso.stp.com
下,为了将Cookie写入到其父级域名stp.com
下,我们需要更改 SSO-Server 端的 yml 配置:yaml 风格sa-token:
cookie:
# 配置 Cookie 作用域
domain: stp.com
这个配置原本是被注释掉的,现在将其打开。另外我们格外需要注意:在SSO模式一测试完毕之后,一定要将这个配置再次注释掉,因为模式一与模式二三使用不同的授权流程,这行配置会影响到我们模式二和模式三的正常运行。
7.4.3 SSO-Client
引入依赖
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</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.38.0</version>
</dependency>
新建 Controller 控制器
/**
* Sa-Token-SSO Client端 Controller
* @author click33
*/
@RestController
public class SsoClientController {
// SSO-Client端:首页
@RequestMapping("/")
public String index() {
String authUrl = SaSsoManager.getClientConfig().splicingAuthUrl();
String solUrl = SaSsoManager.getClientConfig().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());
}
}
application.yml 配置
# 端口
server:
port: 9001
# Sa-Token 配置
sa-token:
# SSO-相关配置
sso-client:
# SSO-Server端主机地址
server-url: http://sso.stp.com:9000
# 配置 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端 Demo
*/
@SpringBootApplication
public class SaSso1ClientApplication {
public static void main(String[] args) {
SpringApplication.run(SaSso1ClientApplication.class, args);
System.out.println();
System.out.println("---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------");
System.out.println("配置信息:" + SaSsoManager.getClientConfig());
System.out.println("测试访问应用端一: http://s1.stp.com:9001");
System.out.println("测试访问应用端二: http://s2.stp.com:9001");
System.out.println("测试访问应用端三: http://s3.stp.com:9001");
System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456");
System.out.println();
}
}
7.4.4 访问测试
http://s1.stp.com:9001/ http://s2.stp.com:9001/ http://s3.stp.com:9001/
7.5模式二 URL重定向传播会话
sa-token-demo-sso-server sa-token-demo-sso1-client
[URL重定向传播会话]
的方式做到单点登录。7.5.1 设计思路
前端的 Token
无法在多个系统下共享。后端的 Session
无法在多个系统间共享。
用户在 子系统 点击 [登录]
按钮。用户跳转到子系统登录接口 /sso/login
,并携带back参数
记录初始页面URL。
形如: http://{sso-client}/sso/login?back=xxx
redirect参数
记录子系统的登录页URL。形如: http://{sso-server}/sso/auth?redirect=xxx?back=xxx
/sso/login
,并携带ticket码
参数。形如: http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx
ticket码
从 SSO-Redis
中获取账号id,并在子系统登录此账号会话。back
页面。[登录]
按钮,由于此用户在SSO认证中心已有会话存在, 所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。为什么不直接回传 Token,而是先回传 Ticket,再用 Ticket 去查询对应的账号id? Token 作为长时间有效的会话凭证,在任何时候都不应该直接暴露在 URL 之中(虽然 Token 直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机) 为了不让系统安全处于亚健康状态,Sa-Token-SSO 选择先回传 Ticket,再由 Ticket 获取账号id,且 Ticket 一次性用完即废,提高安全性。
7.5.2 SSO-Server
准备工作
(C:\windows\system32\drivers\etc\hosts)
,添加以下IP映射,方便我们进行测试: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
去除 SSO-Server 的 Cookie 作用域配置
sa-token:
#cookie:
# 配置 Cookie 作用域
#domain: stp.com
7.5.3 SSO-Client
创建 SSO-Client 端项目
sa-token-demo-sso2-client
,引入依赖:Maven 方式<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</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.38.0</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 SaSsoClientProcessor.instance.dister();
}
}
配置SSO认证中心地址
application.yml
配置如下信息:# 端口
server:
port: 9001
# sa-token配置
sa-token:
# SSO-相关配置
sso-client:
# SSO-Server 端主机地址
server-url: http://sa-sso-server.com:9000
# 配置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
注意点: sa-token.alone-redis
的配置需要和SSO-Server端连接同一个Redis(database 值也要一样!database 值也要一样!database 值也要一样!重说三!)
写启动类
@SpringBootApplication
public class SaSso2ClientApplication {
public static void main(String[] args) {
SpringApplication.run(SaSso2ClientApplication.class, args);
System.out.println();
System.out.println("---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------");
System.out.println("配置信息:" + SaSsoManager.getClientConfig());
System.out.println("测试访问应用端一: http://sa-sso-client1.com:9001");
System.out.println("测试访问应用端二: http://sa-sso-client2.com:9001");
System.out.println("测试访问应用端三: http://sa-sso-client3.com:9001");
System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456");
System.out.println();
}
}
7.5.4 测试访问
SSO-Server
与 SSO-Client
,然后从浏览器访问:http://sa-sso-client1.com:9001/登录
按钮,页面会被重定向到登录中心(3) SSO-Server提示我们在认证中心尚未登录,我们点击 doLogin登录
按钮进行模拟登录(4) SSO-Server认证中心登录成功,我们回到刚才的页面刷新页面(5) 页面被重定向至Client
端首页,并提示登录成功,至此,Client1
应用已单点登录成功!(6) 我们再次访问Client2
:http://sa-sso-client2.com:9001/(7) 提示未登录,我们点击 登录
按钮,会直接提示登录成功(8) 同样的方式,我们打开Client3
,也可以直接登录成功:http://sa-sso-client3.com:9001/至此,测试完毕!可以看出,除了在Client1
端我们需要手动登录一次之外,在Client2端
和Client3端
都是可以无需再次认证,直接登录成功的。我们可以通过 F12控制台 Network 跟踪整个过程7.6 模式三 Http请求获取会话
先实战SSO模式二!因为模式三仅仅属于模式二的一个特殊场景
7.6.1 问题分析
Client 端无法直连 Redis 校验 ticket,取出账号id。 Client 端无法与 Server 端共用一套会话,需要自行维护子会话。
sa-token-demo-sso-server sa-token-demo-sso3-client
7.6.2 在Client 端更改 Ticket 校验方式
sa-token:
sso-client:
# 打开模式三(使用Http请求校验ticket)
is-http: true
http://sa-sso-client1.com:9001/ http://sa-sso-client2.com:9001/ http://sa-sso-client3.com:9001/
注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰
7.6.3 获取 UserInfo
首先在 Server 端开放一个查询数据的接口
// 示例:获取数据接口(用于在模式三下,为 client 端开放拉取数据的接口)
@RequestMapping("/sso/getData")
public SaResult getData(String apiType, String loginId) {
System.out.println("---------------- 获取数据 ----------------");
System.out.println("apiType=" + apiType);
System.out.println("loginId=" + loginId);
// 校验签名:只有拥有正确秘钥发起的请求才能通过校验
SaSignUtil.checkRequest(SaHolder.getRequest());
// 自定义返回结果(模拟)
return SaResult.ok()
.set("id", loginId)
.set("name", "LinXiaoYu")
.set("sex", "女")
.set("age", 18);
}
如果配置了 “不同 client 不同秘钥” 模式,则需要将上述的:SaSignUtil.checkRequest(SaHolder.getRequest());
改为以下方式:String client = SaHolder.getRequest().getHeader("client"); SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest());
在 Client 端调用此接口查询数据
SsoClientController
中新增接口// 查询我的账号信息
@RequestMapping("/sso/myInfo")
public Object myInfo() {
// 组织请求参数
Map<String, Object> map = new HashMap<>();
map.put("apiType", "userinfo");
map.put("loginId", StpUtil.getLoginId());
// 发起请求
Object resData = SaSsoUtil.getData(map);
System.out.println("sso-server 返回的信息:" + resData);
return resData;
}
7.6.4 单点注销
// 在 `sa-token.is-share=true` 的情况下,调用此代码即可单点注销:
StpUtil.logout();
// 在 `sa-token.is-share=false` 的情况下,调用此代码即可单点注销:
StpUtil.logout(StpUtil.getLoginId());
增加 pom.xml 配置
<!-- Http请求工具 -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.26</version>
</dependency>
forest.log-enabled=false
关闭 Forest 框架自身的日志打印,这不是必须的,你可以将其打开。SSO-Client 端新增配置:API调用秘钥
application.yml
增加:yaml 风格sa-token:
sign:
# API 接口调用秘钥
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
forest:
# 关闭 forest 请求日志打印
log-enabled: false
SSO-Client 配置 http 请求处理器
// 配置SSO相关参数
@Autowired
private void configSso(SaSsoClientConfig ssoClient) {
// 配置Http请求处理器
ssoClient.sendHttp = url -> {
System.out.println("------ 发起请求:" + url);
String resStr = Forest.get(url).executeAsString();
System.out.println("------ 请求结果:" + resStr);
return resStr;
};
}
启动测试
http://sa-sso-client1.com:9001/ http://sa-sso-client2.com:9001/ http://sa-sso-client3.com:9001/
**[注销]**
按钮,即可单点注销成功(打开另外两个client,刷新一下页面,登录态丢失)。PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。例如,我们使用 Apifox 接口测试工具 可以做到同样的效果:测试完毕!7.6.5 总结
模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。 模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。 模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。
7.7前后端分离架构下SSO
后端server:端口9000 前端server:端口8848 后端client:端口9001 前端client:端口8849
7.7.1 SSO-Client后端
新建H5Controller开放接口
@RestController
public class H5Controller {
// 当前是否登录
@RequestMapping("/sso/isLogin")
public Object isLogin() {
return SaResult.data(StpUtil.isLogin());
}
// 返回SSO认证中心登录地址
@RequestMapping("/sso/getSsoAuthUrl")
public SaResult getSsoAuthUrl(String clientLoginUrl) {
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
return SaResult.data(serverAuthUrl);
}
// 根据ticket进行登录
@RequestMapping("/sso/doLoginByTicket")
public SaResult doLoginByTicket(String ticket) {
Object loginId = SaSsoProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket");
if(loginId != null) {
StpUtil.login(loginId);
return SaResult.data(StpUtil.getTokenValue());
}
return SaResult.error("无效ticket:" + ticket);
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
增加跨域过滤器CorsFilter.java
配置统一认证地址是server的前端页面
sa-token:
# SSO-相关配置
sso:
# SSO-Server端 统一认证地址
# auth-url: http://sa-sso-server.com:9000/sso/auth #前后端一体配置
auth-url: http://127.0.0.1:8848/sso-auth.html #前后端分离sso-server配置,
# 是否打开单点注销接口
is-slo: true
7.7.2 SSO-Client前端
新建前端项目
sa-token-demo-sso-client-h5
,在根目录添加测试文件:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-Token-SSO-Client端-测试页(前后端分离版)</title>
</head>
<body>
<h2>Sa-Token SSO-Client 应用端(前后端分离版)</h2>
<p>当前是否登录:<b class="is-login"></b></p>
<p>
<a href="javascript:location.href='sso-login.html?back=' + encodeURIComponent(location.href);">登录</a>
<a href="javascript:location.href=baseUrl + '/sso/logout?satoken=' + localStorage.satoken + '&back=' + encodeURIComponent(location.href);">注销</a>
</p>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script type="text/javascript">
// 后端接口地址
var baseUrl = "http://sa-sso-client1.com:9001";
// 查询当前会话是否登录
$.ajax({
url: baseUrl + '/sso/isLogin',
type: "post",
dataType: 'json',
headers: {
"X-Requested-With": "XMLHttpRequest",
"satoken": localStorage.getItem("satoken")
},
success: function(res){
$('.is-login').html(res.data + '');
},
error: function(xhr, type, errorThrown){
return alert("异常:" + JSON.stringify(xhr));
}
});
</script>
</body>
</html>
添加登录处理文件sso-login.html
index.html
一样放在根目录下测试运行
7.7.3 SSO-Server后端
@RestController
public class H5Controller {
/**
* 获取 redirectUrl
*/
@RequestMapping("/sso/getRedirectUrl")
private Object getRedirectUrl(String redirect, String mode, String client) {
// 未登录情况下,返回 code=401
if(StpUtil.isLogin() == false) {
return SaResult.code(401);
}
// 已登录情况下,构建 redirectUrl
if(SaSsoConsts.MODE_SIMPLE.equals(mode)) {
// 模式一
SaSsoUtil.checkRedirectUrl(SaFoxUtil.decoderUrl(redirect));
return SaResult.data(redirect);
} else {
// 模式二或模式三
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), client, redirect);
return SaResult.data(redirectUrl);
}
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
7.7.4 SSO-Server前端
SSO测试与模式2相同
7.8Sa-Token-OAuth2.0 模块
7.8.1 简介
功能点 | SSO单点登录 | OAuth2.0 |
授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面 密码式(Password):Client直接拿着用户的账号密码换取授权Token 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权
7.8.2 实战案例
准备工作
(C:\windows\system32\drivers\etc\hosts)
,添加以下IP映射,方便我们进行测试:127.0.0.1 sa-oauth-server.com
127.0.0.1 sa-oauth-client.com
引入依赖
sa-token-demo-oauth2-server
(不会的同学自行百度或参考仓库示例),添加pom依赖:Maven 方式<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token-OAuth2.0 模块 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-oauth2</artifactId>
<version>1.38.0</version>
</dependency>
开放服务
SaOAuth2TemplateImpl
/**
* Sa-Token OAuth2.0 整合实现
*/
@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {
// 根据 id 获取 Client 信息
@Override
public SaClientModel getClientModel(String clientId) {
// 此为模拟数据,真实环境需要从数据库查询
if("1001".equals(clientId)) {
return new SaClientModel()
.setClientId("1001")
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
.setAllowUrl("*")
.setContractScope("userinfo")
.setIsAutoMode(true);
}
return null;
}
// 根据ClientId 和 LoginId 获取openid
@Override
public String getOpenid(String clientId, Object loginId) {
// 此为模拟数据,真实环境需要从数据库查询
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
}
}
SaOAuth2ServerController
/**
* Sa-OAuth2 Server端 控制器
*/
@RestController
public class SaOAuth2ServerController {
// 处理所有OAuth相关请求
@RequestMapping("/oauth2/*")
public Object request() {
System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.serverRequest();
}
// Sa-OAuth2 定制化配置
@Autowired
public void setSaOAuth2Config(SaOAuth2Config cfg) {
cfg.
// 配置:未登录时返回的View
setNotLoginView(() -> {
String msg = "当前会话在OAuth-Server端尚未登录,请先访问"
+ "<a href='/oauth2/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+ "进行登录之后,刷新页面开始授权";
return msg;
}).
// 配置:登录处理函数
setDoLoginHandle((name, pwd) -> {
if("sa".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok();
}
return SaResult.error("账号名或密码错误");
}).
// 配置:确认授权时返回的View
setConfirmView((clientId, scope) -> {
String msg = "<p>应用 " + clientId + " 请求授权:" + scope + "</p>"
+ "<p>请确认:<a href='/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scope + "' target='_blank'> 确认授权 </a></p>"
+ "<p>确认之后刷新页面</p>";
return msg;
})
;
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
setDoLoginHandle
函数里如果要获取name, pwd以外的参数,可通过SaHolder.getRequest().getParam("xxx")
来获取 3、创建启动类:/**
* 启动:Sa-OAuth2 Server端
*/
@SpringBootApplication
public class SaOAuth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(SaOAuth2ServerApplication.class, args);
System.out.println("\nSa-Token-OAuth Server 端启动成功");
}
}
授权码模式访问测试
http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.cc&scope=userinfo
http://sa-oauth-server.com:8001/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}
Access-Token
、Refresh-Token
、openid
等授权信息测试完毕客户端测试
OAuth2-Server
与 OAuth2-Client
,然后从浏览器访问:http://sa-oauth-client.com:8002如图,可以针对OAuth2.0四种模式进行详细测试7.8.3 OAuth2开放接口详解
7.8.3.1 模式一:授权码(Authorization Code)
获取授权码
根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符)
http://sa-oauth-server.com:8001/oauth2/authorize
?response_type=code
&client_id={value}
&redirect_uri={value}
&scope={value}
&state={value}
文档太长,超过了 平台限制........
这部分详细内容略,请参见 技术自由圈 PDF 《史上最全的 用户登录与权限设计 系统文档:Sa-Token学习圣经》
7.8.3.2 模式二:隐藏式(Implicit)
http://sa-oauth-server.com:8001/oauth2/authorize
?response_type=token
&client_id={value}
&redirect_uri={value}
&scope={value}
$state={value}
参数 | 是否必填 | 说明 |
redirect_uri#token=xxxx-xxxx-xxxx-xxxx
7.8.3.3 模式三:密码式(Password)
http://sa-oauth-server.com:8001/oauth2/token
?grant_type=password
&client_id={value}
&client_secret={value}
&username={value}
&password={value}
参数 | 是否必填 | 说明 |
{
"code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述
"msg": "ok",
"data": {
"access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token值
"refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token值
"expires_in": 7199, // Access-Token剩余有效期,单位秒
"refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒
"client_id": "1001", // 应用id
"scope": "", // 此令牌包含的权限
"openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" // openid
}
}
7.8.3.4 模式四:凭证式(Client Credentials)
Access-Token
,代表用户对第三方应用的授权, 在OAuth2.0中还有一种针对 Client级别的授权, 即:Client-Token
,代表应用自身的资源授权 在Client端的后台访问以下接口:http://sa-oauth-server.com:8001/oauth2/client_token
?grant_type=client_credentials
&client_id={value}
&client_secret={value}
参数 | 是否必填 | 说明 |
{
"code": 200,
"msg": "ok",
"data": {
"client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值
"expires_in": 7199, // Token剩余有效时间,单位秒
"client_id": "1001", // 应用id
"scope": null // 包含权限
}
}
Client-Token
具有延迟作废特性,即:在每次获取最新Client-Token
的时候,旧Client-Token
不会立即过期,而是作为Past-Token
再次储存起来, 资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用说在最后:有问题找老架构取经
被裁之后, 空窗1年/空窗2年, 如何 起死回生 ?
案例1:42岁被裁2年,天快塌了,急救1个月,拿到开发经理offer,起死回生
案例2:35岁被裁6个月, 职业绝望,转架构急救上岸,DDD和3高项目太重要了
案例3:失业15个月,学习40天拿offer, 绝境翻盘,如何实现?
被裁之后,100W 年薪 到手, 如何 人生逆袭?
100W案例,100W年薪的底层逻辑是什么? 如何实现年薪百万? 如何远离 中年危机?
如何 逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢