1
0
mirror of synced 2026-05-22 22:53:16 +00:00

Compare commits

...

108 Commits

Author SHA1 Message Date
click33 87efc4e3d7 Merge branch 'master' of https://gitcode.com/dromara/sa-token into dev 2025-02-01 22:01:12 +08:00
click33 dacf86ebff v1.40.0 update .. 2025-02-01 21:59:18 +08:00
click33 1fe9a644c7 v1.40.0 update . 2025-01-31 23:28:02 +08:00
click33 8cbb09d6b7 v1.40.0 update 2025-01-31 22:33:25 +08:00
click33 26302877d0 docs: 补全 1.40.0 版本更新文档 2025-01-31 06:00:43 +08:00
click33 e53802e9f7 docs: 文档补全开源大事记记录 2025-01-30 23:46:42 +08:00
click33 ef6c99452f docs: 文档新增团队成员展示 2025-01-30 22:51:49 +08:00
click33 e8b1249c0b docs: 补全赞助者名单 2025-01-27 23:48:06 +08:00
click33 ec7bf0dc1e docs: 整理文档 2025-01-26 15:59:01 +08:00
click33 85eafcd8b1 Merge pull request #667 from LiHaoGit/dev
添加 集成MongoDB 的文档示例
2025-01-26 15:50:28 +08:00
click33 77a28f1db5 chore(sso): 优化 demo 注释提示 2025-01-26 15:21:37 +08:00
click33 e2003b542d docs: 补全赞助者名单 2025-01-16 21:52:13 +08:00
click33 58330904e0 docs: 整理文档 2025-01-16 21:51:06 +08:00
刘潇 57f9825a5b !322 docs:新增集成 MongoDB 的示例
Merge pull request !322 from lihao/mongodb
2025-01-16 13:25:23 +00:00
click33 735627761a docs: 新增投稿博客 2025-01-15 23:29:00 +08:00
click33 a4ef404d73 docs: 补全赞助者名单 2025-01-15 23:10:50 +08:00
click33 b7b13fe4ed feat(plugin): 新增 sa-token-spring-el 插件,用于支持 SpEL 表达式注解鉴权 2025-01-15 22:28:50 +08:00
click33 079376107a docs: 补全封禁模块相关文档 2025-01-12 01:14:37 +08:00
click33 5c0ca64a00 feat(core): 封禁模块新增支持实时从数据库查询数据 2025-01-11 21:49:58 +08:00
click33 a4be9928aa fix(doc): 修复不正确的注释信息 2025-01-09 19:49:10 +08:00
click33 71242c15b5 fix(core): 新增对分号字符的 path 路径校验 2025-01-09 14:47:50 +08:00
click33 4afc0f9b54 docs: 更新赞助者名单 2025-01-09 10:36:21 +08:00
click33 8db48f65dd docs: 文档新增视频账号链接 2025-01-08 20:02:37 +08:00
click33 34947f2705 docs: 更新赞助者名单 2025-01-08 15:12:28 +08:00
click33 00d4c3f672 chore: freemarker 集成插件标注参考资料链接 2025-01-08 09:43:14 +08:00
click33 3192717c0f feat(plugin): 新增 freemarker 集成插件。 fix: #651 2025-01-08 07:11:08 +08:00
click33 6c4cdf514e feat(code): 新增 Cookie 自定义属性支持。fix: #693 2025-01-07 15:40:04 +08:00
click33 b59bb39aea docs(oauth2): 文档新增 oauth2 server 前后台分离参考支持 2025-01-06 14:33:01 +08:00
click33 c91412e542 feat(oauth2): 新增 oauth2 server 端前后台分离示例 2025-01-06 14:32:23 +08:00
click33 59253dc6f0 chore(sso): 删除 sso demo 示例中不必要的代码内容 2025-01-06 14:31:17 +08:00
click33 51a12fd9e5 docs(oauth2): 修整滞后的配置信息示例 2025-01-03 03:31:36 +08:00
click33 8ff85904f8 docs: 新增 gitcode g-star badge 展示 2024-12-27 07:32:53 +08:00
lihao 24907ae6c2 update sa-token-doc/up/integ-spring-mongod.md.
修改注释

Signed-off-by: lihao <dahao@qq.com>
2024-12-22 14:23:17 +00:00
click33 248fd0c3b6 fix(core): 修复 SaFoxUtil.joinSharpParam 方法中不正确的注释 2024-12-18 15:49:41 +08:00
click33 f424908743 docs: 增加 SpringBoot3.x 版本配置 Redis 注意事项。fix: #688 2024-12-15 17:59:10 +08:00
lihao 0644006efe 修改示例代码的链接名称 2024-12-12 15:55:58 +08:00
lihao 290fea17e0 补充示例代码 2024-12-12 15:53:09 +08:00
lihao 85a1c5ba6e docs:新增集成 MongoDB 的示例 2024-12-12 15:19:19 +08:00
click33 1a89be8daa docs(sso): “不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示,fix: #IAFZXL 2024-12-12 09:58:17 +08:00
click33 d8bd196d24 chore(core): 优化 active-timeout 的检查与续期操作,同一请求内只会检查与续期一次 2024-12-11 17:34:33 +08:00
click33 80879ba3b8 chore(sso): 去除 demo-sso/sso-server 中多余的异常处理函数
Closes #IAT367
2024-12-11 11:43:25 +08:00
click33 b5908c8ac0 chore(core): 去除登录流程中不必要的冗余代码 2024-12-10 18:54:26 +08:00
click33 c62fdc9276 fix(core): 修复部分场景下登录后已存在的 token-session 没有被续期的问题,fixes #IA8U1O 2024-12-10 18:19:12 +08:00
click33 1c3b02fc13 docs(sso): 新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节 2024-12-09 17:53:48 +08:00
click33 6f1094c361 feat: 新增 SaFirewallStrategy 防火墙策略:请求 path 黑名单校验、非法字符校验、白名单放行 2024-12-08 11:17:42 +08:00
click33 11492df031 chore: solon 依赖升级至 3.0.4 2024-12-08 11:15:23 +08:00
click33 9a53222baa docs(sso): 修复 "用户数据同步/迁移" 章节 不正确的代码示例 2024-12-07 04:53:59 +08:00
click33 f221d4ce97 chore(oauth2): 新建 SaOAuth2ResourcesController.java 以期更简单明了的展示 OAuth2 资源端搭建 2024-12-06 17:52:21 +08:00
click33 2bb45214d0 chore(oauth2): 修复 OAuth2 前端测试页不正确的描述信息 2024-12-06 17:22:50 +08:00
click33 a7895eff88 chore(oauth2): 修复 SaOAuth2DataLoader#getUnionid 不正确的方法注释 2024-12-06 17:17:23 +08:00
click33 4caa6a8074 docs: 修复不正确的样式 2024-12-06 17:15:53 +08:00
click33 e52045d3da docs(oauth2): 新增 UnionId 描述 2024-11-29 11:33:10 +08:00
click33 20da538a3f chore(oauth2): 修复不正确的注释描述 2024-11-29 11:32:03 +08:00
click33 21d5e02c67 feat(oauth2): 新增 UnionId 联合id 实现 2024-11-28 08:26:58 +08:00
click33 e7694bd6fb perf(sso): sso 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式 2024-11-27 12:23:02 +08:00
click33 494030506d feat(oauth2): 新增 SaOAuth2Util.getCode 等方法,以更方便的获取、校验授权码。 2024-11-27 08:01:59 +08:00
click33 4e70438ba8 feat(oauth2): 为 Access-Token、Client-Token 添加 grantType 字段,以记录该数据的授权类型 2024-11-27 07:41:30 +08:00
click33 d6b5975bdf feat(oauth2): 为 CodeModel、AccessTokenModel、RefreshTokenModel、ClientTokenModel 添加 createTime 字段,以记录该数据的创建时间 2024-11-26 19:46:42 +08:00
click33 a8851cf54d feat(oauth2): 增加回收 Refresh-Token 方法: revokeRefreshTokenrevokeRefreshTokenByIndex 2024-11-26 18:36:29 +08:00
click33 d1a0402c52 fix: 修复错误方法名 deleteGrantScope(String state) -> deleteState(String state) 2024-11-25 06:29:55 +08:00
click33 bb37c972ff fix: 修复全局配置项 sa-token.oauth2-server.oidc.iss 无效的问题 fixes #12 2024-11-25 06:11:22 +08:00
click33 982b0dde21 docs: 优化按钮配色 2024-11-25 06:10:13 +08:00
click33 1bfb26c191 docs: 增加“API接口参数签名”章节 视频讲解链接(B站抓蛙师) 2024-11-25 04:21:31 +08:00
click33 3cfe6c866e docs: 新增“fox说技术”视频教程链接 2024-11-23 04:53:04 +08:00
click33 7e2388b3e9 docs: 新增 dromara 新晋项目 mica-mqtt 链接 2024-11-23 04:46:08 +08:00
click33 2843ef85bc docs: 更新赞助者名单 2024-11-23 04:25:49 +08:00
click33 82b9f23945 fix: 修复 demo 示例 layer 无效 cdn 链接 2024-11-20 14:15:23 +08:00
click33 192b2fdd09 feat: 增加 oauth2 client 前端测试页 2024-11-16 17:41:59 +08:00
click33 2ce1328cdc chore: oauth2-server demo 数据加载方式改为 SysClientMockDao 查询方式 2024-11-16 17:41:35 +08:00
click33 442cf9db41 chore: oauth-server demo 增加跨域处理 2024-11-16 17:40:51 +08:00
click33 601433a638 refactor: 优化 oauth2-server 登录页测试账号显示 2024-11-16 17:37:14 +08:00
刘潇 d1203142b0 !317 update 友情链接
Merge pull request !317 from Troy/N/A
2024-11-09 17:10:04 +00:00
Troy ea8a966852 update 友情链接
Signed-off-by: Troy <1573555987@qq.com>
2024-11-09 14:17:45 +00:00
click33 23d5df85e3 docs: 修复最新版本Chrome浏览器文档部分动画效果失效的问题 2024-11-06 21:01:50 +08:00
click33 ca1a8ec239 docs: 更换部分项目展示logo 2024-11-06 21:01:22 +08:00
click33 df551cf0b5 docs: 补全赞助者名单 2024-11-06 21:00:56 +08:00
click33 150373ebae docs: 文档优化 2024-10-30 17:51:55 +08:00
click33 f8807ef2bc docs: 修复 OAuth2 相关文档不正确描述之处 2024-10-30 16:24:26 +08:00
click33 0bcfeb231b docs: 补全 dromara 成员项目链接 2024-10-30 14:06:43 +08:00
click33 56e39b4fba docs: 文档首页首屏增加需求提交按钮 2024-10-30 13:53:05 +08:00
click33 6b38a16102 docs: 调整首页样式 2024-10-30 13:36:23 +08:00
click33 b395240136 docs: 补全赞助者名单 2024-10-30 12:15:05 +08:00
click33 adfdacbdf2 Merge branch 'dev' of https://gitee.com/dromara/sa-token into dev 2024-10-30 11:43:54 +08:00
click33 8da8b50880 docs: 修复不正确的视频链接地址 2024-10-30 11:43:22 +08:00
click33 57556625cc docs: 修复文档部分不准确描述 2024-10-30 11:42:39 +08:00
刘潇 be8ec9c1be !314 调整 SaRequest:getParamNames() 改为 Collection 类型(之前为 List)
Merge pull request !314 from 西东/dev
2024-10-07 12:48:04 +00:00
noear d8e9e98152 调整 SaRequest:getParamNames() 改为 Collection 类型(之前为 List) 2024-10-07 20:44:55 +08:00
刘潇 bbb30e7f96 !313 solon 升为 3.0.1
Merge pull request !313 from 西东/dev
2024-10-07 12:40:09 +00:00
noear d3d11ce2b9 solon 升为 3.0.1 2024-10-07 20:23:55 +08:00
noear a4b63f5875 solon 升为 3.0.1 2024-10-07 20:22:04 +08:00
noear 824a4fec30 solon 升为 3.0.1 2024-10-07 20:19:45 +08:00
noear d75e4b799d Merge branch 'dev' of https://gitee.com/noear_admin/sa-token into dev 2024-09-22 21:02:58 +08:00
noear 8b6dc460a5 solon 升为 2.9.2 2024-09-22 21:02:49 +08:00
刘潇 1fa07884f3 !311 在oidc模式下,如果客户端携带有nonce随机数参数,认证服务端在生成的idtoken中需要返回客户端提交的nonce随机数,供客户端进行校验。
Merge pull request !311 from t_wang/dev
2024-09-05 05:30:48 +00:00
wangtao 203bf304cb feat:在oidc模式下,如果客户端携带有nonce随机数参数,认证服务端在生成的idtoken中需要返回客户端提交的nonce随机数,供客户端进行校验。
来源:http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
2024-09-03 15:48:16 +08:00
click33 121b857135 修复文档错误 2024-09-02 17:41:06 +08:00
click33 b8fcf37bc4 update README.md 2024-08-31 15:05:32 +08:00
click33 ffecda729a 整理首页案例 2024-08-31 14:46:14 +08:00
click33 ce6e5749f3 更新赞助者名单 2024-08-31 12:40:57 +08:00
click33 99e7f45a5c 补全 Dromara 项目列表 2024-08-31 12:39:34 +08:00
click33 9c828ccddc 修复部分错误之处 2024-08-29 21:48:49 +08:00
click33 8c2ff8be9a 补上遗漏的更新日志 2024-08-29 10:37:14 +08:00
click33 e3d93c7103 修正文档部分错误之处 2024-08-28 23:08:42 +08:00
click33 7db36d5a97 文档修复 2024-08-28 21:30:45 +08:00
click33 f9113ddce5 完善赞助者名单 2024-08-28 12:44:10 +08:00
shengzhang_ 6954483255 merge dev into master
v1.39.0 update

Created-by: shengzhang_
Author-id: 774020
MR-id: 152152
Commit-by: click33;zhongjun;goodsWox;grasse;巴掌大叔;cuiguiyang
Merged-by: shengzhang_
E2E-issues: 
Description: 兼容请求/oauth2/token接口时Basic中携带clientId和clientSecret的场景
更新赞助者列表
补全 dromara 项目列表
更新赞助列表信息
!308 处理解析 JWT 时的 JSONException
...

See merge request: dromara/sa-token!2
2024-08-28 10:02:48 +08:00
lihao eb4bd150c3 集成MongoDB 2024-08-14 18:16:37 +08:00
shengzhang_ e52daf2cd6 merge dev into master
sync dev master

Created-by: shengzhang_
Author-id: 774020
MR-id: 103634
Commit-by: click33;黄先生;刘潇;Jelex;林钟一六;noear
Merged-by: shengzhang_
E2E-issues: 
Description: sync dev master

