重构文档结构

This commit is contained in:
click33
2022-10-10 00:59:08 +08:00
parent a27ed0ce88
commit 94e572c087
120 changed files with 286 additions and 49 deletions

View File

@@ -0,0 +1,98 @@
# Http Basic 认证
Http Basic 是 http 协议中最基础的认证方式,其有两个特点:
- 简单、易集成。
- 功能支持度低。
在 Sa-Token 中使用 Http Basic 认证非常简单,只需调用几个简单的方法
---
### 1、启用 Http Basic 认证
首先我们在一个接口中,调用 Http Basic 校验:
``` java
@RequestMapping("test3")
public SaResult test3() {
SaBasicUtil.check("sa:123456");
// ... 其它代码
return SaResult.ok();
}
```
全局异常处理:
``` java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
```
然后我们访问这个接口时,浏览器会强制弹出一个表单:
![sa-basic.png](https://oss.dev33.cn/sa-token/doc/sa-basic.png 's-w-sh')
当我们输入账号密码后 `sa / 123456`,才可以继续访问数据:
![sa-basic-ok.png](https://oss.dev33.cn/sa-token/doc/sa-basic-ok.png 's-w-sh')
### 2、其它启用方式
``` java
// 对当前会话进行 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"));
});
}
```
### 3、URL 认证
除了访问后再输入账号密码外,我们还可以在 URL 中直接拼接账号密码通过 Basic 认证,例如:
``` url
http://sa:123456@127.0.0.1:8081/test/test3
```

232
sa-token-doc/up/disable.md Normal file
View File

@@ -0,0 +1,232 @@
# 账号封禁
之前的章节中,我们学习了 踢人下线 和 强制注销 功能,用于清退违规账号。
在部分场景下,我们还需要将其 **账号封禁**,以防止其再次登录。
---
### 1、账号封禁
对指定账号进行封禁:
``` java
// 封禁指定账号
StpUtil.disable(10001, 86400);
```
参数含义:
- 参数1要封禁的账号id。
- 参数2封禁时间单位此为 86400秒 = 1天此值为 -1 时,代表永久封禁)。
注意点:对于正在登录的账号,将其封禁并不会使它立即掉线,如果我们需要它即刻下线,可采用先踢再封禁的策略,例如:<br>
``` java
// 先踢下线
StpUtil.kickout(10001);
// 再封禁账号
StpUtil.disable(10001, 86400);
```
待到下次登录时,我们先校验一下这个账号是否已被封禁:
``` java
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 通过校验后,再进行登录:
StpUtil.login(10001);
```
!> 旧版本在 `StpUtil.login()` 时会自动校验账号是否被封禁v1.31.0 之后将 校验封禁 和 登录 两个动作分离成两个方法,不再自动校验,请注意其中的逻辑更改。
此模块所有方法:
``` java
// 封禁指定账号
StpUtil.disable(10001, 86400);
// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001);
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2
StpUtil.getDisableTime(10001);
// 解除封禁
StpUtil.untieDisable(10001);
```
### 2、分类封禁
有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务。
假设我们在开发一个电商系统,对于违规账号的处罚,我们设定三种分类封禁:
- 1、封禁评价能力账号A 因为多次虚假好评,被限制订单评价功能。
- 2、封禁下单能力账号B 因为多次薅羊毛,被限制下单功能。
- 3、封禁开店能力账号C 因为店铺销售假货,被限制开店功能。
相比于封禁账号的一刀切处罚,这里的关键点在于:每一项能力封禁的同时,都不会对其它能力造成影响。
也就是说我们需要一种只对部分服务进行限制的能力,对应到代码层面,就是只禁止部分接口的调用。
``` java
// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);
```
参数释义:
- 参数1要封禁的账号id。
- 参数2针对这个账号要封禁的服务标识可以是任意的自定义字符串
- 参数3要封禁的时间单位此为 86400秒 = 1天此值为 -1 时,代表永久封禁)。
分类封禁模块所有可用API
``` java
/*
* 以下示例中:"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");
```
通过以上示例,你应该大致可以理解 `业务封禁 -> 业务校验` 的处理步骤。
有关分类封禁的所有方法:
``` java
// 封禁:指定账号的指定服务
StpUtil.disable(10001, "<业务标识>", 86400);
// 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001, "<业务标识>");
// 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001, "<业务标识>");
// 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁)
StpUtil.getDisableTime(10001, "<业务标识>");
// 解封:指定账号的指定服务
StpUtil.untieDisable(10001, "<业务标识>");
```
### 3、阶梯封禁
对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 “阶梯” 一般有两种形式:
- 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延……
- 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等……
基于处罚时间的阶梯,我们只需在封禁时 `StpUtil.disable(10001, 86400)` 传入不同的封禁时间即可,下面我们着重探讨一下基于处罚力度的阶梯形式。
假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度:
- 1、轻度违规封禁其发帖、评论能力但允许其点赞、关注等操作。
- 2、中度违规封禁其发帖、评论、点赞、关注等一切与别人互动的能力但允许其浏览帖子、浏览评论。
- 3、重度违规封禁其登录功能限制一切能力。
解决这种需求的关键在于,我们需要把不同处罚力度,量化成不同的处罚等级,比如上述的 `轻度`、`中度`、`重度` 3 个力度,
我们将其量化为`一级封禁`、`二级封禁`、`三级封禁` 3个等级数字越大代表封禁力度越高。
然后我们就可以使用阶梯封禁的API进行鉴权了
``` java
// 阶梯封禁,参数:封禁账号、封禁级别、封禁时间
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` 时,框架就会抛出异常。
如果业务足够复杂,我们还可能将 分类封禁 和 阶梯封禁 组合使用:
``` java
// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间
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);
```
### 4、使用注解完成封禁校验
首先我们需要注册 Sa-Token 全局拦截器(可参考 [注解鉴权](/use/at-check) 章节),然后我们就可以使用以下注解校验账号是否封禁
``` java
// 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法
@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();
}
```

