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

Compare commits

...

153 Commits

Author SHA1 Message Date
shengzhang a8117a8021 v1.15更新 2021-03-23 02:24:37 +08:00
shengzhang c574e7f8d7 SaRouterUtil迁移到core核心包,优化依赖架构 2021-03-22 17:02:09 +08:00
shengzhang 36afaf74d3 token前缀校验改为强制模式,如果配置了前缀,则前端提交token时必须带有前缀 2021-03-22 00:04:05 +08:00
shengzhang 55b9c87d6a Session新增timeout操作API 2021-03-21 23:06:06 +08:00
shengzhang 50ffda7bef SaTokenManager新增stpLogicMap集合,记录所有StpLogic的初始化,方便查找 2021-03-20 16:51:18 +08:00
shengzhang a122a8b083 StpUtil添加setTokenValue方法 2021-03-18 23:51:56 +08:00
shengzhang 69ce298a8a 适配token前缀模式 2021-03-18 13:06:24 +08:00
省长 f92e64b9cf update sa-token-doc/doc/use/jur-auth.md. 2021-03-16 17:27:13 +08:00
省长 d3138cefc8 !21 update sa-token-doc/doc/use/jur-auth.md.
Merge pull request !21 from AppleOfGray/N/A
2021-03-16 17:23:41 +08:00
AppleOfGray 71f2522f08 update sa-token-doc/doc/use/jur-auth.md. 2021-03-16 16:43:05 +08:00
省长 a5e9bd6714 !20 update sa-token-doc/doc/use/session.md.
Merge pull request !20 from AppleOfGray/N/A
2021-03-16 16:16:44 +08:00
AppleOfGray 27bb436a90 update sa-token-doc/doc/use/session.md. 2021-03-16 16:13:39 +08:00
省长 d75c91f0ab !19 update sa-token-doc/doc/use/many-account.md.
Merge pull request !19 from AppleOfGray/N/A
2021-03-16 15:56:38 +08:00
AppleOfGray 8ee9f215a6 update sa-token-doc/doc/use/many-account.md. 2021-03-16 15:55:36 +08:00
shengzhang 1df216879c 文档集成Redis章节新增redis配置示例说明,感谢群友 @-) 提供的建议 2021-03-16 14:44:38 +08:00
click33 91998af186 Merge pull request #48 from zhangzi0291/patch-1
修改cookie中没有path的问题
2021-03-16 00:36:34 +08:00
zhangzi0291 f33df38f64 修改cookie中没有path的问题
addCookies时如果path没有指定,path默认为/
2021-03-16 00:33:12 +08:00
shengzhang aaf4bb8931 Session新增getString(key)方法,方便取值时类型转换 2021-03-15 17:05:07 +08:00
shengzhang 5808710bbf 修复错别字Request->Response 2021-03-14 14:39:21 +08:00
shengzhang 62787c3257 更改路由拦截示例 2021-03-14 00:04:11 +08:00
省长 6dd39505f9 增加友链 2021-03-13 17:28:03 +08:00
省长 d86f72b469 update README.md. 2021-03-13 17:25:41 +08:00
省长 7b7920123a update README.md. 2021-03-13 17:23:46 +08:00
shengzhang 8872982455 优化文档 2021-03-13 00:54:39 +08:00
shengzhang 80f07ae01e 技术栈 2021-03-13 00:47:43 +08:00
shengzhang 540730d9a4 优化文档 2021-03-12 09:51:37 +08:00
省长 03c8abb269 update README.md. 2021-03-12 09:46:11 +08:00
shengzhang f9ad71db66 v1.14.0 更新 2021-03-12 01:45:13 +08:00
shengzhang 9fe0e1f97b 优化文档 2021-03-11 01:06:27 +08:00
shengzhang 9f6717f8c0 更换二维码地址 2021-03-10 14:17:43 +08:00
shengzhang af2d04ca1f 更换二维码 2021-03-10 14:04:58 +08:00
省长 64e6b58a05 update README.md. 2021-03-10 14:01:56 +08:00
shengzhang 275b3cec6f 完成 [记住我]模式 开发文档 2021-03-10 02:37:45 +08:00
省长 e92ff9fb7d !18 update README.md.
Merge pull request !18 from ZhuBJ0510/N/A
2021-03-09 15:02:10 +08:00
ZhuBJ0510 cffd462cd8 update README.md. 2021-03-09 15:01:57 +08:00
省长 9ca2cad5e6 !17 update README.md.
Merge pull request !17 from ZhuBJ0510/N/A
2021-03-09 14:57:28 +08:00
ZhuBJ0510 a076c8f8fc update README.md. 2021-03-09 14:33:17 +08:00
省长 dbddd05a6e !16 update sa-token-doc/doc/use/route-check.md.
Merge pull request !16 from AppleOfGray/N/A
2021-03-09 10:00:17 +08:00
AppleOfGray 18760010fc update sa-token-doc/doc/use/route-check.md. 2021-03-09 09:59:39 +08:00
shengzhang 9151f9a5c8 优化文档 2021-03-09 00:29:57 +08:00
shengzhang cdaf9fa65a Merge branch 'dev' of https://gitee.com/sz6/sa-token into dev 2021-03-08 23:27:38 +08:00
shengzhang c316746f0d [密码加密]模块的文档 2021-03-08 23:27:08 +08:00
省长 0695ee6cdf update sa-token-doc/doc/use/many-account.md. 2021-03-08 09:01:42 +08:00
shengzhang 3716b874e3 新增密码加密算法工具类 2021-03-08 02:13:18 +08:00
shengzhang 255a15033e 集成Redis章节提供常见问题说明 2021-03-07 00:15:53 +08:00
省长 ad70bb0300 update sa-token-doc/doc/more/link.md.
增加 vue-next-admin 友链
2021-03-06 14:26:41 +08:00
shengzhang 8c35d838ff SaSession新增refreshDataMap方法,用于一次性重置整个数据集合 2021-03-05 13:57:24 +08:00
shengzhang 1eae6861d1 更新微信群聊二维码 2021-03-04 10:13:00 +08:00
shengzhang bbcc016fc7 SaOAuth2Config sa-token oauth2 配置类所有set方法新增支持链式调用 2021-03-03 10:47:32 +08:00
省长 c62f9482df update sa-token-doc/doc/README.md. 2021-03-02 11:24:28 +08:00
省长 d2c0ac3f52 update README.md. 2021-03-02 11:23:24 +08:00
shengzhang 51b76f07f4 起草[记住我]模式开发文档 2021-03-02 11:01:59 +08:00
shengzhang 0b32a1f552 优化源码注释 2021-03-02 09:22:55 +08:00
shengzhang f7cf015324 新增SaLoginModel登录参数Model,适配[记住我]模式 **[重要]** 2021-03-01 02:20:22 +08:00
shengzhang 4965149c23 新增:权限认证文档新增[如何把权限精确搭到按钮级]示例说明 2021-02-28 02:05:48 +08:00
省长 7268279736 !15 update README.md.
Merge pull request !15 from ZhuBJ0510/N/A
2021-02-22 17:56:13 +08:00
ZhuBJ0510 20304aa717 update README.md. 2021-02-22 17:54:16 +08:00
省长 0d46d232b4 update README.md. 2021-02-20 23:56:18 +08:00
RockMan 64aa2edf9f update sa-token-doc/doc/README.md. 2021-02-20 10:19:41 +08:00
RockMan cb72d4601c update README.md. 2021-02-20 10:18:43 +08:00
shengzhang 4617ac8226 刷新access_token时不改变refresh_token 2021-02-18 09:11:48 +08:00
shengzhang 7e0f592a0f 实现 OAuth2.0 模块 2021-02-17 18:07:45 +08:00
省长 1df97edaf8 !14 update README.md.
Merge pull request !14 from ZhuBJ0510/N/A
2021-02-16 17:11:34 +08:00
ZhuBJ0510 494da1029d update README.md. 2021-02-16 17:11:16 +08:00
省长 2b8c3cefb9 !13 update sa-token-doc/doc/README.md.
Merge pull request !13 from ZhuBJ0510/N/A
2021-02-16 17:09:09 +08:00
ZhuBJ0510 0d50eb9f85 update sa-token-doc/doc/README.md. 2021-02-16 17:08:50 +08:00
省长 8ce9a70dbf update sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java. 2021-02-14 00:44:55 +08:00
省长 f1919769a4 !12 update sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java.
Merge pull request !12 from xiaoshitou/N/A
2021-02-14 00:41:13 +08:00
xiaoshitou 85e5e2b558 update sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java. 2021-02-13 18:28:11 +08:00
shengzhang 8e4532e1f6 文档优化排版 2021-02-12 03:23:34 +08:00
shengzhang c986cf3034 代码添加行号 2021-02-12 01:57:31 +08:00
RockMan f96f186140 update sa-token-doc/doc/more/common-questions.md. 2021-02-09 14:39:17 +08:00
shengzhang 4695c42d83 优化文档样式 2021-02-09 01:36:27 +08:00
shengzhang 95a133040b 更新日志 2021-02-09 01:20:05 +08:00
shengzhang 30b47573c4 v1.13.0更新 2021-02-09 01:17:52 +08:00
shengzhang b38091217a 完善文档,准备发版 2021-02-08 20:17:48 +08:00
shengzhang deed69f80d 完善集群分布式下的解决方案 2021-02-08 20:01:31 +08:00
shengzhang a11ad64d41 完成同域模式下的单点登录 2021-02-08 19:23:37 +08:00
省长 af0b22854b update README.md. 2021-02-07 22:16:30 +08:00
RockMan 0f582395cf !11 完善readme
Merge pull request !11 from RockMan/N/A
2021-02-07 12:02:27 +08:00
RockMan 64a0f4a6cd 晚上readme 2021-02-07 12:01:51 +08:00
RockMan 51f35403d5 update sa-token-doc/doc/fun/not-login-scene.md. 2021-02-07 10:35:22 +08:00
shengzhang be7d08639c 优化首页介绍 2021-02-06 20:35:49 +08:00
省长 bbadbc76d2 update README.md. 2021-02-06 20:34:28 +08:00
省长 7580b21810 update sa-token-doc/doc/use/dao-extend.md. 2021-02-06 16:14:08 +08:00
RockMan 9e7619e689 update sa-token-doc/doc/fun/not-login-scene.md. 2021-02-06 15:54:29 +08:00
RockMan a608f29ed1 update sa-token-doc/doc/fun/token-timeout.md. 2021-02-06 15:52:49 +08:00
RockMan 48307dcd4a update sa-token-doc/doc/more/link.md. 2021-02-06 15:44:40 +08:00
RockMan d4eb41dd6a update sa-token-doc/doc/use/config.md. 2021-02-06 15:34:23 +08:00
RockMan 59d3b985ec update sa-token-doc/doc/use/token-style.md. 2021-02-06 15:33:43 +08:00
RockMan 6e3f6103ca update sa-token-doc/doc/use/route-check.md. 2021-02-06 15:24:11 +08:00
RockMan ce9dd4f5e8 update sa-token-doc/doc/use/many-account.md. 2021-02-06 15:17:58 +08:00
RockMan 8aa1852870 update sa-token-doc/doc/use/not-cookie.md. 2021-02-06 14:31:41 +08:00
RockMan e329928177 update sa-token-doc/doc/use/dao-extend.md. 2021-02-06 14:06:26 +08:00
RockMan 8c58c489d7 update sa-token-doc/doc/use/kick.md. 2021-02-06 13:53:46 +08:00
RockMan 2d90aac488 update sa-token-doc/doc/use/session.md.
update sa-token-doc/doc/use/session.md.
2021-02-06 13:51:30 +08:00
RockMan b8f31da9a2 update sa-token-doc/doc/start/example.md.
update sa-token-doc/doc/start/example.md.
2021-02-06 13:42:42 +08:00
RockMan 285f02578a update README.md.
优化介绍
2021-02-06 12:16:00 +08:00
shengzhang 487e15b1f1 Maven版本号更改为变量形式 2021-02-04 23:03:02 +08:00
省长 643d82cde6 完善贡献者名单
完善贡献者名单
2021-02-04 22:10:27 +08:00
shengzhang 5dd02b3528 新增autoRenew配置,用于控制是否打开自动续签模式 2021-02-04 21:54:54 +08:00
省长 b211de69ca 完善readme.md
完善readme.md
2021-02-04 21:19:46 +08:00
shengzhang 25a40b3797 抽象到 2021-02-04 21:12:05 +08:00
shengzhang b18b8a07b5 异常统一继承SaTokenException、SaSession的创建抽象到接口 2021-02-04 21:10:45 +08:00
省长 10f0571ef2 update README.md.
修改群聊二维码尺寸
2021-02-04 12:17:53 +08:00
省长 b3544ed655 !10 update README.md.
Merge pull request !10 from ZhuBJ0510/N/A
2021-02-04 12:00:20 +08:00
ZhuBJ0510 a5a4da10b6 update README.md. 2021-02-04 11:45:15 +08:00
shengzhang 1678ee7475 完善文档 2021-02-04 00:11:11 +08:00
shengzhang f3742aeca7 完善readme介绍 2021-02-04 00:07:02 +08:00
省长 0989bf8bb7 更改拼写错误
更改拼写错误
2021-02-02 11:11:18 +08:00
RockMan 2052a11e9e update sa-token-doc/doc/use/mock-person.md.
fix: 文档示例错误
2021-02-01 11:30:21 +08:00
shengzhang 47e879ad5e 完善文档 2021-01-31 23:28:42 +08:00
shengzhang 40331c2e3f 修复文档权限认证处错误之处 2021-01-31 01:58:40 +08:00
shengzhang 26ee628b33 完善源码注释 2021-01-30 18:06:14 +08:00
shengzhang 03e38c549a 增加微信交流群二维码 2021-01-29 18:57:06 +08:00
省长 bf02d7a1e5 增加微信交流群二维码
增加微信交流群二维码
2021-01-29 18:33:36 +08:00
shengzhang a3ed0ec1fd 去除排名提示信息 2021-01-24 23:24:25 +08:00
shengzhang 8b0976f8c6 优化readme.md 2021-01-24 23:22:25 +08:00
shengzhang a068441829 优化文档 2021-01-24 22:23:17 +08:00
shengzhang 0d652e42b7 优化文档 2021-01-24 20:18:28 +08:00
省长 cb0fd8cdcf !9 style 优化 if else 结构
Merge pull request !9 from xiaoshitou/dev
2021-01-24 17:44:29 +08:00
xiaoshitou 43151743b8 style 优化 if else 结构
equals 调用时应该 常量值应该在前, 避免空指针异常
2021-01-23 23:51:40 +08:00
shengzhang c3303e4212 完善注释 2021-01-22 20:58:49 +08:00
shengzhang 575f8f38bc 优化文档样式 2021-01-21 18:09:50 +08:00
RockMan 56d197b90c 完善readme.md
完善readme.md
2021-01-21 17:34:25 +08:00
RockMan 4b38feb60b 新增文章
新增文章
2021-01-19 13:57:19 +08:00
shengzhang bbc2ccb860 优化readme 2021-01-17 17:31:40 +08:00
省长 99fd9ebf65 完善 README.md.
完善 README.md.
2021-01-17 17:22:17 +08:00
RockMan ab8667a0c4 添加博客链接
添加博客链接
2021-01-16 10:13:40 +08:00
RockMan 94ce6d0d73 update README.md.
新增知乎专栏链接
2021-01-16 10:03:11 +08:00
RockMan b3373214be update sa-token-doc/doc/use/mock-person.md.
修复描述错误
2021-01-14 14:55:49 +08:00
shengzhang 87292cb7d5 v1.12.1更新 2021-01-14 02:06:39 +08:00
shengzhang 3b9f6b71e9 集成Gitalk评论系统 2021-01-13 20:35:33 +08:00
省长 251ac4cf0f update README.md.
优化描述
2021-01-13 20:32:02 +08:00
RockMan 5f9193095e update sa-token-doc/doc/use/route-check.md.
完善新增特性文档
2021-01-13 09:42:12 +08:00
shengzhang d1d25f4d49 新增SaRouterUtil,可优雅的路由拦截式鉴权 2021-01-13 02:00:47 +08:00
RockMan 37bcee92ed update sa-token-doc/doc/use/route-check.md.
简化示例
2021-01-12 22:53:16 +08:00
RockMan 028e4e56f0 update sa-token-doc/doc/use/at-check.md.
写通顺点
2021-01-12 22:31:45 +08:00
RockMan d65469ba3e !8 将aop模式和注册拦截器模式说明位置互换
Merge pull request !8 from AppleOfGray/N/A
2021-01-12 22:00:27 +08:00
AppleOfGray bcb82395d5 将aop模式和注册拦截器模式说明位置互换 2021-01-12 19:16:17 +08:00
RockMan 88d1f47227 readme.md完善代码示例
readme.md完善代码示例
2021-01-12 11:49:35 +08:00
click33 866c0de19d Merge pull request #6 from auster9021/patch-1
Update StpLogic.java
2021-01-12 01:32:56 +08:00
shengzhang ba640295c5 v1.12.0 版本更新 2021-01-12 01:01:23 +08:00
shengzhang 1ce7b945d8 完善文档 2021-01-11 23:17:15 +08:00
shengzhang 4bfad95bad 新增身份临时切换功能 2021-01-11 23:10:11 +08:00
shengzhang c7f3b6d493 路由拦截器 增加参数 2021-01-11 21:18:55 +08:00
shengzhang c3bedaef99 v.1.12.0新特性:路由拦截式鉴权 2021-01-11 20:08:17 +08:00
Auster 983c16aa58 Update StpLogic.java 2021-01-11 17:26:02 +08:00
shengzhang ac4ac37175 取消判断同账号免重复登陆 2021-01-10 23:01:34 +08:00
shengzhang 713758c304 新增jwt测试 2021-01-10 18:39:32 +08:00
shengzhang 857e260a0a jwt集成示例 2021-01-10 18:36:35 +08:00
shengzhang f1503d93a0 优化文档首页介绍 2021-01-10 00:55:45 +08:00
shengzhang 1648422617 修正更新日期 2021-01-10 00:49:29 +08:00
150 changed files with 10059 additions and 1753 deletions
+1
View File
@@ -6,6 +6,7 @@ bin/
unpackage/
.classpath
.project
*.iml
.factorypath
/.factorypath
+132 -57
View File
@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150" style="margin-bottom: 10px;">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.15.0</h1>
<h4 align="center">这可能是史上功能最全的Java权限认证框架!</h4>
<h4 align="center">
<a href="https://gitee.com/sz6/sa-token/stargazers"><img src="https://gitee.com/sz6/sa-token/badge/star.svg"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.11.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.15.0-2B9939"></a>
<a href="https://github.com/click33/sa-token/stargazers"><img src="https://img.shields.io/github/stars/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/watchers"><img src="https://img.shields.io/github/watchers/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/network/members"><img src="https://img.shields.io/github/forks/click33/sa-token"></a>
@@ -25,24 +25,43 @@
- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token)
- [开源不易,求鼓励,点个star吧](###)
## sa-token是什么?
sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登录认证、权限认证、Session会话等一系列由此衍生的权限相关业务
在架构设计上`sa-token`拒绝引入复杂的概念,以实际业务需求为第一目标进行定向突破,例如踢人下线、自动续签、同端互斥登录等常见业务在框架内均可以一行代码调用实现,简单粗暴,拒绝复杂!
## Sa-Token是什么?
sa-token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题
对于传统Session会话模型的N多难题,例如难以分布式、水平扩展性差,难以兼容前后台分离环境,多会话管理混乱等,
`sa-token`独创了以账号为主的`Id-Session`模式,同时又兼容了传统以token为主的`Token-Session`模式,两者彼此独立,互不干扰,
让你在进行会话管理时可以如鱼得水,在`sa-toekn`的强力加持下,权限问题将不再成为业务逻辑的瓶颈!
框架针对踢人下线、自动续签、前后台分离、分布式会话……等常见业务进行N多适配,通过sa-token,你可以以一种极简的方式实现系统的权限认证部分
总的来说,与其它权限认证框架相比,`sa-token`具有以下优势:
1. 上手简单:可零配置启动框架,能自动化的均已自动化,不让你费脑子
2. 功能强大:能集成的功能全部集成,不让你用个框架还要自己给框架打各种补丁
3. API简单易用:同样的一个功能,可能在别的框架中需要上百行代码,但是在sa-token中统统一行代码调个方法即可解决
4. 组件易于扩展:框架中几乎所有组件都提供了对应的扩展接口,90%以上的逻辑都可以按需重写
与其它权限认证框架相比,`sa-token` 具有以下优势:
1. **简单** :可零配置启动框架,真正的开箱即用,低成本上手
2. **强大** :目前已集成几十项权限相关特性,涵盖了大部分业务场景的解决方案
3. **易用** :如丝般顺滑的API调用,大量高级特性统统只需一行代码即可实现
4. **高扩展** 几乎所有组件都提供了扩展接口,90%以上的逻辑都可以按需重写
有了sa-token是时候和那些老旧权限框架说拜拜了
有了sa-token你所有的权限认证问题,都不再是问题
## Sa-Token 能做什么?
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC权限模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
- **分布式会话** —— 提供jwt集成和共享数据中心两种分布式会话方案
- **单点登录** —— 一处登录,处处通行
- **模拟他人账号** —— 实时操作任意用户状态数据
- **临时身份切换** —— 将会话身份临时切换为其它账号
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配restful模式
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **会话治理** —— 提供方便灵活的会话查询接口
- **记住我模式** —— 适配[记住我]模式,重启浏览器免验证
- **密码加密** —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
- **组件自动注入** —— 零配置与Spring等框架集成
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 代码示例
@@ -50,51 +69,67 @@ sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登
sa-token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
``` java
// 在登录时写入当前会话的账号id
StpUtil.setLoginId(10001);
// 在登录时写入当前会话的账号id
StpUtil.setLoginId(10001);
// 然后在任意需要校验登录处调用以下API --- 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
StpUtil.checkLogin();
// 然后在任意需要校验登录处调用以下API
// 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
StpUtil.checkLogin();
```
至此,我们已经借助sa-token框架完成登录授权!
此时的你小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗?
事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权!
当你受够Shiro、Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,sa-token的API设计是多么的清爽!
权限认证示例 (只有具有`user:add`权限的会话才可以进入请求)
``` java
@SaCheckPermission("user:add")
@RequestMapping("/user/insert")
public String insert(SysUser user) {
// ...
return "用户增加";
}
```
如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大
将某个账号踢下线 (待到对方再次访问系统时会抛出`NotLoginException`异常)
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
// 使账号id为10001的会话注销登录
StpUtil.logoutByLoginId(10001);
```
除了以上的示例,sa-token还可以一行代码完成以下功能:
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
```
sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档
## 涵盖功能
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
- **持久层扩展** —— 可集成redis、MongoDB等专业缓存中间件
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **组件自动注入** —— 零配置与Spring等框架集成
- **会话治理** —— 提供方便灵活的会话查询接口
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 迭代模式
sa-token的功能提案主要来源于社区,这意味着人人都可以参与到sa-token的功能定制,决定框架的未来走向,
如果你有好的想法,可以在issues提出或者加入群一起交流,对于社区的提出的功能要求,主要分为以下几类:
- 对框架新增特性功能且比较简单,会在第一时间进行开发
- 对框架新增特性功能但比较复杂,会延后几个版本制定相应的计划后进行开发
- 与框架设计理念不太相符,或超出权限认证范畴,将会视需求人数决定是否开发
## 参与贡献
众人拾柴火焰高,sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦
众人拾柴火焰高,万丈高楼众人起!
sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦,对框架有卓越贡献者将会出现在贡献者名单里
1. 在gitee或者github上fork一份代码到自己的仓库
2. clone自己的仓库到本地电脑
@@ -107,20 +142,60 @@ sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳
## 建议贡献的地方
- 修复源码现有bug,或优化代码架构,或增加新的实用功能
- 完善在线文档,或者修复现有描述错误之处
- 更多的第三方框架集成方案,更多的demo示例:比如SSM版搭建步骤
- 您可以参考项目issues与需求墙进行贡献
目前框架的主要有以下部分需要大家一起参与贡献:
- 核心代码:该部分需要开发者了解整个框架的架构,遵循已有代码规范进行bug修复或提交新功能
- 文档部分:需要以清晰明了的语句书写文档,力求简单易读,授人以鱼同时更授人以渔
- 社区建设:如果框架帮助到了您,希望您可以加入qq群参与交流,对不熟悉框架的新人进行排难解惑
- 框架推广:一个优秀的开源项目不能仅靠闭门造车,它还需要一定的推广方案让更多的人一起参与到项目中
- 其它部分:您可以参考项目issues与需求墙进行贡献
## 贡献者名单
[省长](https://gitee.com/sz6)、
[RockMan](https://gitee.com/njx33)、
[click33](https://github.com/click33)、
[AppleOfGray](https://gitee.com/appleOfGray)、
[Auster](https://github.com/auster9021)、
[ZhuBJ0510](https://gitee.com/zhubj0510)、
[legg](https://gitee.com/legg321)、
[xiaoshitou](https://gitee.com/smallstoneZ)、
[zhangjiaxiaozhuo](https://gitee.com/zhangjiaxiaozhuo)、
[离你多远](https://gitee.com/liniduoyuan)
## 知乎专栏
- [初识sa-token,一行代码搞定登录授权!](https://zhuanlan.zhihu.com/p/344106099)
- [一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录](https://zhuanlan.zhihu.com/p/344511415)
- [浅谈踢人下线的设计思路!(附代码实现方案)](https://zhuanlan.zhihu.com/p/345844002)
- 文章已在 [csdn](https://blog.csdn.net/shengzhang_/article/details/112593247)、
[掘金](https://juejin.cn/post/6917250126650015751)、
[开源中国](https://my.oschina.net/u/3503445/blog/4897816)、
[博客园](https://www.cnblogs.com/shengzhang/p/14275558.html)、
[知乎](https://zhuanlan.zhihu.com/p/344106099)
等平台连载中...欢迎投稿
## 使用sa-token的开源项目
[**[ sa-plus]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/sz6/sa-plus)
如果您的项目使用了sa-token,欢迎提交pr
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ OkHttps ]** 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing)
## 交流群
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) ,欢迎你的加入
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM)
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=230')
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=150')
**微信群**
![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230')
(扫码添加微信,备注:sa-token,邀您加入群聊)
<br>
+32 -1
View File
@@ -8,7 +8,7 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<packaging>pom</packaging>
<version>1.11.0</version>
<version>1.15.0</version>
<!-- 项目介绍 -->
<name>sa-token</name>
@@ -23,6 +23,7 @@
<module>sa-token-dao-redis</module>
<module>sa-token-dao-redis-jackson</module>
<module>sa-token-spring-aop</module>
<module>sa-token-oauth2</module>
</modules>
<!-- 开源协议 apache 2.0 -->
@@ -37,6 +38,7 @@
<!-- 一些属性 -->
<properties>
<sa-token-version>1.15.0</sa-token-version>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
@@ -64,6 +66,7 @@
<groupId>org.sonatype.oss</groupId>
<artifactId>oss-parent</artifactId>
<version>7</version>
<relativePath/>
</parent>
<!-- 仓库依赖 -->
@@ -85,6 +88,34 @@
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<versionRange>[1.0.0,)</versionRange>
<goals>
<goal>enforce</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
+2 -1
View File
@@ -9,4 +9,5 @@ unpackage/
.factorypath
.idea/
.idea/
.iml
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.15.0</version>
</parent>
<packaging>jar</packaging>
@@ -1,5 +1,8 @@
package cn.dev33.satoken;
import java.util.HashMap;
import java.util.Map;
import cn.dev33.satoken.action.SaTokenAction;
import cn.dev33.satoken.action.SaTokenActionDefaultImpl;
import cn.dev33.satoken.config.SaTokenConfig;
@@ -12,6 +15,7 @@ import cn.dev33.satoken.servlet.SaTokenServlet;
import cn.dev33.satoken.servlet.SaTokenServletDefaultImpl;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpInterfaceDefaultImpl;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
@@ -21,132 +25,143 @@ import cn.dev33.satoken.util.SaTokenInsideUtil;
*/
public class SaTokenManager {
/**
* 配置文件 Bean
*/
private static SaTokenConfig config;
public static SaTokenConfig getConfig() {
if (config == null) {
initConfig();
}
return config;
}
public static void setConfig(SaTokenConfig config) {
SaTokenManager.config = config;
if(config.getIsV()) {
SaTokenInsideUtil.printSaToken();
}
}
public synchronized static void initConfig() {
public static SaTokenConfig getConfig() {
if (config == null) {
setConfig(SaTokenConfigFactory.createConfig());
// 如果对象为空,则使用框架默认方式初始化
synchronized (SaTokenManager.class) {
if (config == null) {
setConfig(SaTokenConfigFactory.createConfig());
}
}
}
return config;
}
/**
* 持久化 Bean
*/
public static SaTokenDao saTokenDao;
public static SaTokenDao getSaTokenDao() {
if (saTokenDao == null) {
initSaTokenDao();
}
return saTokenDao;
}
private static SaTokenDao saTokenDao;
public static void setSaTokenDao(SaTokenDao saTokenDao) {
if(SaTokenManager.saTokenDao != null && (SaTokenManager.saTokenDao instanceof SaTokenDaoDefaultImpl)) {
((SaTokenDaoDefaultImpl)SaTokenManager.saTokenDao).endRefreshTimer();
((SaTokenDaoDefaultImpl)SaTokenManager.saTokenDao).endRefreshThread();
}
SaTokenManager.saTokenDao = saTokenDao;
}
public synchronized static void initSaTokenDao() {
public static SaTokenDao getSaTokenDao() {
if (saTokenDao == null) {
setSaTokenDao(new SaTokenDaoDefaultImpl());
// 如果对象为空,则使用框架默认方式初始化
synchronized (SaTokenManager.class) {
if (saTokenDao == null) {
setSaTokenDao(new SaTokenDaoDefaultImpl());
}
}
}
return saTokenDao;
}
/**
* 权限认证 Bean
*/
public static StpInterface stpInterface;
public static StpInterface getStpInterface() {
if (stpInterface == null) {
initStpInterface();
}
return stpInterface;
}
private static StpInterface stpInterface;
public static void setStpInterface(StpInterface stpInterface) {
SaTokenManager.stpInterface = stpInterface;
}
public synchronized static void initStpInterface() {
public static StpInterface getStpInterface() {
if (stpInterface == null) {
setStpInterface(new StpInterfaceDefaultImpl());
// 如果对象为空,则使用框架默认方式初始化
synchronized (SaTokenManager.class) {
if (stpInterface == null) {
setStpInterface(new StpInterfaceDefaultImpl());
}
}
}
return stpInterface;
}
/**
* 框架行为 Bean
*/
public static SaTokenAction saTokenAction;
public static SaTokenAction getSaTokenAction() {
if (saTokenAction == null) {
initSaTokenAction();
}
return saTokenAction;
}
private static SaTokenAction saTokenAction;
public static void setSaTokenAction(SaTokenAction saTokenAction) {
SaTokenManager.saTokenAction = saTokenAction;
}
public synchronized static void initSaTokenAction() {
public static SaTokenAction getSaTokenAction() {
if (saTokenAction == null) {
setSaTokenAction(new SaTokenActionDefaultImpl());
// 如果对象为空,则使用框架默认方式初始化
synchronized (SaTokenManager.class) {
if (saTokenAction == null) {
setSaTokenAction(new SaTokenActionDefaultImpl());
}
}
}
return saTokenAction;
}
/**
* Cookie操作 Bean
*/
public static SaTokenCookie saTokenCookie;
public static SaTokenCookie getSaTokenCookie() {
if (saTokenCookie == null) {
initSaTokenCookie();
}
return saTokenCookie;
}
private static SaTokenCookie saTokenCookie;
public static void setSaTokenCookie(SaTokenCookie saTokenCookie) {
SaTokenManager.saTokenCookie = saTokenCookie;
}
public synchronized static void initSaTokenCookie() {
public static SaTokenCookie getSaTokenCookie() {
if (saTokenCookie == null) {
setSaTokenCookie(new SaTokenCookieDefaultImpl());
// 如果对象为空,则使用框架默认方式初始化
synchronized (SaTokenManager.class) {
if (saTokenCookie == null) {
setSaTokenCookie(new SaTokenCookieDefaultImpl());
}
}
}
return saTokenCookie;
}
/**
* Servlet操作 Bean
*/
public static SaTokenServlet saTokenServlet;
public static SaTokenServlet getSaTokenServlet() {
if (saTokenServlet == null) {
initSaTokenServlet();
}
return saTokenServlet;
}
private static SaTokenServlet saTokenServlet;
public static void setSaTokenServlet(SaTokenServlet saTokenServlet) {
SaTokenManager.saTokenServlet = saTokenServlet;
}
public synchronized static void initSaTokenServlet() {
public static SaTokenServlet getSaTokenServlet() {
if (saTokenServlet == null) {
setSaTokenServlet(new SaTokenServletDefaultImpl());
// 如果对象为空,则使用框架默认方式初始化
if (saTokenServlet == null) {
setSaTokenServlet(new SaTokenServletDefaultImpl());
}
}
return saTokenServlet;
}
/**
* StpLogic集合, 记录框架所有成功初始化的StpLogic
*/
public static Map<String, StpLogic> stpLogicMap = new HashMap<String, StpLogic>();
/**
* 向集合中 put 一个 StpLogic
* @param stpLogic StpLogic
*/
public static void putStpLogic(StpLogic stpLogic) {
stpLogicMap.put(stpLogic.getLoginKey(), stpLogic);
}
/**
* 根据 LoginKey 获取对应的StpLogic,如果不存在则返回null
* @param loginKey 对应的LoginKey
* @return 对应的StpLogic
*/
public static StpLogic getStpLogic(String loginKey) {
return stpLogicMap.get(loginKey);
}
}
@@ -1,19 +1,28 @@
package cn.dev33.satoken.action;
import cn.dev33.satoken.session.SaSession;
/**
* sa-token内置操作接口
* sa-token逻辑代理接口
* <p>此接口将会代理框架内部的一些关键性逻辑,方便开发者进行按需重写</p>
* @author kong
*
*/
public interface SaTokenAction {
/**
* 生成一个token
* @param loginId 账号id
* @param loginKey 账号标识key
* 根据一定的算法生成一个token
* @param loginId 账号id
* @param loginKey 账号体系key
* @return 一个token
*/
public String createToken(Object loginId, String loginKey);
/**
* 根据 SessionId 创建一个 Session
* @param sessionId Session的Id
* @return 创建后的Session
*/
public SaSession createSession(String sessionId);
}
@@ -3,6 +3,8 @@ package cn.dev33.satoken.action;
import java.util.UUID;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.util.SaTokenConsts;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
@@ -14,44 +16,47 @@ public class SaTokenActionDefaultImpl implements SaTokenAction {
/**
* 生成一个token
* 根据一定的算法生成一个token
*/
@Override
public String createToken(Object loginId, String loginKey) {
// 生成各种花式token
// 根据配置的tokenStyle生成不同风格的token
String tokenStyle = SaTokenManager.getConfig().getTokenStyle();
// uuid
if(tokenStyle.equals("uuid")) {
// uuid
if(SaTokenConsts.TOKEN_STYLE_UUID.equals(tokenStyle)) {
return UUID.randomUUID().toString();
}
// 简单uuid (不带下划线)
else if(tokenStyle.equals("simple-uuid")) {
if(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID.equals(tokenStyle)) {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// 32位随机字符串
else if(tokenStyle.equals("random-32")) {
if(SaTokenConsts.TOKEN_STYLE_RANDOM_32.equals(tokenStyle)) {
return SaTokenInsideUtil.getRandomString(32);
}
// 64位随机字符串
else if(tokenStyle.equals("random-64")) {
if(SaTokenConsts.TOKEN_STYLE_RANDOM_64.equals(tokenStyle)) {
return SaTokenInsideUtil.getRandomString(64);
}
// 128位随机字符串
else if(tokenStyle.equals("random-128")) {
if(SaTokenConsts.TOKEN_STYLE_RANDOM_128.equals(tokenStyle)) {
return SaTokenInsideUtil.getRandomString(128);
}
// tik风格 (2_14_16)
else if(tokenStyle.equals("tik")) {
if(SaTokenConsts.TOKEN_STYLE_RANDOM_TIK.equals(tokenStyle)) {
return SaTokenInsideUtil.getRandomString(2) + "_" + SaTokenInsideUtil.getRandomString(14) + "_" + SaTokenInsideUtil.getRandomString(16) + "__";
}
// 默认,还是uuid
else {
return UUID.randomUUID().toString();
}
// 默认,还是uuid
return UUID.randomUUID().toString();
}
/**
* 根据 SessionId 创建一个 Session
*/
@Override
public SaSession createSession(String sessionId) {
return new SaSession(sessionId);
}
}
@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标注一个路由方法,当前会话必须已登录才能通过
* 登录校验:标注一个方法,当前会话必须已登录才能进入该方法
* <p> 可标注在类上,其效果等同于标注在此类的所有方法上
* @author kong
*
@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标注一个路由方法,当前会话必须具有指定权限才可以通过
* 权限校验:标注一个方法,当前会话必须具有指定权限才能进入该方法
* <p> 可标注在类上,其效果等同于标注在此类的所有方法上
* @author kong
*
@@ -16,13 +16,13 @@ import java.lang.annotation.Target;
public @interface SaCheckPermission {
/**
* 需要验的权限码
* @return 需要验的权限码
* 需要验的权限码
* @return 需要验的权限码
*/
String [] value() default {};
/**
* 指定验证模式AND还是OR,默认AND
* 验证模式AND | OR,默认AND
* @return 验证模式
*/
SaMode mode() default SaMode.AND;
@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标注一个路由方法,当前会话必须具有指定角色标识才可以通过
* 角色校验:标注一个方法,当前会话必须具有指定角色标识才能进入该方法
* <p> 可标注在类上,其效果等同于标注在此类的所有方法上
* @author kong
*
@@ -16,13 +16,13 @@ import java.lang.annotation.Target;
public @interface SaCheckRole {
/**
* 需要验的角色标识
* @return 需要验证的权限码
* 需要验的角色标识
* @return 需要校验的角色标识
*/
String [] value() default {};
/**
* 指定验证模式AND还是OR,默认AND
* 验证模式AND | OR,默认AND
* @return 验证模式
*/
SaMode mode() default SaMode.AND;
@@ -1,7 +1,7 @@
package cn.dev33.satoken.annotation;
/**
* 指定注解鉴权的验证模式
* 注解鉴权的验证模式
* @author kong
*
*/
@@ -1,244 +1,325 @@
package cn.dev33.satoken.config;
/**
* sa-token 配置类Model
* sa-token 配置类 Model
* <p>
* 你可以通过yml、properties、java代码等形式配置本类参数,具体请查阅官方文档: http://sa-token.dev33.cn/
*
* @author kong
*
*/
public class SaTokenConfig {
/** token名称 (同时也是cookie名称) */
private String tokenName = "satoken";
/** token有效期单位/秒 默认30天, -1代表永久 */
private long timeout = 30 * 24 * 60 * 60;
/** token临时有效期 (指定时间内无操作就视为token过期) 单位/秒, 默认-1 代表不限制 (例如可以设置为1800代表30分钟内无操作就过期) */
private long activityTimeout = -1;
/** 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) */
private Boolean allowConcurrentLogin = true;
/** 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
private Boolean isShare = true;
private String tokenName = "satoken";
/** token的长久有效期(单位:秒) 默认30天, -1代表永久 */
private long timeout = 30 * 24 * 60 * 60;
/**
* token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
* (例如可以设置为1800代表30分钟内无操作就过期)
*/
private long activityTimeout = -1;
/** 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) */
private Boolean allowConcurrentLogin = true;
/** 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
private Boolean isShare = true;
/** 是否尝试从请求体里读取token */
private Boolean isReadBody = true;
private Boolean isReadBody = true;
/** 是否尝试从header里读取token */
private Boolean isReadHead = true;
private Boolean isReadHead = true;
/** 是否尝试从cookie里读取token */
private Boolean isReadCookie = true;
/** token风格 */
private String tokenStyle = "uuid";
private Boolean isReadCookie = true;
/** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */
private String tokenStyle = "uuid";
/** 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 */
private int dataRefreshPeriod = 30;
/** 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录) */
private Boolean tokenSessionCheckLogin = true;
/** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) */
private Boolean autoRenew = true;
/** 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 */
private String cookieDomain;
/** 获取token专属session时是否必须登录 (如果配置为true,会在每次获取token专属session时校验是否登录) */
private Boolean tokenSessionCheckLogin = true;
/** token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */
private String tokenPrefix;
/** 是否在初始化配置时打印版本字符画 */
private Boolean isV = true;
/**
* @return tokenName
* @return token名称 (同时也是cookie名称)
*/
public String getTokenName() {
return tokenName;
}
/**
* @param tokenName 要设置的 tokenName
* @param tokenName token名称 (同时也是cookie名称)
* @return 对象自身
*/
public void setTokenName(String tokenName) {
public SaTokenConfig setTokenName(String tokenName) {
this.tokenName = tokenName;
return this;
}
/**
* @return timeout
* @return token的长久有效期(单位:秒) 默认30天, -1代表永久
*/
public long getTimeout() {
return timeout;
}
/**
* @param timeout 要设置的 timeout
* @param timeout token的长久有效期(单位:秒) 默认30天, -1代表永久
* @return 对象自身
*/
public void setTimeout(long timeout) {
public SaTokenConfig setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* @return activityTimeout
* @return token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
* (例如可以设置为1800代表30分钟内无操作就过期)
*/
public long getActivityTimeout() {
return activityTimeout;
}
/**
* @param activityTimeout 要设置的 activityTimeout
* @param activityTimeout token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
* (例如可以设置为1800代表30分钟内无操作就过期)
* @return 对象自身
*/
public void setActivityTimeout(long activityTimeout) {
public SaTokenConfig setActivityTimeout(long activityTimeout) {
this.activityTimeout = activityTimeout;
return this;
}
/**
* @return allowConcurrentLogin
* @return 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
*/
public Boolean getAllowConcurrentLogin() {
return allowConcurrentLogin;
}
/**
* @param allowConcurrentLogin 要设置的 allowConcurrentLogin
* @param allowConcurrentLogin 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
* @return 对象自身
*/
public void setAllowConcurrentLogin(Boolean allowConcurrentLogin) {
public SaTokenConfig setAllowConcurrentLogin(Boolean allowConcurrentLogin) {
this.allowConcurrentLogin = allowConcurrentLogin;
return this;
}
/**
* @return isShare
* @return 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
*/
public Boolean getIsShare() {
return isShare;
}
/**
* @param isShare 要设置的 isShare
* @param isShare 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
* @return 对象自身
*/
public void setIsShare(Boolean isShare) {
public SaTokenConfig setIsShare(Boolean isShare) {
this.isShare = isShare;
return this;
}
/**
* @return isReadBody
* @return 是否尝试从请求体里读取token
*/
public Boolean getIsReadBody() {
return isReadBody;
}
/**
* @param isReadBody 要设置的 isReadBody
* @param isReadBody 是否尝试从请求体里读取token
* @return 对象自身
*/
public void setIsReadBody(Boolean isReadBody) {
public SaTokenConfig setIsReadBody(Boolean isReadBody) {
this.isReadBody = isReadBody;
return this;
}
/**
* @return isReadHead
* @return 是否尝试从header里读取token
*/
public Boolean getIsReadHead() {
return isReadHead;
}
/**
* @param isReadHead 要设置的 isReadHead
* @param isReadHead 是否尝试从header里读取token
* @return 对象自身
*/
public void setIsReadHead(Boolean isReadHead) {
public SaTokenConfig setIsReadHead(Boolean isReadHead) {
this.isReadHead = isReadHead;
return this;
}
/**
* @return isReadCookie
* @return 是否尝试从cookie里读取token
*/
public Boolean getIsReadCookie() {
return isReadCookie;
}
/**
* @param isReadCookie 要设置的 isReadCookie
* @param isReadCookie 是否尝试从cookie里读取token
* @return 对象自身
*/
public void setIsReadCookie(Boolean isReadCookie) {
public SaTokenConfig setIsReadCookie(Boolean isReadCookie) {
this.isReadCookie = isReadCookie;
return this;
}
/**
* @return tokenStyle
* @return token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
*/
public String getTokenStyle() {
return tokenStyle;
}
/**
* @param tokenStyle 要设置的 tokenStyle
* @param tokenStyle token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
* @return 对象自身
*/
public void setTokenStyle(String tokenStyle) {
public SaTokenConfig setTokenStyle(String tokenStyle) {
this.tokenStyle = tokenStyle;
return this;
}
/**
* @return dataRefreshPeriod
* @return 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理
*/
public int getDataRefreshPeriod() {
return dataRefreshPeriod;
}
/**
* @param dataRefreshPeriod 要设置的 dataRefreshPeriod
* @param dataRefreshPeriod 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒)
* ,默认值30秒,设置为-1代表不启动定时清理
* @return 对象自身
*/
public void setDataRefreshPeriod(int dataRefreshPeriod) {
public SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) {
this.dataRefreshPeriod = dataRefreshPeriod;
return this;
}
/**
* @return tokenSessionCheckLogin
* @return 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录)
*/
public Boolean getTokenSessionCheckLogin() {
return tokenSessionCheckLogin;
}
/**
* @param tokenSessionCheckLogin 要设置的 tokenSessionCheckLogin
* @param tokenSessionCheckLogin 获取[token专属session]时是否必须登录
* (如果配置为true,会在每次获取[token-session]时校验是否登录)
* @return 对象自身
*/
public void setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
public SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
return this;
}
/**
* @return isV
* @return 是否打开了自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作)
*/
public Boolean getAutoRenew() {
return autoRenew;
}
/**
* @param autoRenew 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作)
* @return 对象自身
*/
public SaTokenConfig setAutoRenew(Boolean autoRenew) {
this.autoRenew = autoRenew;
return this;
}
/**
* @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
*/
public String getCookieDomain() {
return cookieDomain;
}
/**
* @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
* @return 对象自身
*/
public SaTokenConfig setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
return this;
}
/**
* @return token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
*/
public String getTokenPrefix() {
return tokenPrefix;
}
/**
* @param tokenPrefix token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
* @return 对象自身
*/
public SaTokenConfig setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
return this;
}
/**
* @return 是否在初始化配置时打印版本字符画
*/
public Boolean getIsV() {
return isV;
}
/**
* @param isV 要设置的 isV
* @param isV 是否在初始化配置时打印版本字符画
* @return 对象自身
*/
public void setIsV(Boolean isV) {
public SaTokenConfig setIsV(Boolean isV) {
this.isV = isV;
return this;
}
/**
* toString
*/
@Override
public String toString() {
return "SaTokenConfig [tokenName=" + tokenName + ", timeout=" + timeout + ", activityTimeout=" + activityTimeout
+ ", allowConcurrentLogin=" + allowConcurrentLogin + ", isShare=" + isShare + ", isReadBody="
+ isReadBody + ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie + ", tokenStyle="
+ tokenStyle + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin="
+ tokenSessionCheckLogin + ", isV=" + isV + "]";
+ tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain
+ ", tokenPrefix=" + tokenPrefix + ", isV=" + isV + "]";
}
}
@@ -8,44 +8,46 @@ import java.util.Map;
import java.util.Properties;
/**
* sa-token配置文件建工厂类
* sa-token配置文件的构建工厂类
* <p>
* 只有在非IOC环境下才会用到此类
*
* @author kong
*
*/
public class SaTokenConfigFactory {
/**
* 默认配置文件地址
* 配置文件地址
*/
public static String configPath = "sa-token.properties";
/**
* 根据指定路径获取配置信息
* @return 一个SaTokenConfig对象
* 根据configPath路径获取配置信息
*
* @return 一个SaTokenConfig对象
*/
public static SaTokenConfig createConfig() {
Map<String, String> map = readPropToMap(configPath);
if(map == null){
if (map == null) {
// throw new RuntimeException("找不到配置文件:" + configPath, null);
}
return (SaTokenConfig)initPropByMap(map, new SaTokenConfig());
return (SaTokenConfig) initPropByMap(map, new SaTokenConfig());
}
/**
* 将指定路径的properties配置文件读取到Map中
* 工具方法: 将指定路径的properties配置文件读取到Map中
*
* @param propertiesPath 配置文件地址
* @return 一个Map
*/
private static Map<String, String> readPropToMap(String propertiesPath){
Map<String, String> map = new HashMap<String, String>();
private static Map<String, String> readPropToMap(String propertiesPath) {
Map<String, String> map = new HashMap<String, String>(16);
try {
InputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath);
if(is == null){
return null;
}
InputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath);
if (is == null) {
return null;
}
Properties prop = new Properties();
prop.load(is);
for (String key : prop.stringPropertyNames()) {
@@ -55,38 +57,41 @@ public class SaTokenConfigFactory {
throw new RuntimeException("配置文件(" + propertiesPath + ")加载失败", e);
}
return map;
}
/**
* 将 Map 的值映射到 Model 上
* @param map 属性集合
* @param obj 对象或类型
* @return 返回实例化后的对象
*/
private static Object initPropByMap(Map<String, String> map, Object obj){
if(map == null){
map = new HashMap<String, String>();
}
// 1、取出类型
Class<?> cs = null;
if(obj instanceof Class){ // 如果是一个类型,则将obj=null,以便完成静态属性反射赋值
cs = (Class<?>)obj;
obj = null;
}else{ // 如果是一个对象,则取出其类型
cs = obj.getClass();
}
// 2、遍历类型属性,反射赋值
for (Field field : cs.getDeclaredFields()) {
}
/**
* 工具方法: 将 Map 的值映射到一个 Model 上
*
* @param map 属性集合
* @param obj 对象, 或类型
* @return 返回实例化后的对象
*/
private static Object initPropByMap(Map<String, String> map, Object obj) {
if (map == null) {
map = new HashMap<String, String>(16);
}
// 1、取出类型
Class<?> cs = null;
if (obj instanceof Class) {
// 如果是一个类型,则将obj=null,以便完成静态属性反射赋值
cs = (Class<?>) obj;
obj = null;
} else {
// 如果是一个对象,则取出其类型
cs = obj.getClass();
}
// 2、遍历类型属性,反射赋值
for (Field field : cs.getDeclaredFields()) {
String value = map.get(field.getName());
if (value == null) {
continue; // 如果为空代表没有配置此项
// 如果为空代表没有配置此项
continue;
}
try {
Object valueConvert = getObjectByClass(value, field.getType()); // 转换值类型
Object valueConvert = getObjectByClass(value, field.getType());
field.setAccessible(true);
field.set(obj, valueConvert);
} catch (IllegalArgumentException e) {
@@ -95,41 +100,39 @@ public class SaTokenConfigFactory {
throw new RuntimeException("属性赋值出错:" + field.getName(), e);
}
}
return obj;
}
/**
* 将字符串转化为指定数据类型
* @param str 值
* @param cs 要转换的类型
* @return 转化好的结果
*/
@SuppressWarnings("unchecked")
private static <T>T getObjectByClass(String str, Class<T> cs){
Object value = null;
if(str == null){
value = null;
}else if (cs.equals(String.class)) {
value = str;
} else if (cs.equals(int.class)||cs.equals(Integer.class)) {
value = new Integer(str);
} else if (cs.equals(long.class)||cs.equals(Long.class)) {
value = new Long(str);
} else if (cs.equals(short.class)||cs.equals(Short.class)) {
value = new Short(str);
} else if (cs.equals(float.class)||cs.equals(Float.class)) {
value = new Float(str);
} else if (cs.equals(double.class)||cs.equals(Double.class)) {
value = new Double(str);
} else if (cs.equals(boolean.class)||cs.equals(Boolean.class)) {
value = new Boolean(str);
}else{
throw new RuntimeException("未能将值:" + str + ",转换类型为:" + cs, null);
}
return (T)value;
return obj;
}
/**
* 工具方法: 将字符串转化为指定数据类型
*
* @param str 值
* @param cs 要转换的类型
* @return 转化好的结果
*/
@SuppressWarnings("unchecked")
private static <T> T getObjectByClass(String str, Class<T> cs) {
Object value = null;
if (str == null) {
value = null;
} else if (cs.equals(String.class)) {
value = str;
} else if (cs.equals(int.class) || cs.equals(Integer.class)) {
value = new Integer(str);
} else if (cs.equals(long.class) || cs.equals(Long.class)) {
value = new Long(str);
} else if (cs.equals(short.class) || cs.equals(Short.class)) {
value = new Short(str);
} else if (cs.equals(float.class) || cs.equals(Float.class)) {
value = new Float(str);
} else if (cs.equals(double.class) || cs.equals(Double.class)) {
value = new Double(str);
} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
value = new Boolean(str);
} else {
throw new RuntimeException("未能将值:" + str + ",转换类型为:" + cs, null);
}
return (T) value;
}
}
@@ -5,7 +5,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* sa-token 对cookie的相关操作 接口类
* sa-token 对cookie的相关操作 接口类
*
* @author kong
*
*/
@@ -13,36 +14,41 @@ public interface SaTokenCookie {
/**
* 在request对象中获取指定Cookie
* @param request request对象
* @param cookieName Cookie名称
*
* @param request request对象
* @param cookieName Cookie名称
* @return 查找到的Cookie对象
*/
public Cookie getCookie(HttpServletRequest request, String cookieName);
/**
* 添加Cookie
* @param response response对象
* @param name Cookie名称
* @param value Cookie
* @param path Cookie路径
*
* @param response response对象
* @param name Cookie名称
* @param value Cookie
* @param path Cookie路径
* @param domain Cookie的作用域
* @param timeout 过期时间 (秒)
*/
public void addCookie(HttpServletResponse response, String name, String value, String path, int timeout);
public void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout);
/**
* 删除Cookie
* @param request request对象
* @param response response对象
*
* @param request request对象
* @param response response对象
* @param name Cookie名称
*/
public void delCookie(HttpServletRequest request, HttpServletResponse response, String name);
/**
* 修改Cookie的value值
* @param request request对象
*
* @param request request对象
* @param response response对象
* @param name Cookie名称
* @param value Cookie值
* @param name Cookie名称
* @param value Cookie值
*/
public void updateCookie(HttpServletRequest request, HttpServletResponse response, String name, String value);
@@ -5,14 +5,15 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* sa-token 对cookie的相关操作 接口实现类
* sa-token 对cookie的相关操作 接口实现类
*
* @author kong
*
*/
public class SaTokenCookieDefaultImpl implements SaTokenCookie {
/**
* 获取指定cookie
* 获取指定cookie
*/
@Override
public Cookie getCookie(HttpServletRequest request, String cookieName) {
@@ -20,15 +21,15 @@ public class SaTokenCookieDefaultImpl implements SaTokenCookie {
}
/**
* 添加cookie
* 添加cookie
*/
@Override
public void addCookie(HttpServletResponse response, String name, String value, String path, int timeout) {
SaTokenCookieUtil.addCookie(response, name, value, path, timeout);
public void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout) {
SaTokenCookieUtil.addCookie(response, name, value, path, domain, timeout);
}
/**
* 删除cookie
* 删除cookie
*/
@Override
public void delCookie(HttpServletRequest request, HttpServletResponse response, String name) {
@@ -36,7 +37,7 @@ public class SaTokenCookieDefaultImpl implements SaTokenCookie {
}
/**
* 修改cookie的value值
* 修改cookie的value值
*/
@Override
public void updateCookie(HttpServletRequest request, HttpServletResponse response, String name, String value) {
@@ -4,16 +4,20 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
* Cookie操作工具类
* @author kong
* Cookie操作工具类
*
* @author kong
*/
public class SaTokenCookieUtil {
/**
* 在request对象中获取指定Cookie
* @param request request对象
* @param cookieName Cookie名称
*
* @param request request对象
* @param cookieName Cookie名称
* @return 查找到的Cookie对象
*/
public static Cookie getCookie(HttpServletRequest request, String cookieName) {
@@ -30,17 +34,22 @@ public class SaTokenCookieUtil {
/**
* 添加cookie
*
* @param response response
* @param name Cookie名称
* @param name Cookie名称
* @param value Cookie值
* @param path Cookie写入路径
* @param timeout Cookie有效期 (秒)
* @param domain Cookie的作用域
* @param timeout Cookie有效期 (秒)
*/
public static void addCookie(HttpServletResponse response, String name, String value, String path, int timeout) {
public static void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout) {
Cookie cookie = new Cookie(name, value);
if (path == null) {
if(SaTokenInsideUtil.isEmpty(path) == true) {
path = "/";
}
if(SaTokenInsideUtil.isEmpty(domain) == false) {
cookie.setDomain(domain);
}
cookie.setPath(path);
cookie.setMaxAge(timeout);
response.addCookie(cookie);
@@ -48,16 +57,17 @@ public class SaTokenCookieUtil {
/**
* 删除Cookie
*
* @param request request对象
* @param response response对象
* @param name Cookie名称
* @param name Cookie名称
*/
public static void delCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie != null && (name).equals(cookie.getName())) {
addCookie(response, name, null, null, 0);
addCookie(response, name, null, null, null, 0);
return;
}
}
@@ -66,10 +76,11 @@ public class SaTokenCookieUtil {
/**
* 修改cookie的value值
* @param request request对象
* @param response response对象
* @param name Cookie名称
* @param value Cookie
*
* @param request request对象
* @param response response对象
* @param name Cookie名称
* @param value Cookie值
*/
public static void updateCookie(HttpServletRequest request, HttpServletResponse response, String name,
String value) {
@@ -77,11 +88,11 @@ public class SaTokenCookieUtil {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie != null && (name).equals(cookie.getName())) {
addCookie(response, name, value, cookie.getPath(), cookie.getMaxAge());
addCookie(response, name, value, cookie.getPath(), cookie.getDomain(), cookie.getMaxAge());
return;
}
}
}
}
}
}
@@ -24,7 +24,7 @@ public interface SaTokenDao {
* @param key 键名称
* @return value
*/
public String getValue(String key);
public String get(String key);
/**
* 写入指定key-value键值对,并设定过期时间 (单位: 秒)
@@ -32,20 +32,20 @@ public interface SaTokenDao {
* @param value 值
* @param timeout 过期时间 (单位: 秒)
*/
public void setValue(String key, String value, long timeout);
public void set(String key, String value, long timeout);
/**
* 修改指定key-value键值对 (过期时间取原来的值)
* 修改指定key-value键值对 (过期时间不变)
* @param key 键名称
* @param value 值
*/
public void updateValue(String key, String value);
public void update(String key, String value);
/**
* 删除一个指定的key
* @param key 键名称
*/
public void deleteKey(String key);
public void delete(String key);
/**
* 获取指定key的剩余存活时间 (单位: 秒)
@@ -62,6 +62,51 @@ public interface SaTokenDao {
public void updateTimeout(String key, long timeout);
// --------------------- Object相关 ---------------------
/**
* 根据key获取Object,如果没有,则返回空
* @param key 键名称
* @return object
*/
public Object getObject(String key);
/**
* 写入指定键值对,并设定过期时间 (单位: 秒)
* @param key 键名称
* @param object 值
* @param timeout 过期时间 (单位: 秒)
*/
public void setObject(String key, Object object, long timeout);
/**
* 修改指定键值对 (过期时间不变)
* @param key 键名称
* @param object 值
*/
public void updateObject(String key, Object object);
/**
* 删除一个指定的Object
* @param key 键名称
*/
public void deleteObject(String key);
/**
* 获取指定key的剩余存活时间 (单位: 秒)
* @param key 指定key
* @return 这个key的剩余存活时间
*/
public long getObjectTimeout(String key);
/**
* 修改指定key的剩余存活时间 (单位: 秒)
* @param key 指定key
* @param timeout 过期时间
*/
public void updateObjectTimeout(String key, long timeout);
// --------------------- Session相关 ---------------------
/**
@@ -69,40 +114,52 @@ public interface SaTokenDao {
* @param sessionId 键名称
* @return SaSession
*/
public SaSession getSession(String sessionId);
public default SaSession getSession(String sessionId) {
return (SaSession)getObject(sessionId);
}
/**
* 将指定Session持久化
* @param session 要保存的session对象
* @param timeout 过期时间 (单位: 秒)
*/
public void saveSession(SaSession session, long timeout);
public default void setSession(SaSession session, long timeout) {
setObject(session.getId(), session, timeout);
}
/**
* 更新指定session
* 更新指定session
* @param session 要更新的session对象
*/
public void updateSession(SaSession session);
public default void updateSession(SaSession session) {
updateObject(session.getId(), session);
}
/**
* 删除一个指定的session
* @param sessionId sessionId
*/
public void deleteSession(String sessionId);
public default void deleteSession(String sessionId) {
deleteObject(sessionId);
}
/**
* 获取指定SaSession的剩余存活时间 (单位: 秒)
* @param sessionId 指定SaSession
* @return 这个SaSession的剩余存活时间 (单位: 秒)
*/
public long getSessionTimeout(String sessionId);
public default long getSessionTimeout(String sessionId) {
return getObjectTimeout(sessionId);
}
/**
* 修改指定SaSession的剩余存活时间 (单位: 秒)
* @param sessionId sessionId
* @param timeout 过期时间
*/
public void updateSessionTimeout(String sessionId, long timeout);
public default void updateSessionTimeout(String sessionId, long timeout) {
updateObjectTimeout(sessionId, timeout);
}
// --------------------- 会话管理 ---------------------
@@ -113,7 +170,7 @@ public interface SaTokenDao {
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
* @return 查询到的数据集合
*/
public List<String> searchData(String prefix, String keyword, int start, int size);
@@ -4,13 +4,9 @@ package cn.dev33.satoken.dao;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.util.SaTaskUtil;
import cn.dev33.satoken.util.SaTaskUtil.FunctionRunClass;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
@@ -22,7 +18,7 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
/**
* 所有数据集合
* 数据集合
*/
public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
@@ -35,26 +31,26 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
* 构造函数
*/
public SaTokenDaoDefaultImpl() {
initRefreshTimer();
initRefreshThread();
}
// ------------------------ String 读写操作
@Override
public String getValue(String key) {
public String get(String key) {
clearKeyByTimeout(key);
return (String)dataMap.get(key);
}
@Override
public void setValue(String key, String value, long timeout) {
public void set(String key, String value, long timeout) {
dataMap.put(key, value);
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
@Override
public void updateValue(String key, String value) {
public void update(String key, String value) {
if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
@@ -62,7 +58,7 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
}
@Override
public void deleteKey(String key) {
public void delete(String key) {
dataMap.remove(key);
expireMap.remove(key);
}
@@ -76,45 +72,49 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
public void updateTimeout(String key, long timeout) {
expireMap.put(key, System.currentTimeMillis() + timeout * 1000);
}
// ------------------------ Session 读写操作
// ------------------------ Object 读写操作
@Override
public SaSession getSession(String sessionId) {
clearKeyByTimeout(sessionId);
return (SaSession)dataMap.get(sessionId);
public Object getObject(String key) {
clearKeyByTimeout(key);
return dataMap.get(key);
}
@Override
public void saveSession(SaSession session, long timeout) {
dataMap.put(session.getId(), session);
expireMap.put(session.getId(), (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
public void setObject(String key, Object object, long timeout) {
dataMap.put(key, object);
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
@Override
public void updateSession(SaSession session) {
if(getKeyTimeout(session.getId()) == SaTokenDao.NOT_VALUE_EXPIRE) {
public void updateObject(String key, Object object) {
if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
// 无动作
}
@Override
public void deleteSession(String sessionId) {
dataMap.remove(sessionId);
expireMap.remove(sessionId);
public void deleteObject(String key) {
dataMap.remove(key);
expireMap.remove(key);
}
@Override
public long getObjectTimeout(String key) {
return getKeyTimeout(key);
}
@Override
public void updateObjectTimeout(String key, long timeout) {
expireMap.put(key, System.currentTimeMillis() + timeout * 1000);
}
@Override
public long getSessionTimeout(String sessionId) {
return getKeyTimeout(sessionId);
}
@Override
public void updateSessionTimeout(String sessionId, long timeout) {
expireMap.put(sessionId, System.currentTimeMillis() + timeout * 1000);
}
// ------------------------ Session 读写操作
// 使用接口默认实现
// ------------------------ 过期时间相关操作
@@ -163,9 +163,15 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
// --------------------- 定时清理过期数据
/**
* 定时任务对象
* 执行数据清理的线程
*/
public Timer refreshTimer;
public Thread refreshThread;
/**
* 是否继续执行数据清理的线程标记
*/
public boolean refreshFlag;
/**
* 清理所有已经过期的key
@@ -180,30 +186,46 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
/**
* 初始化定时任务
*/
public void initRefreshTimer() {
// 如果已经被初始化过了, 则停止它
if(this.refreshTimer != null) {
this.endRefreshTimer();
}
// 开始新的定时任务
if(SaTokenManager.getConfig().getDataRefreshPeriod() < 0) {
public void initRefreshThread() {
// 如果配置了<=0的值,则不启动定时清理
if(SaTokenManager.getConfig().getDataRefreshPeriod() <= 0) {
return;
}
int period = SaTokenManager.getConfig().getDataRefreshPeriod() * 1000;
this.refreshTimer = SaTaskUtil.setInterval(new FunctionRunClass() {
@Override
public void run() {
refreshDataMap();
// 启动定时刷新
this.refreshFlag = true;
this.refreshThread = new Thread(() -> {
for (;;) {
try {
try {
// 如果已经被标记为结束
if(refreshFlag == false) {
return;
}
// 执行清理
refreshDataMap();
} catch (Exception e) {
e.printStackTrace();
}
// 休眠N秒
int dataRefreshPeriod = SaTokenManager.getConfig().getDataRefreshPeriod();
if(dataRefreshPeriod <= 0) {
dataRefreshPeriod = 1;
}
Thread.sleep(dataRefreshPeriod * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}, period, period);
});
refreshThread.start();
}
/**
* 结束定时任务
*/
public void endRefreshTimer() {
this.refreshTimer.cancel();
public void endRefreshThread() {
this.refreshFlag = false;
}
@@ -215,6 +237,8 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
public List<String> searchData(String prefix, String keyword, int start, int size) {
return SaTokenInsideUtil.searchList(expireMap.keySet(), prefix, keyword, start, size);
}
@@ -7,10 +7,10 @@ import java.util.List;
* 一个异常:代表用户没有登录
* @author kong
*/
public class NotLoginException extends RuntimeException {
public class NotLoginException extends SaTokenException {
/**
*
* 序列化版本号
*/
private static final long serialVersionUID = 6806129545290130142L;
@@ -56,6 +56,7 @@ public class NotLoginException extends RuntimeException {
* 异常类型
*/
private String type;
/**
* 获取异常类型
* @return 异常类型
@@ -69,6 +70,7 @@ public class NotLoginException extends RuntimeException {
* loginKey
*/
private String loginKey;
/**
* 获得loginKey
* @return loginKey
@@ -78,8 +80,6 @@ public class NotLoginException extends RuntimeException {
}
/**
* 构造方法创建一个
* @param message 异常消息
@@ -87,7 +87,6 @@ public class NotLoginException extends RuntimeException {
* @param type 类型
*/
public NotLoginException(String message, String loginKey, String type) {
// 这里到底要不要拼接上login_key呢?纠结
super(message);
this.loginKey = loginKey;
this.type = type;
@@ -101,19 +100,19 @@ public class NotLoginException extends RuntimeException {
*/
public static NotLoginException newInstance(String loginKey, String type) {
String message = null;
if(type.equals(NOT_TOKEN)) {
if(NOT_TOKEN.equals(type)) {
message = NOT_TOKEN_MESSAGE;
}
else if(type.equals(INVALID_TOKEN)) {
else if(INVALID_TOKEN.equals(type)) {
message = INVALID_TOKEN_MESSAGE;
}
else if(type.equals(TOKEN_TIMEOUT)) {
else if(TOKEN_TIMEOUT.equals(type)) {
message = TOKEN_TIMEOUT_MESSAGE;
}
else if(type.equals(BE_REPLACED)) {
else if(BE_REPLACED.equals(type)) {
message = BE_REPLACED_MESSAGE;
}
else if(type.equals(KICK_OUT)) {
else if(KICK_OUT.equals(type)) {
message = KICK_OUT_MESSAGE;
}
else {
@@ -4,48 +4,49 @@ import cn.dev33.satoken.stp.StpUtil;
/**
* 没有指定权限码,抛出的异常
*
* @author kong
*
*/
public class NotPermissionException extends RuntimeException {
public class NotPermissionException extends SaTokenException {
/**
*
* 序列化版本号
*/
private static final long serialVersionUID = 6806129545290130142L;
/** 权限码 */
private String code;
/**
* @return 获得权限码
* @return 获得权限码
*/
public String getCode() {
return code;
}
/**
* loginKey
* loginKey
*/
private String loginKey;
/**
* 获得loginKey
* @return loginKey
/**
* 获得loginKey
*
* @return loginKey
*/
public String getLoginKey() {
return loginKey;
}
public NotPermissionException(String code) {
this(code, StpUtil.stpLogic.loginKey);
}
public NotPermissionException(String code, String loginKey) {
// 这里到底要不要拼接上loginKey呢?纠结
super("无此权限:" + code);
this.code = code;
this.loginKey = loginKey;
}
this(code, StpUtil.stpLogic.loginKey);
}
public NotPermissionException(String code, String loginKey) {
super("无此权限:" + code);
this.code = code;
this.loginKey = loginKey;
}
}
@@ -4,48 +4,50 @@ import cn.dev33.satoken.stp.StpUtil;
/**
* 没有指定角色标识,抛出的异常
*
* @author kong
*
*/
public class NotRoleException extends RuntimeException {
public class NotRoleException extends SaTokenException {
/**
*
* 序列化版本号
*/
private static final long serialVersionUID = 8243974276159004739L;
/** 角色标识 */
private String role;
/**
* @return 获得角色标识
* @return 获得角色标识
*/
public String getRole() {
return role;
}
/**
* loginKey
* loginKey
*/
private String loginKey;
/**
* 获得loginKey
* @return loginKey
/**
* 获得loginKey
*
* @return loginKey
*/
public String getLoginKey() {
return loginKey;
}
public NotRoleException(String role) {
this(role, StpUtil.stpLogic.loginKey);
}
this(role, StpUtil.stpLogic.loginKey);
}
public NotRoleException(String role, String loginKey) {
// 这里到底要不要拼接上loginKey呢?纠结
super("无此角色:" + role);
this.role = role;
this.loginKey = loginKey;
}
super("无此角色:" + role);
this.role = role;
this.loginKey = loginKey;
}
}
@@ -2,24 +2,44 @@ package cn.dev33.satoken.exception;
/**
* sa-token框架内部逻辑发生错误抛出的异常
* (自定义此异常可方便开发者在做全局异常处理时分辨异常类型)
*
* @author kong
*
*/
public class SaTokenException extends RuntimeException {
/**
*
* 序列化版本号
*/
private static final long serialVersionUID = 6806129545290130132L;
/**
* 构建一个异常
* @param message 异常描述信息
* 构建一个异常
*
* @param message 异常描述信息
*/
public SaTokenException(String message) {
super(message);
}
super(message);
}
/**
* 构建一个异常
*
* @param cause 异常对象
*/
public SaTokenException(Throwable cause) {
super(cause);
}
/**
* 构建一个异常
*
* @param message 异常信息
* @param cause 异常对象
*/
public SaTokenException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,49 @@
package cn.dev33.satoken.fun;
/**
* 根据boolean变量,决定是否执行一个函数
*
* @author kong
*
*/
public class IsRunFunction {
/**
* 变量
*/
public final Boolean isRun;
/**
* 设定一个变量,如果为true,则执行exe函数
*
* @param isRun 变量
*/
public IsRunFunction(boolean isRun) {
this.isRun = isRun;
}
/**
* 当 isRun == true 时执行此函数
* @param function 函数
* @return 对象自身
*/
public IsRunFunction exe(SaFunction function) {
if (isRun) {
function.run();
}
return this;
}
/**
* 当 isRun == false 时执行此函数
* @param function 函数
* @return 对象自身
*/
public IsRunFunction noExe(SaFunction function) {
if (!isRun) {
function.run();
}
return this;
}
}
@@ -0,0 +1,16 @@
package cn.dev33.satoken.fun;
/**
* 设定一个函数,方便在Lambda表达式下的函数式编程
*
* @author kong
*
*/
public interface SaFunction {
/**
* 执行的方法
*/
public void run();
}
@@ -0,0 +1,23 @@
package cn.dev33.satoken.router;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 执行验证方法的辅助类
*
* @author kong
*
*/
public interface SaRouteFunction {
/**
* 执行验证的方法
*
* @param request request对象
* @param response response对象
* @param handler 处理对象
*/
public void run(HttpServletRequest request, HttpServletResponse response, Object handler);
}
@@ -0,0 +1,130 @@
package cn.dev33.satoken.router;
import java.util.Arrays;
import java.util.List;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.fun.IsRunFunction;
import cn.dev33.satoken.fun.SaFunction;
/**
* 对路由匹配符相关操作的封装工具类
* @author kong
*
*/
public class SaRouterUtil {
// -------------------- 路由匹配相关 --------------------
/**
* 校验指定路由匹配符是否可以匹配成功指定路径
* @param pattern 路由匹配符
* @param path 需要匹配的路径
* @return 是否匹配成功
*/
public static boolean isMatch(String pattern, String path) {
return SaTokenManager.getSaTokenServlet().matchPath(pattern, path);
}
/**
* 校验指定路由匹配符是否可以匹配成功指定路径
* @param patterns 路由匹配符
* @param path 需要匹配的路径集合
* @return 是否匹配成功
*/
public static boolean isMatch(List<String> patterns, String path) {
for (String pattern : patterns) {
if(isMatch(pattern, path)) {
return true;
}
}
return false;
}
/**
* 校验指定路由匹配符是否可以匹配成功当前URI
* @param pattern 路由匹配符
* @return 是否匹配成功
*/
public static boolean isMatchCurrURI(String pattern) {
return isMatch(pattern, SaTokenManager.getSaTokenServlet().getRequest().getRequestURI());
}
/**
* 校验指定路由匹配符是否可以匹配成功当前URI
* @param patterns 路由匹配符
* @return 是否匹配成功
*/
public static boolean isMatchCurrURI(List<String> patterns) {
return isMatch(patterns, SaTokenManager.getSaTokenServlet().getRequest().getRequestURI());
}
// -------------------- 执行相关 --------------------
/**
* 使用路由匹配符与当前URI执行匹配,如果匹配成功则执行验证函数
* @param pattern 路由匹配符
* @param function 要执行的方法
*/
public static void match(String pattern, SaFunction function) {
if(isMatchCurrURI(pattern)) {
function.run();
}
}
/**
* 使用路由匹配符与当前URI执行匹配 (并指定排除匹配符),如果匹配成功则执行验证函数
* @param pattern 路由匹配符
* @param excludePattern 要排除的路由匹配符
* @param function 要执行的方法
*/
public static void match(String pattern, String excludePattern, SaFunction function) {
if(isMatchCurrURI(pattern)) {
if(isMatchCurrURI(excludePattern) == false) {
function.run();
}
}
}
/**
* 使用路由匹配符集合与当前URI执行匹配,如果匹配成功则执行验证函数
* @param patterns 路由匹配符集合
* @param function 要执行的方法
*/
public static void match(List<String> patterns, SaFunction function) {
if(isMatchCurrURI(patterns)) {
function.run();
}
}
/**
* 使用路由匹配符集合与当前URI执行匹配 (并指定排除匹配符),如果匹配成功则执行验证函数
* @param patterns 路由匹配符集合
* @param excludePatterns 要排除的路由匹配符集合
* @param function 要执行的方法
*/
public static void match(List<String> patterns, List<String> excludePatterns, SaFunction function) {
if(isMatchCurrURI(patterns)) {
if(isMatchCurrURI(excludePatterns) == false) {
function.run();
}
}
}
/**
* 使用路由匹配符集合与当前URI执行匹配,如果匹配成功则执行验证函数
* @param patterns 路由匹配符集合
* @return 匹配结果包装对象
*/
public static IsRunFunction match(String... patterns) {
boolean matchResult = isMatch(Arrays.asList(patterns), SaTokenManager.getSaTokenServlet().getRequest().getRequestURI());
return new IsRunFunction(matchResult);
}
}
@@ -0,0 +1,61 @@
package cn.dev33.satoken.secure;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
/**
* Base64工具类
* @author kong
*
*/
public class SaBase64Util {
private static Base64.Encoder encoder = Base64.getEncoder();
private static Base64.Decoder decoder = Base64.getDecoder();
/**
* Base64编码,byte[] 转 String
* @param bytes byte[]
* @return 字符串
*/
public static String encodeBytesToString(byte[] bytes){
return encoder.encodeToString(bytes);
}
/**
* Base64解码,String 转 byte[]
* @param text 字符串
* @return byte[]
*/
public static byte[] decodeStringToBytes(String text){
return decoder.decode(text);
}
/**
* Base64编码,String 转 String
* @param text 字符串
* @return Base64格式字符串
*/
public static String encode(String text){
try {
return encoder.encodeToString(text.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Base64解码,String 转 String
* @param base64Text Base64格式字符串
* @return 字符串
*/
public static String decode(String base64Text){
try {
return new String(decoder.decode(base64Text), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,443 @@
package cn.dev33.satoken.secure;
import java.security.InvalidParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import cn.dev33.satoken.exception.SaTokenException;
/**
* sa-token 常见加密算法工具类
*
* @author kong
*
*/
public class SaSecureUtil {
/**
* Base64编码
*/
private static Base64.Encoder encoder = Base64.getEncoder();
/**
* Base64解码
*/
private static Base64.Decoder decoder = Base64.getDecoder();
// ----------------------- 摘要加密 -----------------------
/**
* md5加密
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String md5(String str) {
str = (str == null ? "" : str);
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
try {
byte[] btInput = str.getBytes();
MessageDigest mdInst = MessageDigest.getInstance("MD5");
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char[] strA = new char[j * 2];
int k = 0;
for (byte byte0 : md) {
strA[k++] = hexDigits[byte0 >>> 4 & 0xf];
strA[k++] = hexDigits[byte0 & 0xf];
}
return new String(strA);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* sha1加密
*
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String sha1(String str) {
try {
str = (str == null ? "" : str);
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] b = str.getBytes();
md.update(b);
byte[] b2 = md.digest();
int len = b2.length;
String strA = "0123456789abcdef";
char[] ch = strA.toCharArray();
char[] chs = new char[len * 2];
for (int i = 0, k = 0; i < len; i++) {
byte b3 = b2[i];
chs[k++] = ch[b3 >>> 4 & 0xf];
chs[k++] = ch[b3 & 0xf];
}
return new String(chs);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* sha256加密
*
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String sha256(String str) {
try {
str = (str == null ? "" : str);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes("UTF-8"));
byte[] bytes = messageDigest.digest();
StringBuilder builder = new StringBuilder();
String temp;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
builder.append("0");
}
builder.append(temp);
}
return builder.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* md5加盐加密: md5(md5(str) + md5(salt))
* @param str 字符串
* @param salt 盐
* @return 加密后的字符串
*/
public static String md5BySalt(String str, String salt) {
return md5(md5(str) + md5(salt));
}
// ----------------------- 对称加密 AES -----------------------
/**
* 默认密码算法
*/
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* AES加密
*
* @param key 加密的密钥
* @param text 需要加密的字符串
* @return 返回Base64转码后的加密数据
*/
public static String aesEncrypt(String key, String text) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
byte[] byteContent = text.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(byteContent);
return encoder.encodeToString(result);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* AES解密
* @param key 加密的密钥
* @param text 已加密的密文
* @return 返回解密后的数据
*/
public static String aesDecrypt(String key, String text) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(decoder.decode(text));
return new String(result, "utf-8");
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* 生成加密秘钥
* @param password 秘钥
* @return SecretKeySpec
* @throws NoSuchAlgorithmException
*/
private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException {
KeyGenerator kg = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(password.getBytes());
kg.init(128, random);
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), "AES");
}
// ----------------------- 非对称加密 RSA -----------------------
private static final String ALGORITHM = "RSA";
private static final int KEY_SIZE = 1024;
// ---------- 5个常用方法
/**
* 生成密钥对
* @return Map对象 (private=私钥, public=公钥)
* @throws Exception 异常
*/
public static HashMap<String, String> rsaGenerateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
KeyPair keyPair;
try {
keyPairGenerator.initialize(KEY_SIZE,
new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes()));
keyPair = keyPairGenerator.generateKeyPair();
} catch (InvalidParameterException e) {
throw e;
} catch (NullPointerException e) {
throw e;
}
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
HashMap<String, String> map = new HashMap<String, String>(16);
map.put("private", encoder.encodeToString(rsaPrivateKey.getEncoded()));
map.put("public", encoder.encodeToString(rsaPublicKey.getEncoded()));
return map;
}
/**
* RSA公钥加密
* @param publicKeyString 公钥
* @param content 内容
* @return 加密后内容
*/
public static String rsaEncryptByPublic(String publicKeyString, String content) {
try {
// 获得公钥对象
PublicKey publicKey = getPublicKeyFromString(publicKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11;
byte[][] arrays = splitBytes(content.getBytes(), splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(bytesToHexString(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA私钥加密
* @param privateKeyString 私钥
* @param content 内容
* @return 加密后内容
*/
public static String rsaEncryptByPrivate(String privateKeyString, String content) {
try {
PrivateKey privateKey = getPrivateKeyFromString(privateKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11;
byte[][] arrays = splitBytes(content.getBytes(), splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(bytesToHexString(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA公钥解密
* @param publicKeyString 公钥
* @param content 已加密内容
* @return 解密后内容
*/
public static String rsaDecryptByPublic(String publicKeyString, String content) {
try {
PublicKey publicKey = getPublicKeyFromString(publicKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8;
byte[] contentBytes = hexStringToBytes(content);
byte[][] arrays = splitBytes(contentBytes, splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(new String(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA私钥解密
* @param privateKeyString 公钥
* @param content 已加密内容
* @return 解密后内容
*/
public static String rsaDecryptByPrivate(String privateKeyString, String content) {
try {
PrivateKey privateKey = getPrivateKeyFromString(privateKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8;
byte[] contentBytes = hexStringToBytes(content);
byte[][] arrays = splitBytes(contentBytes, splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(new String(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
// ---------- 获取*钥
/** 根据公钥字符串获取 公钥对象 */
private static PublicKey getPublicKeyFromString(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 过滤掉\r\n
key = key.replace("\r\n", "");
// 取得公钥
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decoder.decode(key));
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
return publicKey;
}
/** 根据私钥字符串获取 私钥对象 */
private static PrivateKey getPrivateKeyFromString(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 过滤掉\r\n
key = key.replace("\r\n", "");
// 取得私钥
PKCS8EncodedKeySpec x509KeySpec = new PKCS8EncodedKeySpec(decoder.decode(key));
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(x509KeySpec);
return privateKey;
}
// ---------- 一些辅助方法
/** 根据限定的每组字节长度,将字节数组分组 */
private static byte[][] splitBytes(byte[] bytes, int splitLength) {
// bytes与splitLength的余数
int remainder = bytes.length % splitLength;
// 数据拆分后的组数,余数不为0时加1
int quotient = remainder != 0 ? bytes.length / splitLength + 1 : bytes.length / splitLength;
byte[][] arrays = new byte[quotient][];
byte[] array = null;
for (int i = 0; i < quotient; i++) {
// 如果是最后一组(quotient-1),同时余数不等于0,就将最后一组设置为remainder的长度
if (i == quotient - 1 && remainder != 0) {
array = new byte[remainder];
System.arraycopy(bytes, i * splitLength, array, 0, remainder);
} else {
array = new byte[splitLength];
System.arraycopy(bytes, i * splitLength, array, 0, splitLength);
}
arrays[i] = array;
}
return arrays;
}
/** 将字节数组转换成16进制字符串 */
private static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer(bytes.length);
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(0xFF & bytes[i]);
if (temp.length() < 2) {
sb.append(0);
}
sb.append(temp);
}
return sb.toString();
}
/** 将16进制字符串转换成字节数组 */
private static byte[] hexStringToBytes(String hex) {
int len = (hex.length() / 2);
hex = hex.toUpperCase();
byte[] result = new byte[len];
char[] chars = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(chars[pos]) << 4 | toByte(chars[pos + 1]));
}
return result;
}
/** 将char转换为byte */
private static byte toByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
}
@@ -4,24 +4,33 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet相关操作
* Servlet相关操作接口
*
* @author kong
*
*/
public interface SaTokenServlet {
/**
* 获取当前请求的 Request 对象
* 获取当前请求的 Request 对象
*
* @return 当前请求的Request对象
*/
public HttpServletRequest getRequest();
/**
* 获取当前请求的 Response 对象
* 获取当前请求的 Response 对象
*
* @return 当前请求的response对象
*/
public HttpServletResponse getResponse();
/**
* 校验指定路由匹配符是否可以匹配成功指定路径
* @param pattern 路由匹配符
* @param path 需要匹配的路径
* @return 是否匹配成功
*/
public boolean matchPath(String pattern, String path);
}
@@ -3,28 +3,39 @@ package cn.dev33.satoken.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.dev33.satoken.exception.SaTokenException;
/**
* sa-token 对SaTokenServlet接口默认实现类
* sa-token 对SaTokenServlet接口默认实现类
*
* @author kong
*
*/
public class SaTokenServletDefaultImpl implements SaTokenServlet {
/**
* 获取当前请求的Request对象
* 获取当前请求的Request对象
*/
@Override
public HttpServletRequest getRequest() {
throw new RuntimeException("请实现SaTokenServlet接口后进行Servlet相关操作");
throw new SaTokenException("SaTokenServlet接口未实现");
}
/**
* 获取当前请求的Response对象
* 获取当前请求的Response对象
*/
@Override
public HttpServletResponse getResponse() {
throw new RuntimeException("请实现SaTokenServlet接口后进行Servlet相关操作");
throw new SaTokenException("SaTokenServlet接口未实现");
}
/**
* 校验指定路由匹配符是否可以匹配成功指定路径
*/
@Override
public boolean matchPath(String pattern, String path) {
throw new SaTokenException("SaTokenServlet接口未实现");
}
}
@@ -9,213 +9,494 @@ import java.util.concurrent.ConcurrentHashMap;
import cn.dev33.satoken.SaTokenManager;
/**
* session会话
* Session Model
*
* @author kong
*
*/
public class SaSession implements Serializable {
private static final long serialVersionUID = 1L;
/** 此会话的id */
private String id;
/** 此会话的创建时间 */
/** 此Session的id */
private String id;
/** 此Session的创建时间 */
private long createTime;
/** 此会话的所有数据 */
private Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
/** 此Session的所有挂载数据 */
private final Map<String, Object> dataMap = new ConcurrentHashMap<>();
// ----------------------- 构建相关
/**
* 构建一个 session对象
* 构建一个Session对象
*/
public SaSession() {}
/**
* 构建一个 session对象
* @param id session的id
*/
public SaSession(String id) {
this.id = id;
this.createTime = System.currentTimeMillis();
public SaSession() {
}
/**
* 获取此会话id
* @return 此会话的id
*/
/**
* 构建一个Session对象
* @param id Session的id
*/
public SaSession(String id) {
this.id = id;
this.createTime = System.currentTimeMillis();
}
/**
* 获取此Session的id
* @return 此会话的id
*/
public String getId() {
return id;
}
/**
* 返回当前会话创建时间
* 写入此Session的id
* @param id SessionId
* @return 对象自身
*/
public SaSession setId(String id) {
this.id = id;
return this;
}
/**
* 返回当前会话创建时间
* @return 时间戳
*/
public long getCreateTime() {
return createTime;
}
// ----------------------- tokenSign相关
/**
* 本session绑定的token签名列表
* 写入此Session的创建时间
* @param createTime 时间戳
* @return 对象自身
*/
private List<TokenSign> tokenSignList = new Vector<TokenSign>();
public SaSession setCreateTime(long createTime) {
this.createTime = createTime;
return this;
}
// ----------------------- TokenSign相关
/**
* 返回token签名列表
* 此Session绑定的token签名列表
*/
private final List<TokenSign> tokenSignList = new Vector<>();
/**
* 返回token签名列表的拷贝副本
*
* @return token签名列表
*/
public List<TokenSign> getTokenSignList() {
return new Vector<>(tokenSignList);
}
/**
* 查找一个token签名
* @param tokenValue token值
* 查找一个token签名
*
* @param tokenValue token值
* @return 查找到的tokenSign
*/
public TokenSign getTokenSign(String tokenValue) {
for (TokenSign tokenSign : getTokenSignList()) {
if(tokenSign.getValue().equals(tokenValue)){
return tokenSign;
}
if (tokenSign.getValue().equals(tokenValue)) {
return tokenSign;
}
}
return null;
}
/**
* 添加一个token签名
*
* @param tokenSign token签名
*/
public void addTokenSign(TokenSign tokenSign) {
// 如果已经存在于列表中,则无需再次添加
// 如果已经存在于列表中,则无需再次添加
for (TokenSign tokenSign2 : getTokenSignList()) {
if(tokenSign2.getValue().equals(tokenSign.getValue())){
return;
}
if (tokenSign2.getValue().equals(tokenSign.getValue())) {
return;
}
}
// 添加并更新
// 添加并更新
tokenSignList.add(tokenSign);
update();
}
/**
* 移除一个token签名
* @param tokenValue token名称
*
* @param tokenValue token名称
*/
public void removeTokenSign(String tokenValue) {
TokenSign tokenSign = getTokenSign(tokenValue);
if(tokenSignList.remove(tokenSign)) {
if (tokenSignList.remove(tokenSign)) {
update();
}
}
// ----------------------- 存取值
// ----------------------- 存取值
/**
* 写入一个值
* @param key 名称
* @param value 值
* 写入一个值
*
* @param key 名称
* @param value 值
*/
public void setAttribute(String key, Object value) {
dataMap.put(key, value);
update();
}
/**
* 取出一个值
* 取出一个值
*
* @param key 名称
* @return 值
* @return 值
*/
public Object getAttribute(String key) {
return dataMap.get(key);
}
/**
* 取值,并指定取不到值时的默认值
* @param key 名称
* @param defaultValue 取不到值的时候返回的默认值
* 取值,并指定取不到值时的默认值
*
* @param key 名称
* @param defaultValue 取不到值的时候返回的默认值
* @return value
*/
public Object getAttribute(String key, Object defaultValue) {
Object value = getAttribute(key);
if(value != null) {
if (value != null) {
return value;
}
return defaultValue;
}
/**
* 移除一个值
* 移除一个值
*
* @param key 要移除的值的名字
*/
public void removeAttribute(String key) {
dataMap.remove(key);
update();
}
/**
* 清空所有值
* 清空所有值
*/
public void clearAttribute() {
dataMap.clear();
update();
}
/**
* 是否含有指定key
* @param key 是否含有指定值
* @return 是否含有
* 是否含有指定key
*
* @param key 是否含有指定值
* @return 是否含有
*/
public boolean containsAttribute(String key) {
return dataMap.keySet().contains(key);
public boolean containsAttribute(String key) {
return dataMap.containsKey(key);
}
/**
* 返回当前session会话所有key
* @return 所有值的key列表
* 返回当前session会话所有key
*
* @return 所有值的key列表
*/
public Set<String> attributeKeys() {
return dataMap.keySet();
}
/**
* 获取数据集合(如果更新map里的值,请调用session.update()方法避免数据过时
* @return 返回底层储存值的map对象
* 获取数据挂载集合(如果更新map里的值,请调用session.update()方法避免产生脏数据
*
* @return 返回底层储存值的map对象
*/
public Map<String, Object> getDataMap() {
return dataMap;
}
// ----------------------- 一些操作
/**
* 将这个session从持久库更新一下
* 写入数据集合 (不改变底层对象,只将此dataMap所有数据进行替换)
* @param dataMap 数据集合
*/
public void refreshDataMap(Map<String, Object> dataMap) {
this.dataMap.clear();
this.dataMap.putAll(dataMap);
this.update();
}
// ----------------------- 一些操作
/**
* 将这个Session从持久库更新一下
*/
public void update() {
SaTokenManager.getSaTokenDao().updateSession(this);
}
/** 注销会话(注销后,此session会话将不再存储服务器上) */
/** 注销会话 (注销后,此session会话将不再存储服务器上) */
public void logout() {
SaTokenManager.getSaTokenDao().deleteSession(this.id);
}
/** 如果这个token的tokenSign数量为零,则直接注销会话 */
/** 当Session上的tokenSign数量为零,注销会话 */
public void logoutByTokenSignCountToZero() {
if(tokenSignList.size() == 0) {
if (tokenSignList.size() == 0) {
logout();
}
}
/**
* 获取此Session的剩余存活时间 (单位: 秒)
* @return 此Session的剩余存活时间 (单位: 秒)
*/
public long getTimeout() {
return SaTokenManager.getSaTokenDao().getSessionTimeout(this.id);
}
/**
* 修改此Session的剩余存活时间
* @param timeout 过期时间 (单位: 秒)
*/
public void updateTimeout(long timeout) {
SaTokenManager.getSaTokenDao().updateSessionTimeout(this.id, timeout);
}
/**
* 修改此Session的最小剩余存活时间 (只有在Session的过期时间低于指定的minTimeout时才会进行修改)
* @param minTimeout 过期时间 (单位: 秒)
*/
public void updateMinTimeout(long minTimeout) {
if(getTimeout() < minTimeout) {
SaTokenManager.getSaTokenDao().updateSessionTimeout(this.id, minTimeout);
}
}
/**
* 修改此Session的最大剩余存活时间 (只有在Session的过期时间高于指定的maxTimeout时才会进行修改)
* @param maxTimeout 过期时间 (单位: 秒)
*/
public void updateMaxTimeout(long maxTimeout) {
if(getTimeout() > maxTimeout) {
SaTokenManager.getSaTokenDao().updateSessionTimeout(this.id, maxTimeout);
}
}
// ----------------------- 存取值 (类型转换)
/**
* 写值
* @param key 名称
* @param value 值
*/
public void set(String key, Object value) {
dataMap.put(key, value);
update();
}
/**
* 写值(只有在此key原本无值的时候才会写入)
* @param key 名称
* @param value 值
*/
public void setDefaultValue(String key, Object value) {
if(has(key) == false) {
dataMap.put(key, value);
update();
}
}
/**
* 取值
* @param key key
* @return 值
*/
public Object get(String key) {
return dataMap.get(key);
}
/**
*
* 取值 (指定默认值)
* @param <T> 默认值的类型
* @param key key
* @param defaultValue 取不到值时返回的默认值
* @return 值
*/
public <T> T get(String key, T defaultValue) {
return getValueByDefaultValue(get(key), defaultValue);
}
/**
* 取值 (转String类型)
* @param key key
* @return 值
*/
public String getString(String key) {
Object value = get(key);
if(value == null) {
return null;
}
return String.valueOf(value);
}
/**
* 取值 (转int类型)
* @param key key
* @return 值
*/
public int getInt(String key) {
return getValueByDefaultValue(get(key), 0);
}
/**
* 取值 (转long类型)
* @param key key
* @return 值
*/
public long getLong(String key) {
return getValueByDefaultValue(get(key), 0L);
}
/**
* 取值 (转double类型)
* @param key key
* @return 值
*/
public double getDouble(String key) {
return getValueByDefaultValue(get(key), 0.0);
}
/**
* 取值 (转float类型)
* @param key key
* @return 值
*/
public float getFloat(String key) {
return getValueByDefaultValue(get(key), 0.0f);
}
/**
* 取值 (指定转换类型)
* @param <T> 泛型
* @param key key
* @param cs 指定转换类型
* @return 值
*/
public <T> T getModel(String key, Class<T> cs) {
return getValueByClass(get(key), cs);
}
/**
* 取值 (指定转换类型, 并指定值为Null时返回的默认值)
* @param <T> 泛型
* @param key key
* @param cs 指定转换类型
* @param defaultValue 值为Null时返回的默认值
* @return 值
*/
@SuppressWarnings("unchecked")
public <T> T getModel(String key, Class<T> cs, Object defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return (T)defaultValue;
}
return getValueByClass(value, cs);
}
/**
* 是否含有某个key
* @param key has
* @return 是否含有
*/
public boolean has(String key) {
return !valueIsNull(get(key));
}
// --------- 工具方法
/**
* 判断一个值是否为null
* @param value 指定值
* @return 此value是否为null
*/
public boolean valueIsNull(Object value) {
return value == null || value.equals("");
}
/**
* 将指定值转化为指定类型
* @param <T> 泛型
* @param obj 值
* @param cs 类型
* @return 转换后的值
*/
@SuppressWarnings("unchecked")
protected <T> T getValueByClass(Object obj, Class<T> cs) {
// 如果 obj 本来就是 cs 类型
if(obj != null && obj.getClass().equals(cs)) {
return (T)obj;
}
// 开始转换
String obj2 = String.valueOf(obj);
Object obj3 = null;
if (cs.equals(String.class)) {
obj3 = obj2;
} else if (cs.equals(int.class) || cs.equals(Integer.class)) {
obj3 = Integer.valueOf(obj2);
} else if (cs.equals(long.class) || cs.equals(Long.class)) {
obj3 = Long.valueOf(obj2);
} else if (cs.equals(short.class) || cs.equals(Short.class)) {
obj3 = Short.valueOf(obj2);
} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
obj3 = Byte.valueOf(obj2);
} else if (cs.equals(float.class) || cs.equals(Float.class)) {
obj3 = Float.valueOf(obj2);
} else if (cs.equals(double.class) || cs.equals(Double.class)) {
obj3 = Double.valueOf(obj2);
} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
obj3 = Boolean.valueOf(obj2);
} else {
obj3 = (T)obj;
}
return (T)obj3;
}
/**
* 根据默认值来获取值
* @param <T> 泛型
* @param value 值
* @param defaultValue 默认值
* @return 转换后的值
*/
@SuppressWarnings("unchecked")
protected <T> T getValueByDefaultValue(Object value, T defaultValue) {
// 如果 obj 为 null,则直接返回默认值
if(valueIsNull(value)) {
return (T)defaultValue;
}
// 开始转换
Class<T> cs = (Class<T>) defaultValue.getClass();
return getValueByClass(value, cs);
}
}
@@ -3,68 +3,71 @@ package cn.dev33.satoken.session;
import cn.dev33.satoken.SaTokenManager;
/**
* 自定义sa-session工具类
* 自定义Session工具类
*
* @author kong
*
*/
public class SaSessionCustomUtil {
/**
* 添加上指定前缀,防止恶意伪造session
* 添加上指定前缀,防止恶意伪造session
*/
public static String sessionKey = "custom";
/**
* 组织一下自定义session的id
* 组织一下自定义Session的id
*
* @param sessionId 会话id
* @return sessionId
*/
public static String getSessionKey(String sessionId) {
public static String splicingSessionKey(String sessionId) {
return SaTokenManager.getConfig().getTokenName() + ":" + sessionKey + ":session:" + sessionId;
}
/**
* 指定key的session是否存在
/**
* 验证指定key的Session是否存在
*
* @param sessionId session的id
* @return 是否存在
* @return 是否存在
*/
public boolean isExists(String sessionId) {
return SaTokenManager.getSaTokenDao().getSession(getSessionKey(sessionId)) != null;
return SaTokenManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)) != null;
}
/**
* 获取指定key的session
/**
* 获取指定key的Session
*
* @param sessionId key
* @param isCreate 如果没有,是否新建并返回
* @param isCreate 如果此Session尚未在DB创建,是否新建并返回
* @return SaSession
*/
public static SaSession getSessionById(String sessionId, boolean isCreate) {
SaSession session = SaTokenManager.getSaTokenDao().getSession(getSessionKey(sessionId));
if(session == null && isCreate) {
session = new SaSession(getSessionKey(sessionId));
SaTokenManager.getSaTokenDao().saveSession(session, SaTokenManager.getConfig().getTimeout());
SaSession session = SaTokenManager.getSaTokenDao().getSession(splicingSessionKey(sessionId));
if (session == null && isCreate) {
session = SaTokenManager.getSaTokenAction().createSession(sessionId);
SaTokenManager.getSaTokenDao().setSession(session, SaTokenManager.getConfig().getTimeout());
}
return session;
}
/**
* 获取指定key的session, 如果没有则新建并返回
/**
* 获取指定key的Session, 如果此Session尚未在DB创建,则新建并返回
*
* @param sessionId key
* @return session对象
* @return session对象
*/
public static SaSession getSessionById(String sessionId) {
return getSessionById(sessionId, true);
}
/**
* 删除指定key的session
* @param sessionId 删除指定key
/**
* 删除指定key的session
*
* @param sessionId 指定key
*/
public static void deleteSessionById(String sessionId) {
SaTokenManager.getSaTokenDao().deleteSession(getSessionKey(sessionId));
SaTokenManager.getSaTokenDao().deleteSession(splicingSessionKey(sessionId));
}
}
@@ -3,66 +3,62 @@ package cn.dev33.satoken.session;
import java.io.Serializable;
/**
* 挂在到SaSession上的token签名
* token签名 Model
*
* 挂在到SaSession上的token签名
*
* @author kong
*
*/
public class TokenSign implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1406115065849845073L;
/**
* token值
* token值
*/
private String value;
/**
* 所在设备标识
* 所在设备标识
*/
private String device;
/** 构建一个 */
public TokenSign() {}
/**
* 构建一个
* @param value token值
* @param device 所在设备标识
*/
public TokenSign(String value, String device) {
this.value = value;
this.device = device;
public TokenSign() {
}
/**
* @return value
* 构建一个
*
* @param value token值
* @param device 所在设备标识
*/
public TokenSign(String value, String device) {
this.value = value;
this.device = device;
}
/**
* @return token value
*/
public String getValue() {
return value;
}
/**
* @return device
* @return token登录设备
*/
public String getDevice() {
return device;
}
@Override
public String toString() {
return "TokenSign [value=" + value + ", device=" + device + "]";
}
}
@@ -0,0 +1,142 @@
package cn.dev33.satoken.stp;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaTokenConsts;
/**
* 调用 `StpUtil.setLogin()` 时的 [配置参数 Model ]
* @author kong
*
*/
public class SaLoginModel {
/**
* 此次登录的客户端设备标识
*/
public String device;
/**
* 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
*/
public Boolean isLastingCookie;
/**
* 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
*/
public Long timeout;
/**
* @return device
*/
public String getDevice() {
return device;
}
/**
* @param device 要设置的 device
* @return 对象自身
*/
public SaLoginModel setDevice(String device) {
this.device = device;
return this;
}
/**
* @return isLastingCookie
*/
public Boolean getIsLastingCookie() {
return isLastingCookie;
}
/**
* @param isLastingCookie 要设置的 isLastingCookie
* @return 对象自身
*/
public SaLoginModel setIsLastingCookie(Boolean isLastingCookie) {
this.isLastingCookie = isLastingCookie;
return this;
}
/**
* @return timeout
*/
public Long getTimeout() {
return timeout;
}
/**
* @param timeout 要设置的 timeout
* @return 对象自身
*/
public SaLoginModel setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* @return cookie时长
*/
public int getCookieTimeout() {
if(isLastingCookie == false) {
return -1;
}
if(timeout == SaTokenDao.NEVER_EXPIRE) {
return Integer.MAX_VALUE;
}
return (int)(long)timeout;
}
/**
* 构建对象,初始化默认值
* @return 对象自身
*/
public SaLoginModel build() {
return build(SaTokenManager.getConfig());
}
/**
* 构建对象,初始化默认值
* @param config 配置对象
* @return 对象自身
*/
public SaLoginModel build(SaTokenConfig config) {
if(device == null) {
device = SaTokenConsts.DEFAULT_LOGIN_DEVICE;
}
if(isLastingCookie == null) {
isLastingCookie = true;
}
if(timeout == null) {
timeout = config.getTimeout();
}
return this;
}
/**
* 静态方法获取一个 SaLoginModel 对象
* @return SaLoginModel 对象
*/
public static SaLoginModel create() {
return new SaLoginModel();
}
/**
* toString
*/
@Override
public String toString() {
return "SaLoginModel [device=" + device + ", isLastingCookie=" + isLastingCookie + ", timeout=" + timeout + "]";
}
}
@@ -1,7 +1,8 @@
package cn.dev33.satoken.stp;
/**
* 用来描述一个token常用信息的类
* token信息Model: 用来描述一个token常用参数
*
* @author kong
*
*/
@@ -13,177 +14,173 @@ public class SaTokenInfo {
/** token值 */
public String tokenValue;
/** 当前是否已经登录 */
/** 此token是否已经登录 */
public Boolean isLogin;
/** 当前loginId,未登录时为null */
/** 此token对应的LoginId,未登录时为null */
public Object loginId;
/** 当前loginKey */
/** LoginKey账号体系标识 */
public String loginKey;
/** token剩余有效期 (单位: 秒) */
public long tokenTimeout;
/** session剩余有效时间 (单位: 秒) */
/** User-Session剩余有效时间 (单位: 秒) */
public long sessionTimeout;
/** token专属session剩余有效时间 (单位: 秒) */
/** Token-Session剩余有效时间 (单位: 秒) */
public long tokenSessionTimeout;
/**
* token剩余无操作有效时间
*/
/** token剩余无操作有效时间 (单位: 秒) */
public long tokenActivityTimeout;
/** 当前登录设备 */
/** 登录设备标识 */
public String loginDevice;
/**
* @return tokenName
* @return token名称
*/
public String getTokenName() {
return tokenName;
}
/**
* @param tokenName 要设置的 tokenName
* @param tokenName token名称
*/
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
}
/**
* @return tokenValue
* @return token
*/
public String getTokenValue() {
return tokenValue;
}
/**
* @param tokenValue 要设置的 tokenValue
* @param tokenValue token值
*/
public void setTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
}
/**
* @return isLogin
* @return 此token是否已经登录
*/
public Boolean getIsLogin() {
return isLogin;
}
/**
* @param isLogin 要设置的 isLogin
* @param isLogin 此token是否已经登录
*/
public void setIsLogin(Boolean isLogin) {
this.isLogin = isLogin;
}
/**
* @return loginId
* @return 此token对应的LoginId,未登录时为null
*/
public Object getLoginId() {
return loginId;
}
/**
* @param loginId 要设置的 loginId
* @param loginId 此token对应的LoginId,未登录时为null
*/
public void setLoginId(Object loginId) {
this.loginId = loginId;
}
/**
* @return loginKey
* @return LoginKey账号体系标识
*/
public String getLoginKey() {
return loginKey;
}
/**
* @param loginKey 要设置的 loginKey
* @param loginKey LoginKey账号体系标识
*/
public void setLoginKey(String loginKey) {
this.loginKey = loginKey;
}
/**
* @return tokenTimeout
* @return token剩余有效期 (单位: 秒)
*/
public long getTokenTimeout() {
return tokenTimeout;
}
/**
* @param tokenTimeout 要设置的 tokenTimeout
* @param tokenTimeout token剩余有效期 (单位: 秒)
*/
public void setTokenTimeout(long tokenTimeout) {
this.tokenTimeout = tokenTimeout;
}
/**
* @return sessionTimeout
* @return User-Session剩余有效时间 (单位: 秒)
*/
public long getSessionTimeout() {
return sessionTimeout;
}
/**
* @param sessionTimeout 要设置的 sessionTimeout
* @param sessionTimeout User-Session剩余有效时间 (单位: 秒)
*/
public void setSessionTimeout(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
/**
* @return tokenSessionTimeout
* @return Token-Session剩余有效时间 (单位: 秒)
*/
public long getTokenSessionTimeout() {
return tokenSessionTimeout;
}
/**
* @param tokenSessionTimeout 要设置的 tokenSessionTimeout
* @param tokenSessionTimeout Token-Session剩余有效时间 (单位: 秒)
*/
public void setTokenSessionTimeout(long tokenSessionTimeout) {
this.tokenSessionTimeout = tokenSessionTimeout;
}
/**
* @return tokenActivityTimeout
* @return token剩余无操作有效时间 (单位: 秒)
*/
public long getTokenActivityTimeout() {
return tokenActivityTimeout;
}
/**
* @param tokenActivityTimeout 要设置的 tokenActivityTimeout
* @param tokenActivityTimeout token剩余无操作有效时间 (单位: 秒)
*/
public void setTokenActivityTimeout(long tokenActivityTimeout) {
this.tokenActivityTimeout = tokenActivityTimeout;
}
/**
* @return loginDevice
* @return 登录设备标识
*/
public String getLoginDevice() {
return loginDevice;
}
/**
* @param loginDevice 要设置的 loginDevice
* @param loginDevice 登录设备标识
*/
public void setLoginDevice(String loginDevice) {
this.loginDevice = loginDevice;
}
/**
* toString
*/
@Override
public String toString() {
return "SaTokenInfo [tokenName=" + tokenName + ", tokenValue=" + tokenValue + ", isLogin=" + isLogin
@@ -191,18 +188,5 @@ public class SaTokenInfo {
+ ", sessionTimeout=" + sessionTimeout + ", tokenSessionTimeout=" + tokenSessionTimeout
+ ", tokenActivityTimeout=" + tokenActivityTimeout + ", loginDevice=" + loginDevice + "]";
}
}
@@ -3,26 +3,28 @@ package cn.dev33.satoken.stp;
import java.util.List;
/**
* 开放权限证接口,方便重写
* 权限证接口,实现此接口即可集成权限认证功能
*
* @author kong
*/
public interface StpInterface {
/**
* 返回指定loginId所拥有的权限码集合
* @param loginId 账号id
* @param loginKey 具体的stp标识
* @return 该账号id具有的权限码集合
* 返回指定 LoginId 所拥有的权限码集合
*
* @param loginId 账号id
* @param loginKey 账号体系标识
* @return 该账号id具有的权限码集合
*/
public List<String> getPermissionList(Object loginId, String loginKey);
/**
* 返回指定loginId所拥有的角色标识集合
* @param loginId 账号id
* @param loginKey 具体的stp标识
* @return 该账号id具有的角色标识集合
*
* @param loginId 账号id
* @param loginKey 账号体系标识
* @return 该账号id具有的角色标识集合
*/
public List<String> getRoleList(Object loginId, String loginKey);
}
@@ -4,7 +4,10 @@ import java.util.ArrayList;
import java.util.List;
/**
* 对StpInterface接口默认的实现类
* 对StpInterface接口默认的实现类
* <p>
* 如果开发者没有实现StpInterface接口,则使用此默认实现
*
* @author kong
*/
public class StpInterfaceDefaultImpl implements StpInterface {
@@ -8,6 +8,7 @@ import java.util.Objects;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.annotation.SaCheckLogin;
@@ -19,9 +20,11 @@ import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.fun.SaFunction;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.session.TokenSign;
import cn.dev33.satoken.util.SaTokenConsts;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
* sa-token 权限验证,逻辑实现类
@@ -37,13 +40,33 @@ public class StpLogic {
public String loginKey = "";
/**
* 初始化StpLogic, 并制定loginKey
* @param loginKey 账号标识
* 初始化StpLogic, 并指定LoginKey
* @param loginKey 账号体系标识
*/
public StpLogic(String loginKey) {
this.loginKey = loginKey;
// 在 SaTokenManager 中记录下此 StpLogic,以便根据 LoginKey 进行查找此对象
SaTokenManager.putStpLogic(this);
}
/**
* 获取当前StpLogin的LoginKey
* @return 当前StpLogin的loginKey
*/
public String getLoginKey(){
return loginKey;
}
/**
* 写入当前StpLogin的LoginKey
* @param loginKey loginKey
* @return 对象自身
*/
public StpLogic setLoginKey(String loginKey){
this.loginKey = loginKey;
return this;
}
// =================== 获取token 相关 ===================
@@ -52,21 +75,46 @@ public class StpLogic {
* @return 此StpLogic的token名称
*/
public String getTokenName() {
return getKeyTokenName();
return splicingKeyTokenName();
}
/**
* 随机生成一个tokenValue
* 创建一个tokenValue
* @param loginId loginId
* @return 生成的tokenValue
*/
public String createTokenValue(Object loginId) {
// 去除掉所有逗号
return SaTokenManager.getSaTokenAction().createToken(loginId, loginKey).replaceAll(",", "");
return SaTokenManager.getSaTokenAction().createToken(loginId, loginKey);
}
/**
* 在当前会话写入当前tokenValue
* @param tokenValue token值
* @param cookieTimeout Cookie存活时间(秒)
*/
public void setTokenValue(String tokenValue, int cookieTimeout){
SaTokenConfig config = getConfig();
// 将token保存到本次request里
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
// 判断是否配置了token前缀
String tokenPrefix = config.getTokenPrefix();
if(SaTokenInsideUtil.isEmpty(tokenPrefix)) {
request.setAttribute(splicingKeyJustCreatedSave(), tokenValue);
} else {
// 如果配置了token前缀,则拼接上前缀一起写入
request.setAttribute(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue);
}
// 注入Cookie
if(config.getIsReadCookie() == true){
HttpServletResponse response = SaTokenManager.getSaTokenServlet().getResponse();
SaTokenManager.getSaTokenCookie().addCookie(response, getTokenName(), tokenValue,
"/", config.getCookieDomain(), cookieTimeout);
}
}
/**
* 获取当前tokenValue
* 获取当前tokenValue
* @return 当前tokenValue
*/
public String getTokenValue(){
@@ -77,37 +125,40 @@ public class StpLogic {
String tokenValue = null;
// 1. 尝试从request里读取
if(request.getAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY) != null) {
tokenValue = String.valueOf(request.getAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY));
if(request.getAttribute(splicingKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(request.getAttribute(splicingKeyJustCreatedSave()));
}
// 2. 尝试从请求体里面读取
if(tokenValue == null && config.getIsReadBody() == true){
if(tokenValue == null && config.getIsReadBody()){
tokenValue = request.getParameter(keyTokenName);
}
// 3. 尝试从header读取
if(tokenValue == null && config.getIsReadHead() == true){
// 3. 尝试从header读取
if(tokenValue == null && config.getIsReadHead()){
tokenValue = request.getHeader(keyTokenName);
}
// 4. 尝试从cookie里读取
if(tokenValue == null && config.getIsReadCookie() == true){
if(tokenValue == null && config.getIsReadCookie()){
Cookie cookie = SaTokenManager.getSaTokenCookie().getCookie(request, keyTokenName);
if(cookie != null){
tokenValue = cookie.getValue();
}
}
// 5. 返回
// 5. 如果打开了前缀模式
String tokenPrefix = getConfig().getTokenPrefix();
if(SaTokenInsideUtil.isEmpty(tokenPrefix) == false && SaTokenInsideUtil.isEmpty(tokenValue) == false) {
// 如果token以指定的前缀开头, 则裁剪掉它, 否则视为未提供token
if(tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT)) {
tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length());
} else {
tokenValue = null;
}
}
// 6. 返回
return tokenValue;
}
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public String getLoginKey(){
return loginKey;
}
/**
* 获取当前会话的token信息
* @return token信息
@@ -135,75 +186,89 @@ public class StpLogic {
* @param loginId 登录id,建议的类型:(long | int | String
*/
public void setLoginId(Object loginId) {
setLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE);
setLoginId(loginId, new SaLoginModel());
}
/**
* 在当前会话上登录id
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id,建议的类型:(long | int | String
* @param device 设备标识
*/
public void setLoginId(Object loginId, String device) {
// ------ 0、如果当前会话已经登录上了此LoginId,且登录设备相同,则立即返回
Object loggedId = getLoginIdDefaultNull();
if(loggedId != null && loggedId.toString().equals(loginId.toString())) {
String loggedDevice = getLoginDevice();
if(loggedDevice != null && loggedDevice.equals(device)) {
return;
}
}
setLoginId(loginId, new SaLoginModel().setDevice(device));
}
/**
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id,建议的类型:(long | int | String
* @param isLastingCookie 是否为持久Cookie
*/
public void setLoginId(Object loginId, boolean isLastingCookie) {
setLoginId(loginId, new SaLoginModel().setIsLastingCookie(isLastingCookie));
}
/**
* 在当前会话上登录id, 并指定所有登录参数Model
* @param loginId 登录id,建议的类型:(long | int | String
* @param loginModel 此次登录的参数Model
*/
public void setLoginId(Object loginId, SaLoginModel loginModel) {
// ------ 1、获取相应对象
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
SaTokenConfig config = getConfig();
SaTokenDao dao = SaTokenManager.getSaTokenDao();
loginModel.build(config);
// ------ 2、生成一个token
String tokenValue = null;
// --- 如果允许并发登录
if(config.getAllowConcurrentLogin() == true) {
// 如果配置为共享token, 则尝试从session签名记录里取出token
// 如果配置为共享token, 则尝试从Session签名记录里取出token
if(config.getIsShare() == true) {
tokenValue = getTokenValueByLoginId(loginId, device);
tokenValue = getTokenValueByLoginId(loginId, loginModel.getDevice());
}
} else {
// --- 如果不允许并发登录
// 如果此时[id-session]不为null,说明此账号在其他地正在登录,现在需要先把其它地的同设备token标记为被顶下线
// 如果此时[user-session]不为null,说明此账号在其他地正在登录,现在需要先把其它地的同设备token标记为被顶下线
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
List<TokenSign> tokenSignList = session.getTokenSignList();
for (TokenSign tokenSign : tokenSignList) {
if(tokenSign.getDevice().equals(device)) {
dao.updateValue(getKeyTokenValue(tokenSign.getValue()), NotLoginException.BE_REPLACED); // 1. 将此token 标记为已顶替
clearLastActivity(tokenSign.getValue()); // 2. 清理掉[token-最后操作时间]
session.removeTokenSign(tokenSign.getValue()); // 3. 清理账号session上的token签名记录
if(tokenSign.getDevice().equals(loginModel.getDevice())) {
// 1. 将此token 标记为已顶替
dao.update(splicingKeyTokenValue(tokenSign.getValue()), NotLoginException.BE_REPLACED);
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenSign.getValue());
// 3. 清理user-session上的token签名记录
session.removeTokenSign(tokenSign.getValue());
}
}
}
}
// 如果至此,仍未成功创建tokenValue
// 如果至此,仍未成功创建tokenValue, 则开始生成一个
if(tokenValue == null) {
tokenValue = createTokenValue(loginId);
}
// ------ 3. 获取[id-session] (如果还没有创建session, 则新建, 如果已经创建,则续期)
// ------ 3. 获取[User-Session] (如果还没有创建session, 则新建, 如果已经创建,则续期)
SaSession session = getSessionByLoginId(loginId, false);
if(session == null) {
session = getSessionByLoginId(loginId);
} else {
dao.updateSessionTimeout(session.getId(), config.getTimeout());
session.updateMinTimeout(loginModel.getTimeout());
}
// 在session上记录token签名
session.addTokenSign(new TokenSign(tokenValue, device));
session.addTokenSign(new TokenSign(tokenValue, loginModel.getDevice()));
// ------ 4. 持久化其它数据
dao.setValue(getKeyTokenValue(tokenValue), String.valueOf(loginId), config.getTimeout()); // token -> uid
request.setAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY, tokenValue); // 将token保存到本次request里
setLastActivityToNow(tokenValue); // 写入 [最后操作时间]
if(config.getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)config.getTimeout());
}
// token -> uid
dao.set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), loginModel.getTimeout());
// 写入 [最后操作时间]
setLastActivityToNow(tokenValue);
// 在当前会话写入当前tokenValue
setTokenValue(tokenValue, loginModel.getCookieTimeout());
}
/**
@@ -231,20 +296,20 @@ public class StpLogic {
clearLastActivity(tokenValue);
// 2. 尝试清除token-id键值对 (先从db中获取loginId值,如果根本查不到loginId,那么无需继续操作 )
String loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
String loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) {
return;
}
SaTokenManager.getSaTokenDao().deleteKey(getKeyTokenValue(tokenValue));
SaTokenManager.getSaTokenDao().delete(splicingKeyTokenValue(tokenValue));
// 2. 尝试清理账号session上的token签名 (如果为null或已被标记为异常, 那么无需继续执行 )
// 3. 尝试清理账号session上的token签名 (如果为null或已被标记为异常, 那么无需继续执行 )
SaSession session = getSessionByLoginId(loginId, false);
if(session == null) {
return;
}
session.removeTokenSign(tokenValue);
// 3. 尝试注销session
// 4. 尝试注销session
session.logoutByTokenSignCountToZero();
}
@@ -264,13 +329,13 @@ public class StpLogic {
* @param device 设备标识 (填null代表所有注销设备)
*/
public void logoutByLoginId(Object loginId, String device) {
// 先获取这个账号的[id-session], 如果为null,则不执行任何操作
// 1. 先获取这个账号的[id-session], 如果为null,则不执行任何操作
SaSession session = getSessionByLoginId(loginId);
if(session == null) {
return;
}
// 循环token签名列表,开始删除相关信息
// 2. 循环token签名列表,开始删除相关信息
List<TokenSign> tokenSignList = session.getTokenSignList();
for (TokenSign tokenSign : tokenSignList) {
if(device == null || tokenSign.getDevice().equals(device)) {
@@ -279,12 +344,12 @@ public class StpLogic {
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenValue);
// 3. 标记:已被踢下线
SaTokenManager.getSaTokenDao().updateValue(getKeyTokenValue(tokenValue), NotLoginException.KICK_OUT); // 标记:已被踢下线
SaTokenManager.getSaTokenDao().update(splicingKeyTokenValue(tokenValue), NotLoginException.KICK_OUT);
// 4. 清理账号session上的token签名
session.removeTokenSign(tokenValue);
}
}
// 尝试注销session
// 3. 尝试注销session
session.logoutByTokenSignCountToZero();
}
@@ -311,13 +376,17 @@ public class StpLogic {
* @return 账号id
*/
public Object getLoginId() {
// 如果获取不到token,则抛出:无token
// 如果正在[临时身份切换], 则返回临时身份
if(isSwitch()) {
return getSwitchLoginId();
}
// 如果获取不到token,则抛出: 无token
String tokenValue = getTokenValue();
if(tokenValue == null) {
throw NotLoginException.newInstance(loginKey, NotLoginException.NOT_TOKEN);
}
// 查找此token对应loginId, 则抛出:无效token
String loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
// 查找此token对应loginId, 如果找不到则抛出:无效token
String loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null) {
throw NotLoginException.newInstance(loginKey, NotLoginException.INVALID_TOKEN);
}
@@ -329,14 +398,16 @@ public class StpLogic {
if(loginId.equals(NotLoginException.BE_REPLACED)) {
throw NotLoginException.newInstance(loginKey, NotLoginException.BE_REPLACED);
}
// 如果是已经被踢下线了, 则抛出:已被踢下线
// 如果是已经被踢下线了, 则抛出:已被踢下线
if(loginId.equals(NotLoginException.KICK_OUT)) {
throw NotLoginException.newInstance(loginKey, NotLoginException.KICK_OUT);
}
// 检查是否已经 [临时过期],同时更新[最后操作时间]
checkActivityTimeout(tokenValue);
updateLastActivityToNow(tokenValue);
// 至此,返回loginId
// 如果配置了自动续签, 则: 检查是否已经 [临时过期],同时更新[最后操作时间]
if(getConfig().getAutoRenew()) {
checkActivityTimeout(tokenValue);
updateLastActivityToNow(tokenValue);
}
// 至此,返回loginId
return loginId;
}
@@ -371,13 +442,17 @@ public class StpLogic {
* @return 账号id
*/
public Object getLoginIdDefaultNull() {
// 如果正在[临时身份切换]
if(isSwitch()) {
return getSwitchLoginId();
}
// 如果连token都是空的,则直接返回
String tokenValue = getTokenValue();
if(tokenValue == null) {
return null;
}
// loginId为null或者在异常项里面,均视为未登录
Object loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
// loginId为null或者在异常项里面,均视为未登录, 返回null
Object loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) {
return null;
}
@@ -402,10 +477,6 @@ public class StpLogic {
* @return 账号id
*/
public int getLoginIdAsInt() {
// Object loginId = getLoginId();
// if(loginId instanceof Integer) {
// return (Integer)loginId;
// }
return Integer.valueOf(String.valueOf(getLoginId()));
}
@@ -414,10 +485,6 @@ public class StpLogic {
* @return 账号id
*/
public long getLoginIdAsLong() {
// Object loginId = getLoginId();
// if(loginId instanceof Long) {
// return (Long)loginId;
// }
return Long.valueOf(String.valueOf(getLoginId()));
}
@@ -427,15 +494,21 @@ public class StpLogic {
* @return 登录id
*/
public Object getLoginIdByToken(String tokenValue) {
if(tokenValue != null) {
Object loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
if(loginId != null) {
return loginId;
}
if(tokenValue == null) {
return null;
}
return null;
return getLoginIdNotHandle(tokenValue);
}
/**
* 获取指定token对应的登录id (不做任何特殊处理)
* @param tokenValue token值
* @return loginId
*/
public String getLoginIdNotHandle(String tokenValue) {
return SaTokenManager.getSaTokenDao().get(splicingKeyTokenValue(tokenValue));
}
// =================== session相关 ===================
@@ -448,8 +521,8 @@ public class StpLogic {
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
SaSession session = SaTokenManager.getSaTokenDao().getSession(sessionId);
if(session == null && isCreate) {
session = new SaSession(sessionId);
SaTokenManager.getSaTokenDao().saveSession(session, getConfig().getTimeout());
session = SaTokenManager.getSaTokenAction().createSession(sessionId);
SaTokenManager.getSaTokenDao().setSession(session, getConfig().getTimeout());
}
return session;
}
@@ -470,7 +543,7 @@ public class StpLogic {
* @return SaSession
*/
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
return getSessionBySessionId(getKeySession(loginId), isCreate);
return getSessionBySessionId(splicingKeySession(loginId), isCreate);
}
/**
@@ -509,7 +582,7 @@ public class StpLogic {
* @return session会话
*/
public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {
return getSessionBySessionId(getKeyTokenSession(tokenValue), isCreate);
return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate);
}
/**
@@ -518,7 +591,7 @@ public class StpLogic {
* @return session会话
*/
public SaSession getTokenSessionByToken(String tokenValue) {
return getSessionBySessionId(getKeyTokenSession(tokenValue), true);
return getSessionBySessionId(splicingKeyTokenSession(tokenValue), true);
}
/**
@@ -534,17 +607,17 @@ public class StpLogic {
// 如果配置忽略token登录校验,则必须保证token不为null (token为null的时候随机创建一个)
String tokenValue = getTokenValue();
if(tokenValue == null || Objects.equals(tokenValue, "")) {
// 随机一个token送给ta
// 随机一个token送给Ta
tokenValue = createTokenValue(null);
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY, tokenValue);
setLastActivityToNow(tokenValue); // 写入 [最后操作时间]
if(getConfig().getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)getConfig().getTimeout());
}
// 写入 [最后操作时间]
setLastActivityToNow(tokenValue);
// 在当前会话写入这个tokenValue
int cookieTimeout = (int)(getConfig().getTimeout() == SaTokenDao.NEVER_EXPIRE ? Integer.MAX_VALUE : getConfig().getTimeout());
setTokenValue(tokenValue, cookieTimeout);
}
}
// 返回这个token对应的专属session
return getSessionBySessionId(getKeyTokenSession(getTokenValue()), isCreate);
return getSessionBySessionId(splicingKeyTokenSession(getTokenValue()), isCreate);
}
/**
@@ -568,7 +641,7 @@ public class StpLogic {
return;
}
// 将[最后操作时间]标记为当前时间戳
SaTokenManager.getSaTokenDao().setValue(getKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout());
SaTokenManager.getSaTokenDao().set(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout());
}
/**
@@ -581,7 +654,7 @@ public class StpLogic {
return;
}
// 删除[最后操作时间]
SaTokenManager.getSaTokenDao().deleteKey(getKeyLastActivityTime(tokenValue));
SaTokenManager.getSaTokenDao().delete(splicingKeyLastActivityTime(tokenValue));
// 清除标记
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY);
}
@@ -633,7 +706,7 @@ public class StpLogic {
if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) {
return;
}
SaTokenManager.getSaTokenDao().updateValue(getKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()));
SaTokenManager.getSaTokenDao().update(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()));
}
/**
@@ -653,7 +726,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenTimeout() {
return SaTokenManager.getSaTokenDao().getTimeout(getKeyTokenValue(getTokenValue()));
return SaTokenManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValue()));
}
/**
@@ -662,7 +735,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenTimeoutByLoginId(Object loginId) {
return SaTokenManager.getSaTokenDao().getTimeout(getKeyTokenValue(getTokenValueByLoginId(loginId)));
return SaTokenManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId)));
}
/**
@@ -679,7 +752,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getSessionTimeoutByLoginId(Object loginId) {
return SaTokenManager.getSaTokenDao().getSessionTimeout(getKeySession(loginId));
return SaTokenManager.getSaTokenDao().getSessionTimeout(splicingKeySession(loginId));
}
/**
@@ -696,7 +769,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenSessionTimeoutByTokenValue(String tokenValue) {
return SaTokenManager.getSaTokenDao().getSessionTimeout(getKeyTokenSession(tokenValue));
return SaTokenManager.getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue));
}
/**
@@ -723,8 +796,8 @@ public class StpLogic {
}
// ------ 开始查询
// 获取相关数据
String keyLastActivityTime = getKeyLastActivityTime(tokenValue);
String lastActivityTimeString = SaTokenManager.getSaTokenDao().getValue(keyLastActivityTime);
String keyLastActivityTime = splicingKeyLastActivityTime(tokenValue);
String lastActivityTimeString = SaTokenManager.getSaTokenDao().get(keyLastActivityTime);
// 查不到,返回-2
if(lastActivityTimeString == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
@@ -744,7 +817,7 @@ public class StpLogic {
// =================== 角色验证操作 ===================
/**
* 指定账号id是否含有角色标识
* 指定账号id是否含有角色标识, 返回true或false
* @param loginId 账号id
* @param role 角色标识
* @return 是否含有指定角色标识
@@ -755,7 +828,7 @@ public class StpLogic {
}
/**
* 当前账号id是否含有指定角色标识
* 当前账号是否含有指定角色标识, 返回true或false
* @param role 角色标识
* @return 是否含有指定角色标识
*/
@@ -764,7 +837,7 @@ public class StpLogic {
}
/**
* 当前账号是否含有指定角色标识,没有就抛出异常
* 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
* @param role 角色标识
*/
public void checkRole(String role) {
@@ -774,7 +847,7 @@ public class StpLogic {
}
/**
* 当前账号是否含有指定角色标识, [指定多个,必须全都有]
* 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
* @param roleArray 角色标识数组
*/
public void checkRoleAnd(String... roleArray){
@@ -782,13 +855,13 @@ public class StpLogic {
List<String> roleList = SaTokenManager.getStpInterface().getRoleList(loginId, loginKey);
for (String role : roleArray) {
if(roleList.contains(role) == false) {
throw new NotRoleException(role, this.loginKey); // 没有权限抛出异常
throw new NotRoleException(role, this.loginKey);
}
}
}
/**
* 当前账号是否含有指定角色标识, [指定多个,有一个就可以通过]
* 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
* @param roleArray 角色标识数组
*/
public void checkRoleOr(String... roleArray){
@@ -796,11 +869,12 @@ public class StpLogic {
List<String> roleList = SaTokenManager.getStpInterface().getRoleList(loginId, loginKey);
for (String role : roleArray) {
if(roleList.contains(role) == true) {
return; // 有的话提前退出
// 有的话提前退出
return;
}
}
if(roleArray.length > 0) {
throw new NotRoleException(roleArray[0], this.loginKey); // 没有权限抛出异常
throw new NotRoleException(roleArray[0], this.loginKey);
}
}
@@ -808,7 +882,7 @@ public class StpLogic {
// =================== 权限验证操作 ===================
/**
* 指定账号id是否含有指定权限
* 指定账号id是否含有指定权限, 返回true或false
* @param loginId 账号id
* @param permission 权限码
* @return 是否含有指定权限
@@ -819,7 +893,7 @@ public class StpLogic {
}
/**
* 当前账号id是否含有指定权限
* 当前账号是否含有指定权限, 返回true或false
* @param permission 权限码
* @return 是否含有指定权限
*/
@@ -828,7 +902,7 @@ public class StpLogic {
}
/**
* 当前账号是否含有指定权限 没有就抛出异常
* 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
* @param permission 权限码
*/
public void checkPermission(String permission) {
@@ -838,7 +912,7 @@ public class StpLogic {
}
/**
* 当前账号是否含有指定权限, [指定多个,必须全都有]
* 当前账号是否含有指定权限 [指定多个,必须全部验证通过]
* @param permissionArray 权限码数组
*/
public void checkPermissionAnd(String... permissionArray){
@@ -846,13 +920,13 @@ public class StpLogic {
List<String> permissionList = SaTokenManager.getStpInterface().getPermissionList(loginId, loginKey);
for (String permission : permissionArray) {
if(permissionList.contains(permission) == false) {
throw new NotPermissionException(permission, this.loginKey); // 没有权限抛出异常
throw new NotPermissionException(permission, this.loginKey);
}
}
}
/**
* 当前账号是否含有指定权限, [指定多个,有一个就可以通过]
* 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
* @param permissionArray 权限码数组
*/
public void checkPermissionOr(String... permissionArray){
@@ -860,11 +934,12 @@ public class StpLogic {
List<String> permissionList = SaTokenManager.getStpInterface().getPermissionList(loginId, loginKey);
for (String permission : permissionArray) {
if(permissionList.contains(permission) == true) {
return; // 有的话提前退出
// 有的话提前退出
return;
}
}
if(permissionArray.length > 0) {
throw new NotPermissionException(permissionArray[0], this.loginKey); // 没有权限抛出异常
throw new NotPermissionException(permissionArray[0], this.loginKey);
}
}
@@ -942,7 +1017,7 @@ public class StpLogic {
return null;
}
// 如果session为null的话直接返回 null
SaSession session = getSession(false);
SaSession session = getSessionByLoginId(getLoginIdDefaultNull(), false);
if(session == null) {
return null;
}
@@ -967,7 +1042,7 @@ public class StpLogic {
* @return token集合
*/
public List<String> searchTokenValue(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenValue(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeyTokenValue(""), keyword, start, size);
}
/**
@@ -978,7 +1053,7 @@ public class StpLogic {
* @return sessionId集合
*/
public List<String> searchSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeySession(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeySession(""), keyword, start, size);
}
/**
@@ -989,7 +1064,7 @@ public class StpLogic {
* @return sessionId集合
*/
public List<String> searchTokenSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenSession(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeyTokenSession(""), keyword, start, size);
}
@@ -999,7 +1074,7 @@ public class StpLogic {
* 获取key:客户端 tokenName
* @return key
*/
public String getKeyTokenName() {
public String splicingKeyTokenName() {
return getConfig().getTokenName();
}
@@ -1008,7 +1083,7 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyTokenValue(String tokenValue) {
public String splicingKeyTokenValue(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token:" + tokenValue;
}
@@ -1017,7 +1092,7 @@ public class StpLogic {
* @param loginId 账号id
* @return key
*/
public String getKeySession(Object loginId) {
public String splicingKeySession(Object loginId) {
return getConfig().getTokenName() + ":" + loginKey + ":session:" + loginId;
}
@@ -1026,7 +1101,7 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyTokenSession(String tokenValue) {
public String splicingKeyTokenSession(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token-session:" + tokenValue;
}
@@ -1035,10 +1110,26 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyLastActivityTime(String tokenValue) {
public String splicingKeyLastActivityTime(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":last-activity:" + tokenValue;
}
/**
* 在进行身份切换时,使用的存储key
* @return key
*/
public String splicingKeySwitch() {
return SaTokenConsts.SWITCH_TO_SAVE_KEY + loginKey;
}
/**
* 如果token为本次请求新创建的,则以此字符串为key存储在当前request中
* @return key
*/
public String splicingKeyJustCreatedSave() {
return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginKey;
}
// =================== Bean对象代理 ===================
@@ -1052,7 +1143,7 @@ public class StpLogic {
}
// =================== 注解鉴权 ===================
// =================== 其它方法 ===================
/**
* 对一个Method对象进行注解检查(注解鉴权内部实现)
@@ -1111,6 +1202,58 @@ public class StpLogic {
// 验证通过
}
// =================== 身份切换 ===================
/**
* 临时切换身份为指定loginId
* @param loginId 指定loginId
*/
public void switchTo(Object loginId) {
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(splicingKeySwitch(), loginId);
}
/**
* 结束临时切换身份
*/
public void endSwitch() {
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(splicingKeySwitch());
}
/**
* 当前是否正处于[身份临时切换]中
* @return 是否正处于[身份临时切换]中
*/
public boolean isSwitch() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(splicingKeySwitch()) != null;
}
/**
* 返回[身份临时切换]的loginId
* @return 返回[身份临时切换]的loginId
*/
public Object getSwitchLoginId() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(splicingKeySwitch());
}
/**
* 在一个代码段里方法内,临时切换身份为指定loginId
* @param loginId 指定loginId
* @param function 要执行的方法
*/
public void switchTo(Object loginId, SaFunction function) {
try {
switchTo(loginId);
function.run();
} catch (Exception e) {
throw e;
} finally {
endSwitch();
}
}
@@ -2,6 +2,7 @@ package cn.dev33.satoken.stp;
import java.util.List;
import cn.dev33.satoken.fun.SaFunction;
import cn.dev33.satoken.session.SaSession;
/**
@@ -14,7 +15,16 @@ public class StpUtil {
* 底层的 StpLogic 对象
*/
public static StpLogic stpLogic = new StpLogic("login");
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public static String getLoginKey(){
return stpLogic.getLoginKey();
}
// =================== 获取token 相关 ===================
@@ -26,22 +36,23 @@ public class StpUtil {
return stpLogic.getTokenName();
}
/**
* 在当前会话写入当前tokenValue
* @param tokenValue token值
* @param cookieTimeout Cookie存活时间(秒)
*/
public static void setTokenValue(String tokenValue, int cookieTimeout){
stpLogic.setTokenValue(tokenValue, cookieTimeout);
}
/**
* 获取当前tokenValue
* 获取当前tokenValue
* @return 当前tokenValue
*/
public static String getTokenValue() {
return stpLogic.getTokenValue();
}
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public static String getLoginKey(){
return stpLogic.getLoginKey();
}
/**
* 获取当前会话的token信息
* @return token信息
@@ -62,13 +73,31 @@ public class StpUtil {
}
/**
* 在当前会话上登录id
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id,建议的类型:(long | int | String
* @param device 设备标识
*/
public static void setLoginId(Object loginId, String device) {
stpLogic.setLoginId(loginId, device);
}
/**
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id,建议的类型:(long | int | String
* @param isLastingCookie 是否为持久Cookie
*/
public static void setLoginId(Object loginId, boolean isLastingCookie) {
stpLogic.setLoginId(loginId, isLastingCookie);
}
/**
* 在当前会话上登录id, 并指定所有登录参数Model
* @param loginId 登录id,建议的类型:(long | int | String
* @param loginModel 此次登录的参数Model
*/
public static void setLoginId(Object loginId, SaLoginModel loginModel) {
stpLogic.setLoginId(loginId, loginModel);
}
/**
* 当前会话注销登录
@@ -308,7 +337,7 @@ public class StpUtil {
// =================== 角色验证操作 ===================
/**
* 指定账号id是否含有角色标识
* 指定账号id是否含有角色标识, 返回true或false
* @param loginId 账号id
* @param role 角色标识
* @return 是否含有指定角色标识
@@ -318,7 +347,7 @@ public class StpUtil {
}
/**
* 当前账号id是否含有指定角色标识
* 当前账号是否含有指定角色标识, 返回true或false
* @param role 角色标识
* @return 是否含有指定角色标识
*/
@@ -327,7 +356,7 @@ public class StpUtil {
}
/**
* 当前账号是否含有指定角色标识,没有就抛出异常
* 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
* @param role 角色标识
*/
public static void checkRole(String role) {
@@ -335,7 +364,7 @@ public class StpUtil {
}
/**
* 当前账号是否含有指定角色标识, [指定多个,必须全都有]
* 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
* @param roleArray 角色标识数组
*/
public static void checkRoleAnd(String... roleArray){
@@ -343,7 +372,7 @@ public class StpUtil {
}
/**
* 当前账号是否含有指定角色标识, [指定多个,有一个就可以通过]
* 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
* @param roleArray 角色标识数组
*/
public static void checkRoleOr(String... roleArray){
@@ -354,46 +383,46 @@ public class StpUtil {
// =================== 权限验证操作 ===================
/**
* 指定账号id是否含有指定权限
* 指定账号id是否含有指定权限, 返回true或false
* @param loginId 账号id
* @param permissionCode 权限码
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermission(Object loginId, String permissionCode) {
return stpLogic.hasPermission(loginId, permissionCode);
public static boolean hasPermission(Object loginId, String permission) {
return stpLogic.hasPermission(loginId, permission);
}
/**
* 当前账号id是否含有指定权限
* @param permissionCode 权限码
* 当前账号是否含有指定权限, 返回true或false
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermission(String permissionCode) {
return stpLogic.hasPermission(permissionCode);
public static boolean hasPermission(String permission) {
return stpLogic.hasPermission(permission);
}
/**
* 当前账号是否含有指定权限 没有就抛出异常
* @param permissionCode 权限码
* 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
* @param permission 权限码
*/
public static void checkPermission(String permissionCode) {
stpLogic.checkPermission(permissionCode);
public static void checkPermission(String permission) {
stpLogic.checkPermission(permission);
}
/**
* 当前账号是否含有指定权限, [指定多个,必须全都有]
* @param permissionCodeArray 权限码数组
* 当前账号是否含有指定权限 [指定多个,必须全部验证通过]
* @param permissionArray 权限码数组
*/
public static void checkPermissionAnd(String... permissionCodeArray) {
stpLogic.checkPermissionAnd(permissionCodeArray);
public static void checkPermissionAnd(String... permissionArray) {
stpLogic.checkPermissionAnd(permissionArray);
}
/**
* 当前账号是否含有指定权限, [指定多个,有一个就可以通过]
* @param permissionCodeArray 权限码数组
* 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
* @param permissionArray 权限码数组
*/
public static void checkPermissionOr(String... permissionCodeArray) {
stpLogic.checkPermissionOr(permissionCodeArray);
public static void checkPermissionOr(String... permissionArray) {
stpLogic.checkPermissionOr(permissionArray);
}
@@ -486,4 +515,39 @@ public class StpUtil {
}
// =================== 身份切换 ===================
/**
* 临时切换身份为指定loginId
* @param loginId 指定loginId
*/
public static void switchTo(Object loginId) {
stpLogic.switchTo(loginId);
}
/**
* 结束临时切换身份
*/
public static void endSwitch() {
stpLogic.endSwitch();
}
/**
* 当前是否正处于[身份临时切换]中
* @return 是否正处于[身份临时切换]中
*/
public static boolean isSwitch() {
return stpLogic.isSwitch();
}
/**
* 在一个代码段里方法内,临时切换身份为指定loginId
* @param loginId 指定loginId
* @param function 要执行的方法
*/
public static void switchTo(Object loginId, SaFunction function) {
stpLogic.switchTo(loginId, function);
}
}
@@ -1,57 +0,0 @@
package cn.dev33.satoken.util;
import java.util.Timer;
import java.util.TimerTask;
/**
* 任务调度Util
* @author kong
*
*/
public class SaTaskUtil {
/**
* 延时指定毫秒执行一个函数
* @param fc 要执行的函数
* @param delay 延时的毫秒数
* @return timer任务对象
*/
public static Timer setTimeout(FunctionRunClass fc, int delay) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
fc.run();
timer.cancel();
}
}, delay);
return timer;
}
/**
* 延时delay毫秒,每隔period毫秒执行一个函数
* @param fc 要执行的函数
* @param delay 延时的毫秒数
* @param period 每隔多少毫秒执行一次
* @return timer任务对象
*/
public static Timer setInterval(FunctionRunClass fc, int delay, int period) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
fc.run();
}
}, delay, period);
return timer;
}
/**
* 封装一个内部类,便于操作
* @author kong
*/
public static interface FunctionRunClass{
public void run();
}
}
@@ -1,45 +1,87 @@
package cn.dev33.satoken.util;
/**
* 定义sa-token的所有常量
* sa-token常量
* @author kong
*
*/
public class SaTokenConsts {
// =================== sa-token版本信息 ===================
/**
* sa-token 版本号
*/
public static final String VERSION_NO = "v1.11.0";
public static final String VERSION_NO = "v1.15.0";
/**
* sa-token 开源地址
*/
public static final String GITHUB_URL = "https://github.com/click33/sa-token";
// =================== 常量key标记 ===================
/**
* 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 JUST_CREATED_SAVE_KEY
* 常量key标记: 如果token为本次请求新创建的,则以此字符串为key存储在当前request中
*/
public static final String JUST_CREATED_SAVE_KEY = "JUST_CREATED_SAVE_KEY_";
/**
* 如果本次请求已经验证过[无操作过期], 则以此值存储在当前request中 TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY
* 常量key标记: 如果本次请求已经验证过[无操作过期], 则以此值存储在当前request中
*/
public static final String TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY = "TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY_";
/**
* 在登录时,默认使用的设备名称
* 常量key标记: 在登录时,默认使用的设备名称
*/
public static final String DEFAULT_LOGIN_DEVICE = "default-device";
// /**
// * 在用一个字符串存储多个token时,所使用的分隔符
// */
// public static final String MULTIPLE_TOKEN_SEPARATOR = ",";
/**
* 常量key标记: 在进行临时身份切换时使用的key
*/
public static final String SWITCH_TO_SAVE_KEY = "SWITCH_TO_SAVE_KEY_";
// =================== token-style 相关 ===================
/**
* token风格: uuid
*/
public static final String TOKEN_STYLE_UUID = "uuid";
/**
* token风格: 简单uuid (不带下划线)
*/
public static final String TOKEN_STYLE_SIMPLE_UUID = "simple-uuid";
/**
* token风格: 32位随机字符串
*/
public static final String TOKEN_STYLE_RANDOM_32 = "random-32";
/**
* token风格: 64位随机字符串
*/
public static final String TOKEN_STYLE_RANDOM_64 = "random-64";
/**
* token风格: 128位随机字符串
*/
public static final String TOKEN_STYLE_RANDOM_128 = "random-128";
/**
* token风格: tik风格 (2_14_16)
*/
public static final String TOKEN_STYLE_RANDOM_TIK = "tik";
// =================== 其它 ===================
/**
* 连接token前缀和token值的字符
*/
public static final String TOKEN_CONNECTOR_CHAT = " ";
}
@@ -7,31 +7,28 @@ import java.util.List;
import java.util.Random;
/**
* sa-token 内部代码工具类
* sa-token 内部代码工具类
*
* @author kong
*
*/
public class SaTokenInsideUtil {
/**
* 打印 sa-token 版本字符画
* 打印 sa-token 版本字符画
*/
public static void printSaToken() {
String str =
"____ ____ ___ ____ _ _ ____ _ _ \r\n" +
"[__ |__| __ | | | |_/ |___ |\\ | \r\n" +
"___] | | | |__| | \\_ |___ | \\| \r\n" +
"sa-token" + SaTokenConsts.VERSION_NO + " \r\n" +
"GitHub" + SaTokenConsts.GITHUB_URL; // + "\r\n";
String str = "____ ____ ___ ____ _ _ ____ _ _ \r\n" + "[__ |__| __ | | | |_/ |___ |\\ | \r\n"
+ "___] | | | |__| | \\_ |___ | \\| \r\n" + "sa-token" + SaTokenConsts.VERSION_NO
+ " \r\n" + "GitHub" + SaTokenConsts.GITHUB_URL; // + "\r\n";
System.out.println(str);
}
/**
* 生成指定长度的随机字符串
* @param length 字符串的长度
* @return 一个随机字符串
* 生成指定长度的随机字符串
*
* @param length 字符串的长度
* @return 一个随机字符串
*/
public static String getRandomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -43,68 +40,78 @@ public class SaTokenInsideUtil {
}
return sb.toString();
}
/**
* 指定字符串是否为null或者空字符串
* @param str 指定字符串
* @return 是否为null或者空字符串
*/
public static boolean isEmpty(String str) {
return str == null || "".equals(str);
}
/**
* 以当前时间戳和随机int数字拼接一个随机字符串
* @return 随机字符串
* 以当前时间戳和随机int数字拼接一个随机字符串
*
* @return 随机字符串
*/
public static String getMarking28() {
return System.currentTimeMillis() + "" + new Random().nextInt(Integer.MAX_VALUE);
}
/**
* 从集合里查询数据
* @param dataList 数据集合
* @param prefix 前缀
* @param keyword 关键字
* @param start 起始位置 (-1代表查询所有)
* @param size 获取条数
* @return 符合条件的新数据集合
*
* @param dataList 数据集合
* @param prefix 前缀
* @param keyword 关键字
* @param start 起始位置 (-1代表查询所有)
* @param size 获取条数
* @return 符合条件的新数据集合
*/
public static List<String> searchList(Collection<String> dataList, String prefix, String keyword, int start, int size) {
if(prefix == null) {
public static List<String> searchList(Collection<String> dataList, String prefix, String keyword, int start,
int size) {
if (prefix == null) {
prefix = "";
}
if(keyword == null) {
if (keyword == null) {
keyword = "";
}
// 挑选出所有符合条件的
// 挑选出所有符合条件的
List<String> list = new ArrayList<String>();
Iterator<String> keys = dataList.iterator();
while (keys.hasNext()) {
String key = keys.next();
if(key.startsWith(prefix) && key.indexOf(keyword) > -1) {
if (key.startsWith(prefix) && key.indexOf(keyword) > -1) {
list.add(key);
}
}
// 取指定段数据
return searchList(list, start, size);
}
/**
* 从集合里查询数据
* @param list 数据集合
*
* @param list 数据集合
* @param start 起始位置 (-1代表查询所有)
* @param size 获取条数
* @return 符合条件的新数据集合
* @param size 获取条数
* @return 符合条件的新数据集合
*/
public static List<String> searchList(List<String> list, int start, int size) {
// 取指定段数据
if(start < 0) {
if (start < 0) {
return list;
}
int end = start + size;
List<String> list2 = new ArrayList<String>();
for (int i = start; i < end; i++) {
if(i >= list.size()) {
if (i >= list.size()) {
return list2;
}
list2.add(list.get(i));
}
return list2;
}
}
+2 -1
View File
@@ -9,4 +9,5 @@ unpackage/
.factorypath
.idea/
.idea/
.iml
+2 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.15.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,7 +20,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>
@@ -17,11 +17,13 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
* sa-token持久层的实现类, 基于redis (to jackson)
* sa-token持久层的实现类, 基于redis (使用 jackson 序列化方式)
*
* @author kong
*
*/
@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {
@@ -32,17 +34,17 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
public ObjectMapper objectMapper;
/**
* string专用
* String专用
*/
@Autowired
public StringRedisTemplate stringRedisTemplate;
/**
* SaSession专用
* Object专用
*/
public RedisTemplate<String, SaSession> sessionRedisTemplate;
public RedisTemplate<String, Object> objectRedisTemplate;
@Autowired
public void setSessionRedisTemplate(RedisConnectionFactory connectionFactory) {
public void setObjectRedisTemplate(RedisConnectionFactory connectionFactory) {
// 指定相应的序列化方案
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
@@ -57,15 +59,15 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
System.err.println(e.getMessage());
}
// 构建RedisTemplate
RedisTemplate<String, SaSession> template = new RedisTemplate<String, SaSession>();
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
if(this.sessionRedisTemplate == null) {
this.sessionRedisTemplate = template;
if(this.objectRedisTemplate == null) {
this.objectRedisTemplate = template;
}
}
@@ -74,7 +76,7 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
* 根据key获取value,如果没有,则返回空
*/
@Override
public String getValue(String key) {
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@@ -82,7 +84,7 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
* 写入指定key-value键值对,并设定过期时间(单位:秒)
*/
@Override
public void setValue(String key, String value, long timeout) {
public void set(String key, String value, long timeout) {
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().set(key, value);
@@ -92,22 +94,23 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
}
/**
* 修改指定key-value键值对 (过期时间取原来的值)
* 修改指定key-value键值对 (过期时间不变)
*/
@Override
public void updateValue(String key, String value) {
public void update(String key, String value) {
long expire = getTimeout(key);
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { // -2 = 无此键
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.setValue(key, value, expire);
this.set(key, value, expire);
}
/**
* 删除一个指定的key
*/
@Override
public void deleteKey(String key) {
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@@ -131,7 +134,7 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.setValue(key, this.getValue(key), timeout);
this.set(key, this.get(key), timeout);
}
return;
}
@@ -139,75 +142,77 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
}
/**
* 根据指定key的Session,如果没有,则返回空
* 根据key获取Object,如果没有,则返回空
*/
@Override
public SaSession getSession(String sessionId) {
return sessionRedisTemplate.opsForValue().get(sessionId);
public Object getObject(String key) {
return objectRedisTemplate.opsForValue().get(key);
}
/**
* 将指定Session持久化
* 写入指定键值对,并设定过期时间 (单位: 秒)
*/
@Override
public void saveSession(SaSession session, long timeout) {
public void setObject(String key, Object object, long timeout) {
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
sessionRedisTemplate.opsForValue().set(session.getId(), session);
objectRedisTemplate.opsForValue().set(key, object);
} else {
sessionRedisTemplate.opsForValue().set(session.getId(), session, timeout, TimeUnit.SECONDS);
objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
}
}
/**
* 更新指定session
* 修改指定键值对 (过期时间不变)
*/
@Override
public void updateSession(SaSession session) {
long expire = getSessionTimeout(session.getId());
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { // -2 = 无此键
public void updateObject(String key, Object object) {
long expire = getObjectTimeout(key);
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.saveSession(session, expire);
this.setObject(key, object, expire);
}
/**
* 删除一个指定的session
* 删除一个指定的object
*/
@Override
public void deleteSession(String sessionId) {
sessionRedisTemplate.delete(sessionId);
public void deleteObject(String key) {
objectRedisTemplate.delete(key);
}
/**
* 获取指定SaSession的剩余存活时间 (单位: 秒)
* 获取指定key的剩余存活时间 (单位: 秒)
*/
@Override
public long getSessionTimeout(String sessionId) {
return sessionRedisTemplate.getExpire(sessionId);
public long getObjectTimeout(String key) {
return objectRedisTemplate.getExpire(key);
}
/**
* 修改指定SaSession的剩余存活时间 (单位: 秒)
* 修改指定key的剩余存活时间 (单位: 秒)
*/
@Override
public void updateSessionTimeout(String sessionId, long timeout) {
public void updateObjectTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getSessionTimeout(sessionId);
long expire = getObjectTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.saveSession(this.getSession(sessionId), timeout);
this.setObject(key, this.getObject(key), timeout);
}
return;
}
sessionRedisTemplate.expire(sessionId, timeout, TimeUnit.SECONDS);
objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 搜索数据
*/
+2 -1
View File
@@ -9,4 +9,5 @@ unpackage/
.factorypath
.idea/
.idea/
.iml
+2 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.15.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,7 +20,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>
@@ -13,49 +13,51 @@ import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.util.SaTokenInsideUtil;
/**
* sa-token持久层的实现类, 基于redis
*
* @author kong
*
*/
@Component
public class SaTokenDaoRedis implements SaTokenDao {
/**
* string专用
* String专用
*/
@Autowired
public StringRedisTemplate stringRedisTemplate;
/**
* SaSession专用
* Objecy专用
*/
public RedisTemplate<String, SaSession> sessionRedisTemplate;
public RedisTemplate<String, Object> objectRedisTemplate;
@Autowired
public void setSessionRedisTemplate(RedisConnectionFactory connectionFactory) {
public void setObjectRedisTemplate(RedisConnectionFactory connectionFactory) {
// 指定相应的序列化方案
StringRedisSerializer keySerializer = new StringRedisSerializer();
JdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer();
// 构建RedisTemplate
RedisTemplate<String, SaSession> template = new RedisTemplate<String, SaSession>();
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
if(this.sessionRedisTemplate == null) {
this.sessionRedisTemplate = template;
if(this.objectRedisTemplate == null) {
this.objectRedisTemplate = template;
}
}
/**
* 根据key获取value,如果没有,则返回空
* 根据key获取value,如果没有,则返回空
*/
@Override
public String getValue(String key) {
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@@ -63,7 +65,7 @@ public class SaTokenDaoRedis implements SaTokenDao {
* 写入指定key-value键值对,并设定过期时间(单位:秒)
*/
@Override
public void setValue(String key, String value, long timeout) {
public void set(String key, String value, long timeout) {
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().set(key, value);
@@ -73,22 +75,23 @@ public class SaTokenDaoRedis implements SaTokenDao {
}
/**
* 修改指定key-value键值对 (过期时间取原来的值)
* 修改指定key-value键值对 (过期时间不变)
*/
@Override
public void updateValue(String key, String value) {
public void update(String key, String value) {
long expire = getTimeout(key);
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { // -2 = 无此键
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.setValue(key, value, expire);
this.set(key, value, expire);
}
/**
* 删除一个指定的key
*/
@Override
public void deleteKey(String key) {
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@@ -112,7 +115,7 @@ public class SaTokenDaoRedis implements SaTokenDao {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.setValue(key, this.getValue(key), timeout);
this.set(key, this.get(key), timeout);
}
return;
}
@@ -120,73 +123,73 @@ public class SaTokenDaoRedis implements SaTokenDao {
}
/**
* 根据指定key的Session,如果没有,则返回空
* 根据key获取Object,如果没有,则返回空
*/
@Override
public SaSession getSession(String sessionId) {
return sessionRedisTemplate.opsForValue().get(sessionId);
public Object getObject(String key) {
return objectRedisTemplate.opsForValue().get(key);
}
/**
* 将指定Session持久化
* 写入指定键值对,并设定过期时间 (单位: 秒)
*/
@Override
public void saveSession(SaSession session, long timeout) {
public void setObject(String key, Object object, long timeout) {
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
sessionRedisTemplate.opsForValue().set(session.getId(), session);
objectRedisTemplate.opsForValue().set(key, object);
} else {
sessionRedisTemplate.opsForValue().set(session.getId(), session, timeout, TimeUnit.SECONDS);
objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
}
}
/**
* 更新指定session
* 修改指定键值对 (过期时间不变)
*/
@Override
public void updateSession(SaSession session) {
long expire = getSessionTimeout(session.getId());
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { // -2 = 无此键
public void updateObject(String key, Object object) {
long expire = getObjectTimeout(key);
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.saveSession(session, expire);
this.setObject(key, object, expire);
}
/**
* 删除一个指定的session
* 删除一个指定的object
*/
@Override
public void deleteSession(String sessionId) {
sessionRedisTemplate.delete(sessionId);
public void deleteObject(String key) {
objectRedisTemplate.delete(key);
}
/**
* 获取指定SaSession的剩余存活时间 (单位: 秒)
* 获取指定key的剩余存活时间 (单位: 秒)
*/
@Override
public long getSessionTimeout(String sessionId) {
return sessionRedisTemplate.getExpire(sessionId);
public long getObjectTimeout(String key) {
return objectRedisTemplate.getExpire(key);
}
/**
* 修改指定SaSession的剩余存活时间 (单位: 秒)
* 修改指定key的剩余存活时间 (单位: 秒)
*/
@Override
public void updateSessionTimeout(String sessionId, long timeout) {
public void updateObjectTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getSessionTimeout(sessionId);
long expire = getObjectTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.saveSession(this.getSession(sessionId), timeout);
this.setObject(key, this.getObject(key), timeout);
}
return;
}
sessionRedisTemplate.expire(sessionId, timeout, TimeUnit.SECONDS);
objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
+12
View File
@@ -0,0 +1,12 @@
target/
node_modules/
bin/
.settings/
unpackage/
.classpath
.project
.idea/
.factorypath
+86
View File
@@ -0,0 +1,86 @@
<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-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/>
</parent>
<!-- 定义sa-token版本号 -->
<properties>
<sa-token-version>1.15.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>
<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token-version}</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>${sa-token-version}</version>
</dependency> -->
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${sa-token-version}</version>
</dependency> -->
<!-- 提供redis连接池 -->
<!-- <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency> -->
<!-- sa-token整合SpringAOP实现注解鉴权 -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>${sa-token-version}</version>
</dependency> -->
<!-- @ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,16 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import cn.dev33.satoken.SaTokenManager;
@SpringBootApplication
public class SaTokenJwtDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenJwtDemoApplication.class, args);
System.out.println("\n启动成功:sa-token配置如下:" + SaTokenManager.getConfig());
}
}
@@ -0,0 +1,232 @@
package com.pj.satoken.jwt;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaTokenConsts;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class SaTokenJwtUtil {
/**
* 秘钥 (随便手打几个字母就好了)
*/
public static final String BASE64_SECURITY = "79e7c69681b8270162386e6daa53d1dd";
/**
* token有效期 (单位: 秒)
*/
public static final long TIMEOUT = 60 * 60 * 2;
public static final String LOGIN_ID_KEY = "loginId";
/**
* 根据userId生成token
* @param loginId 账号id
* @param base64Security 秘钥
* @return jwt-token
*/
public static String createToken(Object loginId) {
// 判断,不可使用默认秘钥
// if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) {
// throw new SaTokenException("请更换秘钥");
// }
// 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用
JwtBuilder builder = Jwts.builder()
.setHeaderParam("type", "JWT")
.claim(LOGIN_ID_KEY, loginId)
.setIssuedAt(new Date()) // 签发日期
.setExpiration(new Date(System.currentTimeMillis() + 1000 * TIMEOUT)) // 有效截止日期
.signWith(SignatureAlgorithm.HS256, BASE64_SECURITY.getBytes()); // 加密算法
//生成JWT
return builder.compact();
}
/**
* 从一个jwt里面解析出Claims
* @param tokenValue token值
* @param base64Security 秘钥
* @return Claims对象
*/
public static Claims getClaims(String tokenValue) {
// System.out.println(tokenValue);
Claims claims = Jwts.parser()
.setSigningKey(BASE64_SECURITY.getBytes())
.parseClaimsJws(tokenValue).getBody();
return claims;
}
/**
* 从一个jwt里面解析loginId
* @param tokenValue token值
* @param base64Security 秘钥
* @return loginId
*/
public static String getLoginId(String tokenValue) {
try {
Object loginId = getClaims(tokenValue).get(LOGIN_ID_KEY);
if(loginId == null) {
return null;
}
return String.valueOf(loginId);
} catch (ExpiredJwtException e) {
// throw NotLoginException.newInstance(StpUtil.stpLogic.loginKey, NotLoginException.TOKEN_TIMEOUT);
return NotLoginException.TOKEN_TIMEOUT;
} catch (MalformedJwtException e) {
throw NotLoginException.newInstance(StpUtil.stpLogic.loginKey, NotLoginException.INVALID_TOKEN);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
static {
// 判断秘钥
if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) {
String warn = "-------------------------------------\n";
warn += "请更换JWT秘钥,不要使用示例默认秘钥\n";
warn += "-------------------------------------";
System.err.println(warn);
}
// 修改默认实现
StpUtil.stpLogic = new StpLogic("login") {
// 重写 (随机生成一个tokenValue)
@Override
public String createTokenValue(Object loginId) {
return SaTokenJwtUtil.createToken(loginId);
}
// 重写 (在当前会话上登录id )
@Override
public void setLoginId(Object loginId, String device) {
// ------ 1、获取相应对象
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
SaTokenConfig config = getConfig();
// ------ 2、生成一个token
String tokenValue = createTokenValue(loginId);
request.setAttribute(splicingKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
if(config.getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", config.getCookieDomain(), (int)config.getTimeout());
}
}
// 重写 (获取指定token对应的登录id)
@Override
public String getLoginIdNotHandle(String tokenValue) {
try {
return SaTokenJwtUtil.getLoginId(tokenValue);
} catch (Exception e) {
return null;
}
}
// 重写 (当前会话注销登录)
@Override
public void logout() {
// 如果连token都没有,那么无需执行任何操作
String tokenValue = getTokenValue();
if(tokenValue == null) {
return;
}
// 如果打开了cookie模式,把cookie清除掉
if(getConfig().getIsReadCookie() == true){
SaTokenManager.getSaTokenCookie().delCookie(SaTokenManager.getSaTokenServlet().getRequest(), SaTokenManager.getSaTokenServlet().getResponse(), getTokenName());
}
}
// 重写 (获取指定key的session)
@Override
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
throw new SaTokenException("jwt has not session");
}
// 重写 (获取当前登录者的token剩余有效时间 (单位: 秒))
@Override
public long getTokenTimeout() {
// 如果没有token
String tokenValue = getTokenValue();
if(tokenValue == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
// 开始取值
Claims claims = null;
try {
claims = SaTokenJwtUtil.getClaims(tokenValue);
} catch (Exception e) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
if(claims == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
Date expiration = claims.getExpiration();
if(expiration == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
return (expiration.getTime() - System.currentTimeMillis()) / 1000;
}
// 重写 (返回当前token的登录设备)
@Override
public String getLoginDevice() {
return SaTokenConsts.DEFAULT_LOGIN_DEVICE;
}
// 重写 (获取当前会话的token信息)
@Override
public SaTokenInfo getTokenInfo() {
SaTokenInfo info = new SaTokenInfo();
info.tokenName = getTokenName();
info.tokenValue = getTokenValue();
info.isLogin = isLogin();
info.loginId = getLoginIdDefaultNull();
info.loginKey = getLoginKey();
info.tokenTimeout = getTokenTimeout();
// info.sessionTimeout = getSessionTimeout();
// info.tokenSessionTimeout = getTokenSessionTimeout();
// info.tokenActivityTimeout = getTokenActivityTimeout();
info.loginDevice = getLoginDevice();
return info;
}
};
}
}
@@ -0,0 +1,100 @@
package com.pj.test;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.pj.util.AjaxJson;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
/**
* 全局异常处理
*/
@RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin")
public class GlobalException {
// 在每个控制器之前触发的操作
@ModelAttribute
public void get(HttpServletRequest request) throws IOException {
}
// 全局异常拦截(拦截项目中的所有异常)
@ExceptionHandler
public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 打印堆栈,以供调试
e.printStackTrace();
// 不同异常返回不同状态码
AjaxJson aj = null;
if (e instanceof NotLoginException) { // 如果是未登录异常
NotLoginException ee = (NotLoginException) e;
aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
} else if(e instanceof NotRoleException) { // 如果是角色异常
NotRoleException ee = (NotRoleException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
} else if(e instanceof NotPermissionException) { // 如果是权限异常
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
} else { // 普通异常, 输出:500 + 异常信息
aj = AjaxJson.getError(e.getMessage());
}
// 返回给前端
return aj;
// 输出到客户端
// response.setContentType("application/json; charset=utf-8"); // http说明,我要返回JSON对象
// response.getWriter().print(new ObjectMapper().writeValueAsString(aj));
}
// 全局异常拦截(拦截项目中的NotLoginException异常)
// @ExceptionHandler(NotLoginException.class)
// public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response)
// throws Exception {
//
// // 打印堆栈,以供调试
// nle.printStackTrace();
//
// // 判断场景值,定制化异常信息
// String message = "";
// if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
// message = "未提供token";
// }
// else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
// message = "token无效";
// }
// else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
// message = "token已过期";
// }
// else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
// message = "token已被顶下线";
// }
// else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
// message = "token已被踢下线";
// }
// else {
// message = "当前会话未登录";
// }
//
// // 返回给前端
// return AjaxJson.getError(message);
// }
}
@@ -0,0 +1,80 @@
package com.pj.test;
import java.util.Date;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pj.util.AjaxJson;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
/**
* 测试专用Controller
* @author kong
*
*/
@RestController
@RequestMapping("/test/")
public class TestJwtController {
// 测试登录接口, 浏览器访问: http://localhost:8081/test/login
@RequestMapping("login")
public AjaxJson login(@RequestParam(defaultValue="10001") String id) {
System.out.println("======================= 进入方法,测试登录接口 ========================= ");
System.out.println("当前会话的token" + StpUtil.getTokenValue());
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull());
StpUtil.setLoginId(id); // 在当前会话登录此账号
System.out.println("登录成功");
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号:" + StpUtil.getLoginId());
// System.out.println("当前登录账号并转为int" + StpUtil.getLoginIdAsInt());
System.out.println("当前登录设备:" + StpUtil.getLoginDevice());
// System.out.println("当前token信息:" + StpUtil.getTokenInfo());
return AjaxJson.getSuccess();
}
// 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo
@RequestMapping("tokenInfo")
public AjaxJson tokenInfo() {
System.out.println("======================= 进入方法,打印当前token信息 ========================= ");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println(tokenInfo);
return AjaxJson.getSuccessData(tokenInfo);
}
// 测试会话session接口, 浏览器访问: http://localhost:8081/test/session
@RequestMapping("session")
public AjaxJson session() throws JsonProcessingException {
System.out.println("======================= 进入方法,测试会话session接口 ========================= ");
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
System.out.println("测试取值name" + StpUtil.getSession().getAttribute("name"));
StpUtil.getSession().setAttribute("name", new Date()); // 写入一个值
System.out.println("测试取值name" + StpUtil.getSession().getAttribute("name"));
System.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession()));
return AjaxJson.getSuccess();
}
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
StpUtil.logoutByLoginId(10001);
return AjaxJson.getSuccess();
}
}
@@ -0,0 +1,162 @@
package com.pj.util;
import java.io.Serializable;
import java.util.List;
/**
* ajax请求返回Json格式数据的封装
*/
public class AjaxJson implements Serializable{
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 200; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
public int code; // 状态码
public String msg; // 描述信息
public Object data; // 携带对象
public Long dataCount; // 数据总数,用于分页
/**
* 返回code
* @return
*/
public int getCode() {
return this.code;
}
/**
* 给msg赋值,连缀风格
*/
public AjaxJson setMsg(String msg) {
this.msg = msg;
return this;
}
public String getMsg() {
return this.msg;
}
/**
* 给data赋值,连缀风格
*/
public AjaxJson setData(Object data) {
this.data = data;
return this;
}
/**
* 将data还原为指定类型并返回
*/
@SuppressWarnings("unchecked")
public <T> T getData(Class<T> cs) {
return (T) data;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = dataCount;
}
// 返回成功
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
public static AjaxJson getSuccessArray(Object... data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
// 返回失败
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
// 返回警告
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
// 返回未登录
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
// 返回没有权限的
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
// 返回一个自定义状态码的
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
// 返回分页和数据的
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回,根据受影响行数的(大于0=ok,小于0=error)
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
// 返回,根据布尔值来确定最终结果的 (true=okfalse=error)
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@SuppressWarnings("rawtypes")
@Override
public String toString() {
String data_string = null;
if(data == null){
} else if(data instanceof List){
data_string = "List(length=" + ((List)data).size() + ")";
} else {
data_string = data.toString();
}
return "{"
+ "\"code\": " + this.getCode()
+ ", \"msg\": \"" + this.getMsg() + "\""
+ ", \"data\": " + data_string
+ ", \"dataCount\": " + dataCount
+ "}";
}
}
@@ -0,0 +1,45 @@
# 端口
server:
port: 8081
spring:
# sa-token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
allow-concurrent-login: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# redis配置
redis:
# Redis数据库索引(默认为0
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间(毫秒)
timeout: 10000ms
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
@@ -0,0 +1,13 @@
target/
.project
.classpath
.settings
/.idea/
node_modules/
bin/
.settings/
unpackage/
/.apt_generated/
/.apt_generated_tests/
@@ -0,0 +1,56 @@
<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>com.pj</groupId>
<artifactId>sa-token-demo-oauth2-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<!-- 指定一些属性 -->
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义sa-token版本号 -->
<sa-token-version>1.15.0</sa-token-version>
</properties>
<dependencies>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token-version}</version>
</dependency>
<!-- OkHttps网络请求库: http://okhttps.ejlchina.com/ -->
<dependency>
<groupId>com.ejlchina</groupId>
<artifactId>okhttps</artifactId>
<version>2.4.5</version>
</dependency>
<!-- ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,18 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动
* @author kong
*/
@SpringBootApplication
public class SaOAuth2ClientApplication {
public static void main(String[] args) {
SpringApplication.run(SaOAuth2ClientApplication.class, args);
System.out.println("\n客户端启动成功,访问: http://localhost:8002/login.html");
}
}
@@ -0,0 +1,76 @@
package com.pj.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ejlchina.okhttps.OkHttps;
import com.pj.utils.AjaxJson;
import com.pj.utils.SoMap;
import cn.dev33.satoken.stp.StpUtil;
/**
* 登录注册Controller
* @author kong
*/
@RestController
public class ClientAccController {
// 返回当前登录者的账号id, 如果未登录, 返回null
@RequestMapping("/getLoginInfo")
public AjaxJson getLoginInfo() {
Object loginId = StpUtil.getLoginIdDefaultNull();
return AjaxJson.getSuccessData(loginId);
}
// 注销登录
@RequestMapping("/logout")
public AjaxJson logout() {
StpUtil.logout();
return AjaxJson.getSuccess();
}
// 根据code码进行登录
@RequestMapping("/doCodeLogin")
public AjaxJson doCodeLogin(String code) {
System.out.println("------------------ 成功进入请求 ------------------");
// 请求服务提供方接口地址,获取 access_token 以及其他信息
// 携带三个关键参数: code、client_id、client_secret
String str = OkHttps.sync("http://localhost:8001/oauth2/getAccessToken")
.addBodyPara("code", code)
.addBodyPara("client_id", "123123123")
.addBodyPara("client_secret", "aaaa-bbbb-cccc-dddd-eeee")
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return AjaxJson.getError(so.getString("msg"));
}
// 根据openid获取其对应的userId
String openid = so.getString("openid");
long userId = getUserIdByOpenid(openid);
// 登录并返回账号信息
StpUtil.setLoginId(userId);
return AjaxJson.getSuccessData(userId).set("openid", openid);
}
// ------------ 模拟方法 ------------------
// 模拟方法:根据openid获取userId
private long getUserIdByOpenid(String openid) {
// 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId
return 10001;
}
}
@@ -0,0 +1,59 @@
package com.pj.controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.pj.utils.AjaxJson;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
/**
* 全局异常拦截
* <p> @ControllerAdvice 可指定包前缀,例如:(basePackages = "com.pj.controller.admin")
* @author kong
*
*/
@ControllerAdvice
public class ExceptionHandle {
/** 全局异常拦截 */
@ResponseBody
@ExceptionHandler
public AjaxJson handlerException(Exception e) {
// 打印堆栈,以供调试
e.printStackTrace();
// 记录日志信息
AjaxJson aj = null;
// ------------- 判断异常类型,提供个性化提示信息
// 如果是未登录异常
if(e instanceof NotLoginException){
aj = AjaxJson.getNotLogin();
}
// 如果是角色异常
else if(e instanceof NotRoleException) {
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getCode());
}
// 如果是权限异常
else if(e instanceof NotPermissionException) {
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
}
// 普通异常输出:500 + 异常信息
else {
aj = AjaxJson.getError(e.getMessage());
}
// 返回到前台
return aj;
}
}
@@ -0,0 +1,223 @@
package com.pj.utils;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ajax请求返回Json格式数据的封装 <br>
* 所有预留字段:<br>
* code=状态码 <br>
* msg=描述信息 <br>
* data=携带对象 <br>
* pageNo=当前页 <br>
* pageSize=页大小 <br>
* startIndex=起始索引 <br>
* dataCount=数据总数 <br>
* pageCount=分页总数 <br>
* <p> 返回范例:</p>
* <pre>
{
"code": 200, // 成功时=200, 失败时=500 msg=失败原因
"msg": "ok",
"data": {}
}
</pre>
*/
public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 200; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
// ============================ 写值取值 ==================================
/** 给code赋值,连缀风格 */
public AjaxJson setCode(int code) {
this.put("code", code);
return this;
}
/** 返回code */
public Integer getCode() {
return (Integer)this.get("code");
}
/** 给msg赋值,连缀风格 */
public AjaxJson setMsg(String msg) {
this.put("msg", msg);
return this;
}
/** 获取msg */
public String getMsg() {
return (String)this.get("msg");
}
/** 给data赋值,连缀风格 */
public AjaxJson setData(Object data) {
this.put("data", data);
return this;
}
/** 获取data */
public String getData() {
return (String)this.get("data");
}
/** 将data还原为指定类型并返回 */
@SuppressWarnings("unchecked")
public <T> T getData(Class<T> cs) {
return (T) this.getData();
}
/** 给dataCount(数据总数)赋值,连缀风格 */
public AjaxJson setDataCount(Long dataCount) {
this.put("dataCount", dataCount);
// 如果提供了数据总数,则尝试计算page信息
if(dataCount != null && dataCount >= 0) {
// 如果:已有page信息
if(get("pageNo") != null) {
this.initPageInfo();
}
// // 或者:是JavaWeb环境
// else if(SoMap.isJavaWeb()) {
// SoMap so = SoMap.getRequestSoMap();
// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
// this.initPageInfo();
// }
}
return this;
}
/** 获取dataCount(数据总数) */
public String getDataCount() {
return (String)this.get("dataCount");
}
/** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
this.put("pageNo", pageNo);
this.put("pageSize", pageSize);
return this;
}
/** 根据 pageSize dataCount,计算startIndex 与 pageCount */
public AjaxJson initPageInfo() {
long pageNo = (long)this.get("pageNo");
long pageSize = (long)this.get("pageSize");
long dataCount = (long)this.get("dataCount");
this.set("startIndex", (pageNo - 1) * pageSize);
long pc = dataCount / pageSize;
this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
return this;
}
/** 写入一个值 自定义key, 连缀风格 */
public AjaxJson set(String key, Object data) {
this.put(key, data);
return this;
}
/** 写入一个Map, 连缀风格 */
public AjaxJson setMap(Map<String, ?> map) {
for (String key : map.keySet()) {
this.put(key, map.get(key));
}
return this;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
this.setDataCount(dataCount == null ? -1 : dataCount);
}
/** 返回成功 */
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
/** 返回失败 */
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
/** 返回警告 */
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
/** 返回未登录 */
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
/** 返回没有权限的 */
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
/** 返回一个自定义状态码的 */
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
/** 返回分页和数据的 */
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
/** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
/** 返回,根据布尔值来确定最终结果的 (true=okfalse=error) */
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
// // 历史版本遗留代码
// public int code; // 状态码
// public String msg; // 描述信息
// public Object data; // 携带对象
// public Long dataCount; // 数据总数,用于分页
}
@@ -0,0 +1,723 @@
package com.pj.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦
* <p>所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用
* <p>最新:2020-12-10 新增部分构造方法
* @author kong
*/
public class SoMap extends LinkedHashMap<String, Object> {
private static final long serialVersionUID = 1L;
public SoMap() {
}
/** 以下元素会在isNull函数中被判定为Null */
public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
public static final List<Object> NULL_ELEMENT_LIST;
static {
NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
}
// ============================= 读值 =============================
/** 获取一个值 */
@Override
public Object get(Object key) {
if("this".equals(key)) {
return this;
}
return super.get(key);
}
/** 如果为空,则返回默认值 */
public Object get(Object key, Object defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return value;
}
/** 转为String并返回 */
public String getString(String key) {
Object value = get(key);
if(value == null) {
return null;
}
return String.valueOf(value);
}
/** 如果为空,则返回默认值 */
public String getString(String key, String defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return String.valueOf(value);
}
/** 转为int并返回 */
public int getInt(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0;
}
return Integer.valueOf(String.valueOf(value));
}
/** 转为int并返回,同时指定默认值 */
public int getInt(String key, int defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return Integer.valueOf(String.valueOf(value));
}
/** 转为long并返回 */
public long getLong(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0;
}
return Long.valueOf(String.valueOf(value));
}
/** 转为double并返回 */
public double getDouble(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0.0;
}
return Double.valueOf(String.valueOf(value));
}
/** 转为boolean并返回 */
public boolean getBoolean(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return false;
}
return Boolean.valueOf(String.valueOf(value));
}
/** 转为Date并返回,根据自定义格式 */
public Date getDateByFormat(String key, String format) {
try {
return new SimpleDateFormat(format).parse(getString(key));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 转为Date并返回,根据格式: yyyy-MM-dd */
public Date getDate(String key) {
return getDateByFormat(key, "yyyy-MM-dd");
}
/** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */
public Date getDateTime(String key) {
return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
}
/** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
@SuppressWarnings("unchecked")
public List<Object> getList(String key) {
Object value = get(key);
List<Object> list = null;
if(value == null || value.equals("")) {
list = new ArrayList<Object>();
}
else if(value instanceof List) {
list = (List<Object>)value;
} else {
list = new ArrayList<Object>();
list.add(value);
}
return list;
}
/** 获取集合 (指定泛型类型) */
public <T> List<T> getList(String key, Class<T> cs) {
List<Object> list = getList(key);
List<T> list2 = new ArrayList<T>();
for (Object obj : list) {
T objC = getValueByClass(obj, cs);
list2.add(objC);
}
return list2;
}
/** 获取集合(逗号分隔式),(指定类型) */
public <T> List<T> getListByComma(String key, Class<T> cs) {
String listStr = getString(key);
if(listStr == null || listStr.equals("")) {
return new ArrayList<>();
}
// 开始转化
String [] arr = listStr.split(",");
List<T> list = new ArrayList<T>();
for (String str : arr) {
if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {
str = str.trim();
}
T objC = getValueByClass(str, cs);
list.add(objC);
}
return list;
}
/** 根据指定类型从map中取值,返回实体对象 */
public <T> T getModel(Class<T> cs) {
try {
return getModelByObject(cs.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 从map中取值,塞到一个对象中 */
public <T> T getModelByObject(T obj) {
// 获取类型
Class<?> cs = obj.getClass();
// 循环复制
for (Field field : cs.getDeclaredFields()) {
try {
// 获取对象
Object value = this.get(field.getName());
if(value == null) {
continue;
}
field.setAccessible(true);
Object valueConvert = getValueByClass(value, field.getType());
field.set(obj, valueConvert);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException("属性取值出错:" + field.getName(), e);
}
}
return obj;
}
/**
* 将指定值转化为指定类型并返回
* @param obj
* @param cs
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getValueByClass(Object obj, Class<T> cs) {
String obj2 = String.valueOf(obj);
Object obj3 = null;
if (cs.equals(String.class)) {
obj3 = obj2;
} else if (cs.equals(int.class) || cs.equals(Integer.class)) {
obj3 = Integer.valueOf(obj2);
} else if (cs.equals(long.class) || cs.equals(Long.class)) {
obj3 = Long.valueOf(obj2);
} else if (cs.equals(short.class) || cs.equals(Short.class)) {
obj3 = Short.valueOf(obj2);
} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
obj3 = Byte.valueOf(obj2);
} else if (cs.equals(float.class) || cs.equals(Float.class)) {
obj3 = Float.valueOf(obj2);
} else if (cs.equals(double.class) || cs.equals(Double.class)) {
obj3 = Double.valueOf(obj2);
} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
obj3 = Boolean.valueOf(obj2);
} else {
obj3 = (T)obj;
}
return (T)obj3;
}
// ============================= 写值 =============================
/**
* 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去)
*/
public void setDefaultValue(String key, Object defaultValue) {
if(isNull(key)) {
set(key, defaultValue);
}
}
/** set一个值,连缀风格 */
public SoMap set(String key, Object value) {
// 防止敏感key
if(key.toLowerCase().equals("this")) {
return this;
}
put(key, value);
return this;
}
/** 将一个Map塞进SoMap */
public SoMap setMap(Map<String, ?> map) {
if(map != null) {
for (String key : map.keySet()) {
this.set(key, map.get(key));
}
}
return this;
}
/** 将一个对象解析塞进SoMap */
public SoMap setModel(Object model) {
if(model == null) {
return this;
}
Field[] fields = model.getClass().getDeclaredFields();
for (Field field : fields) {
try{
field.setAccessible(true);
boolean isStatic = Modifier.isStatic(field.getModifiers());
if(!isStatic) {
this.set(field.getName(), field.get(model));
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
return this;
}
/** 将json字符串解析后塞进SoMap */
public SoMap setJsonString(String jsonString) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> map = new ObjectMapper().readValue(jsonString, Map.class);
return this.setMap(map);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// ============================= 删值 =============================
/** delete一个值,连缀风格 */
public SoMap delete(String key) {
remove(key);
return this;
}
/** 清理所有value为null的字段 */
public SoMap clearNull() {
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(this.isNull(key)) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理指定key */
public SoMap clearIn(String ...keys) {
List<String> keys2 = Arrays.asList(keys);
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(keys2.contains(key) == true) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理掉不在列表中的key */
public SoMap clearNotIn(String ...keys) {
List<String> keys2 = Arrays.asList(keys);
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(keys2.contains(key) == false) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理掉所有key */
public SoMap clearAll() {
clear();
return this;
}
// ============================= 快速构建 =============================
/** 构建一个SoMap并返回 */
public static SoMap getSoMap() {
return new SoMap();
}
/** 构建一个SoMap并返回 */
public static SoMap getSoMap(String key, Object value) {
return new SoMap().set(key, value);
}
/** 构建一个SoMap并返回 */
public static SoMap getSoMap(Map<String, ?> map) {
return new SoMap().setMap(map);
}
/** 将一个对象集合解析成为SoMap */
public static SoMap getSoMapByModel(Object model) {
return SoMap.getSoMap().setModel(model);
}
/** 将一个对象集合解析成为SoMap集合 */
public static List<SoMap> getSoMapByList(List<?> list) {
List<SoMap> listMap = new ArrayList<SoMap>();
for (Object model : list) {
listMap.add(getSoMapByModel(model));
}
return listMap;
}
/** 克隆指定key,返回一个新的SoMap */
public SoMap cloneKeys(String... keys) {
SoMap so = new SoMap();
for (String key : keys) {
so.set(key, this.get(key));
}
return so;
}
/** 克隆所有key,返回一个新的SoMap */
public SoMap cloneSoMap() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key, this.get(key));
}
return so;
}
/** 将所有key转为大写 */
public SoMap toUpperCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key.toUpperCase(), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key转为小写 */
public SoMap toLowerCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key.toLowerCase(), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中下划线转为中划线模式 (kebab-case风格) */
public SoMap toKebabCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordEachKebabCase(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中下划线转为小驼峰模式 */
public SoMap toHumpCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordEachBigFs(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中小驼峰转为下划线模式 */
public SoMap humpToLineCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordHumpToLine(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
// ============================= 辅助方法 =============================
/** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */
public boolean isNull(String key) {
return valueIsNull(get(key));
}
/** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */
public boolean isContainNull(String ...keys) {
for (String key : keys) {
if(this.isNull(key)) {
return true;
}
}
return false;
}
/** 与isNull()相反 */
public boolean isNotNull(String key) {
return !isNull(key);
}
/** 指定key的value是否为null,作用同isNotNull() */
public boolean has(String key) {
return !isNull(key);
}
/** 指定value在此SoMap的判断标准中是否为null */
public boolean valueIsNull(Object value) {
return NULL_ELEMENT_LIST.contains(value);
}
/** 验证指定key不为空,为空则抛出异常 */
public SoMap checkNull(String ...keys) {
for (String key : keys) {
if(this.isNull(key)) {
throw new RuntimeException("参数" + key + "不能为空");
}
}
return this;
}
static Pattern patternNumber = Pattern.compile("[0-9]*");
/** 指定key是否为数字 */
public boolean isNumber(String key) {
String value = getString(key);
if(value == null) {
return false;
}
return patternNumber.matcher(value).matches();
}
/**
* 转为JSON字符串
*/
public String toJsonString() {
try {
// SoMap so = SoMap.getSoMap(this);
return new ObjectMapper().writeValueAsString(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// /**
// * 转为JSON字符串, 带格式的
// */
// public String toJsonFormatString() {
// try {
// return JSON.toJSONString(this, true);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }
// ============================= web辅助 =============================
/**
* 返回当前request请求的的所有参数
* @return
*/
public static SoMap getRequestSoMap() {
// 大善人SpringMVC提供的封装
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(servletRequestAttributes == null) {
throw new RuntimeException("当前线程非JavaWeb环境");
}
// 当前request
HttpServletRequest request = servletRequestAttributes.getRequest();
if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) {
initRequestSoMap(request);
}
return (SoMap)request.getAttribute("currentSoMap");
}
/** 初始化当前request的 SoMap */
private static void initRequestSoMap(HttpServletRequest request) {
SoMap soMap = new SoMap();
Map<String, String[]> parameterMap = request.getParameterMap(); // 获取所有参数
for (String key : parameterMap.keySet()) {
try {
String[] values = parameterMap.get(key); // 获得values
if(values.length == 1) {
soMap.set(key, values[0]);
} else {
List<String> list = new ArrayList<String>();
for (String v : values) {
list.add(v);
}
soMap.set(key, list);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
request.setAttribute("currentSoMap", soMap);
}
/**
* 验证返回当前线程是否为JavaWeb环境
* @return
*/
public static boolean isJavaWeb() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装
if(servletRequestAttributes == null) {
return false;
}
return true;
}
// ============================= 常见key (以下key经常用,所以封装以下,方便写代码) =============================
/** get 当前页 */
public int getKeyPageNo() {
int pageNo = getInt("pageNo", 1);
if(pageNo <= 0) {
pageNo = 1;
}
return pageNo;
}
/** get 页大小 */
public int getKeyPageSize() {
int pageSize = getInt("pageSize", 10);
if(pageSize <= 0 || pageSize > 1000) {
pageSize = 10;
}
return pageSize;
}
/** get 排序方式 */
public int getKeySortType() {
return getInt("sortType");
}
// ============================= 工具方法 =============================
/**
* 将一个一维集合转换为树形集合
* @param list 集合
* @param idKey id标识key
* @param parentIdKey 父id标识key
* @param childListKey 子节点标识key
* @return 转换后的tree集合
*/
public static List<SoMap> listToTree(List<SoMap> list, String idKey, String parentIdKey, String childListKey) {
// 声明新的集合,存储tree形数据
List<SoMap> newTreeList = new ArrayList<SoMap>();
// 声明hash-Map,方便查找数据
SoMap hash = new SoMap();
// 将数组转为Object的形式,key为数组中的id
for (int i = 0; i < list.size(); i++) {
SoMap json = (SoMap) list.get(i);
hash.put(json.getString(idKey), json);
}
// 遍历结果集
for (int j = 0; j < list.size(); j++) {
// 单条记录
SoMap aVal = (SoMap) list.get(j);
// 在hash中取出key为单条记录中pid的值
SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString());
// 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
if (hashVp != null) {
// 检查是否有child属性,有则添加,没有则新建
if (hashVp.get(childListKey) != null) {
@SuppressWarnings("unchecked")
List<SoMap> ch = (List<SoMap>) hashVp.get(childListKey);
ch.add(aVal);
hashVp.put(childListKey, ch);
} else {
List<SoMap> ch = new ArrayList<SoMap>();
ch.add(aVal);
hashVp.put(childListKey, ch);
}
} else {
newTreeList.add(aVal);
}
}
return newTreeList;
}
/** 指定字符串的字符串下划线转大写模式 */
private static String wordEachBig(String str){
String newStr = "";
for (String s : str.split("_")) {
newStr += wordFirstBig(s);
}
return newStr;
}
/** 返回下划线转小驼峰形式 */
private static String wordEachBigFs(String str){
return wordFirstSmall(wordEachBig(str));
}
/** 将指定单词首字母大写 */
private static String wordFirstBig(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1, str.length());
}
/** 将指定单词首字母小写 */
private static String wordFirstSmall(String str) {
return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
}
/** 下划线转中划线 */
private static String wordEachKebabCase(String str) {
return str.replaceAll("_", "-");
}
/** 驼峰转下划线 */
private static String wordHumpToLine(String str) {
return str.replaceAll("[A-Z]", "_$0").toLowerCase();
}
}
@@ -0,0 +1,14 @@
server:
port: 8002
spring:
# 静态文件路径映射
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
# static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-client\src\main\resources\static\
# sa-token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken-client
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>客户端-登录页</title>
<style type="text/css">
*{margin: 0px; padding: 0px;}
.login-box{width: 500px; margin: 50px auto;}
</style>
</head>
<body>
<div class="login-box">
<h2>客户端-登录页</h2> <br>
<div>
当前是否登录: <span class="login-info"></span>
<button onclick="logout()">注销登录</button>
</div>
<br/>
点此按钮开始使用OAuth2.0开放平台快捷登录:
<button onclick="requestOAuth2()">快捷登录</button>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script type="text/javascript">
// 获取登录信息
var currUserId = null;
function getLoginInfo() {
$.ajax({
url: '/getLoginInfo',
data: {},
dataType: 'json',
success: function(res) {
if(res.data == null) {
$(".login-info").html('<b style="color: red;">未登录</b>');
// 如果当前未登录,并且此时url中有code参数, 则尝试使用code码进行登录
var code = getParam('code', null);
if(code != null) {
doCodeLogin();
}
} else {
$(".login-info").html('<b style="color: green;">已登录, userId=' + res.data + '</b>');
currUserId = res.data;
}
},
error: function(e, ee, eee) {
console.log('error');
alert(eee);
}
});
}
getLoginInfo();
// 注销登录
function logout() {
$.ajax({
url: '/logout',
dataType: 'json',
success: function(res) {
alert('注销成功');
location.href = "./login.html";
},
error: function(e, ee, eee) {
alert(eee);
}
});
}
// 请求OAuth2授权
function requestOAuth2() {
// 如果当前已经登录,则必须先退出
if(currUserId != null) {
alert('当前已经登录, 请先注销');
return;
}
// 拼接地址
var url = "http://localhost:8001/oauth2/authorize" +
"?client_id=123123123" + // 应用id
"&scope=userinfo" + // 授权范围
// "&redirect_uri=" + encodeURIComponent(location.href) // 重定向地址
"&redirect_uri=" + encodeURIComponent("http://localhost:8002/login.html") // 重定向地址
"&response_type=code" + // 返回格式
"&state=123456789"; // 授权范围
console.log(url);
location.href = url;
}
// 使用code进行 (从url中获取code码进行登录)
function doCodeLogin() {
var code = getParam('code');
$.ajax({
url: '/doCodeLogin',
data: {
code: code
},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
alert('OAuth2登录成功');
getLoginInfo(); // 刷新user信息
} else {
alert("登录失败:" + res.msg);
}
},
error: function(e, ee, eee) {
alert(eee);
}
});
}
// 从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);
}
</script>
</body>
</html>
@@ -0,0 +1,13 @@
target/
.project
.classpath
.settings
/.idea/
node_modules/
bin/
.settings/
unpackage/
/.apt_generated/
/.apt_generated_tests/
@@ -0,0 +1,68 @@
<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>com.pj</groupId>
<artifactId>sa-token-demo-oauth2-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<!-- 指定一些属性 -->
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义sa-token版本号 -->
<sa-token-version>1.15.0</sa-token-version>
</properties>
<dependencies>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- sa-token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token-version}</version>
</dependency>
<!-- sa-token 实现 oauth2.0 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-oauth2</artifactId>
<version>1.15.0-alpha</version>
</dependency>
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${sa-token-version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency> -->
<!-- ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,18 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动
* @author kong
*/
@SpringBootApplication
public class SaOAuth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(SaOAuth2ServerApplication.class, args);
System.out.println("\n服务端启动成功");
}
}
@@ -0,0 +1,59 @@
package com.pj.controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.pj.utils.AjaxJson;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
/**
* 全局异常拦截
* <p> @ControllerAdvice 可指定包前缀,例如:(basePackages = "com.pj.controller.admin")
* @author kong
*
*/
@ControllerAdvice
public class ExceptionHandle {
/** 全局异常拦截 */
@ResponseBody
@ExceptionHandler
public AjaxJson handlerException(Exception e) {
// 打印堆栈,以供调试
e.printStackTrace();
// 记录日志信息
AjaxJson aj = null;
// ------------- 判断异常类型,提供个性化提示信息
// 如果是未登录异常
if(e instanceof NotLoginException){
aj = AjaxJson.getNotLogin();
}
// 如果是角色异常
else if(e instanceof NotRoleException) {
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getCode());
}
// 如果是权限异常
else if(e instanceof NotPermissionException) {
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
}
// 普通异常输出:500 + 异常信息
else {
aj = AjaxJson.getError(e.getMessage());
}
// 返回到前台
return aj;
}
}
@@ -0,0 +1,147 @@
package com.pj.controller;
import java.io.IOException;
import java.net.URLDecoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pj.utils.AjaxJson;
import com.pj.utils.SoMap;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
import cn.dev33.satoken.oauth2.model.AccessTokenModel;
import cn.dev33.satoken.oauth2.model.CodeModel;
import cn.dev33.satoken.oauth2.model.RequestAuthModel;
import cn.dev33.satoken.stp.StpUtil;
@RestController
@RequestMapping("/oauth2/")
public class OAuth2Controller {
// 获取授权码
@RequestMapping("/authorize")
public AjaxJson authorize(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// 获取参数
System.out.println("------------------ 成功进入请求 ------------------");
// 如果暂未登录,则先跳转到登录页 (转发)
if(StpUtil.isLogin() == false) {
response.setContentType("text/html");
request.getRequestDispatcher("/login.html").forward(request, response);
return AjaxJson.getSuccess();
}
// 构建Model
RequestAuthModel authModel = new RequestAuthModel()
.setClientId(request.getParameter("client_id")) // 应用id
.setScope(request.getParameter("scope")) // 授权类型
.setLoginId(StpUtil.getLoginIdAsLong()) // 当前登录账号id
.setRedirectUri(URLDecoder.decode(request.getParameter("redirect_uri"), "utf-8")) // 重定向地址
.setResponseType(request.getParameter("response_type")) // 返回类型
.setState(request.getParameter("state")) // 状态值
.checkModel(); // 校验参数完整性
// 生成授权码Model
CodeModel codeModel = SaOAuth2Util.generateCode(authModel);
// 打印调试
System.out.println("应用id=" + authModel.getClientId() + "请求授权,授权类型=" + authModel.getResponseType());
System.out.println("重定向地址:" + authModel.getRedirectUri());
System.out.println("拼接完成的redirect_uri: " + codeModel.getRedirectUri());
System.out.println("如果用户拒绝授权,则重定向至: " + codeModel.getRejectUri());
// 如果请求的权限用户已经确认,直接开始重定向授权
if(codeModel.getIsConfirm() == true) {
response.sendRedirect(codeModel.getRedirectUri());
} else {
// 如果请求的权限用户尚未确认,则进入到确定页
request.setAttribute("name", "sdd");
response.sendRedirect("/auth.html?code=" + codeModel.getCode());
}
return AjaxJson.getSuccess();
}
// 根据授权码获取应用信息
@RequestMapping("/getCodeInfo")
public AjaxJson getCodeInfo(String code) {
// 获取codeModel
CodeModel codeModel = SaOAuth2Util.getCode(code);
System.out.println(code);
System.out.println(codeModel);
// 返回
return AjaxJson.getSuccessData(codeModel);
}
// 确认授权一个授权码
@RequestMapping("/confirm")
public AjaxJson confirm(String code) {
// 获取codeModel
CodeModel codeModel = SaOAuth2Util.getCode(code);
if(codeModel == null) {
return AjaxJson.getError("无效code码");
}
// 此处的判断是为了保证当前账号id 和 创建授权码的账号id一致 才可以进行确认
if(codeModel.getLoginId().toString().equals(StpUtil.getLoginIdAsString()) == false) {
return AjaxJson.getError("暂无权限");
}
// 进行确认
SaOAuth2Util.confirmCode(code);
// 返回ok
return AjaxJson.getSuccess();
}
// 根据授权码等参数,获取 access_token 等信息
@RequestMapping("/getAccessToken")
public SoMap getAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取参数
System.out.println("------------------ 成功进入请求 ------------------");
String code = request.getParameter("code"); // code码
String clientId = request.getParameter("client_id"); // 应用id
String clientSecret = request.getParameter("client_secret"); // 应用秘钥
// 校验参数
SaOAuth2Util.checkCodeIdSecret(code, clientId, clientSecret);
// 生成
CodeModel codeModel = SaOAuth2Util.getCode(code);
AccessTokenModel tokenModel = SaOAuth2Util.generateAccessToken(codeModel);
// 生成AccessToken之后,将授权码立即销毁
SaOAuth2Util.deleteCode(code);
// 返回
return SoMap.getSoMap()
.setModel(tokenModel)
.set("code", 200)
.set("msg", "ok");
}
// 根据access_token返回指定的资源
@RequestMapping("/getResources")
public SoMap getResources(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取信息
String accessToken = request.getParameter("access_token");
Object LoginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
System.out.println("LoginId=" + LoginId);
// 根据LoginId获取相应信息...
// 此处仅做模拟
return new SoMap()
.set("nickname", "shengzhang")
.set("acatar", "xxx")
.set("sex", 1);
}
}
@@ -0,0 +1,28 @@
package com.pj.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pj.utils.AjaxJson;
import cn.dev33.satoken.stp.StpUtil;
/**
* 服务端登录Controller
* @author kong
*/
@RestController
public class ServerAccController {
// 登录方法
@RequestMapping("/doLogin")
public AjaxJson test(String username, String password) {
System.out.println("------------------ 成功进入请求 ------------------");
if("test".equals(username) && "test".equals(password)) {
StpUtil.setLoginId(10001);
return AjaxJson.getSuccess();
}
return AjaxJson.getError();
}
}
@@ -0,0 +1,72 @@
package com.pj.oauth2;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
/**
* 使用oauth2.0 所必须的一些自定义实现
* @author kong
*/
@Component
public class SaOAuth2InterfaceImpl implements SaOAuth2Interface {
/*
* ------ 注意: 以下代码均为示例,真实环境需要根据数据库查询相关信息
*/
// 返回此平台所有权限集合
@Override
public List<String> getAppScopeList() {
return Arrays.asList("userinfo");
}
// 返回指定Client签约的所有Scope集合
@Override
public List<String> getClientScopeList(String clientId) {
return Arrays.asList("userinfo");
}
// 获取指定 LoginId 对指定 Client 已经授权过的所有 Scope
@Override
public List<String> getGrantScopeList(Object loginId, String clientId) {
return Arrays.asList();
}
// 返回指定Client允许的回调域名, 多个用逗号隔开, *代表不限制
@Override
public String getClientDomain(String clientId) {
return "*";
}
// 返回指定ClientId的ClientSecret
@Override
public String getClientSecret(String clientId) {
return "aaaa-bbbb-cccc-dddd-eeee";
}
// 根据ClientId和LoginId返回openid
@Override
public String getOpenid(String clientId, Object loginId) {
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
}
// 根据ClientId和openid返回LoginId
@Override
public Object getLoginId(String clientId, String openid) {
return 10001;
}
/*
* 以上函数为开发时必须重写实现,其余函数可以按需重写
*/
}
@@ -0,0 +1,53 @@
package com.pj.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Interface;
/**
* 利用Spring完成自动装配
*
* @author kong
*
*/
@Component
public class SaOAuth2SpringAutowired {
/**
* 获取OAuth2配置Bean
*
* @return 配置对象
*/
@Bean
@ConfigurationProperties(prefix = "spring.sa-token.oauth2")
public SaOAuth2Config getSaOAuth2Config() {
return new SaOAuth2Config();
}
/**
* 注入OAuth2配置Bean
*
* @param saOAuth2Config 配置对象
*/
@Autowired
public void setSaOAuth2Config(SaOAuth2Config saOAuth2Config) {
SaOAuth2Manager.setConfig(saOAuth2Config);
}
/**
* 注入OAuth2接口Bean
*
* @param saOAuth2Interface OAuth2接口Bean
*/
@Autowired(required = false)
public void setSaOAuth2Interface(SaOAuth2Interface saOAuth2Interface) {
SaOAuth2Manager.setInterface(saOAuth2Interface);
}
}
@@ -0,0 +1,223 @@
package com.pj.utils;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ajax请求返回Json格式数据的封装 <br>
* 所有预留字段:<br>
* code=状态码 <br>
* msg=描述信息 <br>
* data=携带对象 <br>
* pageNo=当前页 <br>
* pageSize=页大小 <br>
* startIndex=起始索引 <br>
* dataCount=数据总数 <br>
* pageCount=分页总数 <br>
* <p> 返回范例:</p>
* <pre>
{
"code": 200, // 成功时=200, 失败时=500 msg=失败原因
"msg": "ok",
"data": {}
}
</pre>
*/
public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 200; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
// ============================ 写值取值 ==================================
/** 给code赋值,连缀风格 */
public AjaxJson setCode(int code) {
this.put("code", code);
return this;
}
/** 返回code */
public Integer getCode() {
return (Integer)this.get("code");
}
/** 给msg赋值,连缀风格 */
public AjaxJson setMsg(String msg) {
this.put("msg", msg);
return this;
}
/** 获取msg */
public String getMsg() {
return (String)this.get("msg");
}
/** 给data赋值,连缀风格 */
public AjaxJson setData(Object data) {
this.put("data", data);
return this;
}
/** 获取data */
public String getData() {
return (String)this.get("data");
}
/** 将data还原为指定类型并返回 */
@SuppressWarnings("unchecked")
public <T> T getData(Class<T> cs) {
return (T) this.getData();
}
/** 给dataCount(数据总数)赋值,连缀风格 */
public AjaxJson setDataCount(Long dataCount) {
this.put("dataCount", dataCount);
// 如果提供了数据总数,则尝试计算page信息
if(dataCount != null && dataCount >= 0) {
// 如果:已有page信息
if(get("pageNo") != null) {
this.initPageInfo();
}
// // 或者:是JavaWeb环境
// else if(SoMap.isJavaWeb()) {
// SoMap so = SoMap.getRequestSoMap();
// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
// this.initPageInfo();
// }
}
return this;
}
/** 获取dataCount(数据总数) */
public String getDataCount() {
return (String)this.get("dataCount");
}
/** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */
public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
this.put("pageNo", pageNo);
this.put("pageSize", pageSize);
return this;
}
/** 根据 pageSize dataCount,计算startIndex 与 pageCount */
public AjaxJson initPageInfo() {
long pageNo = (long)this.get("pageNo");
long pageSize = (long)this.get("pageSize");
long dataCount = (long)this.get("dataCount");
this.set("startIndex", (pageNo - 1) * pageSize);
long pc = dataCount / pageSize;
this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
return this;
}
/** 写入一个值 自定义key, 连缀风格 */
public AjaxJson set(String key, Object data) {
this.put(key, data);
return this;
}
/** 写入一个Map, 连缀风格 */
public AjaxJson setMap(Map<String, ?> map) {
for (String key : map.keySet()) {
this.put(key, map.get(key));
}
return this;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
this.setDataCount(dataCount == null ? -1 : dataCount);
}
/** 返回成功 */
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
/** 返回失败 */
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
/** 返回警告 */
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
/** 返回未登录 */
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
/** 返回没有权限的 */
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
/** 返回一个自定义状态码的 */
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
/** 返回分页和数据的 */
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
/** 返回, 根据受影响行数的(大于0=ok,小于0=error) */
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
/** 返回,根据布尔值来确定最终结果的 (true=okfalse=error) */
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
// // 历史版本遗留代码
// public int code; // 状态码
// public String msg; // 描述信息
// public Object data; // 携带对象
// public Long dataCount; // 数据总数,用于分页
}
@@ -0,0 +1,723 @@
package com.pj.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦
* <p>所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用
* <p>最新:2020-12-10 新增部分构造方法
* @author kong
*/
public class SoMap extends LinkedHashMap<String, Object> {
private static final long serialVersionUID = 1L;
public SoMap() {
}
/** 以下元素会在isNull函数中被判定为Null */
public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
public static final List<Object> NULL_ELEMENT_LIST;
static {
NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
}
// ============================= 读值 =============================
/** 获取一个值 */
@Override
public Object get(Object key) {
if("this".equals(key)) {
return this;
}
return super.get(key);
}
/** 如果为空,则返回默认值 */
public Object get(Object key, Object defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return value;
}
/** 转为String并返回 */
public String getString(String key) {
Object value = get(key);
if(value == null) {
return null;
}
return String.valueOf(value);
}
/** 如果为空,则返回默认值 */
public String getString(String key, String defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return String.valueOf(value);
}
/** 转为int并返回 */
public int getInt(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0;
}
return Integer.valueOf(String.valueOf(value));
}
/** 转为int并返回,同时指定默认值 */
public int getInt(String key, int defaultValue) {
Object value = get(key);
if(valueIsNull(value)) {
return defaultValue;
}
return Integer.valueOf(String.valueOf(value));
}
/** 转为long并返回 */
public long getLong(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0;
}
return Long.valueOf(String.valueOf(value));
}
/** 转为double并返回 */
public double getDouble(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return 0.0;
}
return Double.valueOf(String.valueOf(value));
}
/** 转为boolean并返回 */
public boolean getBoolean(String key) {
Object value = get(key);
if(valueIsNull(value)) {
return false;
}
return Boolean.valueOf(String.valueOf(value));
}
/** 转为Date并返回,根据自定义格式 */
public Date getDateByFormat(String key, String format) {
try {
return new SimpleDateFormat(format).parse(getString(key));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 转为Date并返回,根据格式: yyyy-MM-dd */
public Date getDate(String key) {
return getDateByFormat(key, "yyyy-MM-dd");
}
/** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */
public Date getDateTime(String key) {
return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
}
/** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
@SuppressWarnings("unchecked")
public List<Object> getList(String key) {
Object value = get(key);
List<Object> list = null;
if(value == null || value.equals("")) {
list = new ArrayList<Object>();
}
else if(value instanceof List) {
list = (List<Object>)value;
} else {
list = new ArrayList<Object>();
list.add(value);
}
return list;
}
/** 获取集合 (指定泛型类型) */
public <T> List<T> getList(String key, Class<T> cs) {
List<Object> list = getList(key);
List<T> list2 = new ArrayList<T>();
for (Object obj : list) {
T objC = getValueByClass(obj, cs);
list2.add(objC);
}
return list2;
}
/** 获取集合(逗号分隔式),(指定类型) */
public <T> List<T> getListByComma(String key, Class<T> cs) {
String listStr = getString(key);
if(listStr == null || listStr.equals("")) {
return new ArrayList<>();
}
// 开始转化
String [] arr = listStr.split(",");
List<T> list = new ArrayList<T>();
for (String str : arr) {
if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {
str = str.trim();
}
T objC = getValueByClass(str, cs);
list.add(objC);
}
return list;
}
/** 根据指定类型从map中取值,返回实体对象 */
public <T> T getModel(Class<T> cs) {
try {
return getModelByObject(cs.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 从map中取值,塞到一个对象中 */
public <T> T getModelByObject(T obj) {
// 获取类型
Class<?> cs = obj.getClass();
// 循环复制
for (Field field : cs.getDeclaredFields()) {
try {
// 获取对象
Object value = this.get(field.getName());
if(value == null) {
continue;
}
field.setAccessible(true);
Object valueConvert = getValueByClass(value, field.getType());
field.set(obj, valueConvert);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException("属性取值出错:" + field.getName(), e);
}
}
return obj;
}
/**
* 将指定值转化为指定类型并返回
* @param obj
* @param cs
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getValueByClass(Object obj, Class<T> cs) {
String obj2 = String.valueOf(obj);
Object obj3 = null;
if (cs.equals(String.class)) {
obj3 = obj2;
} else if (cs.equals(int.class) || cs.equals(Integer.class)) {
obj3 = Integer.valueOf(obj2);
} else if (cs.equals(long.class) || cs.equals(Long.class)) {
obj3 = Long.valueOf(obj2);
} else if (cs.equals(short.class) || cs.equals(Short.class)) {
obj3 = Short.valueOf(obj2);
} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {
obj3 = Byte.valueOf(obj2);
} else if (cs.equals(float.class) || cs.equals(Float.class)) {
obj3 = Float.valueOf(obj2);
} else if (cs.equals(double.class) || cs.equals(Double.class)) {
obj3 = Double.valueOf(obj2);
} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {
obj3 = Boolean.valueOf(obj2);
} else {
obj3 = (T)obj;
}
return (T)obj3;
}
// ============================= 写值 =============================
/**
* 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去)
*/
public void setDefaultValue(String key, Object defaultValue) {
if(isNull(key)) {
set(key, defaultValue);
}
}
/** set一个值,连缀风格 */
public SoMap set(String key, Object value) {
// 防止敏感key
if(key.toLowerCase().equals("this")) {
return this;
}
put(key, value);
return this;
}
/** 将一个Map塞进SoMap */
public SoMap setMap(Map<String, ?> map) {
if(map != null) {
for (String key : map.keySet()) {
this.set(key, map.get(key));
}
}
return this;
}
/** 将一个对象解析塞进SoMap */
public SoMap setModel(Object model) {
if(model == null) {
return this;
}
Field[] fields = model.getClass().getDeclaredFields();
for (Field field : fields) {
try{
field.setAccessible(true);
boolean isStatic = Modifier.isStatic(field.getModifiers());
if(!isStatic) {
this.set(field.getName(), field.get(model));
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
return this;
}
/** 将json字符串解析后塞进SoMap */
public SoMap setJsonString(String jsonString) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> map = new ObjectMapper().readValue(jsonString, Map.class);
return this.setMap(map);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// ============================= 删值 =============================
/** delete一个值,连缀风格 */
public SoMap delete(String key) {
remove(key);
return this;
}
/** 清理所有value为null的字段 */
public SoMap clearNull() {
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(this.isNull(key)) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理指定key */
public SoMap clearIn(String ...keys) {
List<String> keys2 = Arrays.asList(keys);
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(keys2.contains(key) == true) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理掉不在列表中的key */
public SoMap clearNotIn(String ...keys) {
List<String> keys2 = Arrays.asList(keys);
Iterator<String> iterator = this.keySet().iterator();
while(iterator.hasNext()) {
String key = iterator.next();
if(keys2.contains(key) == false) {
iterator.remove();
this.remove(key);
}
}
return this;
}
/** 清理掉所有key */
public SoMap clearAll() {
clear();
return this;
}
// ============================= 快速构建 =============================
/** 构建一个SoMap并返回 */
public static SoMap getSoMap() {
return new SoMap();
}
/** 构建一个SoMap并返回 */
public static SoMap getSoMap(String key, Object value) {
return new SoMap().set(key, value);
}
/** 构建一个SoMap并返回 */
public static SoMap getSoMap(Map<String, ?> map) {
return new SoMap().setMap(map);
}
/** 将一个对象集合解析成为SoMap */
public static SoMap getSoMapByModel(Object model) {
return SoMap.getSoMap().setModel(model);
}
/** 将一个对象集合解析成为SoMap集合 */
public static List<SoMap> getSoMapByList(List<?> list) {
List<SoMap> listMap = new ArrayList<SoMap>();
for (Object model : list) {
listMap.add(getSoMapByModel(model));
}
return listMap;
}
/** 克隆指定key,返回一个新的SoMap */
public SoMap cloneKeys(String... keys) {
SoMap so = new SoMap();
for (String key : keys) {
so.set(key, this.get(key));
}
return so;
}
/** 克隆所有key,返回一个新的SoMap */
public SoMap cloneSoMap() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key, this.get(key));
}
return so;
}
/** 将所有key转为大写 */
public SoMap toUpperCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key.toUpperCase(), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key转为小写 */
public SoMap toLowerCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(key.toLowerCase(), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中下划线转为中划线模式 (kebab-case风格) */
public SoMap toKebabCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordEachKebabCase(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中下划线转为小驼峰模式 */
public SoMap toHumpCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordEachBigFs(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
/** 将所有key中小驼峰转为下划线模式 */
public SoMap humpToLineCase() {
SoMap so = new SoMap();
for (String key : this.keySet()) {
so.set(wordHumpToLine(key), this.get(key));
}
this.clearAll().setMap(so);
return this;
}
// ============================= 辅助方法 =============================
/** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */
public boolean isNull(String key) {
return valueIsNull(get(key));
}
/** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */
public boolean isContainNull(String ...keys) {
for (String key : keys) {
if(this.isNull(key)) {
return true;
}
}
return false;
}
/** 与isNull()相反 */
public boolean isNotNull(String key) {
return !isNull(key);
}
/** 指定key的value是否为null,作用同isNotNull() */
public boolean has(String key) {
return !isNull(key);
}
/** 指定value在此SoMap的判断标准中是否为null */
public boolean valueIsNull(Object value) {
return NULL_ELEMENT_LIST.contains(value);
}
/** 验证指定key不为空,为空则抛出异常 */
public SoMap checkNull(String ...keys) {
for (String key : keys) {
if(this.isNull(key)) {
throw new RuntimeException("参数" + key + "不能为空");
}
}
return this;
}
static Pattern patternNumber = Pattern.compile("[0-9]*");
/** 指定key是否为数字 */
public boolean isNumber(String key) {
String value = getString(key);
if(value == null) {
return false;
}
return patternNumber.matcher(value).matches();
}
/**
* 转为JSON字符串
*/
public String toJsonString() {
try {
// SoMap so = SoMap.getSoMap(this);
return new ObjectMapper().writeValueAsString(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// /**
// * 转为JSON字符串, 带格式的
// */
// public String toJsonFormatString() {
// try {
// return JSON.toJSONString(this, true);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }
// ============================= web辅助 =============================
/**
* 返回当前request请求的的所有参数
* @return
*/
public static SoMap getRequestSoMap() {
// 大善人SpringMVC提供的封装
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(servletRequestAttributes == null) {
throw new RuntimeException("当前线程非JavaWeb环境");
}
// 当前request
HttpServletRequest request = servletRequestAttributes.getRequest();
if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) {
initRequestSoMap(request);
}
return (SoMap)request.getAttribute("currentSoMap");
}
/** 初始化当前request的 SoMap */
private static void initRequestSoMap(HttpServletRequest request) {
SoMap soMap = new SoMap();
Map<String, String[]> parameterMap = request.getParameterMap(); // 获取所有参数
for (String key : parameterMap.keySet()) {
try {
String[] values = parameterMap.get(key); // 获得values
if(values.length == 1) {
soMap.set(key, values[0]);
} else {
List<String> list = new ArrayList<String>();
for (String v : values) {
list.add(v);
}
soMap.set(key, list);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
request.setAttribute("currentSoMap", soMap);
}
/**
* 验证返回当前线程是否为JavaWeb环境
* @return
*/
public static boolean isJavaWeb() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装
if(servletRequestAttributes == null) {
return false;
}
return true;
}
// ============================= 常见key (以下key经常用,所以封装以下,方便写代码) =============================
/** get 当前页 */
public int getKeyPageNo() {
int pageNo = getInt("pageNo", 1);
if(pageNo <= 0) {
pageNo = 1;
}
return pageNo;
}
/** get 页大小 */
public int getKeyPageSize() {
int pageSize = getInt("pageSize", 10);
if(pageSize <= 0 || pageSize > 1000) {
pageSize = 10;
}
return pageSize;
}
/** get 排序方式 */
public int getKeySortType() {
return getInt("sortType");
}
// ============================= 工具方法 =============================
/**
* 将一个一维集合转换为树形集合
* @param list 集合
* @param idKey id标识key
* @param parentIdKey 父id标识key
* @param childListKey 子节点标识key
* @return 转换后的tree集合
*/
public static List<SoMap> listToTree(List<SoMap> list, String idKey, String parentIdKey, String childListKey) {
// 声明新的集合,存储tree形数据
List<SoMap> newTreeList = new ArrayList<SoMap>();
// 声明hash-Map,方便查找数据
SoMap hash = new SoMap();
// 将数组转为Object的形式,key为数组中的id
for (int i = 0; i < list.size(); i++) {
SoMap json = (SoMap) list.get(i);
hash.put(json.getString(idKey), json);
}
// 遍历结果集
for (int j = 0; j < list.size(); j++) {
// 单条记录
SoMap aVal = (SoMap) list.get(j);
// 在hash中取出key为单条记录中pid的值
SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString());
// 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
if (hashVp != null) {
// 检查是否有child属性,有则添加,没有则新建
if (hashVp.get(childListKey) != null) {
@SuppressWarnings("unchecked")
List<SoMap> ch = (List<SoMap>) hashVp.get(childListKey);
ch.add(aVal);
hashVp.put(childListKey, ch);
} else {
List<SoMap> ch = new ArrayList<SoMap>();
ch.add(aVal);
hashVp.put(childListKey, ch);
}
} else {
newTreeList.add(aVal);
}
}
return newTreeList;
}
/** 指定字符串的字符串下划线转大写模式 */
private static String wordEachBig(String str){
String newStr = "";
for (String s : str.split("_")) {
newStr += wordFirstBig(s);
}
return newStr;
}
/** 返回下划线转小驼峰形式 */
private static String wordEachBigFs(String str){
return wordFirstSmall(wordEachBig(str));
}
/** 将指定单词首字母大写 */
private static String wordFirstBig(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1, str.length());
}
/** 将指定单词首字母小写 */
private static String wordFirstSmall(String str) {
return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
}
/** 下划线转中划线 */
private static String wordEachKebabCase(String str) {
return str.replaceAll("_", "-");
}
/** 驼峰转下划线 */
private static String wordHumpToLine(String str) {
return str.replaceAll("[A-Z]", "_$0").toLowerCase();
}
}
@@ -0,0 +1,41 @@
server:
port: 8001
spring:
# 静态文件路径映射
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
# static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-server\src\main\resources\static\
# sa-token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken-server
# redis配置
redis:
# Redis数据库索引(默认为0
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间(毫秒)
timeout: 1000ms
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>服务提供方-确认授权页</title>
<style type="text/css">
*{margin: 0px; padding: 0px;}
.login-box{width: 300px; margin: 50px auto; padding: 100px; border: 1px #000 solid;}
</style>
</head>
<body>
<div class="login-box">
<h2>服务提供方-确认授权页</h2> <br>
<div>
<div>应用id: <span class="client_id"></span></div>
<div>请求授权: <span class="scope"></span></div>
<br>
<div>是否同意授权: </div>
<div>
<button onclick="ok()">同意</button>
<button onclick="no()">拒绝</button>
</div>
</div>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script type="text/javascript">
// 获取code详细信息
var code = getParam("code", null);
var codeObj = null;
if(code == null) {
throw alert("无效code");
}
$.ajax({
url: '/oauth2/getCodeInfo',
data: {code: code},
dataType: 'json',
success: function(res) {
if(res.data == null) {
return alert('无效授权码');
}
codeObj = res.data;
$(".client_id").html(res.data.clientId);
$(".scope").html(res.data.scope);
},
error: function(e, ee, eee) {
console.log('error');
alert(eee);
}
});
// 确认授权
function ok() {
$.ajax({
url: '/oauth2/confirm',
data: {code: code},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
// 跳转
location.href = codeObj.redirectUri;
} else {
alert(res.msg);
}
},
error: function(e, ee, eee) {
alert(eee);
}
});
}
// 拒绝授权时,跳入拒绝授权地址
function no() {
// alert(codeObj.rejectUri)
location.href = codeObj.rejectUri;
}
// 从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);
}
</script>
</body>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>服务提供方-登录页</title>
<style type="text/css">
*{margin: 0px; padding: 0px;}
.login-box{width: 400px; margin: 50px auto;}
</style>
</head>
<body>
<div class="login-box">
<h2>服务提供方-登录页</h2> <br>
<div>注:您当前在服务提供方尚未登录,请先登录</div>
<div>测试账号: test test</div> <br>
账号:<input name="username" /> <br>
密码:<input name="password" type="password" /> <br>
<button onclick="doLogin()">登录</button>
</div>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<script type="text/javascript">
// 登录方法
function getLoginId() {
}
// 登录方法
function doLogin() {
console.log('-----------');
$.ajax({
url: '/doLogin',
data: {
username: $('[name=username]').val(),
password: $('[name=password]').val()
},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
alert('登录成功');
location.reload(true);
} else {
alert('登录失败');
}
},
error: function(e) {
console.log('error');
}
});
}
</script>
</body>
</html>
+10 -4
View File
@@ -11,7 +11,13 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/>
</parent>
<!-- 定义sa-token版本号 -->
<properties>
<sa-token-version>1.15.0</sa-token-version>
</properties>
<dependencies>
@@ -29,21 +35,21 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency>
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency> -->
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency> -->
<!-- 提供redis连接池 -->
@@ -56,7 +62,7 @@
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.11.0</version>
<version>${sa-token-version}</version>
</dependency> -->
<!-- @ConfigurationProperties -->
@@ -1,5 +1,6 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -4,34 +4,21 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.interceptor.SaCheckInterceptor;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
/**
* sa-token代码方式进行配置
* @author kong
*
*/
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 获取配置Bean (以代码的方式配置sa-token, 此配置会覆盖yml中的配置 )
// @Primary
// @Bean(name="MySaTokenConfig")
public SaTokenConfig getSaTokenConfig() {
SaTokenConfig config = new SaTokenConfig();
config.setTokenName("satoken"); // token名称 (同时也是cookie名称)
config.setTimeout(30 * 24 * 60 * 60); // token有效期,单位s 默认30天
config.setActivityTimeout(-1); // token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
config.setAllowConcurrentLogin(true); // 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
config.setIsShare(true); // 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
config.setTokenStyle("uuid"); // token风格
return config;
}
// 注册sa-token的拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaCheckInterceptor()).addPathPatterns("/**"); // 全局拦截器
// 注册注解拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
}
@@ -17,8 +17,9 @@ public class StpInterfaceImpl implements StpInterface {
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object login_id, String login_key) {
List<String> list = new ArrayList<String>(); // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
public List<String> getPermissionList(Object loginId, String loginKey) {
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user-add");
list.add("user-delete");
@@ -33,7 +34,8 @@ public class StpInterfaceImpl implements StpInterface {
*/
@Override
public List<String> getRoleList(Object loginId, String loginKey) {
List<String> list = new ArrayList<String>(); // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
@@ -5,9 +5,10 @@ import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import com.pj.util.AjaxJson;
@@ -18,24 +19,24 @@ import cn.dev33.satoken.exception.NotRoleException;
/**
* 全局异常处理
*/
@RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin")
@ControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin")
public class GlobalException {
// 在每个控制器之前触发的操作
// 在当前类每个方法进入之前触发的操作
@ModelAttribute
public void get(HttpServletRequest request) throws IOException {
}
// 全局异常拦截(拦截项目中的所有异常)
@ResponseBody
@ExceptionHandler
public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 打印堆栈,以供调试
System.out.println("全局异常---------------");
e.printStackTrace();
// 不同异常返回不同状态码
@@ -0,0 +1,35 @@
package com.pj.test;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.pj.util.AjaxJson;
import cn.dev33.satoken.stp.StpUtil;
/**
* 测试: 同域单点登录
* @author kong
*/
@RestController
@RequestMapping("/sso/")
public class SSOController {
// 测试:进行登录
@RequestMapping("doLogin")
public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) {
System.out.println("---------------- 进行登录 ");
StpUtil.setLoginId(id);
return AjaxJson.getSuccess("登录成功: " + id);
}
// 测试:是否登录
@RequestMapping("isLogin")
public AjaxJson isLogin() {
System.out.println("---------------- 是否登录 ");
boolean isLogin = StpUtil.isLogin();
return AjaxJson.getSuccess("是否登录: " + isLogin);
}
}
@@ -3,7 +3,6 @@ package com.pj.test;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -145,7 +144,6 @@ public class TestController {
return AjaxJson.getSuccess();
}
// 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo
@RequestMapping("tokenInfo")
public AjaxJson tokenInfo() {
@@ -203,6 +201,18 @@ public class TestController {
return AjaxJson.getSuccess();
}
// 测试身份临时切换: http://localhost:8081/test/switchTo
@RequestMapping("switchTo")
public AjaxJson switchTo() {
System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull());
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
StpUtil.switchTo(10044, () -> {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId());
});
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
return AjaxJson.getSuccess();
}
// 测试会话治理 浏览器访问: http://localhost:8081/test/search
@RequestMapping("search")
@@ -218,24 +228,30 @@ public class TestController {
return AjaxJson.getSuccess();
}
@Autowired
TestService TestService;
// 测试AOP注解鉴权: http://localhost:8081/test/testAOP
@RequestMapping("testAOP")
public AjaxJson testAOP() {
System.out.println("testAOP");
TestService.getList();
return AjaxJson.getSuccess();
// 测试指定设备登录 浏览器访问: http://localhost:8081/test/loginByDevice
@RequestMapping("loginByDevice")
public AjaxJson loginByDevice() {
System.out.println("--------------");
StpUtil.setLoginId(10001, "PC");
return AjaxJson.getSuccessData("登录成功");
}
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
StpUtil.logoutByLoginId(10001);
System.out.println("进来了");
// StpUtil.setLoginId(10001, new SaLoginModel()
// .setDevice("PC") // 此次登录的客户端设备标识, 用于[同端互斥登录]时指定此次登录的设备名称
// .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
// .setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
// );
return AjaxJson.getSuccess("访问成功");
}
// 测试 浏览器访问: http://localhost:8081/test/test2
@RequestMapping("test2")
public AjaxJson test2() {
return AjaxJson.getSuccess();
}
@@ -1,25 +0,0 @@
package com.pj.test;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import cn.dev33.satoken.annotation.SaCheckLogin;
/**
* 用来测试AOP注解鉴权
* @author kong
*
*/
@Service
public class TestService {
@SaCheckLogin
public List<String> getList() {
System.out.println("getList");
return new ArrayList<String>();
}
}
@@ -0,0 +1,22 @@
package com.pj.test;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.stp.StpUtil;
@RestController
@RequestMapping("user")
public class UserController {
// 测试 浏览器访问: http://localhost:8081/user/doLogin
@RequestMapping("doLogin")
public String test(String username, String password) {
if("zhang".equals(username) && "123456".equals(password)) {
StpUtil.setLoginId(10001);
return "登录成功";
}
return "登录失败";
}
}
+131 -57
View File
@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150" style="margin-bottom: 10px;">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.15.0</h1>
<h4 align="center">这可能是史上功能最全的Java权限认证框架!</h4>
<h4 align="center">
<a href="https://gitee.com/sz6/sa-token/stargazers"><img src="https://gitee.com/sz6/sa-token/badge/star.svg"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.11.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.15.0-2B9939"></a>
<a href="https://github.com/click33/sa-token/stargazers"><img src="https://img.shields.io/github/stars/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/watchers"><img src="https://img.shields.io/github/watchers/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/network/members"><img src="https://img.shields.io/github/forks/click33/sa-token"></a>
@@ -25,24 +25,43 @@
- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token)
- [开源不易,求鼓励,点个star吧](###)
## sa-token是什么?
sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登录认证、权限认证、Session会话等一系列由此衍生的权限相关业务
在架构设计上`sa-token`拒绝引入复杂的概念,以实际业务需求为第一目标进行定向突破,例如踢人下线、自动续签、同端互斥登录等常见业务在框架内均可以一行代码调用实现,简单粗暴,拒绝复杂!
## Sa-Token是什么?
sa-token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题
对于传统Session会话模型的N多难题,例如难以分布式、水平扩展性差,难以兼容前后台分离环境,多会话管理混乱等,
`sa-token`独创了以账号为主的`Id-Session`模式,同时又兼容了传统以token为主的`Token-Session`模式,两者彼此独立,互不干扰,
让你在进行会话管理时可以如鱼得水,在`sa-toekn`的强力加持下,权限问题将不再成为业务逻辑的瓶颈!
框架针对踢人下线、自动续签、前后台分离、分布式会话……等常见业务进行N多适配,通过sa-token,你可以以一种极简的方式实现系统的权限认证部分
总的来说,与其它权限认证框架相比,`sa-token`具有以下优势:
1. 上手简单:可零配置启动框架,能自动化的均已自动化,不让你费脑子
2. 功能强大:能集成的功能全部集成,不让你用个框架还要自己给框架打各种补丁
3. API简单易用:同样的一个功能,可能在别的框架中需要上百行代码,但是在sa-token中统统一行代码调个方法即可解决
4. 组件易于扩展:框架中几乎所有组件都提供了对应的扩展接口,90%以上的逻辑都可以按需重写
与其它权限认证框架相比,`sa-token` 具有以下优势:
1. **简单** :可零配置启动框架,真正的开箱即用,低成本上手
2. **强大** :目前已集成几十项权限相关特性,涵盖了大部分业务场景的解决方案
3. **易用** :如丝般顺滑的API调用,大量高级特性统统只需一行代码即可实现
4. **高扩展** 几乎所有组件都提供了扩展接口,90%以上的逻辑都可以按需重写
有了sa-token是时候和那些老旧权限框架说拜拜了
有了sa-token你所有的权限认证问题,都不再是问题
## Sa-Token 能做什么?
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC权限模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
- **分布式会话** —— 提供jwt集成和共享数据中心两种分布式会话方案
- **单点登录** —— 一处登录,处处通行
- **模拟他人账号** —— 实时操作任意用户状态数据
- **临时身份切换** —— 将会话身份临时切换为其它账号
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配restful模式
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **会话治理** —— 提供方便灵活的会话查询接口
- **记住我模式** —— 适配[记住我]模式,重启浏览器免验证
- **密码加密** —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
- **组件自动注入** —— 零配置与Spring等框架集成
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 代码示例
@@ -50,51 +69,67 @@ sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登
sa-token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
``` java
// 在登录时写入当前会话的账号id
StpUtil.setLoginId(10001);
// 在登录时写入当前会话的账号id
StpUtil.setLoginId(10001);
// 然后在任意需要校验登录处调用以下API --- 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
StpUtil.checkLogin();
// 然后在任意需要校验登录处调用以下API
// 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
StpUtil.checkLogin();
```
至此,我们已经借助sa-token框架完成登录授权!
此时的你小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗?
事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权!
当你受够Shiro、Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,sa-token的API设计是多么的清爽!
权限认证示例 (只有具有`user:add`权限的会话才可以进入请求)
``` java
@SaCheckPermission("user:add")
@RequestMapping("/user/insert")
public String insert(SysUser user) {
// ...
return "用户增加";
}
```
如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大
将某个账号踢下线 (待到对方再次访问系统时会抛出`NotLoginException`异常)
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
// 使账号id为10001的会话注销登录
StpUtil.logoutByLoginId(10001);
```
除了以上的示例,sa-token还可以一行代码完成以下功能:
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
```
sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档
## 涵盖功能
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
- **持久层扩展** —— 可集成redis、MongoDB等专业缓存中间件
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **组件自动注入** —— 零配置与Spring等框架集成
- **会话治理** —— 提供方便灵活的会话查询接口
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 迭代模式
sa-token的功能提案主要来源于社区,这意味着人人都可以参与到sa-token的功能定制,决定框架的未来走向,
如果你有好的想法,可以在issues提出或者加入群一起交流,对于社区的提出的功能要求,主要分为以下几类:
- 对框架新增特性功能且比较简单,会在第一时间进行开发
- 对框架新增特性功能但比较复杂,会延后几个版本制定相应的计划后进行开发
- 与框架设计理念不太相符,或超出权限认证范畴,将会视需求人数决定是否开发
## 参与贡献
众人拾柴火焰高,sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦
众人拾柴火焰高,万丈高楼众人起!
sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦,对框架有卓越贡献者将会出现在贡献者名单里
1. 在gitee或者github上fork一份代码到自己的仓库
2. clone自己的仓库到本地电脑
@@ -107,20 +142,59 @@ sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳
## 建议贡献的地方
- 修复源码现有bug,或优化代码架构,或增加新的实用功能
- 完善在线文档,或者修复现有描述错误之处
- 更多的第三方框架集成方案,更多的demo示例:比如SSM版搭建步骤
- 您可以参考项目issues与需求墙进行贡献
目前框架的主要有以下部分需要大家一起参与贡献:
- 核心代码:该部分需要开发者了解整个框架的架构,遵循已有代码规范进行bug修复或提交新功能
- 文档部分:需要以清晰明了的语句书写文档,力求简单易读,授人以鱼同时更授人以渔
- 社区建设:如果框架帮助到了您,希望您可以加入qq群参与交流,对不熟悉框架的新人进行排难解惑
- 框架推广:一个优秀的开源项目不能仅靠闭门造车,它还需要一定的推广方案让更多的人一起参与到项目中
- 其它部分:您可以参考项目issues与需求墙进行贡献
## 贡献者名单
[省长](https://gitee.com/sz6)、
[RockMan](https://gitee.com/njx33)、
[click33](https://github.com/click33)、
[AppleOfGray](https://gitee.com/appleOfGray)、
[Auster](https://github.com/auster9021)、
[ZhuBJ0510](https://gitee.com/zhubj0510)、
[legg](https://gitee.com/legg321)、
[xiaoshitou](https://gitee.com/smallstoneZ)、
[zhangjiaxiaozhuo](https://gitee.com/zhangjiaxiaozhuo)、
[离你多远](https://gitee.com/liniduoyuan)
## 知乎专栏
- [初识sa-token,一行代码搞定登录授权!](https://zhuanlan.zhihu.com/p/344106099)
- [一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录](https://zhuanlan.zhihu.com/p/344511415)
- [浅谈踢人下线的设计思路!(附代码实现方案)](https://zhuanlan.zhihu.com/p/345844002)
- 文章已在 [csdn](https://blog.csdn.net/shengzhang_/article/details/112593247)、
[掘金](https://juejin.cn/post/6917250126650015751)、
[开源中国](https://my.oschina.net/u/3503445/blog/4897816)、
[博客园](https://www.cnblogs.com/shengzhang/p/14275558.html)、
[知乎](https://zhuanlan.zhihu.com/p/344106099)
等平台连载中...欢迎投稿
## 使用sa-token的开源项目
[**[ sa-plus]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/sz6/sa-plus)
如果您的项目使用了sa-token,欢迎提交pr
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ okhttps ]** 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing)
## 交流群
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) ,欢迎你的加入
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM)
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=230')
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=150')
**微信群**
![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230')
(扫码添加微信,备注:sa-token,邀您加入群聊)
<br>
+14 -3
View File
@@ -10,15 +10,23 @@
- [权限验证](/use/jur-auth)
- [Session会话](/use/session)
- [踢人下线](/use/kick)
- [持久层扩展(集成Redis)](/use/dao-extend)
- [无Cookie模式(前后台分离)](/use/not-cookie)
- [持久层扩展集成Redis](/use/dao-extend)
- [无Cookie模式前后台分离](/use/not-cookie)
- [模拟他人](/use/mock-person)
- [多账号验证](/use/many-account)
- [同端互斥登录](/use/mutex-login)
- [注解式鉴权](/use/at-check)
- [路由拦截式鉴权](/use/route-check)
- [花式token](/use/token-style)
- [Token前缀](/use/token-prefix)
- [框架配置](/use/config)
- [会话治理](/use/search-session)
- [记住我模式](/use/remember-me)
- [密码加密](/use/password-secure)
- **进阶**
- [集群、分布式](/senior/dcs)
- [单点登录](/senior/sso)
- [多账号验证](/use/many-account)
- **其它**
- [常见问题](/more/common-questions)
@@ -28,6 +36,9 @@
- **附录**
- [未登录场景值](/fun/not-login-scene)
- [token有效期详解](/fun/token-timeout)
- [Session模型详解](/fun/session-model)
- [TokenInfo参数详解](/fun/token-info)
- [框架源码所有技术栈](/fun/tech-stack)
+43 -32
View File
@@ -5,43 +5,54 @@
## 何为场景值
- 在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分 <br>
- 那么,如何获取场景值呢?废话少说直接上代码:
在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分
| 场景值 | 对应常量 | 含义说明 |
|--- |--- |--- |
| -1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到token |
| -2 | NotLoginException.INVALID_TOKEN| 已读取到token,但是token无效 |
| -3 | NotLoginException.TOKEN_TIMEOUT| 已读取到token,但是token已经过期 |
| -4 | NotLoginException.BE_REPLACED| 已读取到token,但是token已被顶下线 |
| -5 | NotLoginException.KICK_OUT| 已读取到token,但是token已被踢下线 |
那么,如何获取场景值呢?废话少说直接上代码:
``` java
// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token已被踢下线";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return AjaxJson.getError(message);
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token已被踢下线";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return AjaxJson.getError(message);
}
```
<br/>
+21
View File
@@ -0,0 +1,21 @@
# Session模型详解
---
`sa-token`中, `Session` 分为三种, 分别是:
- `User-Session`: 指的是框架为每个`loginId`分配的`Session`
- `Token-Session`: 指的是框架为每个`token`分配的`Session`
- `自定义Session`: 指的是以一个`特定的值`作为SessionId,来分配的`Session`
User-Session和Token-Session到底有什么不同?下面这张图可以解释两者的区别
![../static/session-model.png](../static/session-model.png)
简而言之:
- `Token-Session` 以token为主,只要token不同,那么对应的Session对象就不同
- `User-Session` 以UserId为主,只要token指向的UserId一致,那么对应的Session对象就一致
- `自定义Session` 以特定的key为主,不同key对应不同的Session对象
+15
View File
@@ -0,0 +1,15 @@
# sa-token 源码用到的所有技术栈
包括但不限于以下:
- Maven多模块项目
- Servlet API、临时Cookie与永久Cookie、Request参数获取
- SpringBoot2.0、Redis、Jackson、Hutool、jwt
- SpringBoot自定义starter、Spring包扫码 + 依赖注入、AOP注解切面、yml配置映射、拦截器
- Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程
- package-info注释、Serializable序列化接口、synchronized锁
- java加密算法:MD5、SHA1、SHA256、AES、RSA
- OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配
+22
View File
@@ -0,0 +1,22 @@
# SaTokenInfo 参数详解
token信息Model: 用来描述一个token的常用参数
``` js
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginKey": "login", // LoginKey账号体系标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒)
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备标识
},
}
```
+21 -17
View File
@@ -6,40 +6,44 @@
### timeout
- `timeout`代表token的长久有效期,单位/秒,例如将其配置为`2592000`(30天),代表在30天后,token必定过期,无法继续使用
- `timeout`无法续签,想要继续使用必须重新登录
- `timeout`的值配置为-1后,代表永久有效,不会过期
1. `timeout`代表token的长久有效期,单位/秒,例如将其配置为`2592000`(30天),代表在30天后,token必定过期,无法继续使用
2. `timeout`无法续签,想要继续使用必须重新登录
3. `timeout`的值配置为-1后,代表永久有效,不会过期
### activity-timeout
- `activity-timeout`代表临时有效期,单位/秒,例如将其配置为`1800`(30分钟),代表用户如果30分钟无操作,则此token会立即过期
- 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,token才会过期
- `activity-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签
1. `activity-timeout`代表临时有效期,单位/秒,例如将其配置为`1800`(30分钟),代表用户如果30分钟无操作,则此token会立即过期
2. 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,token才会过期
3. `activity-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签
### 关于activity-timeout的续签
如果`activity-timeout`配置了大于零的值,`sa-token`会在登录时开始计时,在每次直接或间接调用`getLoginId()`时进行一次过期检查与续签操作。
此时会有两种情况:
- 一种是会话无操作时间太长,token已经过期,此时框架会抛出`NotLoginException`异常(场景值=-3)
- 另一种则是会话在`activity-timeout`有效期内通过检查,此时token可以成功续签
1. 一种是会话无操作时间太长,token已经过期,此时框架会抛出`NotLoginException`异常(场景值=-3)
2. 另一种则是会话在`activity-timeout`有效期内通过检查,此时token可以成功续签
### 我可以手动续签吗?
**可以!**
如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,`sa-token`提供两个API供你操作:
- `StpUtil.checkActivityTimeout()`: 检查当前token 是否已经[临时过期],如果已经过期则抛出异常
- `StpUtil.updateLastActivityToNow()`: 续签当前token:(将 [最后操作时间] 更新为当前时间戳)
- 注意:在手动续签时,即时token已经 [临时过期] 也可续签成功,如果此场景下需要提示续签失败,可采用先检查再续签的形式保证token有效性
- 例如以下代码:
1. `StpUtil.checkActivityTimeout()`: 检查当前token 是否已经[临时过期],如果已经过期则抛出异常
2. `StpUtil.updateLastActivityToNow()`: 续签当前token:(将 [最后操作时间] 更新为当前时间戳)
注意:在手动续签时,即时token已经 [临时过期] 也可续签成功,如果此场景下需要提示续签失败,可采用先检查再续签的形式保证token有效性
例如以下代码:
``` java
// 先检查是否已过期
StpUtil.checkActivityTimeout();
// 检查通过后继续续签
StpUtil.updateLastActivityToNow();
// 先检查是否已过期
StpUtil.checkActivityTimeout();
// 检查通过后继续续签
StpUtil.updateLastActivityToNow();
```
同时,你还可以关闭框架的自动续签(在配置文件中配置 `autoRenew=false` ),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作
### timeout与activity-timeout可以同时使用吗?
**可以同时使用!**
**可以同时使用!**
两者的认证逻辑彼此独立,互不干扰,可以同时使用。
+68 -11
View File
@@ -4,13 +4,12 @@
<meta charset="UTF-8">
<title>sa-token</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="description" content="sa-token是一个java权限认证框架,功能全面,上手简单,登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成...,有了sa-token,你所有的权限认证问题,都不再是问题">
<meta name="keywords" content="sa-token,sa-token框架,sa-token文档,java权限认证">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="keywords" content="sa-token|sa-token框架|sa-token文档|sa-token在线文档|sa-token官方文档|权限认证框架">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架,功能全面,上手简单,登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<link rel="stylesheet" href="https://unpkg.zhimg.com/docsify@4.11.3/lib/themes/vue.css">
<link rel="stylesheet" href="./lib/index.css">
<link rel="shortcut icon" type="image/x-icon" href="logo.png">
<link rel="stylesheet" href="./lib/index.css">
<link rel="stylesheet" href="https://unpkg.zhimg.com/docsify@4.11.3/lib/themes/vue.css">
</head>
<body>
<a href="/">
@@ -22,6 +21,11 @@
<nav>
<select onchange="location.href=this.value">
<option value="http://sa-token.dev33.cn/doc/index.html">最新版</option>
<option value="http://sa-token.dev33.cn/v/v1.14.0/doc/index.html">v1.14.0</option>
<option value="http://sa-token.dev33.cn/v/v1.13.0/doc/index.html">v1.13.0</option>
<option value="http://sa-token.dev33.cn/v/v1.12.1/doc/index.html">v1.12.1</option>
<option value="http://sa-token.dev33.cn/v/v1.12.0/doc/index.html">v1.12.0</option>
<option value="http://sa-token.dev33.cn/v/v1.11.0/doc/index.html">v1.11.0</option>
<option value="http://sa-token.dev33.cn/v/v1.10.0/doc/index.html">v1.10.0</option>
<option value="http://sa-token.dev33.cn/v/v1.9.0/doc/index.html">v1.9.0</option>
<option value="http://sa-token.dev33.cn/v/v1.8.0/doc/index.html">v1.8.0</option>
@@ -40,12 +44,12 @@
</div>
<script>
var name = '<img style="width: 50px; height: 50px; vertical-align: middle;" src="logo.png" alt="logo" /> ';
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.11.0</sub>'
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.15.0</sub>'
window.$docsify = {
name: name, // 名字
repo: 'https://github.com/click33/sa-token', // github地址
// themeColor: '#06A3D7', // 主题颜色
basePath: location.pathname.substr(0, location.pathname.lastIndexOf('/') + 1), // 自动计算项目名字
basePath: location.pathname.substr(0, location.pathname.lastIndexOf('/') + 1), // 自动计算项目名字
// basePath: '/sa-token-doc/', // 设置文件加载的父路径, 这在一些带项目名部署的文件中非常有效
auto2top: true, // 是否在切换页面后回到顶部
// coverpage: true, // 开启封面
@@ -60,7 +64,7 @@
alias: {
'/.*/_sidebar.md': '/_sidebar.md'
},
plugins: [ // 自定义插件
plugins: [ // 自定义插件
function(hook, vm) {
// 解析之后执行
hook.afterEach(function(html) {
@@ -69,20 +73,38 @@
var footer = [
'<br/><br/><br/><br/><br/><br/><br/><hr/>',
'<footer>',
'<span>发现错误?想参与编辑? 在 <a href="' + url + '" target="_blank">Gitee</a> 或 <a href="' + url2 + '" target="_blank">GitHub</a> 上编辑此页!</span>',
'<span>发现错误?想参与编辑? 在 <a href="' + url + '" target="_blank">Gitee</a> 或 <a href="' + url2 +
'" target="_blank">GitHub</a> 上编辑此页!</span>',
'</footer>'
].join('');
return html + footer;
});
// 每次路由切换时数据全部加载完成后调用,没有参数。
hook.doneEach(function() {
$('pre code').each(function(){
var lines = $(this).text().split('\n').length;
var $numbering = $('<ul/>').addClass('code-line-box');
$(this)
.addClass('has-numbering')
.parent()
.append($numbering);
for(i=1;i<=lines;i++){
$numbering.append($('<li/>').text(i));
}
});
});
}
]
],
}
</script>
<script src="https://unpkg.zhimg.com/docsify@4.9.4/lib/docsify.min.js"></script>
<script src="https://unpkg.zhimg.com/docsify-copy-code@2.1.0/dist/docsify-copy-code.min.js"></script>
<script src="https://unpkg.zhimg.com/prismjs@1.19.0/components/prism-java.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<!-- 搜索引擎自动提交 -->
<script>
(function() {
var bp = document.createElement('script');
@@ -96,9 +118,44 @@
s.parentNode.insertBefore(bp, s);
})();
</script>
<!-- 友盟 -->
<div style="height: 0px; overflow: hidden;">
<script type="text/javascript" src="https://v1.cnzz.com/z_stat.php?id=1279021391&web_id=1279021391"></script>
<script type="text/javascript" src="https://s4.cnzz.com/z_stat.php?id=1279646043&web_id=1279646043"></script>
</div>
<!-- 百度统计 -->
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?77d7418dd845f98ba1cfee8596eeee3f";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- Gitalk评论 -->
<link rel="stylesheet" href="https://unpkg.zhimg.com/gitalk@1.7.0/dist/gitalk.css">
<script src="https://unpkg.zhimg.com/docsify@4.11.6/lib/plugins/gitalk.min.js"></script>
<script src="https://unpkg.zhimg.com/gitalk@1.7.0/dist/gitalk.min.js"></script>
<script>
function f5Gitalk() {
window.gitalk = new Gitalk({
id: location.hash.replace('#', ''),
clientID: '19939399448841f818a1',
clientSecret: 'af67e0cc14a0f36e171895771c330471cfe36c23',
repo: 'sa-token', // 仓库名称
owner: 'click33',
admin: ['click33'], // 管理员列表
// facebook-like distraction free mode
distractionFreeMode: false
})
}
f5Gitalk();
window.onhashchange = function() {
f5Gitalk();
}
</script>
</body>
</html>
+23 -8
View File
@@ -1,3 +1,5 @@
/* 调整一下左侧树的样式 */
body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;}
.logo-box {position: absolute;left: 30px;top: 10px;cursor: pointer;color: #000;}
.logo-box img {width: 50px;height: 50px;vertical-align: middle;}
.logo-box .logo-text {display: inline-block;vertical-align: middle;font-size: 24px;font-weight: 400;}
@@ -11,12 +13,18 @@
.main-box .markdown-section{max-width: 1000px;}
}
/* ============== 样式优化 ================ */
/* 调整一下左侧树的字体样式 */
.sidebar .sidebar-nav>ul>li>p{font-size: 1.2em; margin-top: 10px;}
.sidebar ul li a{color: #222;}
.sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 16px; /* font-weight: 700; */}
/* ============== 代码样式优化 ================ */
/* 背景变黑 */
.main-box [data-lang]{padding: 0px !important; border-radius: 8px; overflow: hidden;}
.main-box [class^="lang-"]{border: 0px red solid; padding: 1.5em 1.2em;/* background-color: #282828; */ background-color: #090300; color: #FFF;}
.main-box [data-lang]{overflow: auto;}
.main-box [data-lang]{padding: 0px !important; border-radius: 4px;overflow-x: auto; overflow-y: hidden;}
.main-box [v-pre] code{border: 0px red solid; border-radius: 0px; /* background-color: #282828; */ background-color: #111; color: #FFF;}
.main-box [v-pre] code{padding: 1.5em 1.3em; margin-left: 40px !important;}
/* .main-box h2{margin-top: 70px;} */
/* xml语言样式优化 */
@@ -25,7 +33,7 @@
.lang-xml .token.attr-value{color: #A6E22E;}
/* java语言样式优化 */
.main-box .lang-java{color: #01a252; opacity: 1;}
.main-box .lang-java{color: #01a252 !important;; opacity: 1;}
.lang-java .token.keyword{color: #db2d20;}
.lang-java .token.namespace,.lang-java .token.namespace *{color: #01A252; opacity: 1;}
.lang-java .token.class-name,.lang-java .cm-variable{color: #55b5db; opacity: 1;}
@@ -34,15 +42,22 @@
.lang-java .token.punctuation{color: #ddd;}
/* js语言样式优化 */
.main-box .lang-js{color: #01a252;}
.main-box .lang-js{color: #01a252 !important;}
.lang-js .token.comment{color: #CDAB53;}
/* .lang-js .token.string{color: #fded02;} */
.lang-js .token.string{color: #ddd;}
.lang-js .token.punctuation{color: #ddd;}
.gt-container{padding: 1.5em; padding-bottom: 100px;}
/* 调整表格的响应式 */
#main table{margin-left: 25px;}
#main table{margin-left: 0px;}
@media screen and (min-width: 800px) {
#main table tr th{min-width: 150px;}
}
}
/* 代码行号盒子样式 */
.code-line-box {list-style-type: none; border-right: 1px solid #000; position: absolute; top: 0; left: 0; width: 40px;}
.code-line-box {padding: calc(1.5em + 1px) 0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;}
.code-line-box {line-height: inherit !important; background-color: #111; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;}
.code-line-box {font-family: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace;}
+76 -10
View File
@@ -1,21 +1,87 @@
# 常见问题
本篇整理大家在群聊里经常提问的一些问题,如有补充,欢迎提交pr
---
### 用这个框架我需要很多配置吗?
- 零配置开箱即用,但同时也支持自定义配置:参考:[框架配置](use/config)
### 加了注解进行鉴权认证,不生效?
注解鉴权功能默认关闭,两种方式任选其一进行打开:注册注解拦截器、集成`AOP模块`
如果已经打开仍然没有效果,加群说明一下复现步骤
### 如何踢人下线?
- 参考:[踢人下线](use/kick)
### 能否集成redis?
- 参考:[持久层扩展](use/dao-extend)
### 整合Redis时,除了引入pom依赖,还需要做其它的吗?
引入pom依赖后,在框架层面你无须做其它事情,但是你需要为项目指定一下`Redis`的连接信息,参考此文件:[application-dev](https://gitee.com/sz6/sa-plus/blob/master/sp-server/src/main/resources/application-dev.yml)
### 登录方法需要我自己实现吗?
是的,不同于`shiro`等框架,`sa-token`不会在登录流程中强插一脚,开发者比对完用户的账号和密码之后,只需要调用`StpUtil.setLogin(id)`通知一下框架即可
### 一个User对象存进Session后,再取出来时报错:无法从User类型转换成User类型?
群员亲测,当你打开热部署模式后,先存进去的对象,再热刷新后再取出,会报错,关闭热刷新即可解决
### 框架抛出的权限不足异常,我想根据自定义提示信息,可以吗?
可以,在全局异常拦截器里捕获`NotPermissionException`,可以通过`getCode()`获取没有通过认证的权限码,可以据此自定义返回信息
### 我的项目权限模型不是RBAC模型,很复杂,可以集成吗?
无论什么模型,只要能把一个用户具有的所有权限塞到一个List里返回给框架,就能集成
### SaRouterUtil.match 有多个路径需要排除怎么办?
可以点进去源码看一下,`SaRouterUtil.match`方法有多个重载,可以放一个集合, 例如:<br>
`SaRouterUtil.match(Arrays.asList("/**"), Arrays.asList("/login", "/reg"), () -> StpUtil.checkLogin());`
### 为什么StpUtil.setLoginId() 不能直接写入一个User对象?
`StpUtil.setLoginId()`只是为了给当前会话做个唯一标记,通常写入`UserId`即可,如果要存储User对象,可以使用`StpUtil.getSession()`获取Session对象进行存储
### 前后台分离模式下和普通模式有何不同?
主要是失去了`Cookie`无法自动化保存和提交`token秘钥`,可以参考章节:[前后台分离](/use/not-cookie)
### 前后台分离时,前端提交的header参数是叫token还是satoken还是tokenName
默认是satoken,如果想换一个名字,更改一下配置文件的`tokenName`即可
### Springboot环境下采用自定义拦截器排除了某个路径仍然被拦截了?
可能是404了,SpringBoot环境下如果访问接口404后,会被重定向到`/error`,然后被再次拦截,如果是其它原因,欢迎加群反馈
### 权限可以做成动态的吗?
权限本来就是动态的,只有jwt那种模式才是非动态的
### 集成jwt后为什么在 getSession 时提示 jwt has not session ?
`jwt`的招牌便是无须借助服务端完成会话管理,如果集成`jwt`后再次使用`Session`功能,那将又回到了传统`Session`模式,属于自断招牌,此种技术组合没有任何意义,因此jwt集成模式不提供`Session`功能,如果需要`Session`功能,就不要集成`jwt`
### 怎么关闭默认的Cookie模式呢?
在配置文件将`isReadCookie`值配置为`false`
### 怎么关掉每次启动时的字符画打印?
在配置文件将`isV`值配置为`false`
### StpUtil.getSession()必须登录后才能调用吗?如果我想在用户未登录之前存储一些数据应该怎么办?
`StpUtil.getSession()`获取的是`User-Session`,必须登录后才能使用,如果需要在未登录状态下也使用Session功能,请使用`Token-Session` <br>
步骤:先在配置文件里将`tokenSessionCheckLogin`配置为`false`,然后通过`StpUtil.getTokenSession()`获取Session
### 我只使用header来传输token,还需要打开Cookie模式吗?
不需要,如果只使用header来传输token,可以在配置文件关闭Cookie模式,例:`isReadCookie=false`
### 我想让用户修改密码后立即掉线重新登录,应该怎么做?
框架内置 [强制指定账号下线] 的APi,在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()`
### 能否使用在APP、小程序等前后台分离项目中?
- 参考:[无cookie模式](use/not-cookie)
### 还是有不明白到的地方?
- 请在`github`提交`issues`,或者加入qq群交流(群链接在[首页](README?id=交流群)
请在`github`提交`issues`,或者加入qq群交流(群链接在[首页](README?id=交流群)
### 我能为这个框架贡献代码吗?
- **可以**,请参照首页的提交pr步骤 [贡献代码](README?id=贡献代码)
**可以**,请参照首页的提交pr步骤 ,[贡献代码](README?id=贡献代码)
+21 -6
View File
@@ -2,10 +2,25 @@
---
- **sa-admin**[一个多窗口后台模板,流畅、易上手、提高生产力](http://sa-admin.dev33.cn/)
- **sa-vue-admin**[对sa-admin的vue单页版实现, 基于vue-cli、element-ui的单页后台模板](http://sa-vue-admin.dev33.cn/)
- **sa-plus**[一个基于springboot架构的快速开发平台,内置代码生成器,吹灰之力快速CRUD](http://sa-plus.dev33.cn/)
- **sa-doc**[一个基于markdown的接口文档编写工具](http://sa-doc.dev33.cn/)
- **SqlFly**[一个java语言的ORM框架](https://sqlfly.dev33.cn/)
- 虚位以待
#### 集成sa-token的开源项目:
[[sa-plus] 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/sz6/sa-plus)
<br>
#### 推荐项目:
[[OkHttps] - 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[[hasor] - 轻量级ioc/aop框架,采用"微内核+插件"的设计思想](https://gitee.com/zycgit/hasor)
[[sa-admin] - 一个多窗口后台模板,流畅、易上手、提高生产力](https://gitee.com/ejlchina-zhxu/okhttps)
[[vue-next-admin] - 一套为开发者快速开发准备的基于 vue2.x 越看越精彩的后台管理系统一站式平台模板](https://gitee.com/lyt-top/vue-admin-wonderful)
[[小诺快速开发平台] - 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing)
<br>
虚位以待...
+63 -1
View File
@@ -1,7 +1,69 @@
# 更新日志
### 2021-1-9 @v1.11.0
### 2021-3-23 @v1.15.0
- 新增:文档添加源码涉及技术栈说明
- 优化:优化路由拦截器模块文档,更简洁的示例
- 修复:修复非web环境下的错误提示,Request->Response
- 修复:修复Cookie注入时path判断错误,感谢@zhangzi0291提供的PR
- 新增:文档集成Redis章节新增redis配置示例说明,感谢群友 `@-)` 提供的建议
- 新增:增加token前缀模式,可在配置token读取前缀,适配`Bearer token`规范 **[重要]**
- 优化:`SaTokenManager`初始化Bean去除`initXxx`方法,优化代码逻辑
- 新增:`SaTokenManager`新增`stpLogicMap`集合,记录所有`StpLogic`的初始化,方便查找
- 新增:`Session`新增timeout操作API,可灵活修改Session的剩余有效时间
- 新增:token前缀改为强制校验模式,如果配置了前缀,则前端提交token时必须带有
- 优化:精简`SaRouteInterceptor`,只保留自定义验证和默认的登陆验证,去除冗余功能
- 优化:`SaRouterUtil`迁移到core核心包,优化依赖架构
- 优化:默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟
- 新增:`Session`新增各种类型转换API,可快速方便存取值 **[重要]**
- 升级注意:
- `SaRouterUtil`类迁移到核心包,注意更换import地址
- `SaRouteInterceptor`去出冗余API,详情参考路由鉴权部分
### 2021-3-12 @v1.14.0
- 新增:新增`SaLoginModel`登录参数Model,适配 [记住我] 模式 **[重要]**
- 新增:新增 `StpUtil.setLoginId()` 时指定token有效期,可灵活控制用户的一次登录免验证时长
- 新增:新增Cookie时间判断,在`timeout`设置为-1时,`Cookie`有效期将为`Integer.MAX_VALUE` **[重要]**
- 新增:新增密码加密工具类,可快速MD5、SHA1、SHA256、AES、RSA加密 **[重要]**
- 新增:新增 OAuth2.0 模块 **[重要]**
- 新增:`SaTokenConfig`配置类所有set方法支持链式调用
- 新增:`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用
- 优化:`StpLogic`类所有`getKey`方法重名为`splicingKey`,更语义化的函数名称
- 新增:`IsRunFunction`新增`noExe`函数,用于指定当`isRun`值为`false`时执行的函数
- 新增:`SaSession`新增数据存取值操作API
- 优化:优化`SaTokenDao`接口,增加Object操作API
- 优化:jwt示例`createToken`方法去除默认秘钥判断,只在启动项目时打印警告
- 文档:常见问题新增示例(修改密码后如何立即掉线)
- 文档:权限认证文档新增[如何把权限精确搭到按钮级]示例说明
- 文档:优化文档,部分模块添加图片说明
### 2021-2-9 @v1.13.0
- 优化:优化源码注释与文档
- 新增:文档集成Gitalk评论系统
- 优化:源码包`Maven`版本号更改为变量形式
- 修复:文档处方法名`getPermissionList`错误的bug
- 修复:修复`StpUtil.getTokenInfo()`会触发自动续签的bug
- 修复:修复接口 `SaTokenDao``searchData` 函数注释错误
- 新增:`SaSession`的创建抽象到`SaTokenAction`接口,方便按需重写
- 新建:框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理
- 新增:`SaSession`新增`setId()``setCreateTime()`方法,方便部分框架的序列化
- 新增:新增`autoRenew`配置,用于控制是否打开自动续签模式
- 新增:同域模式下的单点登录 **[重要]**
- 新增:完善分布式会话的文档说明
### 2021-1-12 @v1.12.0
- 新增:提供JWT集成示例 **[重要]**
- 新增:新增路由式鉴权,可方便的根据路由匹配鉴权 **[重要]**
- 新增:新增身份临时切换功能,可在一个代码段内将会话临时切换为其它账号 **[重要]**
- 优化:将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`,更语义化的名称
- 优化:优化文档
- 升级:v1.12.1,新增`SaRouterUtil`工具类,更方便的路由鉴权 **[重要]**
### 2021-1-10 @v1.11.0
- 新增:提供AOP注解鉴权方案 **[重要]**
- 优化自动生成token的算法

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