See merge request: click33/sa-token!1
2024-06-21 12:50:40 +00:00
182 changed files with 5619 additions and 773 deletions
+8 -6
View File
@@ -1,15 +1,16 @@
<p align="center">
<img alt="logo" src="https://sa-token.cc/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.39.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.40.0</h1>
<h4 align="center">一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!</h4>
<p align="center">
<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
<a href="https://gitcode.com/dromara/sa-token/overview"><img src="https://gitcode.com/dromara/Sa-Token/star/badge.svg"></a>
<a href="https://github.com/dromara/sa-token/stargazers"><img src="https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/network/members"><img src="https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/watchers"><img src="https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a>
<!-- <a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a> -->
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img src="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
</p>
<!-- <p align="center">学习测试请拉取 master 分支,dev 是在开发分支 (在根目录执行 `git checkout master`)</p> -->
@@ -169,10 +170,11 @@ Sa-Token-OAuth2 模块分为四种授权模式,解决不同场景下的授权
- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken。
- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步
- [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus):重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba Dubbo3.0 Sa-Token Mybatis-Plus MQ OSS ES Xxl-Job Docker 全方位升级 定期同步
- [[Smart-Admin]](https://gitee.com/lab1024/smart-admin)SmartAdmin国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台;
- [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud) 专注于多租户解决方案的微服务中后台快速开发平台。租户模式支持独立数据库(DATASOURCE模式)、共享数据架构(COLUMN模式) 和 非租户模式(NONE模式)✨
- [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin):一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等
- [[ YC-Framework ]](http://framework.youcongtech.com/):致力于打造一款优秀的分布式微服务解决方案
- [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken):重写 Pig 授权方式为 Sa-Token,其他代码不变。
- [[ sa-admin-server ]](https://gitee.com/wlf213/sa-admin-server) 基于 sa-admin-ui 的后台管理开发脚手架
还有更多优秀开源案例无法逐一展示,请参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)
@@ -192,7 +194,7 @@ Sa-Token-OAuth2 模块分为四种授权模式,解决不同场景下的授权
### 代码托管
- Gitee[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token)
- GitHub[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token)
- GitCode[https://gitcode.com/click33/sa-token](https://gitcode.com/click33/sa-token)
- GitCode[https://gitcode.com/dromara/sa-token](https://gitcode.com/dromara/sa-token)
+1
View File
@@ -24,6 +24,7 @@ cd sa-token-demo-springboot-redisson & call mvn clean & cd ..
cd sa-token-demo-ssm & call mvn clean & cd ..
cd sa-token-demo-test & call mvn clean & cd ..
cd sa-token-demo-thymeleaf & call mvn clean & cd ..
cd sa-token-demo-freemarker & call mvn clean & cd ..
cd sa-token-demo-webflux & call mvn clean & cd ..
cd sa-token-demo-webflux-springboot3 & call mvn clean & cd ..
cd sa-token-demo-websocket & call mvn clean & cd ..
+1 -1
View File
@@ -37,7 +37,7 @@
<!-- 一些属性 -->
<properties>
<revision>1.39.0</revision>
<revision>1.40.0</revision>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
+11 -1
View File
@@ -13,7 +13,7 @@
<url>https://github.com/dromara/sa-token</url>
<properties>
<revision>1.39.0</revision>
<revision>1.40.0</revision>
</properties>
<dependencyManagement>
@@ -154,6 +154,11 @@
<artifactId>sa-token-dialect-thymeleaf</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-freemarker</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
@@ -174,6 +179,11 @@
<artifactId>sa-token-spring-aop</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-el</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
@@ -15,6 +15,9 @@
*/
package cn.dev33.satoken.config;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Sa-Token Cookie写入 相关配置
*
@@ -54,6 +57,12 @@ public class SaCookieConfig {
*/
private String sameSite;
/**
* 额外扩展属性
*/
private Map<String, String> extraAttrs = new LinkedHashMap<>();
/**
* 获取:Cookie 作用域
* <p> 写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。 </p>
@@ -140,11 +149,69 @@ public class SaCookieConfig {
return this;
}
/**
* @return 获取额外扩展属性
*/
public Map<String, String> getExtraAttrs() {
return extraAttrs;
}
/**
* 写入额外扩展属性
* @param extraAttrs /
* @return 对象自身
*/
public SaCookieConfig setExtraAttrs(Map<String, String> extraAttrs) {
this.extraAttrs = extraAttrs;
return this;
}
/**
* 追加扩展属性
* @param name /
* @param value /
* @return 对象自身
*/
public SaCookieConfig addExtraAttr(String name, String value) {
if (extraAttrs == null) {
extraAttrs = new LinkedHashMap<>();
}
this.extraAttrs.put(name, value);
return this;
}
/**
* 追加扩展属性
* @param name /
* @return 对象自身
*/
public SaCookieConfig addExtraAttr(String name) {
return this.addExtraAttr(name, null);
}
/**
* 移除指定扩展属性
* @param name /
* @return 对象自身
*/
public SaCookieConfig removeExtraAttr(String name) {
if(extraAttrs != null) {
this.extraAttrs.remove(name);
}
return this;
}
// toString
@Override
public String toString() {
return "SaCookieConfig [domain=" + domain + ", path=" + path + ", secure=" + secure + ", httpOnly=" + httpOnly
+ ", sameSite=" + sameSite + "]";
return "SaCookieConfig [" +
"domain=" + domain +
", path=" + path +
", secure=" + secure +
", httpOnly=" + httpOnly +
", sameSite=" + sameSite +
", extraAttrs=" + extraAttrs +
"]";
}
}
@@ -15,14 +15,16 @@
*/
package cn.dev33.satoken.context.model;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Cookie Model,代表一个 Cookie 应该具有的所有参数
@@ -77,6 +79,13 @@ public class SaCookie {
*/
private String sameSite;
// Cookie 属性参考文章:https://blog.csdn.net/fengbin2005/article/details/136544226
/**
* 额外扩展属性
*/
private Map<String, String> extraAttrs = new LinkedHashMap<>();
/**
* 构造一个
@@ -224,13 +233,73 @@ public class SaCookie {
return this;
}
/**
* @return 获取额外扩展属性
*/
public Map<String, String> getExtraAttrs() {
return extraAttrs;
}
/**
* 写入额外扩展属性
* @param extraAttrs /
* @return 对象自身
*/
public SaCookie setExtraAttrs(Map<String, String> extraAttrs) {
this.extraAttrs = extraAttrs;
return this;
}
/**
* 追加扩展属性
* @param name /
* @param value /
* @return 对象自身
*/
public SaCookie addExtraAttr(String name, String value) {
if (extraAttrs == null) {
extraAttrs = new LinkedHashMap<>();
}
this.extraAttrs.put(name, value);
return this;
}
/**
* 追加扩展属性
* @param name /
* @return 对象自身
*/
public SaCookie addExtraAttr(String name) {
return this.addExtraAttr(name, null);
}
/**
* 移除指定扩展属性
* @param name /
* @return 对象自身
*/
public SaCookie removeExtraAttr(String name) {
if(extraAttrs != null) {
this.extraAttrs.remove(name);
}
return this;
}
// toString
@Override
public String toString() {
return "SaCookie [name=" + name + ", value=" + value + ", maxAge=" + maxAge + ", domain=" + domain + ", path=" + path
+ ", secure=" + secure + ", httpOnly=" + httpOnly + ", sameSite="
+ sameSite + "]";
return "SaCookie [" +
"name=" + name +
", value=" + value +
", maxAge=" + maxAge +
", domain=" + domain +
", path=" + path
+ ", secure=" + secure +
", httpOnly=" + httpOnly +
", sameSite=" + sameSite +
", extraAttrs=" + extraAttrs +
"]";
}
/**
@@ -256,6 +325,7 @@ public class SaCookie {
throw new SaTokenException("无效Value" + value).setCode(SaErrorCode.CODE_12003);
}
// example
// Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax
StringBuilder sb = new StringBuilder();
@@ -287,6 +357,17 @@ public class SaCookie {
sb.append("; SameSite=").append(sameSite);
}
// 扩展属性
if(extraAttrs != null) {
extraAttrs.forEach((k, v) -> {
if(SaFoxUtil.isEmpty(v)) {
sb.append("; ").append(k);
} else {
sb.append("; ").append(k).append("=").append(v);
}
});
}
return sb.toString();
}
@@ -20,6 +20,7 @@ import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.util.SaFoxUtil;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -95,7 +96,7 @@ public interface SaRequest {
* 获取 [ 请求体 ] 里提交的所有参数名称
* @return 参数名称列表
*/
List<String> getParamNames();
Collection<String> getParamNames();
/**
* 获取 [ 请求体 ] 里提交的所有参数
@@ -0,0 +1,32 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.fun.strategy;
import java.util.Map;
import java.util.function.Consumer;
/**
* 函数式接口:SaCheckELRootMap 扩展函数
*
* <p> 参数:SaCheckELRootMap 对象 </p>
*
* @author click33
* @since 1.40.0
*/
@FunctionalInterface
public interface SaCheckELRootMapExtendFunction extends Consumer<Map<String, Object>> {
}
@@ -0,0 +1,119 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.model.wrapperInfo;
/**
* 返回值包装类:描述一个账号是否已被封禁等信息
*
* @author click33
* @since 1.40.0
*/
public class SaDisableWrapperInfo {
/**
* 是否被封禁
*/
public boolean isDisable;
/**
* 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁)
*/
public long disableTime;
/**
* 封禁等级(最小1级,0=未封禁)
*/
public int disableLevel;
/**
* 构建对象
*
* @param isDisable 是否被封禁
* @param disableTime 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁)
* @param disableLevel 封禁等级(最小1级,0=未封禁)
*/
public SaDisableWrapperInfo(boolean isDisable, long disableTime, int disableLevel) {
this.isDisable = isDisable;
this.disableTime = disableTime;
this.disableLevel = disableLevel;
}
/**
* 创建一个已封禁描述对象
* @param disableTime 封禁时间
* @param disableLevel 封禁等级
* @return /
*/
public static SaDisableWrapperInfo createDisabled(long disableTime, int disableLevel) {
return new SaDisableWrapperInfo(true, disableTime, disableLevel);
}
/**
* 创建一个未封禁描述对象
* @return /
*/
public static SaDisableWrapperInfo createNotDisabled() {
return new SaDisableWrapperInfo(false, 0, 0);
}
/**
* 创建一个未封禁描述对象,并指定缓存时间,指定时间内不再重复查询
* @param cacheTime 缓存时间(单位:秒)
* @return /
*/
public static SaDisableWrapperInfo createNotDisabled(long cacheTime) {
return new SaDisableWrapperInfo(false, cacheTime, 0);
}
@Override
public String toString() {
return "SaDisableWrapperInfo{" +
"isDisable=" + isDisable +
", disableTime=" + disableTime +
", disableLevel=" + disableLevel +
'}';
}
// setter / getter 仅为兼容部分框架序列化操作,不建议调用
public boolean getIsDisable() {
return isDisable;
}
public SaDisableWrapperInfo setIsDisable(boolean isDisable) {
this.isDisable = isDisable;
return this;
}
public long getDisableTime() {
return disableTime;
}
public SaDisableWrapperInfo setDisableTime(long disableTime) {
this.disableTime = disableTime;
return this;
}
public int getDisableLevel() {
return disableLevel;
}
public SaDisableWrapperInfo setDisableLevel(int disableLevel) {
this.disableLevel = disableLevel;
return this;
}
}
@@ -15,14 +15,14 @@
*/
package cn.dev33.satoken.stp;
import java.util.LinkedHashMap;
import java.util.Map;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaTokenConsts;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 在调用 `StpUtil.login()` 时的 配置参数 Model,决定登录的一些细节行为 <br>
*
@@ -236,26 +236,6 @@ public class SaLoginModel {
return isLastingCookie;
}
/**
* @return timeout 值 (如果此配置项尚未配置,则取全局配置的值)
*/
public Long getTimeoutOrGlobalConfig() {
if(timeout == null) {
timeout = SaManager.getConfig().getTimeout();
}
return timeout;
}
/**
* @return 是否在登录后将 Token 写入到响应头 (如果此配置项尚未配置,则取全局配置的值)
*/
public Boolean getIsWriteHeaderOrGlobalConfig() {
if(isWriteHeader == null) {
isWriteHeader = SaManager.getConfig().getIsWriteHeader();
}
return isWriteHeader;
}
/**
* 写入扩展数据(只在jwt模式下生效)
* @param key 键
@@ -297,13 +277,11 @@ public class SaLoginModel {
if( ! getIsLastingCookieOrFalse()) {
return -1;
}
if(getTimeoutOrGlobalConfig() == SaTokenDao.NEVER_EXPIRE) {
long _timeout = getTimeout();
if(_timeout == SaTokenDao.NEVER_EXPIRE || _timeout > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (timeout > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int)(long)timeout;
return (int)_timeout;
}
/**
@@ -353,4 +331,31 @@ public class SaLoginModel {
return new SaLoginModel();
}
// ---------------- 过期方法
/**
* 请改为 getTimeout
* @return timeout 值 (如果此配置项尚未配置,则取全局配置的值)
*/
@Deprecated
public Long getTimeoutOrGlobalConfig() {
if(timeout == null) {
timeout = SaManager.getConfig().getTimeout();
}
return timeout;
}
/**
* 请改为 getIsWriteHeader
* @return 是否在登录后将 Token 写入到响应头 (如果此配置项尚未配置,则取全局配置的值)
*/
@Deprecated
public Boolean getIsWriteHeaderOrGlobalConfig() {
if(isWriteHeader == null) {
isWriteHeader = SaManager.getConfig().getIsWriteHeader();
}
return isWriteHeader;
}
}
@@ -15,10 +15,12 @@
*/
package cn.dev33.satoken.stp;
import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;
import java.util.List;
/**
* 权限数据加载接口
* 权限数据加载接口
*
* <p>
* 在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。<br>
@@ -48,4 +50,15 @@ public interface StpInterface {
*/
List<String> getRoleList(Object loginId, String loginType);
/**
* 返回指定账号 id 是否被封禁
*
* @param loginId 账号id
* @param service 业务标识符
* @return 描述该账号是否封禁的包装信息对象
*/
default SaDisableWrapperInfo isDisabled(Object loginId, String service) {
return SaDisableWrapperInfo.createNotDisabled();
}
}
@@ -29,6 +29,7 @@ import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.*;
import cn.dev33.satoken.fun.SaFunction;
import cn.dev33.satoken.listener.SaTokenEventCenter;
import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.session.TokenSign;
import cn.dev33.satoken.strategy.SaStrategy;
@@ -203,7 +204,7 @@ public class StpLogic {
}
// 3. 将 token 写入到当前请求的响应头中
if(loginModel.getIsWriteHeaderOrGlobalConfig()) {
if(loginModel.getIsWriteHeader()) {
setTokenValueToResponseHeader(tokenValue);
}
}
@@ -248,6 +249,7 @@ public class StpLogic {
.setSecure(cfg.getSecure())
.setHttpOnly(cfg.getHttpOnly())
.setSameSite(cfg.getSameSite())
.setExtraAttrs(cfg.getExtraAttrs())
;
SaHolder.getResponse().addCookie(cookie);
}
@@ -471,7 +473,7 @@ public class StpLogic {
String tokenValue = distUsableToken(id, loginModel);
// 4、获取此账号的 Account-Session , 续期
SaSession session = getSessionByLoginId(id, true, loginModel.getTimeoutOrGlobalConfig());
SaSession session = getSessionByLoginId(id, true, loginModel.getTimeout());
session.updateMinTimeout(loginModel.getTimeout());
// 5、在 Account-Session 上记录本次登录的 token 签名
@@ -483,18 +485,24 @@ public class StpLogic {
// 7、写入这个 token 的最后活跃时间 token-last-active
if(isOpenCheckActiveTimeout()) {
setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig());
setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeout());
}
// 8、$$ 发布全局事件:账号 xxx 登录成功
// 8、如果该 token 对应的 Token-Session 已经存在,则需要给其续期
SaSession tokenSession = getTokenSessionByToken(tokenValue, false);
if(tokenSession != null) {
tokenSession.updateMinTimeout(loginModel.getTimeout());
}
// 9、$$ 发布全局事件:账号 xxx 登录成功
SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
// 9、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉
// 10、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉
if(config.getMaxLoginCount() != -1) {
logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
}
// 10、一切处理完毕,返回会话凭证 token
// 11、一切处理完毕,返回会话凭证 token
return tokenValue;
}
@@ -976,16 +984,23 @@ public class StpLogic {
throw NotLoginException.newInstance(loginType, KICK_OUT, KICK_OUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11015);
}
// 7、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结
// 7、token 活跃频率检查
if(isOpenCheckActiveTimeout()) {
checkActiveTimeout(tokenValue);
// storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗
SaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> {
// ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了
// 7.1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结
checkActiveTimeout(tokenValue);
// 8、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout
if(getConfigOrGlobal().getAutoRenew()) {
updateLastActiveToNow(tokenValue);
}
// ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了
// 7.2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout
if(getConfigOrGlobal().getAutoRenew()) {
updateLastActiveToNow(tokenValue);
}
return true;
});
}
// 9、返回 loginId
@@ -1522,26 +1537,18 @@ public class StpLogic {
*/
public void checkActiveTimeout(String tokenValue) {
// storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗
SaStorage storage = SaHolder.getStorage();
storage.get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> {
// 1、获取这个 token 的剩余活跃有效期
long activeTimeout = getTokenActiveTimeoutByToken(tokenValue);
// 1、获取这个 token 的剩余活跃有效期
long activeTimeout = getTokenActiveTimeoutByToken(tokenValue);
// 2、值为 -1 代表此 token 已经被设置永不冻结,无须继续验证
if(activeTimeout == SaTokenDao.NEVER_EXPIRE) {
return;
}
// 2、值为 -1 代表此 token 已经被设置永不冻结,无须继续验证
if(activeTimeout == SaTokenDao.NEVER_EXPIRE) {
return true;
}
// 3、值为 -2 代表已被冻结,此时需要抛出异常
if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {
throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016);
}
// --- 验证通过
return true;
});
// 3、值为 -2 代表已被冻结,此时需要抛出异常
if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {
throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016);
}
}
/**
@@ -2407,8 +2414,8 @@ public class StpLogic {
if(SaFoxUtil.isEmpty(service)) {
throw new SaTokenException("请提供要封禁的服务").setCode(SaErrorCode.CODE_11063);
}
if(level < SaTokenConsts.MIN_DISABLE_LEVEL) {
throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL).setCode(SaErrorCode.CODE_11064);
if(level < SaTokenConsts.MIN_DISABLE_LEVEL && level != 0) {
throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL + " (0除外)").setCode(SaErrorCode.CODE_11064);
}
// 打上封禁标记
@@ -2467,13 +2474,12 @@ public class StpLogic {
*/
public void checkDisableLevel(Object loginId, String service, int level) {
// 1、先前置检查一下这个账号是否被封禁了
String value = getSaTokenDao().get(splicingKeyDisable(loginId, service));
if(SaFoxUtil.isEmpty(value)) {
int disableLevel = getDisableLevel(loginId, service);
if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) {
return;
}
// 2、再判断被封禁的等级是否达到了指定级别
Integer disableLevel = SaFoxUtil.getValueByType(value, int.class);
if(disableLevel >= level) {
throw new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service))
.setCode(SaErrorCode.CODE_11061);
@@ -2498,14 +2504,22 @@ public class StpLogic {
* @return /
*/
public int getDisableLevel(Object loginId, String service) {
// 1、判断是否被封禁了,如果尚未被封禁,返回-2
// 1、先从缓存中查询数据,缓存中有值,以缓存值优先
String value = getSaTokenDao().get(splicingKeyDisable(loginId, service));
if(SaFoxUtil.isEmpty(value)) {
return SaTokenConsts.NOT_DISABLE_LEVEL;
if(SaFoxUtil.isNotEmpty(value)) {
return SaFoxUtil.getValueByType(value, int.class);
}
// 2、转为 int 类型返回
return SaFoxUtil.getValueByType(value, int.class);
// 2、如果缓存中无数据,则从"数据加载器"中再次查询
SaDisableWrapperInfo disableWrapperInfo = SaManager.getStpInterface().isDisabled(loginId, service);
// 如果返回值 disableTime 有效,则代表返回结果需要写入缓存
if(disableWrapperInfo.disableTime == SaTokenDao.NEVER_EXPIRE || disableWrapperInfo.disableTime > 0) {
disableLevel(loginId, service, disableWrapperInfo.disableLevel, disableWrapperInfo.disableTime);
}
// 返回查询结果
return disableWrapperInfo.disableLevel;
}
@@ -17,6 +17,7 @@ package cn.dev33.satoken.strategy;
import cn.dev33.satoken.annotation.*;
import cn.dev33.satoken.annotation.handler.*;
import cn.dev33.satoken.fun.strategy.SaCheckELRootMapExtendFunction;
import cn.dev33.satoken.fun.strategy.SaCheckMethodAnnotationFunction;
import cn.dev33.satoken.fun.strategy.SaGetAnnotationFunction;
import cn.dev33.satoken.fun.strategy.SaIsAnnotationPresentFunction;
@@ -130,4 +131,13 @@ public final class SaAnnotationStrategy {
instance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null;
};
/**
* SaCheckELRootMap 扩展函数
*/
public SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> {
// 默认不做任何处理
};
}
@@ -0,0 +1,113 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.strategy;
import cn.dev33.satoken.exception.RequestPathInvalidException;
import cn.dev33.satoken.fun.strategy.SaCheckRequestPathFunction;
import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction;
/**
* Sa-Token 防火墙策略
*
* @author click33
* @since 1.40.0
*/
public final class SaFirewallStrategy {
private SaFirewallStrategy() {
}
/**
* 全局单例引用
*/
public static final SaFirewallStrategy instance = new SaFirewallStrategy();
// ----------------------- 所有策略
/**
* 请求 path 黑名单
*/
public String[] blackPaths = {};
/**
* 请求 path 白名单
*/
public String[] whitePaths = {};
/**
* 请求 path 不允许出现的字符
*/
public String[] invalidCharacter = {
"//", // //
"\\", // \
"%2e", "%2E", // .
"%2f", "%2F", // /
"%5c", "%5C", // \
";", "%3b", "%3B", // ; // 参考资料:https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
"%25" // 空格
};
/**
* 校验请求 path 的算法
*/
public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> {
// 1、如果在白名单里,则直接放行
for (String item : whitePaths) {
if (requestPath.equals(item)) {
return;
}
}
// 2、如果在黑名单里,则抛出异常
for (String item : blackPaths) {
if (requestPath.equals(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
// 3、检查是否包含非法字符
// 不允许为null
if(requestPath == null) {
throw new RequestPathInvalidException("非法请求:null", null);
}
// 不允许包含非法字符
for (String item : invalidCharacter) {
if (requestPath.contains(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
// 不允许出现跨目录字符
if(requestPath.contains("/.") || requestPath.contains("\\.")) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
};
/**
* 当请求 path 校验不通过时处理方案的算法,自定义示例:
* <pre>
* SaFirewallStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> {
* // 自定义处理逻辑 ...
* };
* </pre>
*/
public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null;
}
@@ -16,7 +16,6 @@
package cn.dev33.satoken.strategy;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.exception.RequestPathInvalidException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.fun.strategy.*;
import cn.dev33.satoken.session.SaSession;
@@ -164,49 +163,6 @@ public final class SaStrategy {
return new StpLogic(loginType);
};
/**
* 请求 path 不允许出现的字符
*/
public static String[] INVALID_CHARACTER = {
"//", "\\",
"%2e", "%2E", // .
"%2f", "%2F", // /
"%5c", "%5C", // \
"%25" // 空格
};
/**
* 校验请求 path 的算法
*/
public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> {
// 不允许为null
if(requestPath == null) {
throw new RequestPathInvalidException("非法请求:null", null);
}
// 不允许包含非法字符
for (String item : INVALID_CHARACTER) {
if (requestPath.contains(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
// 不允许出现跨目录
if(requestPath.contains("/.") || requestPath.contains("\\.")) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
};
/**
* 当请求 path 校验不通过时处理方案的算法,自定义示例:
* <pre>
* SaStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> {
* // 自定义处理逻辑 ...
* };
* </pre>
*/
public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null;
// ----------------------- 重写策略 set连缀风格
@@ -443,15 +443,15 @@ public class SaFoxUtil {
url = "";
}
int index = url.lastIndexOf('#');
// ? 不存在
// # 不存在
if(index == -1) {
return url + '#' + paramStr;
}
// ? 是最后一位
// # 是最后一位
if(index == url.length() - 1) {
return url + paramStr;
}
// ? 是其中一位
// # 是其中一位
if(index < url.length() - 1) {
String separatorChar = "&";
// 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 &
@@ -36,7 +36,7 @@ public class SaTokenConsts {
/**
* Sa-Token 当前版本号
*/
public static final String VERSION_NO = "v1.39.0";
public static final String VERSION_NO = "v1.40.0";
/**
* Sa-Token 开源地址 Gitee
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+1 -1
View File
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -73,7 +73,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-bom</artifactId>
<version>1.39.0</version>
<version>1.40.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
+9 -2
View File
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -45,7 +45,14 @@
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 注解鉴权使用 EL 表达式 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-el</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
@@ -0,0 +1,102 @@
package com.pj.cases.more;
import cn.dev33.satoken.annotation.SaCheckEL;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* SaCheckEL EL表达式注解鉴权示例
*
* @author click33
* @since 2022-10-13
*/
@RestController
@RequestMapping("/check-el/")
public class SaCheckELController {
// 登录校验 ---- http://localhost:8081/check-el/test1
@SaCheckEL("stp.checkLogin()")
@RequestMapping("test1")
public SaResult test1() {
return SaResult.ok();
}
// 角色校验 ---- http://localhost:8081/check-el/test2
@SaCheckEL("stp.checkRole('dev-admin')")
@RequestMapping("test2")
public SaResult test2() {
return SaResult.ok();
}
// 权限校验 ---- http://localhost:8081/check-el/test3
@SaCheckEL("stp.checkPermission('user:edit')")
@RequestMapping("test3")
public SaResult test3() {
return SaResult.ok();
}
// 二级认证 ---- http://localhost:8081/check-el/test4
@SaCheckEL("stp.checkSafe()")
@RequestMapping("test4")
public SaResult test4() {
return SaResult.ok();
}
// 参数长度校验 ---- http://localhost:8081/check-el/test5?name=zhangsan
@SaCheckEL("NEED( #name.length() > 3 )")
@RequestMapping("test5")
public SaResult test5(@RequestParam(defaultValue = "") String name) {
return SaResult.ok().set("name", name);
}
// 参数长度校验,并自定义异常描述信息 ---- http://localhost:8081/check-el/test6?name=z
@SaCheckEL("NEED( #name !=null && #name.length() > 3, 'name长度不够' )")
@RequestMapping("test6")
public SaResult test6(String name) {
return SaResult.ok().set("name", name);
}
// 已登录, 或者查询数据在公开范围内 ---- http://localhost:8081/check-el/test7?id=10044
@SaCheckEL("NEED( stp.isLogin() or (#id != null and #id > 10010) )")
@RequestMapping("test7")
public SaResult test7(long id) {
return SaResult.ok().set("id", id);
}
// SaSession 里取值校验 ---- http://localhost:8081/check-el/test8
@SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )")
@RequestMapping("test8")
public SaResult test8() {
return SaResult.ok();
}
// 多账号体系鉴权测试 ---- http://localhost:8081/check-el/test9
@SaCheckEL("stpUser.checkLogin()")
@RequestMapping("test9")
public SaResult test9() {
return SaResult.ok();
}
// 本模块需要鉴权的权限码
public String permissionCode = "article:add";
// 调用本类的成员变量 ---- http://localhost:8081/check-el/test10
@SaCheckEL("stp.checkPermission( this.permissionCode )")
@RequestMapping("test10")
public SaResult test10() {
return SaResult.ok();
}
// 忽略鉴权测试 ---- http://localhost:8081/check-el/test11
@SaIgnore
@SaCheckEL("stp.checkPermission( 'abc' )")
@RequestMapping("test11")
public SaResult test11() {
return SaResult.ok();
}
}
@@ -120,6 +120,15 @@ public class SaTokenConfigure implements WebMvcConfigurer {
SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
};
// 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象
SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> {
System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet());
// 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权
rootMap.put("stpUser", StpUserUtil.getStpLogic());
};
}
}
@@ -1,12 +1,11 @@
package com.pj.satoken;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.stp.StpInterface;
/**
* 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码
*
@@ -17,7 +17,7 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<dubbo.version>2.7.21</dubbo.version>
<nacos.version>1.4.2</nacos.version>
</properties>
@@ -17,7 +17,7 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<dubbo.version>2.7.21</dubbo.version>
<nacos.version>1.4.2</nacos.version>
</properties>
@@ -17,7 +17,7 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<dubbo.version>3.2.2</dubbo.version>
<nacos.version>2.2.2</nacos.version>
</properties>
@@ -17,7 +17,7 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<dubbo.version>3.2.2</dubbo.version>
<nacos.version>2.2.2</nacos.version>
</properties>
@@ -0,0 +1,71 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-demo-freemarker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Freemarker 视图引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-freemarker</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 热刷新 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>provided</scope>
</dependency>
<!-- @ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,19 @@
package com.pj;
import cn.dev33.satoken.SaManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 参考链接:https://blog.csdn.net/m0_64210833/article/details/135994864
*/
@SpringBootApplication
public class SaTokenFreemarkerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenFreemarkerDemoApplication.class, args);
System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig());
System.out.println("\n测试访问:http://localhost:8081/");
}
}
@@ -0,0 +1,39 @@
package com.pj.satoken;
import cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel;
import cn.dev33.satoken.stp.StpUtil;
import freemarker.template.TemplateModelException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import javax.annotation.PostConstruct;
/**
* [Sa-Token 权限认证] 配置类
*
* @author click33
*/
@Configuration
public class SaTokenConfigure {
@Autowired
FreeMarkerConfigurer configurer;
/**
* 注入 Sa-Token Freemarker 标签模板模型 对象
*/
@PostConstruct
public void setSaTokenTemplateModel() throws TemplateModelException {
// 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签,
// 例如:<#if sa.login()>...</#if>
configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel());
// 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法,
// 例如:<span>${stp.getSession().get('name')}</span>
configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic);
}
}
@@ -0,0 +1,43 @@
package com.pj.satoken;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义权限验证接口扩展
*/
@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-delete");
list.add("user-update");
list.add("user-get");
list.add("article-get");
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;
}
}
@@ -0,0 +1,23 @@
package com.pj.test;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalException {
// 全局异常拦截(拦截项目中的所有异常)
@ExceptionHandler
public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
@@ -0,0 +1,39 @@
package com.pj.test;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* 测试 Controller
*
* @author click33
*/
@RestController
public class TestController {
// 首页
@RequestMapping("/")
public Object index() {
return new ModelAndView("index");
}
// 登录
@RequestMapping("login")
public SaResult login(@RequestParam(defaultValue="10001") String id) {
StpUtil.login(id);
StpUtil.getSession().set("name", "zhangsan");
return SaResult.ok();
}
// 注销
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
@@ -0,0 +1,16 @@
# 端口
server:
port: 8081
spring:
# Freemarker 相关配置
freemarker:
# 指定模板文件的目录
template-loader-path: classpath:/templates
# 指定Freemarker模板文件的后缀名
suffix: .ftl
# 关闭模板缓存,方便测试
cache: false
# 检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
settings:
template_update_delay: 0
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Sa-Token 集成 Freemarker 标签方言</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div class="view-box" style="padding: 30px;">
<h2>Sa-Token 集成 Freemarker 标签方言 —— 测试页面</h2>
<p>当前是否登录:<#if stp.isLogin()>是<#else>否</#if></p>
<p>
<a href="login" target="_blank">登录</a>
<a href="logout" target="_blank">注销</a>
</p>
<p>登录之后才能显示:<@sa.login>value</@sa.login></p>
<p>不登录才能显示:<@sa.notLogin>value</@sa.notLogin></p>
<p>具有角色 admin 才能显示:<@sa.hasRole value="admin">value</@sa.hasRole></p>
<p>同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd></p>
<p>只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr></p>
<p>不具有角色 admin 才能显示:<@sa.notRole value="admin">value</@sa.notRole></p>
<p>具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value</@sa.hasPermission></p>
<p>同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd></p>
<p>只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr></p>
<p>不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value</@sa.notPermission></p>
<p>
从SaSession中取值:
<#if stp.isLogin()>
<span>${stp.getSession().get('name')}</span>
</#if>
</p>
</div>
</body>
</html>
+1 -1
View File
@@ -27,7 +27,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.10</lombok.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+1 -1
View File
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -0,0 +1,633 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-Token-OAuth2 Client 端 - 测试页</title>
<style type="text/css">
body{background-color: #F0F9EB;}
*{margin: 0px; padding: 0px;}
.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}
/* 全局配置盒子 */
.in-cfg-box{line-height: 30px; padding-top: 10px;}
.in-cfg-box .in-cfg-div{display: flex;}
.in-cfg-box .in-cfg-div>span{width: 280px;}
.in-cfg-box .in-cfg-div>input{flex: 1;}
.login-box textarea{width: calc(100% - 1em); padding: 0.5em; min-height: 4em; box-sizing: border-box;}
.login-box button{padding: 5px 15px; margin-top: 5px; margin-bottom: 5px; cursor: pointer; }
.login-box select{ height: 30px; cursor: pointer; }
.login-box h4{margin-top: 20px;}
.btn-box{margin-top: 10px; margin-bottom: 15px;}
.btn-box a{margin-right: 10px;}
.btn-box a:hover{text-decoration:underline !important;}
.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}
.login-box a{text-decoration: none;}
.pst{color: #666; margin-top: 15px;}
.ps{color: #666; margin-bottom: 5px;}
.oauth2-server-urls-box{ padding: 0.5em 0; padding-bottom: 0; margin-bottom: 0.5em; background-color: #ddd;}
.oauth2-server-urls-box .in-cfg-div>span{text-indent: 0.5em;}
/* loading框样式 */
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
</style>
</head>
<body>
<div class="login-box">
<h2>Sa-Token-OAuth2 Client 端 测试页 </h2>
<p class="pst">注:为方便测试,此处将应用秘钥等敏感信息采用前端填写式,真实项目应该改为后端配置。</p>
<br><hr><br>
<h3>配置信息</h3>
<div class="in-cfg-box">
<div class="in-cfg-div"><span>OAuth2 Server 主机地址:</span><input class="in-cfg" name="oauth2_server_url" /></div>
<div class="oauth2-server-urls-box">
<div class="in-cfg-div"><span>OAuth2 Server 授权页地址:</span><input class="in-cfg" name="oauth2_server_auth_url" /></div>
<div class="in-cfg-div"><span>OAuth2 Server 获取 token 地址:</span><input class="in-cfg" name="oauth2_server_token_url" /></div>
<div class="in-cfg-div"><span>OAuth2 Server 刷新 token 地址:</span><input class="in-cfg" name="oauth2_server_refresh_token_url" /></div>
<div class="in-cfg-div"><span>OAuth2 Server 获取 userinfo 地址:</span><input class="in-cfg" name="oauth2_server_userinfo_url" /></div>
<div class="in-cfg-div" style="margin-top: -5px;">&nbsp;<button onclick="autoSplicingUrl();">根据主机 URL 一键拼接授权页等地址</button></div>
</div>
<div class="in-cfg-div"><span>Client Id</span><input class="in-cfg" name="client_id" /></div>
<div class="in-cfg-div"><span>Client Secret</span><input class="in-cfg" name="client_secret" /></div>
<div class="in-cfg-div"><span>重定向授权地址:</span><input class="in-cfg" name="redirect_uri" /></div>
<div class="in-cfg-div"><span>请求 Scope (多个用逗号/空格隔开):</span><input class="in-cfg" name="scope" /></div>
</div>
<div class="btn-box">
<button onclick="clearLocalCfg()">清空配置</button>
<button onclick="clearLocalCfg(); readLocalCfg();">恢复默认配置</button>
<button onclick="autoFillGiteeUrl();">一键填写 Gitee 参数样例</button>
<button onclick="autoFillSaSsoMaxUrl();">一键填写 Sa-Sso-Max 参数样例</button>
<a href="javascript: location.href = location.href.split('?')[0].split('#')[0];">回到首页</a>
</div>
<hr><br>
<h3>模式一:授权码(Authorization Code</h3>
<p class="pst">授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid 等信息</p>
<h4>1、获取授权码</h4>
<button onclick="buildAuthorizationCodeUrl()">构建授权地址</button>
<textarea class="auth-code-url"></textarea>
<button onclick="jumpAuthCodeUrl()">→ 访问上述授权地址</button> <br>
<span>从 URL 上读取到的 code 为:<span class="show-url-code" style="color: green;"></span></span>
<h4>2、获取 Access-Token </h4>
<button onclick="buildCodeTakeTokenUrl()">构建 code 换 Access-Token 接口地址</button>
<textarea class="code-take-token-url"></textarea>
<button onclick="ajaxCodeToAccessToken()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
<textarea class="code-take-token-result"></textarea>
<h4>3、刷新 Access-Token </h4>
<button onclick="buildRefreshTokenUrl()">构建刷新 Access-Token 接口地址 </button>
请先填写 Refresh-Token 值:<input name="refresh-token-input" style="width: 500px;">
<textarea class="refresh-token-url"></textarea>
<p class="ps">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废</p>
<button onclick="ajaxRefreshToken()">→ 请求上述地址,刷新 Access-Token </button> 请求结果显示如下:
<textarea class="refresh-token-result"></textarea>
<h4>4、获取 Userinfo </h4>
<button onclick="buildUserinfoUrl()">构建刷新 Userinfo 接口地址 </button>
请先填写 Access-Token 值:<input name="access-token-input" style="width: 500px;">
<textarea class="userinfo-url"></textarea>
<p class="ps">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </p>
<button onclick="ajaxUserinfoUrl()">→ 请求上述地址,获取 Userinfo 信息 </button>
请求 Method
<select name="userinfo-ajax-method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
请求结果显示如下:
<textarea class="userinfo-result"></textarea>
<h4>5、回收 Access-Token </h4>
<button onclick="buildRevokeTokenUrl()">构建回收 Access-Token 接口地址 </button>
<!-- 请先填写 Access-Token 值:<input name="access-token-input" style="width: 500px;"> -->
<textarea class="revoke-token-url"></textarea>
<p class="ps">回收后,该 Access-Token 将无法再使用(点击上面的 Userinfo 接口试一试)</p>
<button onclick="ajaxRevokeTokenUrl()">→ 请求上述地址,回收 Access-Token </button> 请求结果显示如下:
<textarea class="revoke-token-result"></textarea>
<br><br>
<h3>模式二:隐藏式(Implicit</h3>
<p class="pst">越过授权码的步骤,直接返回token到前端页面( 格式:http://domain.com#token=xxxx-xxxx </p>
<button onclick="buildImplicitUrl()">构建授权地址</button>
<textarea class="implicit-url"></textarea>
<button onclick="jumpImplicitUrl()">→ 访问上述授权地址</button> <br>
<span>从 URL 上读取到的 Access-Token 为:<span class="show-url-access-token" style="color: green;"></span></span>
<br><br>
<h3>模式三:密码式(Password</h3>
<p class="pst">注解在 OAuth2-Client 端,输入用户名和密码获取 Access-Token,此模式只适用于高度信任的客户端</p>
<button onclick="buildPasswordUrl()">构建授权地址</button>
&emsp;账号:<input class="in-cfg" name="username">
&emsp;密码:<input class="in-cfg" name="password">
<textarea class="password-url"></textarea>
<button onclick="ajaxPasswordUrl()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
<textarea class="password-result"></textarea>
<br><br>
<h3>模式四:凭证式(Client Credentials</h3>
<p class="pst">以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权,
即:Client-Token,代表应用自身的资源授权</p>
<button onclick="buildClientTokenUrl()">构建 Client-Token 授权地址</button>
<textarea class="client-token-url"></textarea>
<button onclick="ajaxClientTokenUrl()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
<textarea class="client-token-result"></textarea>
<br><br>
<span>更多资料请参考 Sa-Token 官方文档地址:</span>
<a href="https://sa-token.cc/">https://sa-token.cc/</a>
<div style="height: 200px;"></div>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<!-- 配置缓存读取 -->
<script>
// 缓存前缀
var prefix = "IN_CFG_";
function getLocalCfg(key) {
return localStorage.getItem(prefix + key);
}
function setLocalCfg(key, value) {
localStorage.setItem(prefix + key, value);
}
// 全局配置变动时,存储到本地
$('.in-cfg').bind('input propertychange', function(){
var name = $(this).attr('name');
var value = $(this).val();
setLocalCfg(name, value);
})
// 默认配置
var defaultCfg = {
oauth2_server_url: 'http://sa-oauth-server.com:8000', // OAuth2 服务端主机地址
oauth2_server_auth_url: 'http://sa-oauth-server.com:8000/oauth2/authorize', // OAuth2 授权页地址
oauth2_server_token_url: 'http://sa-oauth-server.com:8000/oauth2/token', // OAuth2 获取 token 地址
oauth2_server_refresh_token_url: 'http://sa-oauth-server.com:8000/oauth2/refresh', // OAuth2 刷新 token 地址
oauth2_server_userinfo_url: 'http://sa-oauth-server.com:8000/oauth2/userinfo', // OAuth2 获取 userinfo 地址
client_id: '1001',
client_secret: 'aaaa-bbbb-cccc-dddd-eeee',
redirect_uri: location.href.split('?')[0].split('#')[0],
scope: 'userinfo,userid,openid,unionid,oidc',
username: 'sa',
password: '123456'
}
// 打开页面时,加载本地缓存数据,本地缓存无数据时加载默认配置
function readLocalCfg() {
$('.in-cfg').each(function(){
var name = $(this).attr('name');
var value = getLocalCfg(name) || defaultCfg[name];
$(this).val(value);
})
}
readLocalCfg();
// 清空配置
function clearLocalCfg() {
$('.in-cfg').each(function(){
$(this).val('');
setLocalCfg($(this).attr('name'), '');
})
}
// 将所有配置保存到本地缓存
function saveAllCfgToLocal() {
$('.in-cfg').each(function(){
setLocalCfg($(this).attr('name'), $(this).val());
})
}
// 根据主机 URL 一键拼接授权页等地址
function autoSplicingUrl() {
var oauth2_server_url = $('[name=oauth2_server_url]').val();
if(!oauth2_server_url) {
return layer.alert('请先配置 OAuth2 Server 主机地址!')
}
$('[name=oauth2_server_auth_url]').val(oauth2_server_url + '/oauth2/authorize');
$('[name=oauth2_server_token_url]').val(oauth2_server_url + '/oauth2/token');
$('[name=oauth2_server_refresh_token_url]').val(oauth2_server_url + '/oauth2/refresh');
$('[name=oauth2_server_userinfo_url]').val(oauth2_server_url + '/oauth2/userinfo');
saveAllCfgToLocal();
layer.msg('已自动拼接:OAuth2 Server 授权页地址、获取 token 地址、刷新 token 地址、获取 userinfo 地址');
}
// 一键填写 Gitee 参数样例
function autoFillGiteeUrl() {
$('[name=oauth2_server_url]').val('https://gitee.com')
$('[name=oauth2_server_auth_url]').val('https://gitee.com/oauth/authorize');
$('[name=oauth2_server_token_url]').val('https://gitee.com/oauth/token');
$('[name=oauth2_server_refresh_token_url]').val('https://gitee.com/oauth/token');
$('[name=oauth2_server_userinfo_url]').val('https://gitee.com/api/v5/user');
$('[name=client_id]').val('<待填写>');
$('[name=client_secret]').val('<待填写>');
$('[name=redirect_uri]').val(defaultCfg.redirect_uri);
$('[name=scope]').val('user_info');
saveAllCfgToLocal();
layer.msg('填写成功');
}
// 一键填写 Sa-Sso-Max 参数样例,参考:http://sa-pro.dev33.cn/
function autoFillSaSsoMaxUrl() {
$('[name=oauth2_server_url]').val('http://sspx-server.dev33.cn')
$('[name=oauth2_server_auth_url]').val('http://sspx-center.dev33.cn/oauth2/authorize');
$('[name=oauth2_server_token_url]').val('http://sspx-server.dev33.cn/oauth2/token');
$('[name=oauth2_server_refresh_token_url]').val('http://sspx-server.dev33.cn/oauth2/refresh');
$('[name=oauth2_server_userinfo_url]').val('http://sspx-server.dev33.cn/oauth2/userinfo');
$('[name=client_id]').val('100001');
$('[name=client_secret]').val('CQ0Nf1LmaYq7Ads8EdmKMtEnZmTVIicAEl2trBi0zVKufmOVY5G5Tu2epfu4');
$('[name=redirect_uri]').val(defaultCfg.redirect_uri);
$('[name=scope]').val('userinfo,openid,unionid');
saveAllCfgToLocal();
layer.msg('填写成功');
}
</script>
<!-- 工具方法 -->
<script>
// 从url中查询到指定名称的参数值
function getParam(name, defaultValue){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == name){return pair[1];}
}
return(defaultValue == undefined ? null : defaultValue);
}
// 从url中查询到指定名称的锚参数值
function getSharpParam(name, defaultValue){
var query = window.location.hash.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == name){return pair[1];}
}
return(defaultValue == undefined ? null : defaultValue);
}
var sa = {};
// 打开loading
sa.loading = function(msg) {
if(window.layer) {
layer.closeAll(); // 开始前先把所有弹窗关了
layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });
}
};
// 隐藏loading
sa.hideLoading = function() {
if(window.layer) {
layer.closeAll();
}
};
// 封装一下Ajax
sa.ajax = function(url, data, successFn, cfg) {
cfg = cfg || {};
sa.loading("正在努力加载...");
setTimeout(function() {
$.ajax({
url: url,
type: cfg.method || "post",
data: data,
dataType: 'json',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'satoken': localStorage.getItem('satoken')
},
success: function(res){
console.log('返回数据:', res);
sa.hideLoading();
successFn(res);
},
error: function(xhr, type, errorThrown){
sa.hideLoading();
if(xhr.status == 0){
return layer.alert('无法连接到服务器,请检查网络');
}
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}, 400);
}
// 从url中查询到指定名称的参数值
function getParam(name, defaultValue){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == name){return pair[1];}
}
return(defaultValue == undefined ? null : defaultValue);
}
// 监听 textarea 内容变化时,高度随之变化
$('textarea').keyup(function(){
var textarea = this;
textarea.style.height = 'auto'; // 去除之前的高度限制
textarea.style.height = textarea.scrollHeight + 14 + 'px';
})
// 重设指定 textarea 的高度
function resizeTextarea(selected){
$(selected).each(function(){
var textarea = this;
textarea.style.height = 'auto'; // 去除之前的高度限制
textarea.style.height = textarea.scrollHeight + 14 + 'px';
})
}
// 重设所有 textarea 的高度
function resizeAllTextarea(){
$('textarea').each(function(){
var textarea = this;
textarea.style.height = 'auto'; // 去除之前的高度限制
textarea.style.height = textarea.scrollHeight + 14 + 'px';
})
}
</script>
<script type="text/javascript">
// --------------------- 模式一 ---------------------
// 构建授权码地址
function buildAuthorizationCodeUrl() {
var url = $('[name=oauth2_server_auth_url]').val()
+ '?response_type=code'
+ '&client_id=' + $('[name=client_id]').val()
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
+ '&scope=' + $('[name=scope]').val()
$('.auth-code-url').val(url);
resizeTextarea('.auth-code-url');
}
// 跳转到授权码授权地址
function jumpAuthCodeUrl() {
var url = $('.auth-code-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
location.href = url;
}
// 默认尝试读取一下 code
var code = getParam('code');
if(code) {
$('.show-url-code').text(code);
}
// 构建 code 换 token 地址
function buildCodeTakeTokenUrl() {
var code = getParam('code');
if(!code) {
return layer.msg('未能获取到 code 参数,请先点击上方的授权地址获取 code ');
}
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/token'
var url = $('[name=oauth2_server_token_url]').val()
+ '?grant_type=authorization_code'
+ '&client_id=' + $('[name=client_id]').val()
+ '&client_secret=' + $('[name=client_secret]').val()
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
+ '&code=' + code
$('.code-take-token-url').val(url);
resizeTextarea('.code-take-token-url');
}
// code 换 Access-Token
function ajaxCodeToAccessToken() {
var url = $('.code-take-token-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
sa.ajax(url, {}, function(res){
if(res.access_token) {
$('[name=access-token-input]').val(res.access_token);
}
if(res.refresh_token) {
$('[name=refresh-token-input]').val(res.refresh_token);
}
var jsonStr = JSON.stringify(res, null, '\t');
$('.code-take-token-result').val(jsonStr);
resizeTextarea('.code-take-token-result');
})
}
// --------- 刷新令牌
// 构建 Tefresh-Token 刷新 Access-Token 地址
function buildRefreshTokenUrl() {
var refresh_token = $('[name=refresh-token-input]').val();
if(!refresh_token) {
return layer.msg('未能获取到 refresh_token 参数,请先点击上方的授权地址获取 refresh_token ');
}
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/refresh'
var url = $('[name=oauth2_server_refresh_token_url]').val()
+ '?grant_type=refresh_token'
+ '&client_id=' + $('[name=client_id]').val()
+ '&client_secret=' + $('[name=client_secret]').val()
+ '&refresh_token=' + refresh_token
$('.refresh-token-url').val(url);
resizeTextarea('.refresh-token-url');
}
// 使用 Tefresh-Token 刷新 Access-Token
function ajaxRefreshToken() {
var url = $('.refresh-token-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
sa.ajax(url, {}, function(res){
if(res.access_token) {
$('[name=access-token-input]').val(res.access_token);
}
if(res.refresh_token) {
$('[name=refresh-token-input]').val(res.refresh_token);
}
var jsonStr = JSON.stringify(res, null, '\t');
$('.refresh-token-result').val(jsonStr);
resizeTextarea('.refresh-token-result');
})
}
// --------- 用户资料
// 构建 Access-Token 获取 Userinfo 地址
function buildUserinfoUrl() {
var access_token = $('[name=access-token-input]').val();
if(!access_token) {
return layer.msg('未能获取到 access_token 参数,请先点击上方的授权地址获取 access_token ');
}
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/userinfo'
var url = $('[name=oauth2_server_userinfo_url]').val()
+ '?access_token=' + access_token
$('.userinfo-url').val(url);
resizeTextarea('.userinfo-url');
}
// 使用 Access-Token 获取 Userinfo
function ajaxUserinfoUrl() {
var url = $('.userinfo-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
var method = $('[name=userinfo-ajax-method]').val();
sa.ajax(url, {}, function(res){
var jsonStr = JSON.stringify(res, null, '\t');
$('.userinfo-result').val(jsonStr);
resizeTextarea('.userinfo-result');
}, {method: method})
}
// --------- 回收令牌
// 构建回收 Access-Token 地址
function buildRevokeTokenUrl() {
var access_token = $('[name=access-token-input]').val();
if(!access_token) {
return layer.msg('未能获取到 access_token 参数,请先点击上方的授权地址获取 access_token ');
}
var url = $('[name=oauth2_server_url]').val() + '/oauth2/revoke'
+ '?client_id=' + $('[name=client_id]').val()
+ '&client_secret=' + $('[name=client_secret]').val()
+ '&access_token=' + access_token
$('.revoke-token-url').val(url);
resizeTextarea('.revoke-token-url');
}
// 回收 Access-Token
function ajaxRevokeTokenUrl() {
var url = $('.revoke-token-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
sa.ajax(url, {}, function(res){
var jsonStr = JSON.stringify(res, null, '\t');
$('.revoke-token-result').val(jsonStr);
resizeTextarea('.revoke-token-result');
})
}
// --------------------- 模式二 ---------------------
// 构建隐藏式 Implicit 地址
function buildImplicitUrl() {
var url = $('[name=oauth2_server_auth_url]').val()
+ '?response_type=token'
+ '&client_id=' + $('[name=client_id]').val()
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
+ '&scope=' + $('[name=scope]').val()
$('.implicit-url').val(url);
resizeTextarea('.implicit-url');
}
// 跳转到 Implicit 授权地址
function jumpImplicitUrl() {
var url = $('.implicit-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
location.href = url;
}
// 默认尝试读取一下 Access-Token
var accessToken = getSharpParam('token');
if(accessToken) {
$('.show-url-access-token').text(accessToken);
$('[name=access-token-input]').val(accessToken);
}
// --------------------- 模式三 ---------------------
// 构建密码 Password 授权地址
function buildPasswordUrl() {
var url = $('[name=oauth2_server_url]').val() + '/oauth2/token'
+ '?grant_type=password'
+ '&client_id=' + $('[name=client_id]').val()
+ '&client_secret=' + $('[name=client_secret]').val()
+ '&username=' + $('[name=username]').val()
+ '&password=' + $('[name=password]').val()
+ '&scope=' + $('[name=scope]').val()
$('.password-url').val(url);
resizeTextarea('.password-url');
}
// 请求密码式 Password 授权地址
function ajaxPasswordUrl() {
var url = $('.password-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
sa.ajax(url, {}, function(res){
if(res.access_token) {
$('[name=access-token-input]').val(res.access_token);
}
if(res.refresh_token) {
$('[name=refresh-token-input]').val(res.refresh_token);
}
var jsonStr = JSON.stringify(res, null, '\t');
$('.password-result').val(jsonStr);
resizeTextarea('.password-result');
})
}
// --------------------- 模式四 ---------------------
// 构建密码 Client-Token 授权地址
function buildClientTokenUrl() {
var url = $('[name=oauth2_server_url]').val() + '/oauth2/client_token'
+ '?grant_type=client_credentials'
+ '&client_id=' + $('[name=client_id]').val()
+ '&client_secret=' + $('[name=client_secret]').val()
+ '&scope=' + $('[name=scope]').val()
$('.client-token-url').val(url);
resizeTextarea('.client-token-url');
}
// 请求 Client-Token 授权地址
function ajaxClientTokenUrl() {
var url = $('.client-token-url').val();
if(!url) {
return layer.msg('请先构建地址');
}
sa.ajax(url, {}, function(res){
var jsonStr = JSON.stringify(res, null, '\t');
$('.client-token-result').val(jsonStr);
resizeTextarea('.client-token-result');
})
}
</script>
</body>
</html>
@@ -17,7 +17,7 @@
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义 Sa-Token 版本号 -->
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -0,0 +1,48 @@
*{margin: 0; padding: 0;}
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
::-webkit-input-placeholder{color: #ccc;}
/* 视图盒子 */
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
/* 背景 EAEFF3 */
.bg-1{height: 100%; background: #E4B17F;}
/* 内容盒子 */
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
.content-box{display: none;}
.region-default{display: block;}
.message-box{width: 100%;; text-align: center;}
/* 登录盒子 */
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
.login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;}
.login-box{display: flex; align-items: center; text-align: center;}
/* 表单 */
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
/* 输入框 */
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
.s-input{font-size: 12px;}
.s-input:focus{border-color: #409eff}
/* 登录按钮 */
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
.s-btn:hover{background-color: #50aEFF;}
/* 重置按钮 */
.reset-box{text-align: left; font-size: 12px;}
.reset-box a{text-decoration: none;}
.reset-box a:hover{text-decoration: underline;}
/* 确认授权按钮 */
.confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;}
.confirm-btn:hover{background-color: #50aEFF;}
/* loading框样式 */
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
@@ -0,0 +1,194 @@
// OAuth-Server 后端 接口地址
var baseUrl = "http://sa-oauth-server.com:8000";
// ----------------------------------- 相关事件 -----------------------------------
// 显示默认区域
function showDefaultRegion(){
$('.content-box').hide();
$('.region-default').show();
}
// 显示登录框区域
function showLoginRegion(){
$('.content-box').hide();
$('.region-login').show();
}
// 显示确认授权框区域
function showConfirmRegion(){
$('.content-box').hide();
$('.region-confirm').show();
$('.show-clientId').text(getParam('client_id'));
$('.show-scope').text(getParam('scope'));
}
// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码
function tryJump(){
var data = location.search.substr(1);
sa.ajax("/oauth2/getRedirectUri", data, function(res) {
// 情况1:客户端未登录,返回 code=401,提示用户登录
if(res.code === 401) {
showLoginRegion();
return;
}
// 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认
if(res.code === 411) {
showConfirmRegion();
return;
}
// 情况3:已登录且请求的 scope 已确认授权,返回 code=200data=最终重定向 url 地址(携带code码参数)
if(res.code == 200) {
console.log('跳转:', res.redirect_uri);
location.href = res.redirect_uri;
return;
}
console.log('未知状态码,', res.code, res);
layer.alert('错误:' + JSON.stringify(res))
})
}
// 登录事件
function doLogin() {
// 开始登录
var data = {
name: $('[name=name]').val(),
pwd: $('[name=pwd]').val()
};
sa.ajax("/oauth2/doLogin", data, function(res) {
if(res.code == 200) {
localStorage.setItem('satoken', res.satoken);
layer.msg('登录成功', {anim: 0, icon: 6 });
setTimeout(function() {
location.reload();
}, 800);
} else {
layer.msg(res.msg, {anim: 6, icon: 2 });
}
})
}
// 确认授权事件
function yes() {
var data = location.search.substr(1) + '&build_redirect_uri=true';
sa.ajax("/oauth2/doConfirm", data, function(res) {
if(res.code == 200) {
layer.msg('确认授权成功,即将跳转...', {anim: 0, icon: 6 });
setTimeout(function() {
console.log('跳转:', res.redirect_uri);
location.href = res.redirect_uri;
}, 800);
} else {
layer.msg(res.msg, {anim: 6, icon: 2 });
}
})
}
// 拒绝授权事件
function no() {
var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
location.href = url;
}
// 页面加载完毕后触发
window.onload = function() {
tryJump();
// 绑定回车事件
$('[name=name],[name=pwd]').bind('keypress', function(event){
if(event.keyCode == "13") {
$('.login-btn').click();
}
});
// 输入框获取焦点
$("[name=name]").focus();
}
// ----------------------------------- 工具函数封装 -----------------------------------
// sa
var sa = {};
// 打开loading
sa.loading = function(msg) {
layer.closeAll(); // 开始前先把所有弹窗关了
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
};
// 隐藏loading
sa.hideLoading = function() {
layer.closeAll();
};
// 封装一下Ajax
sa.ajax = function(url, data, successFn) {
sa.loading("加载中...");
$.ajax({
url: baseUrl + url,
type: "post",
data: data,
dataType: 'json',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'satoken': localStorage.getItem('satoken')
},
success: function(res){
sa.hideLoading();
console.log('返回数据:', res);
successFn(res);
},
error: function(xhr, type, errorThrown){
sa.hideLoading();
if(xhr.status == 0){
return alert('无法连接到服务器,请检查网络');
}
return alert("异常:" + JSON.stringify(xhr));
}
});
}
// 从url中查询到指定名称的参数值
function getParam(name, defaultValue){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}
}
return(defaultValue == undefined ? null : defaultValue);
}
// 在url上拼接上kv参数并返回
function joinParam(url, parameStr) {
if(parameStr == null || parameStr.length == 0) {
return url;
}
var index = url.indexOf('?');
// ? 不存在
if(index == -1) {
return url + '?' + parameStr;
}
// ? 是最后一位
if(index == url.length - 1) {
return url + parameStr;
}
// ? 是其中一位
if(index > -1 && index < url.length - 1) {
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {
return url + '&' + parameStr;
} else {
return url + parameStr;
}
}
}
// 打印信息
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/";
console.log(str);
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Sa-OAuth2-Server 认证中心(前后端分离版)</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./login.css">
</head>
<body>
<div class="view-box">
<div class="bg-1"></div>
<!--
将页面分为三块区域:
- 未登录时显示区域2:登录框。
- 已登录但请求的 scope 尚未手动确认授权,显示区域3:确认授权框。
- 默认显示区域1:提示文字。
-->
<!-- 区域1:默认显示 -->
<div class="content-box region-default">
<div class="login-box">
<div class="message-box">
加载中...
</div>
</div>
</div>
<!-- 区域2:登录框 -->
<div class="content-box region-login">
<div class="login-box">
<div class="from-box">
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2>
<div class="from-item">
<input class="s-input" name="name" placeholder="请输入账号" />
</div>
<div class="from-item">
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
</div>
<div class="from-item">
<button class="s-input s-btn login-btn" onclick="doLogin()">登录</button>
</div>
<div class="from-item reset-box">
<a href="javascript: location.reload();" >刷新</a>
</div>
</div>
</div>
</div>
<!-- 区域3:确认授权框 -->
<div class="content-box region-confirm">
<div class="login-box">
<div class="from-box">
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2><br>
<div>
<div><b>应用ID</b><span class="show-clientId"></span></div>
<div><b>请求授权:</b><span class="show-scope"></span></div>
<br><br><div>------------- 是否同意授权 -------------</div><br>
<div>
<button class="confirm-btn" onclick="yes()">同意</button>
<button class="confirm-btn" onclick="no()">拒绝</button>
</div>
<div style="height: 10px;"></div>
</div>
</div>
</div>
</div>
<!-- 底部 版权 -->
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
This page is provided by Sa-Token-OAuth2
</div>
</div>
<!-- scripts -->
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script src="./login.js"></script>
</body>
</html>
@@ -17,7 +17,7 @@
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义 Sa-Token 版本号 -->
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -0,0 +1,86 @@
package com.pj.mock;
import cn.dev33.satoken.oauth2.consts.GrantType;
import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* SaClientModel 模拟查询操作
*
* @author click33
* @since 2024/11/15
*/
@Component
public class SaClientMockDao {
public List<SaClientModel> list = new ArrayList<>();
/**
* 构造方法,添加三个模拟应用
*/
public SaClientMockDao(){
// 模拟应用1
SaClientModel client1 = new SaClientModel()
.setClientId("1001") // client id
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥
.addAllowRedirectUris("*") // 所有允许授权的 url
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") // 所有签约的权限
.setSubjectId("1000001") // 主体 id (可选)
.addAllowGrantTypes( // 所有允许的授权模式
GrantType.authorization_code, // 授权码式
GrantType.implicit, // 隐藏式
GrantType.refresh_token, // 刷新令牌
GrantType.password, // 密码式
GrantType.client_credentials, // 客户端模式
"phone_code" // 自定义授权模式 手机号验证码登录
);
list.add(client1);
// 模拟应用2
SaClientModel client2 = new SaClientModel()
.setClientId("1002")
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
.addAllowRedirectUris("*")
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc")
.setSubjectId("1000001") // 主体 id (可选)
.addAllowGrantTypes(
GrantType.authorization_code,
GrantType.implicit,
GrantType.refresh_token,
GrantType.password,
GrantType.client_credentials
);
list.add(client2);
// 模拟应用3
SaClientModel client3 = new SaClientModel()
.setClientId("1003")
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
.addAllowRedirectUris("*")
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc")
.addAllowGrantTypes(
GrantType.authorization_code,
GrantType.implicit,
GrantType.refresh_token,
GrantType.password,
GrantType.client_credentials
);
list.add(client3);
}
/**
* 根据应用 id 查找对应的应用,找不到则返回 null
* @param clientId 应用 id
* @return 应用对象
*/
public SaClientModel getClientModel(String clientId) {
return list.stream()
.filter(e -> e.getClientId().equals(clientId))
.findFirst()
.orElse(null);
}
}
@@ -1,8 +1,9 @@
package com.pj.oauth2;
import cn.dev33.satoken.oauth2.consts.GrantType;
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;
import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;
import com.pj.mock.SaClientMockDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -12,34 +13,21 @@ import org.springframework.stereotype.Component;
*/
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
@Autowired
SaClientMockDao saClientMockDao;
// 根据 clientId 获取 Client 信息
@Override
public SaClientModel getClientModel(String clientId) {
// 此为模拟数据,真实环境需要从数据库查询
if("1001".equals(clientId)) {
return new SaClientModel()
.setClientId("1001") // client id
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥
.addAllowRedirectUris("*") // 所有允许授权的 url
.addContractScopes("openid", "userid", "userinfo", "oidc") // 所有签约的权限
.addAllowGrantTypes( // 所有允许的授权模式
GrantType.authorization_code, // 授权码式
GrantType.implicit, // 隐式式
GrantType.refresh_token, // 刷新令牌
GrantType.password, // 密码式
GrantType.client_credentials, // 客户端模式
"phone_code" // 自定义授权模式 手机号验证码登录
)
;
}
return null;
// 此为模拟数据,真实环境需要从数据库查询
return saClientMockDao.getClientModel(clientId);
}
// 根据 clientId 和 loginId 获取 openid
@Override
public String getOpenid(String clientId, Object loginId) {
// 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询
// 此处使用框架默认算法生成 openid,真实项目建议改为从数据库查询
return SaOAuth2DataLoader.super.getOpenid(clientId, loginId);
}
@@ -0,0 +1,51 @@
package com.pj.oauth2;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.template.SaOAuth2Util;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Sa-Token OAuth2 Resources 端 Controller
*
* <p> Resources 端:OAuth2 资源端,允许 Client 端根据 Access-Token 置换相关资源 </p>
*
* <p> 在 OAuth2 中,认证端和资源端:
* 1、可以在一个 Controller 中,也可以在不同的 Controller 中
* 2、可以在同一个项目中,也可以在不同的项目中(在不同项目中时需要两端连同一个 Redis )
* </p>
*
* @author click33
* @since 2024/12/6
*/
@RestController
public class SaOAuth2ResourcesController {
// 示例:获取 userinfo 信息:昵称、头像、性别等等
@RequestMapping("/oauth2/userinfo")
public SaResult userinfo() {
// 获取 Access-Token 对应的账号id
String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
// 校验 Access-Token 是否具有权限: userinfo
SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo");
// 模拟账号信息 (真实环境需要查询数据库获取信息)
Map<String, Object> map = new LinkedHashMap<>();
// map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client
map.put("nickname", "林小林");
map.put("avatar", "http://xxx.com/1.jpg");
map.put("age", "18");
map.put("sex", "");
map.put("address", "山东省 青岛市 城阳区");
return SaResult.ok().setMap(map);
}
}
@@ -1,10 +1,8 @@
package com.pj.oauth2;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
import cn.dev33.satoken.oauth2.template.SaOAuth2Util;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.beans.factory.annotation.Autowired;
@@ -13,11 +11,10 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Sa-Token-OAuth2 Server端 Controller
* Sa-Token-OAuth2 Server 认证端 Controller
*
* @author click33
*/
@@ -43,7 +40,7 @@ public class SaOAuth2ServerController {
oauth2Server.doLoginHandle = (name, pwd) -> {
if("sa".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok();
return SaResult.ok().set("satoken", StpUtil.getTokenValue());
}
return SaResult.error("账号名或密码错误");
};
@@ -58,29 +55,4 @@ public class SaOAuth2ServerController {
}
// ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------
// 获取 userinfo 信息:昵称、头像、性别等等
@RequestMapping("/oauth2/userinfo")
public SaResult userinfo() {
// 获取 Access-Token 对应的账号id
String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
// 校验 Access-Token 是否具有权限: userinfo
SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo");
// 模拟账号信息 (真实环境需要查询数据库获取信息)
Map<String, Object> map = new LinkedHashMap<>();
// map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client
map.put("nickname", "林小林");
map.put("avatar", "http://xxx.com/1.jpg");
map.put("age", "18");
map.put("sex", "");
map.put("address", "山东省 青岛市 城阳区");
return SaResult.ok().setMap(map);
}
}
@@ -51,7 +51,7 @@
// ra.scopes = scopes;
//
// // 5、生成 Access-Token
// AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true);
// AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = "phone_code");
// return at;
// }
//}
@@ -0,0 +1,89 @@
package com.pj.oauth2.h5;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;
import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;
import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;
import cn.dev33.satoken.oauth2.data.model.CodeModel;
import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;
import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口)
*/
@RestController
public class SaOAuth2ServerH5Controller {
/**
* 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx
*
* <p> 情况1:客户端未登录,返回 code=401,提示用户登录 <p/>
* <p> 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认 <p/>
* <p> 情况3:已登录且请求的 scope 已确认授权,返回 code=200redirect_uri=最终重定向 url 地址(携带code码参数) <p/>
*
* @return /
*/
@PostMapping("/oauth2/getRedirectUri")
public Object getRedirectUri() {
// 获取变量
SaRequest req = SaHolder.getRequest();
SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();
SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();
String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type);
// 1、先判断是否开启了指定的授权模式
SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg);
// 2、如果尚未登录, 则先去登录
long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L);
if(loginId == 0L) {
return SaResult.get(401, "need login", null);
}
// 3、构建请求 Model
RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);
// 4、校验:重定向域名是否合法
oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);
// 5、校验:此次申请的Scope,该Client是否已经签约
oauth2Template.checkContractScope(ra.clientId, ra.scopes);
// 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面
boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
if(isNeedCarefulConfirm) {
// code=411,需要用户手动确认授权
return SaResult.get(411, "need confirm", null);
}
// 7、判断授权类型,重定向到不同地址
// 如果是 授权码式,则:开始重定向授权,下放code
if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) {
CodeModel codeModel = dataGenerate.generateCode(ra);
String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
return SaResult.ok().set("redirect_uri", redirectUri);
}
// 如果是 隐藏式,则:开始重定向授权,下放 token
if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) {
AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);
String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);
return SaResult.ok().set("redirect_uri", redirectUri);
}
// 默认返回
throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);
}
}
@@ -1,6 +1,14 @@
package com.pj.satoken;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -21,7 +29,50 @@ public class SaTokenConfigure implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器打开注解鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数: 每次请求执行
.setAuth(obj -> {
SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
// ...
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
SaHolder.getResponse()
// ---------- 设置跨域响应头 ----------
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
})
;
}
}
@@ -24,7 +24,7 @@
</div>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.0/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">
@@ -17,10 +17,10 @@
账号:<input name="name" /> <br>
密码:<input name="pwd" type="password" /> <br>
<button onclick="doLogin()">登录</button>
<span style="color: #666;">(测试账号: sa 123456</span>
<span style="color: #666;">(测试账号: sa / 123456</span>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.0/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
</parent>
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -10,13 +10,13 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.7.0</version>
<version>3.0.1</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
+2 -2
View File
@@ -10,7 +10,7 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.9.1</version>
<version>3.0.4</version>
<relativePath/>
</parent>
@@ -19,7 +19,7 @@
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.traget>17</maven.compiler.traget>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+1 -1
View File
@@ -27,7 +27,7 @@
<!--<spring.version>4.2.5.RELEASE</spring.version>-->
<spring.version>5.3.7</spring.version>
<jackson.version>2.16.1</jackson.version>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -10,14 +10,13 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.7.0</version>
<version>3.0.1</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<solon.version>2.7.0</solon.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -10,13 +10,13 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.7.0</version>
<version>3.0.1</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -10,13 +10,13 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.7.0</version>
<version>3.0.1</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -10,13 +10,13 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.7.0</version>
<version>3.0.1</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -3,7 +3,6 @@
<head>
<title>Sa-SSO-Server 认证中心-登录</title>
<meta charset="utf-8">
<base th:href="@{/}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./login.css">
</head>
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -1,54 +0,0 @@
package com.pj.h5;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 跨域过滤器
* @author click33
*/
@Component
@Order(-200)
public class CorsFilter implements Filter {
static final String OPTIONS = "OPTIONS";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 允许指定域访问跨域资源
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许所有请求方式
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
response.setHeader("Access-Control-Max-Age", "3600");
// 允许的header参数
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
// 如果是预检请求,直接返回
if (OPTIONS.equals(request.getMethod())) {
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
response.getWriter().print("");
return;
}
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
@@ -1,14 +1,12 @@
package com.pj.h5;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.sso.util.SaSsoConsts;
import cn.dev33.satoken.sso.template.SaSsoUtil;
import cn.dev33.satoken.sso.util.SaSsoConsts;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
@@ -41,11 +39,4 @@ public class H5Controller {
}
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
@@ -0,0 +1,64 @@
package com.pj.h5;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*
* @author click33
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数: 每次请求执行
.setAuth(obj -> {
// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
// ...
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
SaHolder.getResponse()
// ---------- 设置跨域响应头 ----------
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
})
;
}
}
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -1,61 +0,0 @@
package com.pj.h5;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 跨域过滤器
* @author click33
*/
@Component
@Order(-200)
public class CorsFilter implements Filter {
static final String OPTIONS = "OPTIONS";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 允许指定域访问跨域资源
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许所有请求方式
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
response.setHeader("Access-Control-Max-Age", "3600");
// 允许的header参数
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
// 如果是预检请求,直接返回
if (OPTIONS.equals(request.getMethod())) {
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
response.getWriter().print("");
return;
}
// System.out.println("*********************************过滤器被使用**************************");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
@@ -0,0 +1,64 @@
package com.pj.h5;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
*
* @author click33
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数: 每次请求执行
.setAuth(obj -> {
// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
// ...
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
SaHolder.getResponse()
// ---------- 设置跨域响应头 ----------
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
})
;
}
}
@@ -8,7 +8,7 @@ sa-token:
sso-client:
# SSO-Server 端主机地址
server-url: http://sa-sso-server.com:9000
# 前后端分离时打开这个
# 在 sso-server 端前后端分离时打开这个(上面的不要注释,auth-url 配置项和 server-url 要同时存在)
# auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
sign:
# API 接口调用秘钥
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+1 -1
View File
@@ -18,7 +18,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
<java.run.main.class>com.pj.SaTokenApplication</java.run.main.class>
</properties>
@@ -1,12 +1,11 @@
package com.pj.satoken;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.stp.StpInterface;
/**
* 自定义权限验证接口扩展
*/
@@ -0,0 +1,27 @@
package com.pj.test;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试专用Controller
* @author click33
*
*/
@RestController
public class Test2Controller {
// 测试登录 ---- http://localhost:8081/test
@RequestMapping("/test")
public SaResult test2() {
StpUtil.login(30003);
System.out.println(StpUtil.getSession().getTimeout());
System.out.println(StpUtil.getStpLogic().getTokenSession(false));
return SaResult.ok();
}
}
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -9,9 +9,9 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token-SSO Server端 Controller
* @author click33
* 测试 Controller
*
* @author click33
*/
@RestController
public class TestController {
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+1 -1
View File
@@ -16,7 +16,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
@@ -17,7 +17,7 @@
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
<sa-token.version>1.40.0</sa-token.version>
</properties>
<dependencies>
+12 -4
View File
@@ -12,7 +12,7 @@
<description>Sa-Token Dependencies</description>
<properties>
<revision>1.39.0</revision>
<revision>1.40.0</revision>
<!-- 统一定义依赖版本号 -->
<springboot.version>2.5.15</springboot.version>
@@ -23,7 +23,8 @@
<servlet-api.version>3.1.0</servlet-api.version>
<jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<solon.version>2.7.0</solon.version>
<freemarker.version>2.3.34</freemarker.version>
<solon.version>3.0.4</solon.version>
<noear-redisx.version>1.6.2</noear-redisx.version>
<noear-snack3.version>3.2.88</noear-snack3.version>
<jfinal.version>4.9.17</jfinal.version>
@@ -200,8 +201,15 @@
<artifactId>thymeleaf</artifactId>
<version>${thymeleaf.version}</version>
</dependency>
<!-- 视图引擎 -->
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<!-- spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
+3 -2
View File
@@ -1,15 +1,16 @@
<p align="center">
<img alt="logo" src="https://sa-token.cc/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.39.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.40.0</h1>
<h5 align="center">一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!</h5>
<p align="center" class="badge-box">
<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
<a href="https://gitcode.com/dromara/sa-token/overview"><img src="https://gitcode.com/dromara/Sa-Token/star/badge.svg"></a>
<a href="https://github.com/dromara/sa-token/stargazers"><img src="https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/network/members"><img src="https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/watchers"><img src="https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub"></a>
<a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a>
<!-- <a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a> -->
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img src="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
</p>
+8
View File
@@ -45,6 +45,7 @@
- [定制化登录页面](/sso/sso-custom-login)
- [自定义API路由](/sso/sso-custom-api)
- [前后端分离下的整合方案](/sso/sso-h5)
- [NoSdk 模式与非 java 项目](/sso/sso-nosdk)
- [平台中心跳转模式](/sso/sso-home-jump)
- [不同 Client 不同秘钥](/sso/sso-diff-key)
- [用户数据同步 / 迁移](/sso/user-data-sync)
@@ -62,6 +63,8 @@
- [自定义 grant_type](/oauth2/oauth2-custom-grant_type)
- [定制化登录页面与授权页面](/oauth2/oauth2-custom-login)
- [自定义 API 路由 ](/oauth2/oauth2-custom-api)
- [OAuth2-Server端前后台分离](/oauth2/oauth2-h5)
- [OpenId 与 UnionId](/oauth2/oauth2-openid)
- [开启 OIDC 协议](/oauth2/oauth2-oidc)
- [使用注解校验 Access-Token](/oauth2/oauth2-at-check)
- [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking)
@@ -84,6 +87,8 @@
- [Alone独立Redis插件](/plugin/alone-redis)
- [持久层扩展](/plugin/dao-extend)
- [和 Thymeleaf 集成](/plugin/thymeleaf-extend)
- [和 Freemarker 集成](/plugin/freemarker-extend)
- [注解鉴权 SpEL 表达式](/plugin/spel-at)
- [和 jwt 集成](/plugin/jwt-extend)
- [和 Dubbo 集成](/plugin/dubbo-extend)
- [和 gRPC 集成](/plugin/grpc-extend)
@@ -127,11 +132,14 @@
- [解决反向代理 uri 丢失的问题](/fun/curr-domain)
- [解决跨域问题](/fun/cors-filter)
- [技术选型:SSO 与 OAuth2 对比](/fun/sso-vs-oauth2)
- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)
- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)
<!-- - [框架源码所有技术栈](/fun/tech-stack) -->
- [从 Shiro、SpringSecurity、JWT 迁移](/fun/auth-framework-function-test)
- [issue 提问模板](/fun/issue-template)
- [为Sa-Token贡献代码](/fun/git-pr)
- [Sa-Token开源大事记](/fun/timeline)
- [团队成员](/fun/team)
- [Sa-Token框架掌握度--在线考试](/fun/sa-token-test)
+1 -1
View File
@@ -129,7 +129,7 @@ StpUtil.hasPermission(permission); // 判断:当前账号是否拥有指定
StpUtil.hasPermission(loginId, permission); // 判断:指定账号是否含有指定权限标识, 返回true或false
StpUtil.hasPermissionAnd(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过]
StpUtil.hasPermissionOr(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可]
StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotRPermissionException
StpUtil.checkPermissionAnd(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过]
StpUtil.checkPermissionOr(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可]
```
+16 -3
View File
@@ -18,7 +18,7 @@
<div class="logo-box">
<img src="logo.png" title="logo" />
<h1 class="logo-text">Sa-Token</h1>
<sub>v1.39.0</sub>
<sub>v1.40.0</sub>
</div>
</a>
</div>
@@ -28,6 +28,7 @@
</div>
<select class="select-version p-none" onchange="location.href=this.value">
<option value="doc.html">最新版</option>
<option value="v/v1.39.0/doc.html">v1.39.0</option>
<option value="v/v1.38.0/doc.html">v1.38.0</option>
<option value="v/v1.37.0/doc.html">v1.37.0</option>
<option value="v/v1.36.0/doc.html">v1.36.0</option>
@@ -109,10 +110,16 @@
</a>
<div class="zk-context">
<div>
<a href="https://www.bilibili.com/video/BV1uZUpYVEst/" target="_blank">fox说技术(7集)</a>
<a href="https://www.bilibili.com/video/BV1eFtRezERp?p=87" target="_blank">架构驿站(11集)</a>
<a href="https://www.bilibili.com/video/BV1Zt421u7gk/" target="_blank">王清江唷(99集)</a>
<a href="https://www.bilibili.com/video/BV1kG411o7Ms/" target="_blank">筑梦信仰-joy20集)</a>
<a href="https://www.bilibili.com/video/BV11u4y197JL/" target="_blank">达达-Java26集)</a>
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
<div class="zk-fengexian"></div>
<a href="javascript: layer.alert('如您有 Sa-Token 相关课程录制,请联系官网文档右侧 < sa-token 小助手 > 进行提交');">
[ + 课程提交 ]
</a>
</div>
</div>
</div>
@@ -183,6 +190,12 @@
<!-- help 按钮 -->
<div class="help-btn">技术求助</div>
<!-- ew-wa -->
<div class="ew-wa">
<p>如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持! <!-- 🤗 --> </p>
<p>加油,工程师!</p>
</div>
</div>
</div>
</div>
@@ -220,7 +233,7 @@
<script src="./static/is-star-plugin.js?v=7"></script>
<script src="./static/is-fill-in-wj-plugin.js?v=7"></script>
<script>
var saTokenTopVersion = '1.39.0'; // Sa-Token最新版本
var saTokenTopVersion = '1.40.0'; // Sa-Token最新版本
var name = '<img style="width: 60px; height: 60px; vertical-align: middle;" src="logo.png" alt="logo" /> ';
name += '<b style="font-size: 28px; vertical-align: middle;">Sa-Token</b> <sub>v' + saTokenTopVersion + '</sub>';
window.$docsify = {
+3 -3
View File
@@ -37,14 +37,14 @@ public @interface CheckAccount {
#### 1.2、第二步,创建注解处理器
实现 `SaAnnotationAbstractHandler` 接口,指定泛型为刚才自定义的注解
实现 `SaAnnotationHandlerInterface` 接口,指定泛型为刚才自定义的注解
``` java
/**
* 注解 CheckAccount 的处理器
*/
@Component
public class CheckAccountHandler implements SaAnnotationAbstractHandler<CheckAccount> {
public class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {
// 指定这个处理器要处理哪个注解
@Override
@@ -156,7 +156,7 @@ public @interface SaUserCheckLogin {
* 注解 SaUserCheckLogin 的处理器
*/
@Component
public class SaUserCheckLoginHandler implements SaAnnotationAbstractHandler<SaUserCheckLogin> {
public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaUserCheckLogin> {
@Override
public Class<SaUserCheckLogin> getHandlerAnnotationClass() {
+69 -19
View File
@@ -230,18 +230,37 @@ clientId + loginId 反查 code
``` js
{
"@class": "cn.dev33.satoken.oauth2.model.AccessTokenModel", // java class 信息
"accessToken": "CqRVp2HrgyklE0BXYWszskGJWAGY7xhGu9Zaco4zJECzGYagCCFWj0jOlHoU", // 资源令牌值
"refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值
"expiresTime": 1722422031510, // 资源令牌到期时间
"refreshExpiresTime": 1725006831511, // 刷新令牌到期时间
"@class": "cn.dev33.satoken.oauth2.data.model.AccessTokenModel", // java class 信息
"accessToken": "Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P", // 资源令牌值
"refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值
"expiresTime": 1738280553695, // 资源令牌到期时间
"refreshExpiresTime": 1740865353760, // 刷新令牌到期时间
"clientId": "1001", // 对应的应用id
"loginId": "10001", // 对应的loginId
"openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid
"scope": "", // 所具有的权限列表,多个用逗号隔开
"expiresIn": 7199, // 资源令牌剩余有效时间,单位秒
"refreshExpiresIn": 2592000 // 刷新令牌剩余有效时间,单位秒
"scopes": [ // 所具有的权限列表
"java.util.ArrayList",
[
"userinfo",
"userid",
"openid",
"unionid",
"oidc"
]
],
"tokenType": "bearer", // tokenType
"grantType": "authorization_code", // 授权方式
"extraData": { // 扩展数据
"@class": "java.util.LinkedHashMap",
"userid": "10001",
"openid": "ded91dc189a437dd1bac2274be167d50",
"unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
},
"createTime": 1738273353694, // 创建时间
"expiresIn": 7199 // 资源令牌剩余有效时间,单位秒
"refreshExpiresIn": 2592000, // 刷新令牌剩余有效时间,单位秒
}
```
</details>
@@ -264,13 +283,29 @@ clientId + loginId 反查 Access-Token
``` js
{
"@class": "cn.dev33.satoken.oauth2.model.RefreshTokenModel", // java class 信息
"refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值
"expiresTime": 1725006831511, // 刷新令牌到期时间
"@class": "cn.dev33.satoken.oauth2.data.model.RefreshTokenModel", // java class 信息
"refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值
"expiresTime": 1740865353760, // 刷新令牌到期时间
"clientId": "1001", // 对应的应用id
"scope": "", // 所具有的权限列表,多个用逗号隔开
"loginId": "10001", // 对应的loginId
"openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid
"scopes": [ // 所具有的权限列表
"java.util.ArrayList",
[
"userinfo",
"userid",
"openid",
"unionid",
"oidc"
]
],
"extraData": { // 扩展数据
"@class": "java.util.LinkedHashMap",
"userid": "10001",
"openid": "ded91dc189a437dd1bac2274be167d50",
"unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
},
"createTime": 1738273353760, // 创建时间
"expiresIn": 2591999 // 刷新令牌剩余有效时间,单位秒
}
```
@@ -295,12 +330,27 @@ clientId + loginId 反查 Refresh-Token
``` js
{
"@class": "cn.dev33.satoken.oauth2.model.ClientTokenModel", // java class 信息
"clientToken": "fWQjBKxprSslmYFLbzen0oa95rOvqnqYKZW3sD8mzamNbabG8b6MPKPP5uCu", // 应用令牌值
"expiresTime": 1722425237153, // 应用令牌到期时间
"@class": "cn.dev33.satoken.oauth2.data.model.ClientTokenModel", // java class 信息
"clientToken": "lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv", // 应用令牌值
"expiresTime": 1738280930646, // 应用令牌到期时间
"clientId": "1001", // 对应的应用id
"scope": null, // 所具有的权限列表,多个用逗号隔开
"expiresIn": 7200 // 应用令牌剩余有效时间,单位秒
"scopes": [ // 所具有的权限列表
"java.util.ArrayList",
[
"userinfo",
"userid",
"openid",
"unionid",
"oidc"
]
],
"tokenType": "bearer", // tokenType
"grantType": "client_credentials", // 授权类型
"extraData": { // 扩展数据
"@class": "java.util.LinkedHashMap"
},
"createTime": 1738273730646, // 创建时间
"expiresIn": 7199 // 应用令牌剩余有效时间,单位秒
}
```
+89
View File
@@ -0,0 +1,89 @@
# 团队成员
### 开发
负责:代码开发、社区维护、issue 处理、pr 审核等。
<table class="team-table">
<tr>
<th>头像</th>
<th>昵称</th>
<th>个人主页</th>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-xiaofengzheng.jpg" /></td>
<td>小风筝(作者)</td>
<td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-AppleOfGray.png" /></td>
<td>AppleOfGray</td>
<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-ly-chn.png" /></td>
<td>ly-chn</td>
<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
</tr>
</table>
### 提案讨论组成员
负责:提案新增、讨论、投票。
<table class="team-table">
<tr>
<th>头像</th>
<th>昵称</th>
<th>个人主页</th>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-xiaofengzheng.jpg" /></td>
<td>刘潇</td>
<td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-AppleOfGray.png" /></td>
<td>AppleOfGray</td>
<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-ly-chn.png" /></td>
<td>ly-chn</td>
<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-moli.jpg" /></td>
<td>茉莉</td>
<td><a href="https://gitee.com/kidoldman" target="_blank">https://gitee.com/kidoldman</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-yaoshui.jpg" /></td>
<td>药水</td>
<td><a href="https://gitee.com/java_pioneer" target="_blank">https://gitee.com/java_pioneer</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-daimouren.png" /></td>
<td>呆某人</td>
<td><a href="https://gitee.com/zhubj0510" target="_blank">https://gitee.com/zhubj0510</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-chunkunqiufa.jpg" /></td>
<td>春困夏倦秋乏</td>
<td><a href="https://gitee.com/uncarbon97" target="_blank">https://gitee.com/uncarbon97</a></td>
</tr>
<tr>
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-danmo.jpg" /></td>
<td>淡墨</td>
<td><a href="https://gitee.com/jinan-jimeng-network_0" target="_blank">https://gitee.com/jinan-jimeng-network_0</a></td>
</tr>
</table>
+6
View File
@@ -27,5 +27,11 @@
- **2022-05-20** 成为 [可信开源社区共同体] 预备成员。
- **2022-08-01** 加入 [中国开源社区 landscape]。
- **2022-08-18** GitHub 第 10000 个 star 里程碑!
- **2023-01-09** 荣获 OSC 2022 年度最热开源项目社区。
- **2023-11-21** 被评为“开放原子基金会2023快速成长开源项目”。
- **2024-04-25** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。
- **2024-08-19** 成为 GitCode G-Star 开源摘星计划毕业项目。
- **2024-11-22** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。
+153 -41
View File
@@ -63,10 +63,16 @@
</a>
<div class="zk-context">
<div>
<a href="https://www.bilibili.com/video/BV1uZUpYVEst/" target="_blank">fox说技术(7集)</a>
<a href="https://www.bilibili.com/video/BV1eFtRezERp?p=87" target="_blank">架构驿站(11集)</a>
<a href="https://www.bilibili.com/video/BV1Zt421u7gk/" target="_blank">王清江唷(99集)</a>
<a href="https://www.bilibili.com/video/BV1kG411o7Ms/" target="_blank">筑梦信仰-joy20集)</a>
<a href="https://www.bilibili.com/video/BV11u4y197JL/" target="_blank">达达-Java26集)</a>
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
<div class="zk-fengexian"></div>
<a href="javascript: layer.alert('如您有 Sa-Token 相关课程录制,请联系官网文档右侧 < sa-token 小助手 > 进行提交');">
[ + 课程提交 ]
</a>
</div>
</div>
</div>
@@ -116,16 +122,18 @@
<div class="main-box">
<div class="content-box">
<!-- <div class="fenge"></div> -->
<h1>Sa-Token<small>v1.39.0</small></h1>
<h1>Sa-Token<small>v1.40.0</small></h1>
<div class="sub-title">
<span class="sub-title-nr">一个轻量级 java 权限认证框架,让鉴权变得简单、优雅!</span>
<div class="gb-cursor">&nbsp;</div>
</div>
<div class="btn-box">
<a class="abtn" href="https://gitee.com/dromara/sa-token" target="_blank">Gitee</a>
<a class="abtn" href="https://github.com/dromara/sa-token" target="_blank">GitHub</a>
<!-- <a class="abtn" href="https://gitee.com/dromara/sa-token" target="_blank">Gitee</a>
<a class="abtn" href="https://github.com/dromara/sa-token" target="_blank">GitHub</a> -->
<a class="abtn" href="doc.html#/more/demand-commit" target="_self">需求提交</a>
<a class="abtn" href="https://gitee.com/sa-tokens/awesome-sa-token" target="_blank">开源案例</a>
<a class="abtn" href="doc.html#/more/join-group" target="_self">加入讨论群</a>
<a class="abtn doc-btn" href="doc.html" target="_self">现在出发</a>
<a class="abtn doc-btn" href="doc.html" target="_self">在线文档</a>
<!-- <a href="https://gitee.com/dromara/sa-token" target="_blank">集成案例</a> -->
</div>
<h4 align="center" class="badge-box">
@@ -144,6 +152,44 @@
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img class="lazy"
data-original="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
</h4>
<div class="qt-pt-box">
<a href="https://gitee.com/dromara/sa-token" target="_blank">
<img src="https://oss.dev33.cn/sa-token/img/gitee.png" alt="">
</a>
<a href="https://gitcode.com/dromara/sa-token" target="_blank">
<img src="https://oss.dev33.cn/sa-token/img/gitcode.png" alt="">
</a>
<a href="https://github.com/dromara/sa-token" target="_blank">
<img src="https://oss.dev33.cn/sa-token/img/github.png" alt="">
</a>
<span class="dmt-link">
<img class="dmt-img" src="https://oss.dev33.cn/sa-token/img/zong-4.png" alt="">
<span class="dmt-tips">B站、抖音、视频号 ...</span>
<div class="dmt-detail">
<h4>关注我们 → 分享“权限认证架构设计”干货视频</h4>
<div class="dmt-item-box">
<div class="dmt-item dmt-item-bilibili">
<a href="https://space.bilibili.com/3546758575557094" target="_blank">
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/bilibili-qr-fang.png" alt="">
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/bilibili-light.png" alt="">
</a>
</div>
<div class="dmt-item dmt-item-douyin">
<a href="https://www.douyin.com/user/MS4wLjABAAAArVqj2lGRurfj-9eO0T12q6_vrbIK-Om9bi3eo4OwB2g" target="_blank">
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/douyin-qr-fang.png" alt="">
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/douyin-light.png" alt="">
</a>
</div>
<div class="dmt-item dmt-item-wxsph">
<a href="javascript: layer.msg('微信视频号暂未提供PC网站,请在手机微信扫码订阅');">
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/wxsph-qr-fang.png" alt="">
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/wxsph-light.png" alt="">
</a>
</div>
</div>
</div>
</a>
</div>
</div>
</div>
@@ -209,19 +255,19 @@
<img src="https://oss.dev33.cn/sa-token/awards/gpv.jpg?x-oss-process=style/st" /> <br>
<p>GVP - Gitee 最有价值开源项目</p>
</div>
<div class="swiper-slide">
<img src="https://oss.dev33.cn/sa-token/awards/g-star.jpg?x-oss-process=style/st" /> <br>
<p>GitCode G-Star 优质开源项目</p>
</div>
<div class="swiper-slide">
<img src="https://oss.dev33.cn/sa-token/awards/osc-2021.jpg?x-oss-process=style/st"/> <br>
<p>OSCHINA 2021 人气指数 TOP 30 开源项目</p>
</div>
<div class="swiper-slide swiper-slide-tx1">
<img src="https://oss.dev33.cn/sa-token/awards/osc-2022.jpg?x-oss-process=style/st" /> <br>
<img src="https://oss.dev33.cn/sa-token/awards/osc-2022--2.jpg?x-oss-process=style/st" /> <br>
<p>OSCHINA 2022 年度最火热中国开源项目社区</p>
</div>
<div class="swiper-slide">
<img src="https://oss.dev33.cn/sa-token/awards/kexin.jpg?x-oss-process=style/st" /> <br>
<p>可信开源社区共同体预备成员</p>
</div>
<div class="swiper-slide">
<div class="swiper-slide swiper-slide-tx1">
<img src="https://oss.dev33.cn/sa-token/awards/kaifangyuanzi2.jpg?x-oss-process=style/st" /> <br>
<p>开放原子基金会2023快速成长开源项目</p>
</div>
@@ -229,6 +275,14 @@
<img src="https://oss.dev33.cn/sa-token/awards/dromara.jpg?x-oss-process=style/st" /> <br>
<p>Dromara 组织顶尖项目(之一)</p>
</div>
<div class="swiper-slide">
<img src="https://oss.dev33.cn/sa-token/awards/kexin.jpg?x-oss-process=style/st" /> <br>
<p>可信开源社区共同体预备成员</p>
</div>
<div class="swiper-slide">
<img src="https://oss.dev33.cn/sa-token/awards/dromara-2024-tzds.jpg?x-oss-process=style/st" /> <br>
<p>所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。</p>
</div>
<div class="swiper-slide" style="width: 750px;">
<img src="https://oss.dev33.cn/sa-token/awards/gitee-top-1.png" /> <br>
<p>Gitee 项目推荐榜 top 1</p>
@@ -264,7 +318,7 @@
<th>赞助人</th>
<th>赞助金额</th>
<th>留言</th>
<th>时间</th>
<th style="width: 100px;">时间</th>
</tr>
</thead>
<tbody>
@@ -301,7 +355,16 @@
<div class="s-fenge"></div>
<h2 class="s-title" style="margin-top: 80px;">优秀开源集成案例</h2>
<div class="feature-box s-case-box">
<!-- mall4j 15.2k -->
<!-- Snowy 19.8K -->
<div class="s-case">
<a href="https://gitee.com/xiaonuobase/snowy" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--snowy.png">
</a>
<h3 class="s-case-title">Snowy</h3>
<span class="s-author"> 小诺开源技术 </span>
<p class="s-case-intro">国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken</p>
</div>
<!-- mall4j 15.9k -->
<div class="s-case">
<a href="https://gitee.com/gz-yami/mall4j" target="_blank" class="s-case-link">
<img class="lazy"
@@ -311,7 +374,7 @@
<span class="s-author"> Mall4j商城系统 </span>
<p class="s-case-intro">基于Spring Boot 3 JDK17的一个商城手脚架。</p>
</div>
<!-- RuoYi-Vue-Plus 8.8k -->
<!-- RuoYi-Vue-Plus 10.7k -->
<div class="s-case">
<a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank" class="s-case-link">
<img class="lazy"
@@ -321,16 +384,16 @@
<span class="s-author"> 疯狂的狮子Li </span>
<p class="s-case-intro">重写 RuoYi-Vue 所有功能,集成 Sa-Token、Mybatis-Plus、Hutool 定期同步</p>
</div>
<!-- Snowy 7.9K -->
<!-- Smart-Admin 7.6K -->
<div class="s-case">
<a href="https://gitee.com/xiaonuobase/snowy" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--snowy.png">
<a href="https://gitee.com/lab1024/smart-admin" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--smart-admin.png">
</a>
<h3 class="s-case-title">Snowy</h3>
<span class="s-author"> 小诺开源技术 </span>
<p class="s-case-intro">国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken</p>
<h3 class="s-case-title">Smart-Admin</h3>
<span class="s-author"> 1024创新实验室 </span>
<p class="s-case-intro">坚持以「高质量代码」为核心,「简洁、高效、安全」的中后台解决方案!</p>
</div>
<!-- SpringBoot_v2 5.8k -->
<!-- SpringBoot_v2 6k -->
<div class="s-case">
<a href="https://gitee.com/bdj/SpringBoot_v2" target="_blank" class="s-case-link">
<img class="lazy"
@@ -340,7 +403,16 @@
<span class="s-author">开源oschina</span>
<p class="s-case-intro">努力打造 springboot 框架的极致细腻的脚手架,原生纯净。</p>
</div>
<!-- RuoYi-Cloud-Plus 3.9K -->
<!-- Lamp-Cloud 5.4K -->
<div class="s-case">
<a href="https://gitee.com/dromara/lamp-cloud" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--lamp-cloud.png">
</a>
<h3 class="s-case-title">灯灯</h3>
<span class="s-author"> 最后 </span>
<p class="s-case-intro">专注于多租户解决方案的微服务中后台快速开发平台。</p>
</div>
<!-- RuoYi-Cloud-Plus 4.9K -->
<div class="s-case">
<a href="https://gitee.com/dromara/RuoYi-Cloud-Plus" target="_blank" class="s-case-link">
<img class="lazy"
@@ -350,8 +422,16 @@
<span class="s-author"> 疯狂的狮子Li </span>
<p class="s-case-intro">重写 RuoYi-Cloud 所有功能 整合 SpringCloudAlibaba、Dubbo3.0、Sa-Token</p>
</div>
<!-- 拾壹博客 1.4K -->
<!-- Orange-Admin 3.3K -->
<div class="s-case">
<a href="https://gitee.com/orangeform/orange-admin" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--orange-admin.png">
</a>
<h3 class="s-case-title">橙单</h3>
<span class="s-author"> orange-form </span>
<p class="s-case-intro">橙单中台化低代码生成器。多应用、多租户、多渠道、工作流、在线表单等。</p>
</div>
<!-- 拾壹博客 1.7K -->
<div class="s-case">
<a href="https://gitee.com/quequnlong/shiyi-blog" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--shiyi-blog.png">
@@ -360,8 +440,8 @@
<span class="s-author"> bule </span>
<p class="s-case-intro">一款 Vue + SpringBoot 前后端分离的博客系统</p>
</div>
<!-- EasyAdmin 1.2k -->
<div class="s-case">
<!-- EasyAdmin 1.4k -->
<!-- <div class="s-case">
<a href="https://gitee.com/lakernote/easy-admin" target="_blank" class="s-case-link">
<img class="lazy"
data-original="https://oss.dev33.cn/sa-token/case/case--easy-admin.png">
@@ -369,25 +449,28 @@
<h3 class="s-case-title">EasyAdmin</h3>
<span class="s-author"> laker </span>
<p class="s-case-intro">轻量级的后台管理系统脚手架,内置代码生成器、权限管理、工作流引擎等</p>
</div>
<!-- iot-iita 1.2K -->
<div class="s-case">
</div> -->
<!-- iot-iita 1.6K -->
<!-- <div class="s-case">
<a href="https://gitee.com/open-iita/iotkit-parent" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--iot-iita.png">
</a>
<h3 class="s-case-title">iot-iita</h3>
<span class="s-author"> 铱塔智联开源 </span>
<p class="s-case-intro">一个轻量级低门槛的物联网平台,包含了多协议设备接入</p>
</div>
</div> -->
<!-- Sa-Plus 1.1K -->
<div class="s-case">
<!-- <div class="s-case">
<a href="https://gitee.com/click33/sa-plus" target="_blank" class="s-case-link">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--sa-plus.png">
</a>
<h3 class="s-case-title">Sa-Plus</h3>
<span class="s-author"> 孔明 </span>
<p class="s-case-intro">一个基于 SpringBoot 的快速开发框架,内置代码生成器</p>
</div>
</div> -->
</div>
<div class="re-text">
<span>
@@ -423,9 +506,9 @@
<!-- <a href="https://www.suancheng.co/" target="_blank" title="山东酸橙网络科技有限公司">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/suanchengdudu.png">
</a> -->
<a href="https://chykj.com/" target="_blank" title="山东察远信息科技有限公司">
<!-- <a href="https://chykj.com/" target="_blank" title="山东察远信息科技有限公司">
<img class="lazy" data-original="https://chykj.com/upload/1/cms/content/cylogoc.jpg">
</a>
</a> -->
<a href="https://ms.airsr.com/" target="_blank" title="北京天衢航空服务有限公司">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/tianquhangkong.png">
</a>
@@ -547,6 +630,15 @@
<a href="https://www.jcodeyun.com/" target="_blank" title="协卓云动(深圳)数字发展有限公司">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/xiezhuoyundong.png">
</a>
<a href="https://www.fakamiao.com/" target="_blank" title="秦皇岛桃猫信息科技有限责任公司">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/taomaoxinxi.png" style="max-height: 50%;">
</a>
<a href="https://jiagouyizhan.com/" target="_blank" title="可持续架构(菏泽)信息科技有限公司">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/jiagouyizhan.png">
</a>
<a href="https://www.symtc.com/" target="_blank" title="沈阳地铁">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/shenyangditie.png">
</a>
</div>
<div style="height: 10px; clear: both;"></div>
<p>
@@ -686,10 +778,10 @@
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/neutrino-proxy.svg"
msg="一款基于 Netty 的、开源的内网穿透神器。">
</a>
<a href="https://chatgpt.cn.obiscr.com/" target="_blank">
<!-- <a href="https://chatgpt.cn.obiscr.com/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/chatgpt.png"
msg="一个支持在 JetBrains 系列 IDE 上运行的 ChatGPT 的插件。">
</a>
</a> -->
<a href="https://gitee.com/dromara/zyplayer-doc" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/zyplayer-doc.png"
msg="zyplayer-doc是一款适合团队和个人使用的WIKI文档管理工具,同时还包含数据库文档、Api接口文档。">
@@ -774,6 +866,10 @@
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/easyAI.png"
msg="Java 傻瓜式 AI 框架。">
</a>
<a href="https://gitee.com/dromara/tianai-captcha" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/tianai-captcha.png"
msg="可能是java界最好的开源行为验证码 captcha、captcha、captcha、captcha、tianai-captcha [滑块验证码、点选验证码、行为验证码、旋转验证码, 滑动验证码]。">
</a>
<a href="https://gitee.com/dromara/mybatis-plus-ext" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/mybatis-plus-ext.png"
msg="mybatis-plus 框架的增强拓展包。">
@@ -803,7 +899,7 @@
msg="DyJava是一款功能强大的抖音Java开发工具包">
</a>
<a href="https://gitee.com/dromara/MilvusPlus" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/MilvusPlus.jpg"
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/MilvusPlus-logo.png"
msg="MilvusPlus(简称 MP)是一个 Milvus 的操作工具,旨在简化与 Milvus 向量数据库的交互,为开发者提供类似 MyBatis-Plus 注解和方法调用风格的直观 API,提高效率而生。">
</a>
<a href="http://www.easy-query.com/easy-query-doc/" target="_blank">
@@ -811,17 +907,33 @@
msg="java下唯一一款同时支持强类型对象关系查询和强类型SQL语法查询的ORM,拥有对象模型筛选、隐式子查询、隐式join、显式子查询、显式join,支持Java/Kotlin">
</a>
<a href="https://gitee.com/dromara/orion-visor" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/orion-visor.png"
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/horizontal.png"
msg="一款高颜值、现代化的智能运维&轻量堡垒机平台。">
</a>
<a href="https://www.ujcms.com/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/ujcms.png"
msg="Java开源网站内容管理系统(java cms)。使用SpringBoot、MyBatis、Vue3、ElementPlus、Vite、TypeScript等技术开发。">
</a>
<a href="https://dromara.org/zh/projects/" target="_blank">
<a href="https://gitee.com/dromara/skyeye" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/skyeye-logo.png"
msg="智能制造一体化,采用Springboot + winUI的低代码平台开发模式。包含30多个应用模块、50多种电子流程">
</a>
<a href="https://domain-admin.cn/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/domain-admin.png"
msg="SSL证书监测平台,申请证书,自动续签,到期提醒。">
</a>
<a href="https://gitee.com/dromara/carbon" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/carbon.png"
msg="轻量级、语义化、对开发者友好的 golang 时间处理库">
</a>
<a href="https://gitee.com/dromara/mica-mqtt" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/mica-mqtt.png"
msg="java mqtt 基于 java aio 实现,开源、简单、易用、低延迟、高性能百万级 java mqtt client 组件和 java mqtt broker 服务。">
</a>
<!-- <a href="https://dromara.org/zh/projects/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/dromara.png"
msg="让每一位开源爱好者,体会到开源的快乐。">
</a>
</a> -->
</div>
<div style="height: 10px; clear: both;"></div>
<p>
@@ -843,11 +955,11 @@
<br>
<h2 class="s-title">友情链接</h2>
<div class="com-box com-box-you">
<a href="https://okhttps.ejlchina.com/" target="_blank">
<a href="https://ok.zhxu.cn/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/okhttps.png"
msg="如艺术一般优雅,像 1、2、3 一样简单,前后端通用,轻量却强大的 HTTP 客户端(同时支持 WebSocket 以及 Stomp 协议)">
</a>
<a href="https://searcher.ejlchina.com/" target="_blank">
<a href="https://bs.zhxu.cn/" target="_blank">
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/bean-searcher.png"
msg="轻量级关系数据库条件检索引擎,使一行代码实现复杂列表检索成为可能!">
</a>
+4
View File
@@ -6,6 +6,10 @@
---
- [[ 公众号 ] sa-token之@SaIgnore注解失效的真正原因及正确姿势](https://mp.weixin.qq.com/s/c6eckHp2M4oz2x3Hea6pGg) 2025-1-15
- [[ 公众号 ] 集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因](https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg) 2024-07-08
- [[ 公众号 ] sa-token前后端分离解决跨域的正确姿势](https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg) 2024-07-06

Some files were not shown because too many files have changed in this diff Show More