mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-09-19 10:08:07 +08:00
重构文档结构
This commit is contained in:
15
sa-token-doc/fun/auth-flow.md
Normal file
15
sa-token-doc/fun/auth-flow.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Sa-Token 功能结构图
|
||||
|
||||
---
|
||||
|
||||
### Sa-Token 功能结构图:
|
||||
|
||||

|
||||
|
||||
### Sa-Token 认证流程图:
|
||||
|
||||

|
||||
|
||||
<!--  -->
|
||||
|
||||
PS:鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。(本流程图使用 [ProcessOn](https://www.processon.com) 绘制完成)
|
3
sa-token-doc/fun/cors-filter.md
Normal file
3
sa-token-doc/fun/cors-filter.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 解决跨域问题
|
||||
|
||||
参考:[https://blog.csdn.net/shengzhang_/article/details/119928794](https://blog.csdn.net/shengzhang_/article/details/119928794)
|
73
sa-token-doc/fun/curr-domain.md
Normal file
73
sa-token-doc/fun/curr-domain.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 解决反向代理 uri 丢失的问题
|
||||
|
||||
---
|
||||
|
||||
使用 `request.getRequestURL()` 可获取当前程序所在外网的访问地址,在 Sa-Token 中,其 `SaHolder.getRequest().getUrl()` 也正是借助此API完成,
|
||||
有很多模块都用到了这个能力,比如SSO单点登录。
|
||||
|
||||
我们可以使用如下代码测试此API
|
||||
``` java
|
||||
// 显示当前程序所在外网的都访问地址
|
||||
@RequestMapping("test")
|
||||
public String test() {
|
||||
return "您访问的是:" + SaHolder.getRequest().getUrl();
|
||||
}
|
||||
```
|
||||
|
||||
从浏览器访问此接口,我们可以看到:
|
||||
|
||||

|
||||
|
||||
此 API 在本地开发时一般可以正常工作,然而如果我们在部署时使用 Nginx 做了一层反向代理后,其最终结果可能会和我们预想的有一点偏差:
|
||||
|
||||

|
||||
|
||||
不仅是 Nginx,所有包含路由转发的地方都有可能导致上述丢失 uri 的现象,解决方案也很简单,既然程序无法自动识别,我们改成手动获取即可,Sa-Token 提供两个方案:
|
||||
|
||||
|
||||
### 方案一:Nginx转发时追加 header 参数
|
||||
|
||||
##### 1、首先在 Nginx 代理转发的地方增加参数
|
||||
|
||||

|
||||
|
||||
重点是这一句:`proxy_set_header Public-Network-URL http://$http_host$request_uri;`
|
||||
|
||||
##### 2、在程序中新增类 `CustomSaTokenContextForSpring.java`,重写获取uri的逻辑
|
||||
|
||||
``` java
|
||||
@Primary
|
||||
@Component
|
||||
public class CustomSaTokenContextForSpring extends SaTokenContextForSpring {
|
||||
|
||||
@Override
|
||||
public SaRequest getRequest() {
|
||||
return new SaRequestForServlet(SpringMVCUtil.getRequest()) {
|
||||
@Override
|
||||
public String getUrl() {
|
||||
if(request.getHeader("Public-Network-URL") != null) {
|
||||
return request.getHeader("Public-Network-URL");
|
||||
}
|
||||
return request.getRequestURL().toString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
其它逻辑保持不变,框架即可正确获取 uri 地址
|
||||
|
||||
!> 注意:步骤一与步骤二需要同步存在,否则可能有前端假传 header 参数造成安全问题
|
||||
|
||||
|
||||
### 方案二:直接在yml中配置当前项目的网络访问地址
|
||||
|
||||
在 `application.yml` 中增加配置:
|
||||
``` yml
|
||||
sa-token:
|
||||
# 配置当前项目的网络访问地址
|
||||
curr-domain: http://local.dev33.cn:8902/api
|
||||
```
|
||||
|
||||
即可避免路由转发过程中丢失 uri 的问题
|
79
sa-token-doc/fun/dynamic-router-check.md
Normal file
79
sa-token-doc/fun/dynamic-router-check.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 参考:把路由拦截鉴权动态化
|
||||
|
||||
框架提供的示例是硬代码写死的,不过稍微做一下更改,你就可以让他动态化
|
||||
|
||||
---
|
||||
|
||||
参考如下:
|
||||
|
||||
``` java
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
SaRouter
|
||||
.match("/**")
|
||||
.notMatch(excludePaths())
|
||||
.check(r -> StpUtil.checkLogin());
|
||||
})).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
// 动态获取哪些 path 可以忽略鉴权
|
||||
public List<String> excludePaths() {
|
||||
// 此处仅为示例,实际项目你可以写任意代码来查询这些path
|
||||
return Arrays.asList("/path1", "/path2", "/path3");
|
||||
}
|
||||
```
|
||||
|
||||
如果不仅仅是登录校验,还需要鉴权,那也很简单:
|
||||
``` java
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
// 遍历校验规则,依次鉴权
|
||||
Map<String, String> rules = getAuthRules();
|
||||
for (String path : rules.keySet()) {
|
||||
SaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path)));
|
||||
}
|
||||
})).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
// 动态获取鉴权规则
|
||||
public Map<String, String> getAuthRules() {
|
||||
// key 代表要拦截的 path,value 代表需要校验的权限
|
||||
Map<String, String> authMap = new LinkedHashMap<>();
|
||||
authMap.put("/user/**", "user");
|
||||
authMap.put("/admin/**", "admin");
|
||||
authMap.put("/article/**", "article");
|
||||
// 更多规则 ...
|
||||
return authMap;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
错误的写法:
|
||||
|
||||
``` java
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
StpUtil.checkLogin();
|
||||
}))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(excludePaths());
|
||||
}
|
||||
|
||||
// 动态获取哪些 path 可以忽略鉴权
|
||||
public List<String> excludePaths() {
|
||||
// 此处仅为示例,实际项目你可以写任意代码来查询这些path
|
||||
return Arrays.asList("/path1", "/path2", "/path3");
|
||||
}
|
||||
```
|
||||
|
||||
错误点:因为 lambda 表达式之外的代码只会在启动时执行一次,所以 `excludePaths()` 方法是无法做到动态化读取的,
|
||||
若要在项目运行时动态读写,必须把调用 `excludePaths()` 的时机放在 lambda 里。
|
||||
|
||||
|
||||
|
||||
|
107
sa-token-doc/fun/exception-code.md
Normal file
107
sa-token-doc/fun/exception-code.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 异常细分状态码
|
||||
|
||||
---
|
||||
|
||||
### 获取异常细分状态码
|
||||
|
||||
Sa-Token 中的基础异常类是 `SaTokenException`,在此基础上,又针对一些特定场景划分出诸如 `NotLoginException`、`NotPermissionException` 等。
|
||||
|
||||
但是框架中异常抛出点远远多于异常种类的划分,比如在 SSO 插件中,[ redirect 重定向地址无效 ] 和 [ ticket 参数值无效 ] 都会导致 SSO 授权的失败,
|
||||
但是它们抛出的异常都是 `SaSsoException`,如果你需要对这两种异常情形做出不同的处理,仅仅判断异常的 ClassType 显然不够。
|
||||
|
||||
为了解决上述需求,Sa-Token 对每个异常抛出点都会指定一个特定的 code 值,就像这样:
|
||||
|
||||
``` java
|
||||
if(SaFoxUtil.isUrl(url) == false) {
|
||||
throw new SaSsoException("无效redirect:" + url).setCode(SaSsoExceptionCode.CODE_20001);
|
||||
}
|
||||
```
|
||||
|
||||
就像是打上一个特定的标记,不同异常情形标记的 code 码值也会不同,这就为你精细化异常处理提供了前提。
|
||||
|
||||
要在捕获异常时获取这个 code 码也非常简单:
|
||||
|
||||
``` java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(SaTokenException.class)
|
||||
public SaResult handlerSaTokenException(SaTokenException e) {
|
||||
|
||||
// 根据不同异常细分状态码返回不同的提示
|
||||
if(e.getCode() == 20001) {
|
||||
return SaResult.error("redirect 重定向 url 是一个无效地址");
|
||||
}
|
||||
if(e.getCode() == 20002) {
|
||||
return SaResult.error("redirect 重定向 url 不在 allowUrl 允许的范围内");
|
||||
}
|
||||
if(e.getCode() == 20004) {
|
||||
return SaResult.error("提供的 ticket 是无效的");
|
||||
}
|
||||
// 更多 code 码判断 ...
|
||||
|
||||
// 默认的提示
|
||||
return SaResult.error("服务器繁忙,请稍后重试...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说,所有异常你都可以通过 `e.getCode()` 的方式获取对应的异常细分状态码。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 异常细分状态码-参照表
|
||||
|
||||
!> 目前仅对 sso 插件和 jwt 插件做了异常状态码细分,后续版本升级会支持更多模块
|
||||
|
||||
**sa-token-code 核心包:**
|
||||
|
||||
| code码值 | 含义 |
|
||||
| :-------- | :-------- |
|
||||
| -1 | 代表这个异常在抛出时未指定异常细分状态码 |
|
||||
|
||||
|
||||
**sa-token-sso 单点登录相关:**
|
||||
|
||||
| code码值 | 含义 |
|
||||
| :-------- | :-------- |
|
||||
| 20001 | `redirect` 重定向 url 是一个无效地址 |
|
||||
| 20002 | `redirect` 重定向 url 不在 allowUrl 允许的范围内 |
|
||||
| 20003 | 接口调用方提供的 `secretkey` 秘钥无效 |
|
||||
| 20004 | 提供的 `ticket` 是无效的 |
|
||||
| 20005 | 在模式三下,sso-client 调用 sso-server 端 校验ticket接口 时,得到的响应是校验失败 |
|
||||
| 20006 | 在模式三下,sso-client 调用 sso-server 端 单点注销接口 时,得到的响应是注销失败 |
|
||||
| 20007 | http 请求调用 提供的 `timestamp` 与当前时间的差距超出允许的范围 |
|
||||
| 20008 | http 请求调用 提供的 `sign` 无效 |
|
||||
| 20009 | 本地系统没有配置 `secretkey` 字段 |
|
||||
|
||||
|
||||
|
||||
**sa-token-jwt 插件相关:**
|
||||
|
||||
| code码值 | 含义 |
|
||||
| :-------- | :-------- |
|
||||
| 40101 | 对 jwt 字符串解析失败 |
|
||||
| 40102 | 此 jwt 的签名无效 |
|
||||
| 40103 | 此 jwt 的 `loginType` 字段不符合预期 |
|
||||
| 40104 | 此 jwt 已超时 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
103
sa-token-doc/fun/git-pr.md
Normal file
103
sa-token-doc/fun/git-pr.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 如何更新在线文档
|
||||
1. 打开要修改的文档页面
|
||||
2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方
|
||||
3. 找到这一行文字
|
||||
|
||||

|
||||
|
||||
4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览)
|
||||
5. 此时会进入当前页面源码预览页面,找到下方按钮组
|
||||
|
||||

|
||||
|
||||
6. 点击编辑按钮
|
||||
7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面)
|
||||
8. 滑动到最下方点击提交审核即可
|
||||
|
||||
|
||||
# 如何提交代码
|
||||
## 环境安装过程
|
||||
1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装
|
||||
2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱)
|
||||
|
||||
```
|
||||
git config --global user.name "这里替换为你在项目中希望展示的昵称"
|
||||
git config --global user.email 这里替换为你的关联邮箱
|
||||
// 查看是否配置正确
|
||||
git config --list
|
||||
```
|
||||
|
||||
3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台
|
||||
4. 具体方法见[Gitee如何配置SSH](https://gitee.com/help/articles/4181#article-header0), [Github如何配置SSH](https://docs.github.com/cn/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account)
|
||||
5. 最小开发环境安装包括[Java JDK 8+](https://pc.qq.com/detail/0/detail_18360.html),[Maven 最新版](http://maven.apache.org/download.cgi) 和[idea IDE 社区版](https://www.jetbrains.com/zh-cn/idea/download/#section=windows)
|
||||
6. 在idea 中[配置Java环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEjava%E7%8E%AF%E5%A2%83)和[配置maven环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEmaven%E7%8E%AF%E5%A2%83), 基础部分不再赘述
|
||||
|
||||
## 项目下载过程
|
||||
1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览)
|
||||
2. 找到页面右上角的按钮组, 点击Forked按钮
|
||||
|
||||

|
||||
|
||||
3. 选择个人仓库并点击确认
|
||||
4. 此时在你的个人仓库中会多了一个Sa-Token项目
|
||||
5. 在新的Sa-Token项目中, 点击  按钮, 点击弹出框里面的复制按钮
|
||||
6. 在本地某空文件夹下右键选择: git bash here
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
14. 在里面输入如下命令, 按换行后自动下载整个项目
|
||||
|
||||
```
|
||||
git clone 这里替换为复制后的链接
|
||||
```
|
||||
|
||||
## 项目载入过程
|
||||
1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑)
|
||||
2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交
|
||||
|
||||
## 项目暂存并提交远程
|
||||
### 方式一
|
||||
1. 在idea中打开项目进入Commit选项
|
||||
|
||||

|
||||
|
||||
2. 勾选需要本地暂存的文件
|
||||
3. 在同一页面的下方输入提示信息
|
||||
|
||||

|
||||
|
||||
4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程
|
||||
### 方式二
|
||||
1. 除了点击Commit and Push按钮外,还有一个地方可以提交git
|
||||
|
||||

|
||||
|
||||
2. 位置在idea右上方的工具栏里面
|
||||
3. 指向左下箭头为拉取项目,可以随时更新
|
||||
4. 打对号为本地暂存
|
||||
5. 指向右上箭头提交远程
|
||||
## 私人项目推送到主项目
|
||||
1. 提交后进入Gitee个人仓库中克隆的Sa-Token项目
|
||||
2. 找到下图的Pull Request按钮
|
||||
|
||||

|
||||
|
||||
3. 点击提交, 进入如下页面
|
||||
|
||||

|
||||
|
||||
4. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交.
|
||||
|
||||
## 远程项目更新
|
||||
1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理?
|
||||
2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈
|
||||
|
||||

|
||||
|
||||
3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用像我之前一样,删除项目又重新fork了.
|
||||
|
||||
## 为什么在国内推荐Gitee
|
||||
1. 近期Github下载网速较慢
|
||||
2. Gitee上中文界面方便操作
|
81
sa-token-doc/fun/issue-template.md
Normal file
81
sa-token-doc/fun/issue-template.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# issue 提问模板
|
||||
|
||||
在线提问链接:[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues)
|
||||
|
||||
> 请在新建 issue 时,尽量复制模板格式进行提交
|
||||
>
|
||||
> 1. 提交之前率先参考 <a href="#/more/common-questions" target="_blank">Sa-Token 常见问题解答</a> 以及善用 Gitee issues 搜索功能,查阅问题是否已有答案,已存在的 issue 就不要再重复提交了。
|
||||
> 2. 问题已得到处理的 issue 请大家及时手动关闭,如果超过24小时没有追问,我们将默认提交者已找到解决方案,关闭issue。
|
||||
> 3. 有时候 issue 提交之后,没有得到及时回复,大家可以加入QQ群@管理员寻求帮助。
|
||||
> 4. 请大家新建 issue 时删除不必要的模板信息、精简语句、**做好代码排版**,对于不方便描述的业务场景,可参阅 <a href="#/more/noun-intro" target="_blank">Sa-Token 名词解释</a> 方便组织语句,这样有助于减低大家的沟通成本。
|
||||
> 5. **代码截图要带上行号!报错信息要把异常堆栈截全!页面截图要把地址栏带上!Ajax请求要把请求地址、请求头、请求参数都截全!**
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 预期不符:
|
||||
``` js
|
||||
### 使用版本:
|
||||
|
||||
|
||||
### 涉及的功能模块:
|
||||
|
||||
|
||||
### 测试步骤:
|
||||
+ 我经过以下步骤测试:
|
||||
|
||||
+ 得出以下结果:
|
||||
|
||||
+ 和文档上描述的不一致:
|
||||
|
||||
+ 我的理解是:
|
||||
|
||||
请问,是我的理解不对,还是文档出了问题?
|
||||
```
|
||||
|
||||
|
||||
### bug反馈:
|
||||
``` js
|
||||
### 使用版本:
|
||||
|
||||
|
||||
### 报错信息:
|
||||
|
||||
|
||||
### 希望结果:
|
||||
|
||||
|
||||
### 复现步骤:
|
||||
|
||||
|
||||
< 备注:如果复现步骤比较复杂,请将 demo 上传到 gitee 并留下地址 >
|
||||
```
|
||||
|
||||
|
||||
### 功能提问:
|
||||
``` js
|
||||
### 对以下问题有疑问:
|
||||
|
||||
|
||||
< 备注:请尽量详细描述问题所在 >
|
||||
```
|
||||
|
||||
|
||||
### 建议增加新功能:
|
||||
``` js
|
||||
### 建议增加的新功能:
|
||||
|
||||
|
||||
### 应用场景阐述:
|
||||
|
||||
|
||||
< 备注:请尽量详细描述功能应用场景 >
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
68
sa-token-doc/fun/jur-cache.md
Normal file
68
sa-token-doc/fun/jur-cache.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 参考:将权限数据放在缓存里
|
||||
前面我们讲解了如何通过`StpInterface`接口注入权限数据,框架默认是不提供缓存能力的,如果你想减小数据库的访问压力,则需要将权限数据放到缓存中
|
||||
|
||||
---
|
||||
|
||||
参考如下:
|
||||
``` java
|
||||
/**
|
||||
* 自定义权限验证接口扩展
|
||||
*/
|
||||
@Component
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
// 返回一个账号所拥有的权限码集合
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
|
||||
// 1. 声明权限码集合
|
||||
List<String> permissionList = new ArrayList<>();
|
||||
|
||||
// 2. 遍历角色列表,查询拥有的权限码
|
||||
for (String roleId : getRoleList(loginId, loginType)) {
|
||||
SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId);
|
||||
List<String> list = roleSession.get("Permission_List", () -> {
|
||||
return ...; // 从数据库查询这个角色所拥有的权限列表
|
||||
});
|
||||
permissionList.addAll(list);
|
||||
}
|
||||
|
||||
// 3. 返回权限码集合
|
||||
return permissionList;
|
||||
}
|
||||
|
||||
// 返回一个账号所拥有的角色标识集合
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
SaSession session = StpUtil.getSessionByLoginId(loginId);
|
||||
return session.get("Role_List", () -> {
|
||||
return ...; // 从数据库查询这个账号id拥有的角色列表
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
##### 疑问:为什么不直接缓存 `[账号id->权限列表]`的关系,而是 `[账号id -> 角色id -> 权限列表]`?
|
||||
|
||||
<!-- ``` java
|
||||
// 在一个账号登录时写入其权限数据
|
||||
RedisUtil.setValue("账号id", <权限列表>);
|
||||
|
||||
// 然后在`StpInterface`接口中,如下方式获取
|
||||
List<String> list = RedisUtil.getValue("账号id");
|
||||
``` -->
|
||||
|
||||
答:`[账号id->权限列表]`的缓存方式虽然更加直接粗暴,却有一个严重的问题:
|
||||
|
||||
- 通常我们系统的权限架构是RBAC模型:权限与用户没有直接的关系,而是:用户拥有指定的角色,角色再拥有指定的权限
|
||||
- 而这种'拥有关系'是动态的,是可以随时修改的,一旦我们修改了它们的对应关系,便要同步修改或清除对应的缓存数据
|
||||
|
||||
现在假设如下业务场景:我们系统中有十万个账号属于同一个角色,当我们变动这个角色的权限时,难道我们要同时清除这十万个账号的缓存信息吗?
|
||||
这显然是一个不合理的操作,同一时间缓存大量清除容易引起Redis的缓存雪崩
|
||||
|
||||
而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时,则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可
|
||||
|
||||
一言以蔽之:权限的缓存模型需要跟着权限模型走,角色缓存亦然
|
||||
|
||||
|
60
sa-token-doc/fun/not-login-scene.md
Normal file
60
sa-token-doc/fun/not-login-scene.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# NotLoginException 场景值
|
||||
|
||||
本篇介绍如何根据`NotLoginException`异常的场景值,来定制化处理未登录的逻辑 <br/>
|
||||
应用场景举例:未登录、被顶下线、被踢下线等场景需要不同方式来处理
|
||||
|
||||
|
||||
## 何为场景值
|
||||
在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分
|
||||
|
||||
| 场景值 | 对应常量 | 含义说明 |
|
||||
|--- |--- |--- |
|
||||
| -1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到token |
|
||||
| -2 | NotLoginException.INVALID_TOKEN| 已读取到token,但是token无效 |
|
||||
| -3 | NotLoginException.TOKEN_TIMEOUT| 已读取到token,但是token已经过期 |
|
||||
| -4 | NotLoginException.BE_REPLACED| 已读取到token,但是token已被顶下线 |
|
||||
| -5 | NotLoginException.KICK_OUT| 已读取到token,但是token已被踢下线 |
|
||||
|
||||
|
||||
|
||||
那么,如何获取场景值呢?废话少说直接上代码:
|
||||
|
||||
|
||||
``` java
|
||||
// 全局异常拦截(拦截项目中的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 {
|
||||
message = "当前会话未登录";
|
||||
}
|
||||
|
||||
// 返回给前端
|
||||
return SaResult.error(message);
|
||||
}
|
||||
```
|
||||
|
||||
<br/>
|
||||
注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理
|
||||
|
132
sa-token-doc/fun/plugin-dev.md
Normal file
132
sa-token-doc/fun/plugin-dev.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Sa-Token 插件开发指南
|
||||
|
||||
---
|
||||
|
||||
插件,从字面意思理解就是可拔插的组件,作用是在不改变 Sa-Token 现有架构的情况下,替换或扩展一部分底层代码逻辑。
|
||||
|
||||
为 Sa-Token 开发插件非常简单,以下是几种可用的方法:
|
||||
|
||||
- 自定义全局策略。
|
||||
- 更改全局组件实现。
|
||||
- 实现自定义SaTokenContext。
|
||||
- 其它自由扩展。
|
||||
|
||||
下面依次介绍这几种方式。
|
||||
|
||||
### 1、自定义全局策略
|
||||
|
||||
Sa-Token 将框架的一些关键逻辑抽象出一个统一的概念 —— 策略,并统一定义在 `SaStrategy` 中,源码参考:[SaStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java) 。
|
||||
|
||||
SaStrategy 的每一个函数都可以单独重写,以 “自定义Token生成策略” 这一需求为例:
|
||||
|
||||
``` java
|
||||
// 重写 Token 生成策略
|
||||
SaStrategy.me.createToken = (loginId, loginType) -> {
|
||||
return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
|
||||
};
|
||||
```
|
||||
|
||||
就像变量的重新赋值一样,我们只需重新指定一个新的策略函数,即可自定义 Token 生成的逻辑。
|
||||
|
||||
|
||||
### 2、更改全局组件实现
|
||||
|
||||
Sa-Token 大部分全局组件都定义在 SaManager 之上([SaManager](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java)),
|
||||
我们只需要更改组件的实现类即可。以 临时令牌认证 模块举例
|
||||
|
||||
##### 1、先自定义一个实现类
|
||||
|
||||
``` java
|
||||
/**
|
||||
* 临时认证模块 自定义实现
|
||||
*/
|
||||
public class MySaTemp implements SaTempInterface {
|
||||
|
||||
@Override
|
||||
public String createToken(Object value, long timeout) {
|
||||
System.out.println("------- 自定义一些逻辑 createToken ");
|
||||
return SaTempInterface.super.createToken(value, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseToken(String token) {
|
||||
System.out.println("------- 自定义一些逻辑 parseToken ");
|
||||
return SaTempInterface.super.parseToken(token);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
##### 2、将自定义实现类绑定在 SaManager 上
|
||||
``` java
|
||||
// 注入
|
||||
SaManager.setSaTemp(new MySaTemp());
|
||||
```
|
||||
|
||||
以上是手动注入方式,如果你是 Spring 的 IOC 环境,则直接在 MySaTemp 实现类加上 @Component 注解即可。
|
||||
|
||||
##### 3、开始测试:
|
||||
|
||||
``` java
|
||||
// 根据 value 创建一个 token
|
||||
String token = SaTempUtil.createToken("10014", 120);
|
||||
System.out.println("生成的Token为:" + token);
|
||||
|
||||
Object value = SaTempUtil.parseToken(token);
|
||||
System.out.println("将Token解析后的值为:" + value);
|
||||
```
|
||||
|
||||
观察控制台输出,检验自定义实现类是否注入成功:
|
||||
|
||||

|
||||
|
||||
|
||||
### 3、实现自定义SaTokenContext
|
||||
SaTokenContext 是对接不同框架的上下文接口,注入流程和第二步类似,篇幅限制,可参考:[自定义 SaTokenContext 指南](/fun/sa-token-context)
|
||||
|
||||
|
||||
### 4、其它自由扩展
|
||||
这种方式就无需注入什么全局组件替换内部实现了,你可以在 Sa-Token 的基础之上封装任何代码,进行功能扩展。
|
||||
|
||||
|
||||
### 5、练练手
|
||||
熟悉了插件开发流程,下面的 [ 待开发插件列表 ] 或许可以给你提供一个练手的方向。
|
||||
|
||||
##### SaTokenContext 实现:
|
||||
|
||||
| 插件 | 功能 | 状态 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| sa-token-solon-starter | Sa-Token 与 Solon 的整合 | <font color="green" >已完成</font> |
|
||||
| sa-token-jfinal-starter | Sa-Token 与 JFinal 的整合 | 待开发 |
|
||||
| sa-token-hasor-starter | Sa-Token 与 Hasor 的整合 | 待开发 |
|
||||
|
||||
##### 标签方言:
|
||||
|
||||
| 插件 | 功能 | 状态 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| sa-token-dialect-thymeleaf | Sa-Token 与 thymeleaf 的整合 | <font color="green" >已完成</font> |
|
||||
| sa-token-dialect-freemarker | Sa-Token 与 freemarker 的整合 | 待开发 |
|
||||
| sa-token-dialect-jsp | Sa-Token 与 jsp 的整合 | 待开发 |
|
||||
| sa-token-dialect-velocity | Sa-Token 与 velocity 的整合 | 待开发 |
|
||||
| sa-token-dialect-beetl | Sa-Token 与 beetl 的整合 | 待开发 |
|
||||
|
||||
##### 持久层扩展:
|
||||
|
||||
| 插件 | 功能 | 状态 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| sa-token-dao-redis | Sa-Token 与 Redis 的整合 | <font color="green" >已完成</font> |
|
||||
| sa-token-dao-memcached | Sa-Token 与 memcached 的整合 | 待开发 |
|
||||
|
||||
##### 其它:
|
||||
任何你认为有价值的功能代码,都可以扩展为插件。
|
||||
|
||||
|
||||
### 6、发布代码
|
||||
插件开发完毕之后,你可以将其pr到官方仓库,或:
|
||||
|
||||
上传到 gitee/github 作为独立项目维护,并发布到 Maven 中央仓库,参考这篇:[https://juejin.cn/post/6844904104834105358](https://juejin.cn/post/6844904104834105358)
|
||||
|
||||
|
||||
|
||||
|
||||
|
171
sa-token-doc/fun/sa-token-context.md
Normal file
171
sa-token-doc/fun/sa-token-context.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 自定义 SaTokenContext 指南
|
||||
|
||||
目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包,
|
||||
如果我们使用的 Web 框架不在上述列表之中,则需要自定义 SaTokenContext 接口的实现完成整合工作。
|
||||
|
||||
---
|
||||
|
||||
### 1、SaTokenContext是什么,为什么要实现 SaTokenContext 接口?
|
||||
|
||||
在鉴权中,必不可少的步骤就是从 `HttpServletRequest` 中读取 Token,然而并不是所有框架都具有 HttpServletRequest 对象,例如在 WebFlux 中,只有 `ServerHttpRequest`,
|
||||
在一些其它Web框架中,可能连 `Request` 的概念都没有。
|
||||
|
||||
那么,Sa-Token 如何只用一套代码就对接到所有 Web 框架呢?
|
||||
|
||||
解决这个问题的关键就在于 `SaTokenContext` 接口,此接口的作用是屏蔽掉不同 Web 框架之间的差异,提供统一的调用API:
|
||||
|
||||

|
||||
|
||||
|
||||
SaTokenContext只是一个接口,没有工作能力,这也就意味着 SaTokenContext 接口的实现是必须的。
|
||||
那么疑问来了,我们之前在 SpringBoot 中引用 Sa-Token 时为什么可以直接使用呢?
|
||||
|
||||
其实原理很简单,`sa-token-spring-boot-starter`集成包中已经内置了`SaTokenContext`的实现:[SaTokenContextForSpring](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java),
|
||||
并且根据 Spring 的自动注入特性,在项目启动时注入到 Sa-Token 中,做到“开箱即用”。
|
||||
|
||||
那么如果我们使用不是 Spring 框架,是不是就必须得手动实现 `SaTokenContext` 接口?答案是肯定的,脱离Spring 环境后,我们就不能再使用`sa-token-spring-boot-starter`集成包了,
|
||||
此时我们只能引入 `sa-token-core` 核心包,然后手动实现 `SaTokenContext` 接口。
|
||||
|
||||
不过不用怕,这个工作很简单,只要跟着下面的文档一步步来,你就可以将 Sa-Token 对接到任意Web框架中。
|
||||
|
||||
|
||||
### 2、实现 Model 接口
|
||||
我们先来观察一下 `SaTokenContext` 接口的签名:
|
||||
``` java
|
||||
/**
|
||||
* Sa-Token 上下文处理器
|
||||
*/
|
||||
public interface SaTokenContext {
|
||||
|
||||
/**
|
||||
* 获取当前请求的 [Request] 对象
|
||||
*/
|
||||
public SaRequest getRequest();
|
||||
|
||||
/**
|
||||
* 获取当前请求的 [Response] 对象
|
||||
*/
|
||||
public SaResponse getResponse();
|
||||
|
||||
/**
|
||||
* 获取当前请求的 [存储器] 对象
|
||||
*/
|
||||
public SaStorage getStorage();
|
||||
|
||||
/**
|
||||
* 校验指定路由匹配符是否可以匹配成功指定路径
|
||||
*/
|
||||
public boolean matchPath(String pattern, String path);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
你可能对 `SaRequest` 比较疑惑,这个对象是干什么用的?正如每个 Web 框架都有 Request 概念的抽象,Sa-Token 也封装了 `Request`、`Response`、`Storage`三者的抽象:
|
||||
|
||||
- `Request`:请求对象,携带着一次请求的所有参数数据。参考:[SaRequest.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java)。
|
||||
- `Response`:响应对象,携带着对客户端一次响应的所有数据。参考:[SaResponse.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java)。
|
||||
- `Storage`:请求上下文对象,提供 [一次请求范围内] 的上下文数据读写。参考:[SaStorage.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java)。
|
||||
|
||||
|
||||
因此,在实现 `SaTokenContext` 之前,你必须先实现这三个 Model 接口。
|
||||
|
||||
先别着急动手,如果你的 Web 框架是基于 Servlet 规范开发的,那么 Sa-Token 已经为你封装好了三个 Model 接口的实现,你要做的就是引入 `sa-token-servlet`包即可:
|
||||
|
||||
``` xml
|
||||
<!-- Sa-Token 权限认证(ServletAPI 集成包) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-servlet</artifactId>
|
||||
<version>${sa.top.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
如果你的 Web 框架不是基于 Servlet 规范,那么你就需要手动实现这三个 Model 接口,我们可以参考 `sa-token-servlet` 是怎样实现的:
|
||||
[SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、
|
||||
[SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、
|
||||
[SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。
|
||||
|
||||
|
||||
### 3、实现 SaTokenContext 接口
|
||||
|
||||
接下来我们奔入主题,提供 `SaTokenContext` 接口的实现,同样我们可以参考 Spring 集成包是怎样实现的:
|
||||
|
||||
``` java
|
||||
/**
|
||||
* Sa-Token 上下文处理器 [ SpringMVC版本实现 ]
|
||||
*/
|
||||
public class SaTokenContextForSpring implements SaTokenContext {
|
||||
|
||||
/**
|
||||
* 获取当前请求的Request对象
|
||||
*/
|
||||
@Override
|
||||
public SaRequest getRequest() {
|
||||
return new SaRequestForServlet(SpringMVCUtil.getRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的Response对象
|
||||
*/
|
||||
@Override
|
||||
public SaResponse getResponse() {
|
||||
return new SaResponseForServlet(SpringMVCUtil.getResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的 [存储器] 对象
|
||||
*/
|
||||
@Override
|
||||
public SaStorage getStorage() {
|
||||
return new SaStorageForServlet(SpringMVCUtil.getRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定路由匹配符是否可以匹配成功指定路径
|
||||
*/
|
||||
@Override
|
||||
public boolean matchPath(String pattern, String path) {
|
||||
return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
详细参考:
|
||||
[SaTokenContextForSpring.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java)
|
||||
|
||||
|
||||
### 4、将自定义实现注入到 Sa-Token 框架中
|
||||
|
||||
有了 `SaTokenContext` 接口的实现,我们还需要将这个实现类注入到 Sa-Token 之中,伪代码参考如下:
|
||||
``` java
|
||||
/**
|
||||
* 程序启动类
|
||||
*/
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 框架启动
|
||||
XxxApplication.run(xxx);
|
||||
|
||||
// 将自定义的 SaTokenContext 实现类注入到框架中
|
||||
SaTokenContext saTokenContext = new SaTokenContextForXxx();
|
||||
SaManager.setSaTokenContext(saTokenContext);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
如果你使用的框架带有自动注入特性,那就更简单了,参考 Spring 集成包的 Bean 注入流程:
|
||||
[注册Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java)、
|
||||
[注入Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java)
|
||||
|
||||
|
||||
### 5、启动项目
|
||||
|
||||
启动项目,尝试打印一下 `SaManager.getSaTokenContext()` 对象,如果输出的是你的自定义实现类,那就证明你已经自定义 `SaTokenContext` 成功了,
|
||||
快来体验一下 Sa-Token 的各种功能吧。
|
||||
|
||||
|
||||
|
||||
|
6
sa-token-doc/fun/sa-token-test.md
Normal file
6
sa-token-doc/fun/sa-token-test.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Sa-Token框架掌握度--在线考试
|
||||
|
||||
---
|
||||
|
||||
此份考卷将测评您对Sa-Token框架的掌握程度(满分100),链接:[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx)
|
||||
|
103
sa-token-doc/fun/session-model.md
Normal file
103
sa-token-doc/fun/session-model.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Sa-Token 中的 Session会话 模型详解
|
||||
|
||||
---
|
||||
|
||||
### 1、User-Session
|
||||
|
||||
提起Session,你脑海中最先浮现的可能就是 JSP 中的 HttpSession,它的工作原理可以大致总结为:
|
||||
|
||||
客户端每次与服务器第一次握手时,会被强制分配一个 `[唯一id]` 作为身份标识,注入到 Cookie 之中,
|
||||
之后每次发起请求时,客户端都要将它提交到后台,服务器根据 `[唯一id]` 找到每个请求专属的Session对象,维持会话
|
||||
|
||||
这种机制简单粗暴,却有N多明显的缺点:
|
||||
|
||||
1. 同一账号分别在PC、APP登录,会被识别为两个不相干的会话
|
||||
2. 一个设备难以同时登录两个账号
|
||||
3. 每次一个新的客户端访问服务器时,都会产生一个新的Session对象,即使这个客户端只访问了一次页面
|
||||
4. 在不支持Cookie的客户端下,这种机制会失效
|
||||
|
||||
|
||||
Sa-Token Session可以理解为 HttpSession 的升级版:
|
||||
|
||||
1. Sa-Token只在调用`StpUtil.login(id)`登录会话时才会产生Session,不会为每个陌生会话都产生Session,节省性能
|
||||
2. 在登录时产生的Session,是分配给账号id的,而不是分配给指定客户端的,也就是说在PC、APP上登录的同一账号所得到的Session也是同一个,所以两端可以非常轻松的同步数据
|
||||
3. Sa-Token支持Cookie、Header、body三个途径提交Token,而不是仅限于Cookie
|
||||
4. 由于不强依赖Cookie,所以只要将Token存储到不同的地方,便可以做到一个客户端同时登录多个账号
|
||||
|
||||
这种为账号id分配的Session,我们给它起一个合适的名字:`User-Session`,你可以通过如下方式操作它:
|
||||
``` java
|
||||
// 获取当前会话的 User-Session
|
||||
SaSession session = StpUtil.getSession();
|
||||
|
||||
// 从 User-Session 中读取、写入数据
|
||||
session.get("name");
|
||||
session.set("name", "张三");
|
||||
```
|
||||
|
||||
使用`User-Session`在不同端同步数据是非常方便的,因为只要 PC 和 APP 登录的账号id一致,它们对应的都是同一个Session,
|
||||
举个应用场景:在PC端点赞的帖子列表,在APP端的点赞记录里也要同步显示出来
|
||||
|
||||
|
||||
### 2、Token-Session
|
||||
|
||||
随着业务推进,我们还可能会遇到一些需要数据隔离的场景:
|
||||
|
||||
> 指定客户端超过两小时无操作就自动下线,如果两小时内有操作,就再续期两小时,直到新的两小时无操作
|
||||
|
||||
那么这种请求访问记录应该存储在哪里呢?放在 User-Session 里吗?
|
||||
|
||||
可别忘了,PC端和APP端可是共享的同一个 User-Session ,如果把数据放在这里,
|
||||
那就意味着,即使用户在PC端一直无操作,只要手机上用户还在不间断的操作,那PC端也不会过期!
|
||||
|
||||
解决这个问题的关键在于,虽然两个设备登录的是同一账号,但是两个它们得到的token是不一样的,
|
||||
Sa-Token针对会话登录,不仅为账号id分配了`User-Session`,同时还为每个token分配了不同的`Token-Session`
|
||||
|
||||
不同的设备端,哪怕登录了同一账号,只要它们得到的token不一致,它们对应的 `Token-Session` 就不一致,这就为我们不同端的独立数据读写提供了支持:
|
||||
|
||||
``` java
|
||||
// 获取当前会话的 Token-Session
|
||||
SaSession session = StpUtil.getTokenSession();
|
||||
|
||||
// 从 Token-Session 中读取、写入数据
|
||||
session.get("name");
|
||||
session.set("name", "张三");
|
||||
```
|
||||
|
||||
### 3、Custom-Session
|
||||
|
||||
除了以上两种Session,Sa-Token还提供了第三种Session,那就是:`Custom-Session`,你可以将其理解为:自定义Session
|
||||
|
||||
Custom-Session不依赖特定的 账号id 或者 token,而是依赖于你提供的SessionId:
|
||||
|
||||
``` java
|
||||
// 获取指定key的 Custom-Session
|
||||
SaSession session = SaSessionCustomUtil.getSessionById("goods-10001");
|
||||
|
||||
// 从 Custom-Session 中读取、写入数据
|
||||
session.get("name");
|
||||
session.set("name", "张三");
|
||||
```
|
||||
|
||||
只要两个自定义Session的Id一致,它们就是同一个Session
|
||||
|
||||
|
||||
### 4、Session模型结构图
|
||||
|
||||
三种Session创建时机:
|
||||
|
||||
- `User-Session`: 指的是框架为每个 账号id 分配的 Session
|
||||
- `Token-Session`: 指的是框架为每个 token 分配的 Session
|
||||
- `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session
|
||||
|
||||
|
||||
**假设三个客户端登录同一账号,且配置了不共享token,那么此时的Session模型是:**
|
||||
|
||||

|
||||
|
||||
简而言之:
|
||||
- `User-Session` 以UserId为主,只要token指向的UserId一致,那么对应的Session对象就一致
|
||||
- `Token-Session` 以token为主,只要token不同,那么对应的Session对象就不同
|
||||
- `Custom-Session` 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象
|
||||
|
||||
|
||||
|
21
sa-token-doc/fun/sso-vs-oauth2.md
Normal file
21
sa-token-doc/fun/sso-vs-oauth2.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 技术选型:[ 单点登录 ] VS [ OAuth2.0 ]
|
||||
|
||||
---
|
||||
|
||||
QQ群库经常有小伙伴提问:项目需要搭建统一认证中心,是用 SSO 方便还是 OAuth2.0 方便呢?针对这个问题,我们列出两者的主要区别以供大家参考:
|
||||
|
||||
|
||||
| 功能点 | SSO单点登录 | OAuth2.0 |
|
||||
| :-------- | :-------- | :-------- |
|
||||
| 统一认证 | 支持度高 | 支持度高 |
|
||||
| 统一注销 | 支持度高 | 支持度低 |
|
||||
| 多个系统会话一致性 | 强一致 | 弱一致 |
|
||||
| 第三方应用授权管理 | 不支持 | 支持度高 |
|
||||
| 自有系统授权管理 | 支持度高 | 支持度低 |
|
||||
| Client级的权限校验 | 不支持 | 支持度高 |
|
||||
| 集成简易度 | 比较简单 | 难度中等 |
|
||||
|
||||
|
||||
注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。
|
||||
|
||||
|
15
sa-token-doc/fun/tech-stack.md
Normal file
15
sa-token-doc/fun/tech-stack.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Sa-Token 源码用到的所有技术栈
|
||||
|
||||
包括但不限于以下:
|
||||
|
||||
- Maven多模块项目
|
||||
- Servlet API、临时Cookie与永久Cookie、Request参数获取
|
||||
- SpringBoot2.0、Redis、Jackson、Hutool、jwt
|
||||
- SpringBoot自定义starter、Spring包扫描 + 依赖注入、AOP注解切面、yml配置映射、拦截器
|
||||
- Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程
|
||||
- package-info注释、Serializable序列化接口、synchronized锁
|
||||
- java加密算法:MD5、SHA1、SHA256、AES、RSA
|
||||
- OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配
|
||||
|
||||
|
||||
|
42
sa-token-doc/fun/three-scope.md
Normal file
42
sa-token-doc/fun/three-scope.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 三大作用域
|
||||
|
||||
---
|
||||
|
||||
Sa-Token 数据存储有三大作用域,分别是:
|
||||
- `SaStorage` - 请求作用域:存储的数据只在一次请求内有效。
|
||||
- `SaSession` - 会话作用域:存储的数据在一次会话范围内有效。
|
||||
- `SaApplication` - 全局作用域:存储的数据在全局范围内有效。
|
||||
|
||||
|
||||
### SaStorage - 请求作用域
|
||||
在 SaStorage 中存储的数据只在一次请求范围内有效,请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。
|
||||
|
||||
``` java
|
||||
SaStorage storage = SaHolder.getStorage();
|
||||
storage.get("key"); // 取值
|
||||
storage.set("key", "value"); // 写值
|
||||
storage.delete("key"); // 删值
|
||||
```
|
||||
|
||||
|
||||
### SaSession - 会话作用域
|
||||
在 SaSession 存储的数据在一次会话范围内有效,会话结束后数据自动清除。必须登录后才能使用 SaSession 对象。
|
||||
|
||||
``` java
|
||||
SaSession session = StpUtil.getSession();
|
||||
session.get("key"); // 取值
|
||||
session.set("key", "value"); // 写值
|
||||
session.delete("key"); // 删值
|
||||
```
|
||||
|
||||
|
||||
### SaApplication - 全局作用域
|
||||
在 SaApplication 存储的数据在全局范围内有效,应用关闭后数据自动清除(如果集成了 Redis 那则是 Redis 关闭后数据自动清除)。使用 SaApplication 时无需处于登录状态。
|
||||
|
||||
``` java
|
||||
SaApplication application = SaHolder.getApplication();
|
||||
application.get("key"); // 取值
|
||||
application.set("key", "value"); // 写值
|
||||
application.delete("key"); // 删值
|
||||
```
|
||||
|
31
sa-token-doc/fun/timeline.md
Normal file
31
sa-token-doc/fun/timeline.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Sa-Token 开源大事记
|
||||
|
||||
---
|
||||
|
||||
- **2020-02-04:** 在 GitHub 提交第一个版本,正式开源。
|
||||
- **2020-09-14:** GitHub star 数量破 100。
|
||||
- **2020-10-26:** Gitee star 数量破 100。
|
||||
- **2021-03-01:** 被 [HelloGitHub] 第 59 期收录推荐。
|
||||
- **2021-03-26:** GitHub star 数量破 1k。
|
||||
- **2021-03-30:** 受 TLog 作者邀请,Sa-Token 加入 dromara 社区。
|
||||
- **2021-03-30:** 被 Gitee 列为推荐项目。
|
||||
- **2021-03-31:** Gitee star 数量破1K。
|
||||
<!-- - **2021-04-09:** GitHub star 数量破2K。 -->
|
||||
<!-- - **2021-05-09:** Gitee star 数量破2K。 -->
|
||||
<!-- - **2021-05-17:** GitHub star 数量破3K。 -->
|
||||
- **2021-06-17:** Sa-Token star 数量 (3529) 超过 Shiro (3506)。
|
||||
<!-- - **2021-07-15:** GitHub star 数量破4K。 -->
|
||||
- **2021-07-26:** GitHub star 数量破5K。
|
||||
<!-- - **2021-07-29:** Gitee star 数量破3K。 -->
|
||||
<!-- - **2021-09-07:** GitHub star 数量破6K。 -->
|
||||
- **2021-09-24:** Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。
|
||||
<!-- - **2021-10-19:** Gitee star 数量破4K。 -->
|
||||
- **2021-11-08:** 荣获开源中国 “码云 GVP 认证”。
|
||||
<!-- - **2021-11-17:** GitHub star 数量破7K。 -->
|
||||
- **2021-11-28:** Gitee star 数量破5K。
|
||||
- **2021-12-27:** 荣获 OSC-2021 最佳软件 Top 30。
|
||||
- **2022-05-20:** 成为 [可信开源社区共同体] 预备成员。
|
||||
- **2022-08-01:** 加入 [中国开源社区 landscape]。
|
||||
- **2022-08-18:** GitHub 第 10000 个 star 里程碑!
|
||||
|
||||
|
22
sa-token-doc/fun/token-info.md
Normal file
22
sa-token-doc/fun/token-info.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# SaTokenInfo 参数详解
|
||||
|
||||
token信息Model: 用来描述一个token的常用参数
|
||||
|
||||
``` js
|
||||
{
|
||||
"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, // User-Session剩余有效时间 (单位: 秒)
|
||||
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
|
||||
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
|
||||
"loginDevice": "default-device" // 登录设备类型
|
||||
},
|
||||
}
|
||||
```
|
119
sa-token-doc/fun/token-timeout.md
Normal file
119
sa-token-doc/fun/token-timeout.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Token有效期详解
|
||||
|
||||
<!-- 本篇介绍Token有效期的详细用法 -->
|
||||
|
||||
Sa-Token 提供两种Token自动过期策略,分别是`timeout`与`activity-timeout`,配置方法如下:
|
||||
|
||||
``` yml
|
||||
sa-token:
|
||||
# Token 有效期,单位:秒,默认30天, -1代表永不过期
|
||||
timeout: 2592000
|
||||
# Token 临时有效期 (指定时间内无操作就视为 Token 过期) 单位: 秒,-1代表不设限
|
||||
activity-timeout: -1
|
||||
```
|
||||
|
||||
两者的区别,可以通过下面的例子体现:
|
||||
|
||||
> 1. 假设你到银行要存钱,首先就要办理一张卡 (要访问系统接口先登录)。
|
||||
> 2. 银行为你颁发一张储蓄卡(系统为你颁发一个Token),以后每次存取钱都要带上这张卡(后续每次访问系统都要提交 Token)。
|
||||
> 3. 银行为这张卡设定两个过期时间:
|
||||
> - 第一个是 `timeout`,代表这张卡的长久有效期,就是指这张卡最长能用多久,假设 `timeout=3年`,那么3年后此卡将被银行删除,想要继续来银行办理业务必须重新办卡(Token 过期后想要访问系统必须重新登录)。
|
||||
> - 第二个就是 `activity-timeout`,代表这张卡的临时有效期,就是指这张卡必须每隔多久来银行一次,假设 `activity-timeout=1月` ,你如果超过1月不来办一次业务,银行就将你的卡冻结,列为长期不动户(Token 长期不访问系统,被冻结,但不会被删除)。
|
||||
> 4. 两个过期策略可以单独配置,也可以同时配置,只要有其中一个有效期超出了范围,这张卡就会变得不可用(两个有效期只要有一个过期了,Token就无法成功访问系统了)。
|
||||
|
||||
下面是对两个过期策略的详细解释:
|
||||
|
||||
### timeout
|
||||
1. `timeout`代表Token的长久有效期,单位/秒,例如将其配置为 2592000 (30天),代表在30天后,Token必定过期,无法继续使用
|
||||
2. `timeout`~~无法续签,想要继续使用必须重新登录~~。v1.29.0+ 版本新增续期方法:`StpUtil.renewTimeout(100)`。
|
||||
3. `timeout`的值配置为-1后,代表永久有效,不会过期
|
||||
|
||||
|
||||
### activity-timeout
|
||||
1. `activity-timeout`代表临时有效期,单位/秒,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期
|
||||
2. 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,Token才会过期
|
||||
3. `activity-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签
|
||||
|
||||
|
||||
### 关于activity-timeout的续签
|
||||
如果`activity-timeout`配置了大于零的值,Sa-Token 会在登录时开始计时,在每次直接或间接调用`getLoginId()`时进行一次过期检查与续签操作。
|
||||
此时会有两种情况:
|
||||
1. 一种是会话无操作时间太长,Token已经过期,此时框架会抛出`NotLoginException`异常(场景值=-3),
|
||||
2. 另一种则是会话在`activity-timeout`有效期内通过检查,此时Token可以成功续签
|
||||
|
||||
|
||||
### 我可以手动续签吗?
|
||||
**可以!**
|
||||
如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,Sa-Token 提供两个API供你操作:
|
||||
1. `StpUtil.checkActivityTimeout()`: 检查当前Token 是否已经[临时过期],如果已经过期则抛出异常
|
||||
2. `StpUtil.updateLastActivityToNow()`: 续签当前Token:(将 [最后操作时间] 更新为当前时间戳)
|
||||
|
||||
注意:在手动续签时,即使Token已经 [临时过期] 也可续签成功,如果此场景下需要提示续签失败,可采用先检查再续签的形式保证Token有效性
|
||||
|
||||
例如以下代码:
|
||||
``` java
|
||||
// 先检查是否已过期
|
||||
StpUtil.checkActivityTimeout();
|
||||
// 检查通过后继续续签
|
||||
StpUtil.updateLastActivityToNow();
|
||||
```
|
||||
|
||||
同时,你还可以关闭框架的自动续签(在配置文件中配置 `autoRenew=false` ),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作
|
||||
|
||||
如果你需要给其它 Token 续签:
|
||||
|
||||
``` java
|
||||
// 为指定 Token 续签
|
||||
StpUtil.stpLogic.updateLastActivityToNow(tokenValue);
|
||||
```
|
||||
|
||||
|
||||
### timeout与activity-timeout可以同时使用吗?
|
||||
**可以同时使用!**
|
||||
两者的认证逻辑彼此独立,互不干扰,可以同时使用。
|
||||
|
||||
### StpUtil 类中哪些公开方法支持临时有效期自动续签?
|
||||
> 直接或间接获取了当前用户id的方法 (间接调用过 StpLogic.getLoginId() 方法)
|
||||
|
||||
| 支持自动续签的方法 |
|
||||
|---|
|
||||
| StpUtil.checkLogin() |
|
||||
| StpUtil.getLoginId() |
|
||||
| StpUtil.getLoginIdAsInt() |
|
||||
| StpUtil.getLoginIdAsString() |
|
||||
| StpUtil.getLoginIdAsLong() |
|
||||
|---|
|
||||
| StpUtil.getSession() |
|
||||
| StpUtil.getSession() |
|
||||
| StpUtil.getTokenSession() |
|
||||
|---|
|
||||
| StpUtil.getRoleList() |
|
||||
| StpUtil.hasRole() |
|
||||
| StpUtil.hasRoleAnd() |
|
||||
| StpUtil.hasRoleOr() |
|
||||
| StpUtil.checkRole() |
|
||||
| StpUtil.checkRoleAnd() |
|
||||
| StpUtil.checkRoleOr() |
|
||||
|---|
|
||||
| StpUtil.getPermissionList() |
|
||||
| StpUtil.hasPermission() |
|
||||
| StpUtil.hasPermissionAnd() |
|
||||
| StpUtil.hasPermissionOr() |
|
||||
| StpUtil.checkPermission() |
|
||||
| StpUtil.checkPermissionAnd() |
|
||||
| StpUtil.checkPermissionOr() |
|
||||
|---|
|
||||
| StpUtil.openSafe() |
|
||||
| StpUtil.isSafe() |
|
||||
| StpUtil.checkSafe() |
|
||||
| StpUtil.getSafeTime() |
|
||||
| StpUtil.closeSafe() |
|
||||
|
||||
> 以下注解都间接调用过 getLoginId() 方法
|
||||
|
||||
| 支持自动续签的注解 |
|
||||
|---|
|
||||
| @SaCheckLogin |
|
||||
| @SaCheckRole |
|
||||
| @SaCheckPermission |
|
||||
| @SaCheckSafe |
|
Reference in New Issue
Block a user