View File

@@ -0,0 +1,117 @@
# 全局过滤器
---
### 组件简述
之前的章节中我们学习了“根据拦截器实现路由拦截鉴权”其实在大多数web框架中使用过滤器可以实现同样的功能本章我们就利用Sa-Token全局过滤器来实现路由拦截器鉴权。
首先我们先梳理清楚一个问题,既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之:
1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。
2. 过滤器可以拦截静态资源,方便我们做一些权限控制。
3. 部分Web框架根本就没有提供拦截器功能但几乎所有的Web框架都会提供过滤器机制。
但是过滤器也有一些缺点,比如:
1. 由于太过底层,导致无法率先拿到`HandlerMethod`对象,无法据此添加一些额外功能。
2. 由于拦截的太全面了,导致我们需要对很多特殊路由(如`/favicon.ico`)做一些额外处理。
3. 在Spring中过滤器中抛出的异常无法进入全局`@ExceptionHandler`,我们必须额外编写代码进行异常处理。
Sa-Token同时提供过滤器和拦截器机制不是为了让谁替代谁而是为了让大家根据自己的实际业务合理选择拥有更多的发挥空间。
### 在 SpringBoot 中注册过滤器
同拦截器一样为了避免不必要的性能浪费Sa-Token全局过滤器默认处于关闭状态若要使用过滤器组件首先你需要注册它到项目中
``` java
/**
* [Sa-Token 权限认证] 配置类
* @author kong
*/
@Configuration
public class SaTokenConfigure {
/**
* 注册 [Sa-Token全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 拦截路由 与 放行路由
.addInclude("/**").addExclude("/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());
})
// 前置函数:在每次认证函数之前执行
.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")
;
})
;
}
}
```
##### 注意事项:
- 在`[认证函数]`里,你可以写和拦截器里一致的代码,进行路由匹配鉴权,参考:[路由拦截鉴权](/use/route-check)。
- 由于过滤器中抛出的异常不进入全局异常处理,所以你必须提供`[异常处理函数]`来处理`[认证函数]`里抛出的异常。
- 在`[异常处理函数]`里的返回值,将作为字符串输出到前端,如果需要定制化返回数据,请注意其中的格式转换。
改写 `setError` 函数的响应格式示例:
``` java
.setError(e -> {
// 设置响应头
SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
// 使用封装的 JSON 工具类转换数据格式
return JSONUtil.toJsonStr( SaResult.error(e.getMessage()) );
})
```
JSON 工具类可参考:[Hutool-Json](https://hutool.cn/docs/#/json/JSONUtil)
### 在 WebFlux 中注册过滤器
`Spring WebFlux`中不提供拦截器机制,因此若你的项目需要路由鉴权功能,过滤器是你唯一的选择,在`Spring WebFlux`注册过滤器的流程与上述流程几乎完全一致,
除了您需要将过滤器名称由`SaServletFilter`更换为`SaReactorFilter`以外,其它所有步骤均可参考以上示例。
``` java
/**
* [Sa-Token 权限认证] 配置类
* @author kong
*/
@Configuration
public class SaTokenConfigure {
/**
* 注册 [Sa-Token全局过滤器]
*/
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 其它代码...
;
}
}
```

View File

@@ -0,0 +1,199 @@
# 全局侦听器
---
### 1、工作原理
Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。
事件触发流程大致如下:
![sa-token-listener](https://oss.dev33.cn/sa-token/doc/sa-token-listener.svg 's-w')
框架默认内置了侦听器 `SaTokenListenerForConsolePrint` 实现:[代码参考](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForConsolePrint.java)
,功能是控制台 log 打印输出,你可以通过配置`sa-token.is-log=true`开启。
要注册自定义的侦听器也非常简单:
1. 新建类实现 `SaTokenListener` 接口。
2. 将实现类注册到 `SaTokenEventCenter` 事件发布中心。
### 2、自定义侦听器实现
##### 2.1、新建实现类:
新建`MySaTokenListener.java`,继承`SaTokenListener`接口,并添加上注解`@Component`,保证此类被`SpringBoot`扫描到:
``` java
/**
* 自定义侦听器的实现
*/
@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");
}
/** 每次创建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");
}
}
```
##### 2.2、将侦听器注册到事件中心:
以上代码由于添加了 `@Component` 注解,会被 SpringBoot 扫描并自动注册到事件中心,此时我们无需手动注册。
如果我们没有添加 `@Component` 注解或者项目属于非 IOC 自动注入环境,则需要我们手动将这个侦听器注册到事件中心:
``` java
// 将侦听器注册到事件发布中心
SaTokenEventCenter.registerListener(new MySaTokenListener());
```
事件中心的其它一些常用方法:
``` java
// 获取已注册的所有侦听器
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);
```
##### 2.3、启动测试:
在 `TestController` 中添加登录测试代码:
``` java
// 测试登录接口
@RequestMapping("login")
public SaResult login() {
System.out.println("登录前");
StpUtil.login(10001);
System.out.println("登录后");
return SaResult.ok();
}
```
启动项目,访问登录接口,观察控制台输出:
![sa-token-listener-println](https://oss.dev33.cn/sa-token/doc/sa-token-listener-println.png 's-w-sh')
### 3、其它注意点
##### 3.1、你可以通过继承 `SaTokenListenerForSimple` 快速实现一个侦听器:
``` java
@Component
public class MySaTokenListener extends SaTokenListenerForSimple {
/*
* SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。
*/
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------- 自定义侦听器实现 doLogin");
}
}
```
##### 3.2、使用匿名内部类的方式注册:
``` java
// 登录时触发
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------------- doLogin");
}
});
```
##### 3.3、使用 try-catch 包裹不安全的代码:
如果你认为你的事件处理代码是不安全的(代码可能在运行时抛出异常),则需要使用 `try-catch` 包裹代码,以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。
``` java
// 登录时触发
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
try {
// 不安全代码需要写在 try-catch 里
// ......
} catch (Exception e) {
e.printStackTrace();
}
}
});
```
##### 3.4、疑问:一个项目可以注册多个侦听器吗?
可以,多个侦听器间彼此独立,互不影响,按照注册顺序依次接受到事件通知。

View File

@@ -0,0 +1,92 @@
# Sa-Token 集成 Redis
---
Sa-token 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:
1. 重启后数据会丢失。
2. 无法在分布式环境中共享数据。
为此Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在 `Redis``Memcached`等专业的缓存中间件中,
做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。
以下是官方提供的 Redis 集成包:
---
### 方式1、Sa-Token 整合 Redis (使用 jdk 默认序列化方式)
``` xml
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>${sa.top.version}</version>
</dependency>
```
优点兼容性好缺点Session 序列化后基本不可读,对开发者来讲等同于乱码。
### 方式2、Sa-Token 整合 Redis使用 jackson 序列化方式)
``` xml
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${sa.top.version}</version>
</dependency>
```
优点Session 序列化后可读性强,可灵活手动修改,缺点:兼容性稍差。
### 集成 Redis 请注意:
**1. 无论使用哪种序列化方式,你都必须为项目提供一个 Redis 实例化方案,例如:**
``` xml
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
```
**2. 引入了依赖,我还需要为 Redis 配置连接信息吗?** <br>
需要!只有项目初始化了正确的 Redis 实例,`Sa-Token`才可以使用 Redis 进行数据持久化,参考以下`yml配置`
``` java
# 端口
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
```
**3. 集成 Redis 后,是我额外手动保存数据,还是框架自动保存?** <br>
框架自动保存。集成 `Redis` 只需要引入对应的 `pom依赖` 即可,框架所有上层 API 保持不变。
**4. 集成包版本问题** <br>
Sa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致,否则可能出现兼容性问题。
<br><br>
更多框架的集成方案正在更新中...

View File

@@ -0,0 +1,189 @@
# 多账号认证
---
### 1、需求场景
有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 `user表``admin表`
在这种场景下,如果两套账号我们都使用 `StpUtil` 类的API进行登录鉴权那么势必会发生逻辑冲突。
在Sa-Token中这个问题的模型叫做多账号体系认证。
要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行。
### 2、演进思路
假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:`StpUtil.login(10001)` 是一样的,
那么问题来了:在`StpUtil.getLoginId()`获取到的账号id如何区分它是User用户还是Admin用户
你可能会想到为他们加一个固定前缀,比如`StpUtil.login("User_" + 10001)``StpUtil.login("Admin_" + 10001)`,这样确实是可以解决问题的,
但是同样的:你需要在`StpUtil.getLoginId()`时再裁剪掉相应的前缀才能获取真正的账号id这样一增一减就让我们的代码变得无比啰嗦。
那么,有没有从框架层面支持的,更优雅的解决方案呢?
### 3、解决方案
前面几篇介绍的api调用都是经过 StpUtil 类的各种静态方法进行授权认证,
而如果我们深入它的源码,[点此阅览](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java) <br/>
就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量`stpLogic`的各个API包装一下进行转发。
这样做有两个优点:
- StpLogic 类的所有函数都可以被重写,按需扩展。
- 在构造方法时随意传入一个不同的 `loginType`,就可以再造一套账号登录体系。
### 4、操作示例
比如说,对于原生`StpUtil`类,我们只做`admin账号`权限认证,而对于`user账号`,我们则:
1. 新建一个新的权限认证类,比如: `StpUserUtil.java`
2.`StpUtil.java`类的全部代码复制粘贴到 `StpUserUtil.java`里。
3. 更改一下其 `LoginType` 比如:
``` java
public class StpUserUtil {
/**
* 账号体系标识
*/
public static final String TYPE = "user"; // 将 LoginType 从`login`改为`user`
// 其它代码 ...
}
```
4. 接下来就可以像调用`StpUtil.java`一样调用 `StpUserUtil.java`了,这两套账号认证的逻辑是完全隔离的。
> 成品样例参考:[码云 StpUserUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/StpUserUtil.java)
### 5、在多账户模式下使用注解鉴权
框架默认的注解鉴权 如`@SaCheckLogin` 只针对原生`StpUtil`进行鉴权。
例如,我们在一个方法上加上`@SaCheckLogin`注解,这个注解只会放行通过`StpUtil.login(id)`进行登录的会话,
而对于通过`StpUserUtil.login(id)`进行登录的会话,则始终不会通过校验。
那么如何告诉`@SaCheckLogin`要鉴别的是哪套账号的登录会话呢很简单你只需要指定一下注解的type属性即可
``` java
// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil`
@SaCheckLogin(type = StpUserUtil.TYPE)
@RequestMapping("info")
public String info() {
return "查询用户信息";
}
```
注:`@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理亦可根据type属性指定其校验的账号体系此属性默认为`""`,代表使用原生`StpUtil`账号体系。
> 使用注解必须[添加注解拦截器](/use/at-check)
### 6、使用注解合并简化代码
交流群里有同学反应,虽然可以根据 `@SaCheckLogin(type = "user")` 指定账号类型,但几十上百个注解都加上这个的话,还是有些繁琐,代码也不够优雅,有么有更简单的解决方案?
我们期待一种`[注解继承/合并]`的能力,即:自定义一个注解,标注上`@SaCheckLogin(type = "user")`
然后在方法上标注这个自定义注解,效果等同于标注`@SaCheckLogin(type = "user")`。
很遗憾JDK默认的注解处理器并没有提供这种`[注解继承/合并]`的能力,不过好在我们可以利用 Spring 的注解处理器,达到同样的目的。
1. 重写Sa-Token默认的注解处理器
``` java
@Configuration
public class SaTokenConfigure {
@Autowired
public void rewriteSaStrategy() {
// 重写Sa-Token的注解处理器增加注解合并功能
SaStrategy.me.getAnnotation = (element, annotationClass) -> {
return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
};
}
}
```
2. 自定义一个注解:
``` java
/**
* 登录认证(User版):只有登录之后才能进入该方法
* <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上)
*/
@SaCheckLogin(type = "user")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaUserCheckLogin {
}
```
3. 接下来就可以使用我们的自定义注解了:
``` java
// 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user")
@SaUserCheckLogin
@RequestMapping("info")
public String info() {
return "查询用户信息";
}
```
注:其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理,
完整示例参考:[码云:自定义注解](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at)。
### 7、同端多登陆
假设我们不仅需要在后台同时集成两套账号我们还需要在一个客户端同时登陆两套账号业务场景举例一个APP中可以同时登陆商家账号和用户账号
如果我们不做任何特殊处理的话,在客户端会发生`token覆盖`新登录的token会覆盖掉旧登录的token从而导致旧登录失效。
那么如何解决这个问题?<br>
很简单,我们只要更改一下 `StpUserUtil` 的 `TokenName` 即可,参考示例如下:
``` java
public class StpUserUtil {
// 使用匿名子类 重写`stpLogic对象`的一些方法
public static StpLogic stpLogic = new StpLogic("user") {
// 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突
@Override
public String splicingKeyTokenName() {
return super.splicingKeyTokenName() + "-user";
}
// 同理你可以按需重写一些其它方法 ...
};
// ...
}
```
再次调用 `StpUserUtil.login(10001)` 进行登录授权时token的名称将不再是 `satoken`,而是我们重写后的 `satoken-user`。
### 8、不同体系不同 SaTokenConfig 配置
如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单,参考示例如下:
``` java
public class StpUserUtil {
// 使用匿名子类 重写`stpLogic对象`的一些方法
public static StpLogic stpLogic = new StpLogic("user") {
// 首先自定义一个 Config 对象
SaTokenConfig config = new SaTokenConfig()
.setTokenName("satoken")
.setTimeout(2592000)
// ... 其它set
;
// 然后重写 stpLogic 配置获取方法
@Override
public SaTokenConfig getConfig() {
return config;
}
};
// ...
}
```

View File

@@ -0,0 +1,59 @@
# 模拟他人
---
以上介绍的 API 都是操作当前账号,对当前账号进行各种鉴权操作,你可能会问,我能不能对别的账号进行一些操作?<br>
比如:查看账号 10001 有无某个权限码、获取 账号 id=10002 的 `User-Session`,等等...
Sa-Token 在 API 设计时充分考虑了这一点暴露出多个api进行此类操作
## 有关操作其它账号的api
``` java
// 获取指定账号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");
```
## 临时身份切换
有时候,我们需要直接将当前会话的身份切换为其它账号,比如:
``` java
// 将当前会话[身份临时切换]为其它账号(本次请求内有效)
StpUtil.switchTo(10044);
// 此时再调用此方法会返回 10044 (我们临时切换到的账号id)
StpUtil.getLoginId();
// 结束 [身份临时切换]
StpUtil.endSwitch();
```
你还可以直接在一个代码段里方法内临时切换身份为指定loginId此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换)
``` java
System.out.println("------- [身份临时切换]调用开始...");
StpUtil.switchTo(10044, () -> {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true
System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044
});
System.out.println("------- [身份临时切换]调用结束...");
```

View File

@@ -0,0 +1,45 @@
# 同端互斥登录
如果你经常使用腾讯QQ就会发现它的登录有如下特点它可以手机电脑同时在线但是不能在两个手机上同时登录一个账号。 <br/>
同端互斥登录指的就是像腾讯QQ一样在同一类型设备上只允许单地点登录在不同类型设备上允许同时在线。
<button class="show-img" img-src="https://oss.dev33.cn/sa-token/doc/g/g3--mutex-login.gif">加载动态演示图</button>
---
## 具体API
在 Sa-Token 中如何做到同端互斥登录? <br/>
首先在配置文件中,将 `isConcurrent` 配置为false然后调用登录等相关接口时声明设备类型即可
#### 指定设备类型登录
``` java
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");
```
调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-4`
#### 指定设备类型强制注销
``` java
// 指定`账号id`和`设备类型`进行强制注销
StpUtil.logout(10001, "PC");
```
如果第二个参数填写null或不填代表将这个账号id所有在线端强制注销被踢出者再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-2`
#### 查询当前登录的设备类型
``` java
// 返回当前token的登录设备类型
StpUtil.getLoginDevice();
```
#### Id 反查 Token
``` java
// 获取指定loginId指定设备类型端的tokenValue
StpUtil.getTokenValueByLoginId(10001, "APP");
```

View File

@@ -0,0 +1,104 @@
# 前后台分离无Cookie模式
---
### 何为无 Cookie 模式?
无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— **前后台分离模式**
常规 Web 端鉴权方法,一般由 `Cookie模式` 完成,而 Cookie 有两个特性:
1. 可由后端控制写入。
2. 每次请求自动提交。
这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)<br/>
而在app、小程序等前后台分离场景中一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊?
见招拆招,其实答案很简单:
- 不能后端控制写入了,就前端自己写入。(难点在**后端如何将 Token 传递到前端**
- 每次请求不能自动提交了,那就手动提交。(难点在**前端如何将 Token 传递到后端**,同时**后端将其读取出来**
### 1、后端将 token 返回到前端
1. 首先调用 `StpUtil.login(id)` 进行登录。
2. 调用 `StpUtil.getTokenInfo()` 返回当前会话的 token 详细参数。
- 此方法返回一个对象,其有两个关键属性:`tokenName``tokenValue`token 的名称和 token 的值)。
- 将此对象传递到前台,让前端人员将这两个值保存到本地。
代码示例:
``` java
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
// 第1步先登录上
StpUtil.login(10001);
// 第2步获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 第3步返回给前端
return SaResult.data(tokenInfo);
}
```
### 2、前端将 token 提交到后端
1. 无论是app还是小程序其传递方式都大同小异。
2. 那就是,将 token 塞到请求`header`里 ,格式为:`{tokenName: tokenValue}`。
3. 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例:
**方式1简单粗暴**
``` js
// 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);
}
});
```
**方式2更加灵活**
``` js
// 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);
}
});
```
4. 只要按照如此方法将`token`值传递到后端Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。
5. 你可能会有疑问,难道我每个`ajax`都要写这么一坨?岂不是麻烦死了?
- 你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。
### 其它解决方案?
如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的`header`参数而已,
而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作。
这其实是对`无Cookie模式`的另一种解决方案,有兴趣的同学可以百度了解一下,在此暂不赘述。

View File

@@ -0,0 +1,85 @@
# 密码加密
严格来讲,密码加密不属于 [权限认证] 的范畴,但是对于大多数系统来讲,密码加密又是安全认证不可或缺的部分,
所以,应大家要求,`Sa-Token`在 v1.14 版本添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法。
### 摘要加密
md5、sha1、sha256
``` java
// md5加密
SaSecureUtil.md5("123456");
// sha1加密
SaSecureUtil.sha1("123456");
// sha256加密
SaSecureUtil.sha256("123456");
// md5加盐加密: md5(md5(str) + md5(salt))
SaSecureUtil.md5BySalt("123456", "salt");
```
### 对称加密
AES加密
``` java
// 定义秘钥和明文
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加密
``` java
// 定义私钥和公钥
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);
```
你可能会有疑问,私钥和公钥这么长的一大串,我怎么弄出来,手写吗?当然不是,调用以下方法生成即可
``` java
// 生成一对公钥和私钥其中Map对象 (private=私钥, public=公钥)
System.out.println(SaSecureUtil.rsaGenerateKeyPair());
```
### Base64编码与解码
``` java
// 文本
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);
```
<br>
如需更多加密算法,可参考 [Hutool-crypto: 加密](https://hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0)

View File

@@ -0,0 +1,85 @@
# [记住我] 模式
---
如图所示,一般网站的登录界面都会有一个 **`[记住我]`** 按钮,当你勾选它后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码:
![../static/login-view.png](https://oss.dev33.cn/sa-token/doc/login-view.png)
那么在Sa-Token中如何做到 [ 记住我 ] 功能呢?
### 在 Sa-Token 中实现记住我功能
Sa-Token的登录授权**默认就是`[记住我]`模式**,为了实现`[非记住我]`模式,你需要在登录时如下设置:
``` java
// 设置登录账号id为10001第二个参数指定是否为[记住我]当此值为false后关闭浏览器后再次打开需要重新登录
StpUtil.login(10001, false);
```
那么Sa-Token实现`[记住我]`的具体原理是?
### 实现原理
Cookie作为浏览器提供的默认会话跟踪机制其生命周期有两种形式分别是
- 临时Cookie有效期为本次会话只要关闭浏览器窗口Cookie就会消失。
- 持久Cookie有效期为一个具体的时间在时间未到期之前即使用户关闭了浏览器Cookie也不会消失。
利用Cookie的此特性我们便可以轻松实现 [记住我] 模式:
- 勾选 [记住我] 按钮时:调用`StpUtil.login(10001, true)`,在浏览器写入一个`持久Cookie`储存 Token此时用户即使重启浏览器 Token 依然有效。
- 不勾选 [记住我] 按钮时:调用`StpUtil.login(10001, false)`,在浏览器写入一个`临时Cookie`储存 Token此时用户在重启浏览器后 Token 便会消失,导致会话失效。
<button class="show-img" img-src="https://oss.dev33.cn/sa-token/doc/g/g3--remember-me.gif">加载动态演示图</button>
### 前后台分离模式下如何实现[记住我]?
此时机智的你😏很快发现一个问题Cookie虽好却无法在前后端分离环境下使用那是不是代表上述方案在APP、小程序等环境中无效
准确的讲答案是肯定的任何基于Cookie的认证方案在前后台分离环境下都会失效原因在于这些客户端默认没有实现Cookie功能不过好在这些客户端一般都提供了替代方案
唯一遗憾的是此场景中token的生命周期需要我们在前端手动控制
以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例,我们可以使用如下方式达到同样的效果:
``` js
// 使用本地存储保存token达到 [持久Cookie] 的效果
uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用globalData保存token达到 [临时Cookie] 的效果
getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx";
```
如果你决定在PC浏览器环境下进行前后台分离模式开发那么更加简单
``` js
// 使用 localStorage 保存token达到 [持久Cookie] 的效果
localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用 sessionStorage 保存token达到 [临时Cookie] 的效果
sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
```
Remember me, it's too easy!
### 登录时指定 Token 有效期
登录时不仅可以指定是否为`[记住我]`模式,还可以指定一个特定的时间作为 Token 有效时长,如下示例:
``` java
// 示例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
);
```

View File

@@ -0,0 +1,94 @@
# 二级认证
在某些敏感操作下,我们需要对已登录的会话进行二次验证。
比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 **[删除]** 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:
1. 保证操作者是当前账号本人。
2. 增加操作步骤,防止误删除重要数据。
这就是我们本篇要讲的 —— 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。
---
### 具体API
`Sa-Token`中进行二级认证非常简单只需要使用以下API
``` java
// 在当前会话 开启二级认证时间为120秒
StpUtil.openSafe(120);
// 获取:当前会话是否处于二级认证时间内
StpUtil.isSafe();
// 检查当前会话是否已通过二级认证,如未通过则抛出异常
StpUtil.checkSafe();
// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime();
// 在当前会话 结束二级认证
StpUtil.closeSafe();
```
### 使用注解进行二级认证
在一个方法上使用 `@SaCheckSafe` 注解,可以在代码进入此方法之前进行一次二级认证
``` java
// 二级认证:必须二级认证之后才能进入该方法
@SaCheckSafe
@RequestMapping("add")
public String add() {
return "用户增加";
}
```
详细使用方法可参考:[注解鉴权](/use/at-check),此处不再赘述
### 一个小示例
一个完整的二级认证业务流程,应该大致如下:
``` java
// 删除仓库
@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("二级认证失败");
}
```
调用步骤:
1. 前端调用 `deleteProject` 接口,尝试删除仓库。
2. 后端校验会话尚未完成二级认证,返回: `请完成二级认证后再次访问接口`。
3. 前端将信息提示给用户,用户输入密码,调用 `openSafe` 接口。
4. 后端比对用户输入的密码完成二级认证有效期为120秒。
5. 前端在 120 秒内再次调用 `deleteProject` 接口,尝试删除仓库。
6. 后端校验会话已完成二级认证,仓库删除成功。

View File

@@ -0,0 +1,86 @@
# 会话治理
尽管框架将大部分操作提供了简易的封装,但在一些特殊场景下,我们仍需要绕过框架,直达数据底层进行一些操作。
Sa-Token提供以下API助你直接操作会话列表
---
## 具体API
``` java
// 查询所有已登录的 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`: 数据开始处索引, 值为-1时代表一次性取出所有数据。
- `size`: 要获取的数据条数。
- `sortType`: 排序方式true=正序先登录的在前false=反序:后登录的在前)。
简单样例:
``` java
// 查询 value 包括 1000 的所有 token结果集从第 0 条开始,返回 10 条
List<String> tokenList = StpUtil.searchTokenValue("1000", 0, 10, true);
for (String token : tokenList) {
System.out.println(token);
}
```
#### 深入:`StpUtil.searchTokenValue` 和 `StpUtil.searchSessionId` 有哪些区别?
- StpUtil.searchTokenValue 查询的是登录产生的所有 Token。
- StpUtil.searchSessionId 查询的是所有已登录账号会话id。
举个例子,项目配置如下:
``` yml
sa-token:
# 允许同一账号在多个设备一起登录
is-concurrent: true
# 同一账号每次登录产生不同的token
is-share: false
```
假设此时账号A在 电脑、手机、平板 依次登录共3次登录账号B在 电脑、手机 依次登录共2次登录那么
- `StpUtil.searchTokenValue` 将返回一共 5 个Token。
- `StpUtil.searchSessionId` 将返回一共 2 个 SessionId。
综上,若要遍历系统所有已登录的会话,代码将大致如下:
``` java
// 获取所有已登录的会话id
List<String> sessionIdList = StpUtil.searchSessionId("", -1, -1, false);
for (String sessionId : sessionIdList) {
// 根据会话id查询对应的 SaSession 对象,此处一个 SaSession 对象即代表一个登录的账号
SaSession session = StpUtil.getSessionBySessionId(sessionId);
// 查询这个账号都在哪些设备登录了依据上面的示例账号A 的 tokenSign 数量是 3账号B 的 tokenSign 数量是 2
List<TokenSign> tokenSignList = session.getTokenSignList();
System.out.println("会话id" + sessionId + ",共在 " + tokenSignList.size() + " 设备登录");
}
```
<br/>
#### 注意事项:
由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据:
- 单机模式下百万会话取出10条 Token 平均耗时 `0.255s`。
- Redis模式下百万会话取出10条 Token 平均耗时 `3.322s`。
请根据业务实际水平合理调用API。
> 如果需要实时获取当前登录人数或者需要在用户退出后自动触发某事件等, 建议采用websocket技术。

View File

@@ -0,0 +1,30 @@
# 自定义 Token 前缀
### 需求场景
在某些系统中前端提交token时会在前面加个固定的前缀例如
``` js
{
"satoken": "Bearer xxxx-xxxx-xxxx-xxxx"
}
```
此时后端如果不做任何特殊处理,框架将会把`Bearer `视为token的一部分无法正常读取token信息导致鉴权失败。
为此我们需要在yml中添加如下配置
``` java
sa-token:
# token前缀
token-prefix: Bearer
```
此时 Sa-Token 便可在读取 Token 时裁剪掉 `Bearer`,成功获取`xxxx-xxxx-xxxx-xxxx`。
### 注意点
1. Token前缀 与 Token值 之间必须有一个空格。
2. 一旦配置了 Token前缀则前端提交 `Token` 时,必须带有前缀,否则会导致框架无法读取 Token。
3. 由于`Cookie`中无法存储空格字符,也就意味配置 Token 前缀后Cookie 鉴权方式将会失效,此时只能将 Token 提交到`header`里进行传输。

View File

@@ -0,0 +1,66 @@
# 自定义 Token 风格
本篇介绍token生成的各种风格以及自定义token生成策略。
---
## 内置风格
Sa-Token默认的token生成策略是uuid风格其模样类似于`623368f0-ae5e-4475-a53f-93e4225f16ae`<br>
如果你对这种风格不太感冒还可以将token生成设置为其他风格。
怎么设置呢只需要在yml配置文件里设置 `sa-token.token-style=风格类型` 即可,其有多种取值:
``` java
// 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 生成策略
如果你觉着以上风格都不是你喜欢的类型,那么你还可以**自定义token生成策略**来定制化token生成风格。 <br>
怎么做呢?只需要重写 `SaStrategy` 策略类的 `createToken` 算法即可:
#### 参考步骤如下:
1、在`SaTokenConfigure`配置类中添加代码:
``` java
@Configuration
public class SaTokenConfigure {
/**
* 重写 Sa-Token 框架内部算法策略
*/
@Autowired
public void rewriteSaStrategy() {
// 重写 Token 生成策略
SaStrategy.me.createToken = (loginId, loginType) -> {
return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
};
}
}
```
2、再次调用 `StpUtil.login(10001)`方法进行登录观察其生成的token样式:
``` html
gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH
```