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

Compare commits

..

28 Commits

Author SHA1 Message Date
shengzhang 600cc98e20 v1.11.0更新 2021-01-10 00:15:23 +08:00
shengzhang 2d3a262e2f 优化自动生成token的算法 2021-01-09 19:34:43 +08:00
shengzhang 6a677ca779 更改模块引用名称 2021-01-09 17:32:40 +08:00
shengzhang dc5bfb6d84 模块名称更换为spring-aop 2021-01-09 17:31:35 +08:00
shengzhang fbff086ed9 v1.11.0 新特性:AOP注解鉴权 2021-01-09 17:24:44 +08:00
shengzhang 1028ac0fe6 修复更新日期标注错误 2021-01-09 02:33:48 +08:00
shengzhang 4abb72bb65 更改文档 2021-01-09 02:23:20 +08:00
shengzhang 79cb976439 优化readme 2021-01-09 01:27:36 +08:00
shengzhang f26424537f v1.10.0 版本更新, 提供会话治理接口 2021-01-09 01:24:37 +08:00
shengzhang 2328f9d654 Merge branch 'dev' of https://gitee.com/sz6/sa-token 2021-01-07 22:05:00 +08:00
shengzhang 22826dac86 v1.10.0新特性:查询所有会话 2021-01-07 22:03:44 +08:00
省长 bbce343a01 !7 update sa-token-doc/doc/use/jur-auth.md.
Merge pull request !7 from AppleOfGray/N/A
2021-01-06 21:19:59 +08:00
AppleOfGray 8ed9166abf update sa-token-doc/doc/use/jur-auth.md. 2021-01-06 21:11:27 +08:00
shengzhang a1ec710efd Merge branch 'master' of https://gitee.com/sz6/sa-token 2021-01-06 18:21:03 +08:00
shengzhang ae9ba2d1fd 优化readme.md 2021-01-06 18:15:32 +08:00
省长 97a5fb2f40 !6 v1.9.0 版本更新
Merge pull request !6 from 省长/dev
2021-01-06 02:43:35 +08:00
shengzhang 128ab7614e 更换文档标题 2021-01-06 02:42:15 +08:00
shengzhang 7f0a3aa1c6 修改文档cdn 2021-01-06 00:48:47 +08:00
shengzhang 69d01e3e6e v1.9.0 版本更新 2021-01-05 18:04:14 +08:00
shengzhang 82ee90f712 同端互斥登录的文档 2021-01-05 16:07:21 +08:00
shengzhang 9733c8777a v1.9.0 新特性 同端互斥登录 2021-01-05 13:58:51 +08:00
shengzhang 4ff6a87ef5 修复权限认证处示例错误 2021-01-05 00:47:17 +08:00
shengzhang 7ffe6cb0e6 补上注解拦截器里漏掉验证@SaCheckRole的bug 2021-01-04 21:42:32 +08:00
shengzhang dbba90d846 spring-boot-starter-data-redis2.3.7.RELEASE 改为 2.3.3.RELEASE 2021-01-04 18:12:46 +08:00
shengzhang 064ef4f12c 优化readme 2021-01-03 19:24:09 +08:00
省长 a8688cc07f !5 update sa-token-doc/doc/use/jur-auth.md.
Merge pull request !5 from legg/N/A
2021-01-02 22:36:48 +08:00
legg 4678e34203 update sa-token-doc/doc/use/jur-auth.md. 2021-01-02 21:55:06 +08:00
shengzhang 81c0200981 修复更新日期标注错误 2021-01-02 04:10:43 +08:00
42 changed files with 1146 additions and 244 deletions
+47 -33
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;">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.8.0</h1>
<h4 align="center">一个JavaWeb轻量级权限认证框架,功能全面,上手简单</h4>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.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.8.0-2B9939"></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/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>
@@ -16,7 +16,7 @@
---
## 😘 在线资料
## 在线资料
- [官网首页:http://sa-token.dev33.cn/](http://sa-token.dev33.cn/)
@@ -24,11 +24,30 @@
- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token)
- [开源不易,求鼓励,点个star吧](https://github.com/click33/sa-token)
- [开源不易,求鼓励,点个star吧](###)
## sa-token是什么?
**sa-token是一个JavaWeb轻量级权限认证框架,其API调用非常简单,有多简单呢?以登录验证为例,你只需要:**
## sa-token是什么?
sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登录认证、权限认证、Session会话等一系列由此衍生的权限相关业务
在架构设计上`sa-token`拒绝引入复杂的概念,以实际业务需求为第一目标进行定向突破,例如踢人下线、自动续签、同端互斥登录等常见业务在框架内均可以一行代码调用实现,简单粗暴,拒绝复杂!
对于传统Session会话模型的N多难题,例如难以分布式、水平扩展性差,难以兼容前后台分离环境,多会话管理混乱等,
`sa-token`独创了以账号为主的`Id-Session`模式,同时又兼容了传统以token为主的`Token-Session`模式,两者彼此独立,互不干扰,
让你在进行会话管理时可以如鱼得水,在`sa-toekn`的强力加持下,权限问题将不再成为业务逻辑的瓶颈!
总的来说,与其它权限认证框架相比,`sa-token`具有以下优势:
1. 上手简单:可零配置启动框架,能自动化的均已自动化,不让你费脑子
2. 功能强大:能集成的功能全部集成,不让你用个框架还要自己给框架打各种补丁
3. API简单易用:同样的一个功能,可能在别的框架中需要上百行代码,但是在sa-token中统统一行代码调个方法即可解决
4. 组件易于扩展:框架中几乎所有组件都提供了对应的扩展接口,90%以上的逻辑都是可以被按需重写的
有了sa-token,是时候和那些老旧权限框架说拜拜了!
## 代码示例
sa-token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
``` java
// 在登录时写入当前会话的账号id
@@ -38,17 +57,7 @@ StpUtil.setLoginId(10001);
StpUtil.checkLogin();
```
**没有复杂的封装!不要任何的配置!只需这两行简单的调用,即可轻松完成系统登录鉴权!**
## 🔥 框架设计思想
与其它权限认证框架相比,`sa-token`尽力保证两点:
- 上手简单:能自动化的配置全部自动化,不让你费脑子
- 功能强大:能涵盖的功能全部涵盖,不让你用个框架还要自己给框架打各种补丁
**如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大**
如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
@@ -60,15 +69,15 @@ StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
```
**sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档**
sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档
## 💦️ 涵盖功能
## 涵盖功能
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 拦截违规调用,不同角色不同授权
- **权限验证** —— 适配RBAC模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
@@ -78,32 +87,37 @@ StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **组件自动注入** —— 零配置与Spring等框架集成
- **会话治理** —— 提供方便灵活的会话查询接口
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 🔨 贡献代码
sa-token欢迎大家贡献代码,为框架添砖加瓦
1. 在github上fork一份到自己的仓库
## 参与贡献
众人拾柴火焰高,sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦
1. 在gitee或者github上fork一份代码到自己的仓库
2. clone自己的仓库到本地电脑
3. 在本地电脑修改、commit、push
4. 提交pr(点击:New Pull Request
5. 等待合并
作者寄语:参与贡献不光只有提交代码一个选择,点一个star、提一个issues都是对开源项目的促进,
如果框架帮助到了你,欢迎你把框架推荐给你的朋友、同事使用,为sa-token的推广做一份贡献
## 🌱 建议贡献的地方
- 修复源码现有bug,或增加新的实用功能
- 完善在线文档,或者修复现有错误之处
- 更多demo示例:比如SSM版搭建步骤
## 建议贡献的地方
- 修复源码现有bug,或优化代码架构,或增加新的实用功能
- 完善在线文档,或者修复现有描述错误之处
- 更多的第三方框架集成方案,更多的demo示例:比如SSM版搭建步骤
- 您可以参考项目issues与需求墙进行贡献
- 如果更新实用功能,可在文档友情链接处留下自己的推广链接
## 🚀 友情链接
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
## 😎 交流群
## 交流群
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) ,欢迎你的加入
+2 -1
View File
@@ -8,7 +8,7 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<packaging>pom</packaging>
<version>1.8.0</version>
<version>1.11.0</version>
<!-- 项目介绍 -->
<name>sa-token</name>
@@ -22,6 +22,7 @@
<module>sa-token-spring-boot-starter</module>
<module>sa-token-dao-redis</module>
<module>sa-token-dao-redis-jackson</module>
<module>sa-token-spring-aop</module>
</modules>
<!-- 开源协议 apache 2.0 -->
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</parent>
<packaging>jar</packaging>
@@ -1,5 +1,7 @@
package cn.dev33.satoken.dao;
import java.util.List;
import cn.dev33.satoken.session.SaSession;
/**
@@ -8,14 +10,14 @@ import cn.dev33.satoken.session.SaSession;
*/
public interface SaTokenDao {
/** 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值) */
public static final Long NEVER_EXPIRE = -1L;
/** 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值) */
public static final Long NOT_VALUE_EXPIRE = -2L;
// --------------------- token相关 ---------------------
/**
* 根据key获取value,如果没有,则返回空
@@ -58,7 +60,9 @@ public interface SaTokenDao {
* @param timeout 过期时间
*/
public void updateTimeout(String key, long timeout);
// --------------------- Session相关 ---------------------
/**
* 根据指定key的Session,如果没有,则返回空
@@ -101,7 +105,17 @@ public interface SaTokenDao {
public void updateSessionTimeout(String sessionId, long timeout);
// --------------------- 会话管理 ---------------------
/**
* 搜索数据
* @param prefix 前缀
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
*/
public List<String> searchData(String prefix, String keyword, int start, int size);
}
@@ -2,6 +2,7 @@ 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;
@@ -10,6 +11,7 @@ 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;
/**
* sa-token持久层默认的实现类 , 基于内存Map
@@ -207,6 +209,14 @@ public class SaTokenDaoDefaultImpl implements SaTokenDao {
// --------------------- 会话管理
@Override
public List<String> searchData(String prefix, String keyword, int start, int size) {
return SaTokenInsideUtil.searchList(expireMap.keySet(), prefix, keyword, start, size);
}
@@ -31,18 +31,18 @@ public class SaTokenInfo {
/** token专属session剩余有效时间 (单位: 秒) */
public long tokenSessionTimeout;
/**
* token剩余无操作有效时间
*/
public long tokenActivityTimeout;
/** 当前登录设备 */
public String loginDevice;
/**
* @return tokenName token名称
* @return tokenName
*/
public String getTokenName() {
return tokenName;
@@ -50,11 +50,9 @@ public class SaTokenInfo {
/**
* @param tokenName 要设置的 tokenName
* @return 对象自身
*/
public SaTokenInfo setTokenName(String tokenName) {
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
return this;
}
/**
@@ -66,11 +64,9 @@ public class SaTokenInfo {
/**
* @param tokenValue 要设置的 tokenValue
* @return 对象自身
*/
public SaTokenInfo setTokenValue(String tokenValue) {
public void setTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
return this;
}
/**
@@ -82,11 +78,9 @@ public class SaTokenInfo {
/**
* @param isLogin 要设置的 isLogin
* @return 对象自身
*/
public SaTokenInfo setIsLogin(Boolean isLogin) {
public void setIsLogin(Boolean isLogin) {
this.isLogin = isLogin;
return this;
}
/**
@@ -98,11 +92,9 @@ public class SaTokenInfo {
/**
* @param loginId 要设置的 loginId
* @return 对象自身
*/
public SaTokenInfo setLoginId(Object loginId) {
public void setLoginId(Object loginId) {
this.loginId = loginId;
return this;
}
/**
@@ -114,13 +106,11 @@ public class SaTokenInfo {
/**
* @param loginKey 要设置的 loginKey
* @return 对象自身
*/
public SaTokenInfo setLoginKey(String loginKey) {
public void setLoginKey(String loginKey) {
this.loginKey = loginKey;
return this;
}
/**
* @return tokenTimeout
*/
@@ -130,11 +120,9 @@ public class SaTokenInfo {
/**
* @param tokenTimeout 要设置的 tokenTimeout
* @return 对象自身
*/
public SaTokenInfo setTokenTimeout(long tokenTimeout) {
public void setTokenTimeout(long tokenTimeout) {
this.tokenTimeout = tokenTimeout;
return this;
}
/**
@@ -146,11 +134,9 @@ public class SaTokenInfo {
/**
* @param sessionTimeout 要设置的 sessionTimeout
* @return 对象自身
*/
public SaTokenInfo setSessionTimeout(long sessionTimeout) {
public void setSessionTimeout(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
return this;
}
/**
@@ -162,13 +148,11 @@ public class SaTokenInfo {
/**
* @param tokenSessionTimeout 要设置的 tokenSessionTimeout
* @return 对象自身
*/
public SaTokenInfo setTokenSessionTimeout(long tokenSessionTimeout) {
public void setTokenSessionTimeout(long tokenSessionTimeout) {
this.tokenSessionTimeout = tokenSessionTimeout;
return this;
}
/**
* @return tokenActivityTimeout
*/
@@ -178,11 +162,23 @@ public class SaTokenInfo {
/**
* @param tokenActivityTimeout 要设置的 tokenActivityTimeout
* @return 对象自身
*/
public SaTokenInfo setTokenActivityTimeout(long tokenActivityTimeout) {
public void setTokenActivityTimeout(long tokenActivityTimeout) {
this.tokenActivityTimeout = tokenActivityTimeout;
return this;
}
/**
* @return loginDevice
*/
public String getLoginDevice() {
return loginDevice;
}
/**
* @param loginDevice 要设置的 loginDevice
*/
public void setLoginDevice(String loginDevice) {
this.loginDevice = loginDevice;
}
@@ -193,8 +189,9 @@ public class SaTokenInfo {
return "SaTokenInfo [tokenName=" + tokenName + ", tokenValue=" + tokenValue + ", isLogin=" + isLogin
+ ", loginId=" + loginId + ", loginKey=" + loginKey + ", tokenTimeout=" + tokenTimeout
+ ", sessionTimeout=" + sessionTimeout + ", tokenSessionTimeout=" + tokenSessionTimeout
+ ", tokenActivityTimeout=" + tokenActivityTimeout + "]";
+ ", tokenActivityTimeout=" + tokenActivityTimeout + ", loginDevice=" + loginDevice + "]";
}
@@ -208,6 +205,4 @@ public class SaTokenInfo {
}
@@ -1,5 +1,6 @@
package cn.dev33.satoken.stp;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -9,6 +10,10 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.exception.NotLoginException;
@@ -17,7 +22,6 @@ import cn.dev33.satoken.exception.NotRoleException;
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 权限验证,逻辑实现类
@@ -119,6 +123,7 @@ public class StpLogic {
info.sessionTimeout = getSessionTimeout();
info.tokenSessionTimeout = getTokenSessionTimeout();
info.tokenActivityTimeout = getTokenActivityTimeout();
info.loginDevice = getLoginDevice();
return info;
}
@@ -130,11 +135,23 @@ public class StpLogic {
* @param loginId 登录id,建议的类型:(long | int | String
*/
public void setLoginId(Object loginId) {
setLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE);
}
/**
* 在当前会话上登录id
* @param loginId 登录id,建议的类型:(long | int | String
* @param device 设备标识
*/
public void setLoginId(Object loginId, String device) {
// ------ 0、如果当前会话已经登录上了此LoginId,则立即返回
// ------ 0、如果当前会话已经登录上了此LoginId,且登录设备相同,则立即返回
Object loggedId = getLoginIdDefaultNull();
if(loggedId != null && loggedId.toString().equals(loginId.toString())) {
return;
String loggedDevice = getLoginDevice();
if(loggedDevice != null && loggedDevice.equals(device)) {
return;
}
}
// ------ 1、获取相应对象
@@ -148,18 +165,20 @@ public class StpLogic {
if(config.getAllowConcurrentLogin() == true) {
// 如果配置为共享token, 则尝试从session签名记录里取出token
if(config.getIsShare() == true) {
tokenValue = getTokenValueByLoginId(loginId);
tokenValue = getTokenValueByLoginId(loginId, device);
}
} else {
// --- 如果不允许并发登录
// 如果此时[id-session]不为null,说明此账号在其他地正在登录,现在需要先把其它地的token标记为被顶下线
// 如果此时[id-session]不为null,说明此账号在其他地正在登录,现在需要先把其它地的同设备token标记为被顶下线
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
List<String> tokenValueList = getTokenValueListByLoginId(loginId);
for (String token : tokenValueList) {
dao.updateValue(getKeyTokenValue(token), NotLoginException.BE_REPLACED); // 1. 将此token 标记为已顶替
clearLastActivity(token); // 2. 清理掉[token-最后操作时间]
session.removeTokenSign(token); // 3. 清理账号session上的token签名
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签名记录
}
}
}
}
@@ -176,7 +195,7 @@ public class StpLogic {
dao.updateSessionTimeout(session.getId(), config.getTimeout());
}
// 在session上记录token签名
session.addTokenSign(new TokenSign(tokenValue, SaTokenConsts.DEFAULT_LOGIN_DEVICE));
session.addTokenSign(new TokenSign(tokenValue, device));
// ------ 4. 持久化其它数据
dao.setValue(getKeyTokenValue(tokenValue), String.valueOf(loginId), config.getTimeout()); // token -> uid
@@ -235,6 +254,16 @@ public class StpLogic {
* @param loginId 账号id
*/
public void logoutByLoginId(Object loginId) {
logoutByLoginId(loginId, null);
}
/**
* 指定loginId指定设备的会话注销登录(踢人下线)
* <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2
* @param loginId 账号id
* @param device 设备标识 (填null代表所有注销设备)
*/
public void logoutByLoginId(Object loginId, String device) {
// 先获取这个账号的[id-session], 如果为null,则不执行任何操作
SaSession session = getSessionByLoginId(loginId);
if(session == null) {
@@ -244,20 +273,21 @@ public class StpLogic {
// 循环token签名列表,开始删除相关信息
List<TokenSign> tokenSignList = session.getTokenSignList();
for (TokenSign tokenSign : tokenSignList) {
// 1. 获取token
String tokenValue = tokenSign.getValue();
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenValue);
// 3. 标记:已被踢下线
SaTokenManager.getSaTokenDao().updateValue(getKeyTokenValue(tokenValue), NotLoginException.KICK_OUT); // 标记:已被踢下线
// 4. 清理账号session上的token签名
session.removeTokenSign(tokenValue);
if(device == null || tokenSign.getDevice().equals(device)) {
// 1. 获取token
String tokenValue = tokenSign.getValue();
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenValue);
// 3. 标记:已被踢下线
SaTokenManager.getSaTokenDao().updateValue(getKeyTokenValue(tokenValue), NotLoginException.KICK_OUT); // 标记:已被踢下线
// 4. 清理账号session上的token签名
session.removeTokenSign(tokenValue);
}
}
// 尝试注销session
session.logoutByTokenSignCountToZero();
}
// 查询相关
/**
@@ -310,7 +340,7 @@ public class StpLogic {
return loginId;
}
/**
/**
* 获取当前会话登录id, 如果未登录,则返回默认值
* @param <T> 返回类型
* @param defaultValue 默认值
@@ -336,7 +366,7 @@ public class StpLogic {
return (T)loginId;
}
/**
/**
* 获取当前会话登录id, 如果未登录,则返回null
* @return 账号id
*/
@@ -367,7 +397,7 @@ public class StpLogic {
return String.valueOf(getLoginId());
}
/**
/**
* 获取当前会话登录id, 并转换为int
* @return 账号id
*/
@@ -379,7 +409,7 @@ public class StpLogic {
return Integer.valueOf(String.valueOf(getLoginId()));
}
/**
/**
* 获取当前会话登录id, 并转换为long
* @return 账号id
*/
@@ -424,6 +454,15 @@ public class StpLogic {
return session;
}
/**
* 获取指定key的session, 如果session尚未创建,则返回null
* @param sessionId sessionId
* @return session对象
*/
public SaSession getSessionBySessionId(String sessionId) {
return getSessionBySessionId(sessionId, false);
}
/**
* 获取指定loginId的session, 如果session尚未创建,isCreate=是否新建并返回
* @param loginId 账号id
@@ -495,7 +534,13 @@ public class StpLogic {
// 如果配置忽略token登录校验,则必须保证token不为null (token为null的时候随机创建一个)
String tokenValue = getTokenValue();
if(tokenValue == null || Objects.equals(tokenValue, "")) {
setLoginId(SaTokenInsideUtil.getMarking28());
// 随机一个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());
}
}
}
// 返回这个token对应的专属session
@@ -513,7 +558,7 @@ public class StpLogic {
// =================== [临时过期] 验证相关 ===================
/**
/**
* 写入指定token的 [最后操作时间] 为当前时间戳
* @param tokenValue 指定token
*/
@@ -601,7 +646,6 @@ public class StpLogic {
}
// =================== 过期时间相关 ===================
/**
@@ -827,7 +871,6 @@ public class StpLogic {
// =================== id 反查token 相关操作 ===================
/**
* 获取指定loginId的tokenValue
* <p> 在配置为允许并发登录时,此方法只会返回队列的最后一个token,
@@ -836,16 +879,38 @@ public class StpLogic {
* @return token值
*/
public String getTokenValueByLoginId(Object loginId) {
List<String> tokenValueList = getTokenValueListByLoginId(loginId);
return getTokenValueByLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE);
}
/**
* 获取指定loginId指定设备端的tokenValue
* <p> 在配置为允许并发登录时,此方法只会返回队列的最后一个token,
* 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId
* @param loginId 账号id
* @param device 设备标识
* @return token值
*/
public String getTokenValueByLoginId(Object loginId, String device) {
List<String> tokenValueList = getTokenValueListByLoginId(loginId, device);
return tokenValueList.size() == 0 ? null : tokenValueList.get(tokenValueList.size() - 1);
}
/**
* 获取指定loginId的tokenValue
* 获取指定loginId的tokenValue集合
* @param loginId 账号id
* @return 此loginId的所有相关token
*/
public List<String> getTokenValueListByLoginId(Object loginId) {
return getTokenValueListByLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE);
}
/**
* 获取指定loginId指定设备端的tokenValue 集合
* @param loginId 账号id
* @param device 设备标识
* @return 此loginId的所有相关token
*/
public List<String> getTokenValueListByLoginId(Object loginId, String device) {
// 如果session为null的话直接返回空集合
SaSession session = getSessionByLoginId(loginId, false);
if(session == null) {
@@ -855,11 +920,78 @@ public class StpLogic {
List<TokenSign> tokenSignList = session.getTokenSignList();
List<String> tokenValueList = new ArrayList<>();
for (TokenSign tokenSign : tokenSignList) {
tokenValueList.add(tokenSign.getValue());
if(tokenSign.getDevice().equals(device)) {
tokenValueList.add(tokenSign.getValue());
}
}
return tokenValueList;
}
/**
* 返回当前token的登录设备
* @return 当前令牌的登录设备
*/
public String getLoginDevice() {
// 如果没有token,直接返回
String tokenValue = getTokenValue();
if(tokenValue == null) {
return null;
}
// 如果还未登录,直接返回 null
if(isLogin() == false) {
return null;
}
// 如果session为null的话直接返回 null
SaSession session = getSession(false);
if(session == null) {
return null;
}
// 遍历解析
List<TokenSign> tokenSignList = session.getTokenSignList();
for (TokenSign tokenSign : tokenSignList) {
if(tokenSign.getValue().equals(tokenValue)) {
return tokenSign.getDevice();
}
}
return null;
}
// =================== 会话管理 ===================
/**
* 根据条件查询token
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return token集合
*/
public List<String> searchTokenValue(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenValue(""), keyword, start, size);
}
/**
* 根据条件查询SessionId
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
*/
public List<String> searchSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeySession(""), keyword, start, size);
}
/**
* 根据条件查询token专属Session的Id
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
*/
public List<String> searchTokenSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenSession(""), keyword, start, size);
}
// =================== 返回相应key ===================
@@ -870,6 +1002,7 @@ public class StpLogic {
public String getKeyTokenName() {
return getConfig().getTokenName();
}
/**
* 获取key tokenValue 持久化 token-id
* @param tokenValue token值
@@ -878,6 +1011,7 @@ public class StpLogic {
public String getKeyTokenValue(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token:" + tokenValue;
}
/**
* 获取key session 持久化
* @param loginId 账号id
@@ -886,6 +1020,7 @@ public class StpLogic {
public String getKeySession(Object loginId) {
return getConfig().getTokenName() + ":" + loginKey + ":session:" + loginId;
}
/**
* 获取key tokenValue的专属session
* @param tokenValue token值
@@ -894,6 +1029,7 @@ public class StpLogic {
public String getKeyTokenSession(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token-session:" + tokenValue;
}
/**
* 获取key: 指定token的最后操作时间 持久化
* @param tokenValue token值
@@ -915,8 +1051,66 @@ public class StpLogic {
return SaTokenManager.getConfig();
}
// =================== 注解鉴权 ===================
/**
* 对一个Method对象进行注解检查(注解鉴权内部实现)
* @param method Method对象
*/
public void checkMethodAnnotation(Method method) {
// ----------- 验证登录
if(method.isAnnotationPresent(SaCheckLogin.class) || method.getDeclaringClass().isAnnotationPresent(SaCheckLogin.class)) {
this.checkLogin();
}
// ----------- 验证角色
// 验证方法上的
SaCheckRole scr = method.getAnnotation(SaCheckRole.class);
if(scr != null) {
String[] roleArray = scr.value();
if(scr.mode() == SaMode.AND) {
this.checkRoleAnd(roleArray);
} else {
this.checkRoleOr(roleArray);
}
}
// 验证类上的
scr = method.getDeclaringClass().getAnnotation(SaCheckRole.class);
if(scr != null) {
String[] roleArray = scr.value();
if(scr.mode() == SaMode.AND) {
this.checkRoleAnd(roleArray);
} else {
this.checkRoleOr(roleArray);
}
}
// ----------- 验证权限
// 验证方法上的
SaCheckPermission scp = method.getAnnotation(SaCheckPermission.class);
if(scp != null) {
String[] permissionArray = scp.value();
if(scp.mode() == SaMode.AND) {
this.checkPermissionAnd(permissionArray);
} else {
this.checkPermissionOr(permissionArray);
}
}
// 验证类上的
scp = method.getDeclaringClass().getAnnotation(SaCheckPermission.class);
if(scp != null) {
String[] permissionArray = scp.value();
if(scp.mode() == SaMode.AND) {
this.checkPermissionAnd(permissionArray);
} else {
this.checkPermissionOr(permissionArray);
}
}
// 验证通过
}
@@ -61,6 +61,15 @@ public class StpUtil {
stpLogic.setLoginId(loginId);
}
/**
* 在当前会话上登录id
* @param loginId 登录id,建议的类型:(long | int | String
* @param device 设备标识
*/
public static void setLoginId(Object loginId, String device) {
stpLogic.setLoginId(loginId, device);
}
/**
* 当前会话注销登录
*/
@@ -85,6 +94,16 @@ public class StpUtil {
stpLogic.logoutByLoginId(loginId);
}
/**
* 指定loginId指定设备的会话注销登录(踢人下线)
* <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2
* @param loginId 账号id
* @param device 设备标识
*/
public static void logoutByLoginId(Object loginId, String device) {
stpLogic.logoutByLoginId(loginId, device);
}
// 查询相关
@@ -175,6 +194,15 @@ public class StpUtil {
return stpLogic.getSessionByLoginId(loginId, isCreate);
}
/**
* 获取指定key的session, 如果session尚未创建,则返回null
* @param sessionId sessionId
* @return session对象
*/
public static SaSession getSessionBySessionId(String sessionId) {
return stpLogic.getSessionBySessionId(sessionId);
}
/**
* 获取指定loginId的session,如果session尚未创建,则新建并返回
* @param loginId 账号id
@@ -381,15 +409,81 @@ public class StpUtil {
public static String getTokenValueByLoginId(Object loginId) {
return stpLogic.getTokenValueByLoginId(loginId);
}
/**
* 获取指定loginId指定设备端的tokenValue
* <p> 在配置为允许并发登录时,此方法只会返回队列的最后一个token,
* 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId
* @param loginId 账号id
* @param device 设备标识
* @return token值
*/
public static String getTokenValueByLoginId(Object loginId, String device) {
return stpLogic.getTokenValueByLoginId(loginId, device);
}
/**
* 获取指定loginId的tokenValue
* 获取指定loginId的tokenValue集合
* @param loginId 账号id
* @return 此loginId的所有相关token
*/
public static List<String> getTokenValueListByLoginId(Object loginId) {
return stpLogic.getTokenValueListByLoginId(loginId);
}
/**
* 获取指定loginId指定设备端的tokenValue集合
* @param loginId 账号id
* @param device 设备标识
* @return 此loginId的所有相关token
*/
public static List<String> getTokenValueListByLoginId(Object loginId, String device) {
return stpLogic.getTokenValueListByLoginId(loginId, device);
}
/**
* 返回当前token的登录设备
* @return 当前令牌的登录设备
*/
public static String getLoginDevice() {
return stpLogic.getLoginDevice();
}
// =================== 会话管理 ===================
/**
* 根据条件查询token
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return token集合
*/
public static List<String> searchTokenValue(String keyword, int start, int size) {
return stpLogic.searchTokenValue(keyword, start, size);
}
/**
* 根据条件查询SessionId
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
*/
public static List<String> searchSessionId(String keyword, int start, int size) {
return stpLogic.searchSessionId(keyword, start, size);
}
/**
* 根据条件查询token专属Session的Id
* @param keyword 关键字
* @param start 开始处索引 (-1代表查询所有)
* @param size 获取数量
* @return sessionId集合
*/
public static List<String> searchTokenSessionId(String keyword, int start, int size) {
return stpLogic.searchTokenSessionId(keyword, start, size);
}
}
@@ -10,7 +10,7 @@ public class SaTokenConsts {
/**
* sa-token 版本号
*/
public static final String VERSION_NO = "v1.8.0";
public static final String VERSION_NO = "v1.11.0";
/**
* sa-token 开源地址
@@ -1,9 +1,13 @@
package cn.dev33.satoken.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
/**
* sa-token 工具类
* sa-token 内部代码工具类
* @author kong
*
*/
@@ -49,4 +53,58 @@ public class SaTokenInsideUtil {
}
/**
* 从集合里查询数据
* @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) {
prefix = "";
}
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) {
list.add(key);
}
}
// 取指定段数据
return searchList(list, start, size);
}
/**
* 从集合里查询数据
* @param list 数据集合
* @param start 起始位置 (-1代表查询所有)
* @param size 获取条数
* @return 符合条件的新数据集合
*/
public static List<String> searchList(List<String> list, int start, int size) {
// 取指定段数据
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()) {
return list2;
}
list2.add(list.get(i));
}
return list2;
}
}
+3 -3
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,13 +20,13 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.7.RELEASE</version>
<version>2.3.3.RELEASE</version>
</dependency>
</dependencies>
@@ -1,6 +1,9 @@
package cn.dev33.satoken.dao;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,6 +18,7 @@ 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)
@@ -120,6 +124,17 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
*/
@Override
public void updateTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.setValue(key, this.getValue(key), timeout);
}
return;
}
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
@@ -179,7 +194,28 @@ public class SaTokenDaoRedisJackson implements SaTokenDao {
*/
@Override
public void updateSessionTimeout(String sessionId, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getSessionTimeout(sessionId);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.saveSession(this.getSession(sessionId), timeout);
}
return;
}
sessionRedisTemplate.expire(sessionId, timeout, TimeUnit.SECONDS);
}
/**
* 搜索数据
*/
@Override
public List<String> searchData(String prefix, String keyword, int start, int size) {
Set<String> keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*");
List<String> list = new ArrayList<String>(keys);
return SaTokenInsideUtil.searchList(list, start, size);
}
}
+3 -3
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,13 +20,13 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.7.RELEASE</version>
<version>2.3.3.RELEASE</version>
</dependency>
</dependencies>
@@ -1,5 +1,8 @@
package cn.dev33.satoken.dao;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,6 +14,7 @@ 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
@@ -101,6 +105,17 @@ public class SaTokenDaoRedis implements SaTokenDao {
*/
@Override
public void updateTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.setValue(key, this.getValue(key), timeout);
}
return;
}
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
@@ -160,7 +175,31 @@ public class SaTokenDaoRedis implements SaTokenDao {
*/
@Override
public void updateSessionTimeout(String sessionId, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getSessionTimeout(sessionId);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久,那么再次set一次
this.saveSession(this.getSession(sessionId), timeout);
}
return;
}
sessionRedisTemplate.expire(sessionId, timeout, TimeUnit.SECONDS);
}
/**
* 搜索数据
*/
@Override
public List<String> searchData(String prefix, String keyword, int start, int size) {
Set<String> keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*");
List<String> list = new ArrayList<String>(keys);
return SaTokenInsideUtil.searchList(list, start, size);
}
}
+10 -3
View File
@@ -29,21 +29,21 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency> -->
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency> -->
<!-- 提供redis连接池 -->
@@ -51,6 +51,13 @@
<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>1.11.0</version>
</dependency> -->
<!-- @ConfigurationProperties -->
<dependency>
@@ -1,21 +0,0 @@
package com.pj.satoken;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.action.SaTokenActionDefaultImpl;
/**
* 继承sa-token行为Bean默认实现, 重写部分逻辑
* @author kong
*
*/
@Component
public class MySaTokenAction extends SaTokenActionDefaultImpl {
// 重写token生成策略
// @Override
// public String createToken(Object loginId, String loginKey) {
// return SaTokenInsideUtil.getRandomString(60);
// }
}
@@ -9,6 +9,8 @@ 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;
@@ -0,0 +1,66 @@
package com.pj.test;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pj.util.AjaxJson;
import com.pj.util.Ttime;
import cn.dev33.satoken.stp.StpUtil;
/**
* 压力测试
* @author kong
*
*/
@RestController
@RequestMapping("/s-test/")
public class StressTestController {
// 测试 浏览器访问: http://localhost:8081/s-test/login
// 测试前,请先将 is-read-cookie 配置为 false
@RequestMapping("login")
public AjaxJson login() {
// StpUtil.getTokenSession().logout();
// StpUtil.logoutByLoginId(10001);
int count = 10; // 循环多少轮
int loginCount = 10000; // 每轮循环多少次
// 循环10次 取平均时间
List<Double> list = new ArrayList<>();
for (int i = 1; i <= count; i++) {
System.out.println("\n---------------------第" + i + "轮---------------------");
Ttime t = new Ttime().start();
// 每次登录的次数
for (int j = 1; j <= loginCount; j++) {
StpUtil.setLoginId("1000" + j, "PC-" + j);
if(j % 1000 == 0) {
System.out.println("已登录:" + j);
}
}
t.end();
list.add((t.returnMs() + 0.0) / 1000);
System.out.println("" + i + "" + "用时:" + t.toString());
}
// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());
System.out.println("\n---------------------测试结果---------------------");
System.out.println(list.size() + "次测试: " + list);
double ss = 0;
for (int i = 0; i < list.size(); i++) {
ss += list.get(i);
}
System.out.println("平均用时: " + ss / list.size());
return AjaxJson.getSuccess();
}
}
@@ -1,16 +1,21 @@
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;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pj.util.AjaxJson;
import com.pj.util.Ttime;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.session.SaSessionCustomUtil;
import cn.dev33.satoken.stp.SaTokenInfo;
@@ -38,7 +43,8 @@ public class TestController {
System.out.println("登录成功");
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号:" + StpUtil.getLoginId());
System.out.println("当前登录账号:" + StpUtil.getLoginIdAsInt()); // 获取登录id并转为int
// System.out.println("当前登录账号并转为int" + StpUtil.getLoginIdAsInt());
System.out.println("当前登录设备:" + StpUtil.getLoginDevice());
// System.out.println("当前token信息:" + StpUtil.getTokenInfo());
return AjaxJson.getSuccess();
@@ -151,13 +157,14 @@ public class TestController {
// 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck
@SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过
@SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过
@SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过
@RequestMapping("atCheck")
public AjaxJson atCheck() {
System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= ");
System.out.println("只有通过注解鉴权,才能进入此方法");
StpUtil.checkActivityTimeout();
StpUtil.updateLastActivityToNow();
// StpUtil.checkActivityTimeout();
// StpUtil.updateLastActivityToNow();
return AjaxJson.getSuccess();
}
@@ -188,14 +195,49 @@ public class TestController {
// 返回
return AjaxJson.getSuccess();
}
// 测试登录接口, 按照设备登录, 浏览器访问: http://localhost:8081/test/login2
@RequestMapping("login2")
public AjaxJson login2(@RequestParam(defaultValue="10001") String id, @RequestParam(defaultValue="PC") String device) {
StpUtil.setLoginId(id, device);
return AjaxJson.getSuccess();
}
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
// 测试会话治理 浏览器访问: http://localhost:8081/test/search
@RequestMapping("search")
public AjaxJson search() {
System.out.println("--------------");
Ttime t = new Ttime().start();
List<String> tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10);
for (String v : tokenValue) {
// SaSession session = StpUtil.getSessionBySessionId(sid);
System.out.println(v);
}
System.out.println("用时:" + t.end().toString());
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/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
StpUtil.logoutByLoginId(10001);
return AjaxJson.getSuccess();
}
}
@@ -0,0 +1,25 @@
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>();
}
}
@@ -1,4 +1,4 @@
package com.pj.test;
package com.pj.util;
import java.io.Serializable;
import java.util.List;
@@ -0,0 +1,63 @@
package com.pj.util;
/**
* 用于测试用时
* @author kong
*
*/
public class Ttime {
private long start=0; //开始时间
private long end=0; //结束时间
public static Ttime t = new Ttime(); //static快捷使用
/**
* 开始计时
* @return
*/
public Ttime start() {
start=System.currentTimeMillis();
return this;
}
/**
* 结束计时
*/
public Ttime end() {
end=System.currentTimeMillis();
return this;
}
/**
* 返回所用毫秒数
*/
public long returnMs() {
return end-start;
}
/**
* 格式化输出结果
*/
public void outTime() {
System.out.println(this.toString());
}
/**
* 结束并格式化输出结果
*/
public void endOutTime() {
this.end().outTime();
}
@Override
public String toString() {
return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s
}
}
@@ -22,7 +22,7 @@ spring:
# redis配置
redis:
# Redis数据库索引(默认为0
database: 1
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
@@ -30,7 +30,7 @@ spring:
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间(毫秒)
timeout: 1000ms
timeout: 10000ms
lettuce:
pool:
# 连接池最大连接数
+47 -33
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;">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.8.0</h1>
<h4 align="center">一个JavaWeb轻量级权限认证框架,功能全面,上手简单</h4>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.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.8.0-2B9939"></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/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>
@@ -16,7 +16,7 @@
---
## 😘 在线资料
## 在线资料
- [官网首页:http://sa-token.dev33.cn/](http://sa-token.dev33.cn/)
@@ -24,11 +24,30 @@
- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token)
- [开源不易,求鼓励,点个star吧](https://github.com/click33/sa-token)
- [开源不易,求鼓励,点个star吧](###)
## sa-token是什么?
**sa-token是一个JavaWeb轻量级权限认证框架,其API调用非常简单,有多简单呢?以登录验证为例,你只需要:**
## sa-token是什么?
sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登录认证、权限认证、Session会话等一系列由此衍生的权限相关业务
在架构设计上`sa-token`拒绝引入复杂的概念,以实际业务需求为第一目标进行定向突破,例如踢人下线、自动续签、同端互斥登录等常见业务在框架内均可以一行代码调用实现,简单粗暴,拒绝复杂!
对于传统Session会话模型的N多难题,例如难以分布式、水平扩展性差,难以兼容前后台分离环境,多会话管理混乱等,
`sa-token`独创了以账号为主的`Id-Session`模式,同时又兼容了传统以token为主的`Token-Session`模式,两者彼此独立,互不干扰,
让你在进行会话管理时可以如鱼得水,在`sa-toekn`的强力加持下,权限问题将不再成为业务逻辑的瓶颈!
总的来说,与其它权限认证框架相比,`sa-token`具有以下优势:
1. 上手简单:可零配置启动框架,能自动化的均已自动化,不让你费脑子
2. 功能强大:能集成的功能全部集成,不让你用个框架还要自己给框架打各种补丁
3. API简单易用:同样的一个功能,可能在别的框架中需要上百行代码,但是在sa-token中统统一行代码调个方法即可解决
4. 组件易于扩展:框架中几乎所有组件都提供了对应的扩展接口,90%以上的逻辑都是可以被按需重写的
有了sa-token,是时候和那些老旧权限框架说拜拜了!
## 代码示例
sa-token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
``` java
// 在登录时写入当前会话的账号id
@@ -38,17 +57,7 @@ StpUtil.setLoginId(10001);
StpUtil.checkLogin();
```
**没有复杂的封装!不要任何的配置!只需这两行简单的调用,即可轻松完成系统登录鉴权!**
## 🔥 框架设计思想
与其它权限认证框架相比,`sa-token`尽力保证两点:
- 上手简单:能自动化的配置全部自动化,不让你费脑子
- 功能强大:能涵盖的功能全部涵盖,不让你用个框架还要自己给框架打各种补丁
**如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大**
如果上面的示例能够证明`sa-token`的简单,那么以下API则可以证明`sa-token`的强大
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
@@ -60,15 +69,15 @@ StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
```
**sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档**
sa-token的API众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档
## 💦️ 涵盖功能
## 涵盖功能
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 拦截违规调用,不同角色不同授权
- **权限验证** —— 适配RBAC模型,不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
@@ -78,32 +87,37 @@ StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **花式token生成** —— 内置六种token风格,还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略,灵活搭配使用,还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- **组件自动注入** —— 零配置与Spring等框架集成
- **会话治理** —— 提供方便灵活的会话查询接口
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
## 🔨 贡献代码
sa-token欢迎大家贡献代码,为框架添砖加瓦
1. 在github上fork一份到自己的仓库
## 参与贡献
众人拾柴火焰高,sa-token秉承着开放的思想,欢迎大家贡献代码,为框架添砖加瓦
1. 在gitee或者github上fork一份代码到自己的仓库
2. clone自己的仓库到本地电脑
3. 在本地电脑修改、commit、push
4. 提交pr(点击:New Pull Request
5. 等待合并
作者寄语:参与贡献不光只有提交代码一个选择,点一个star、提一个issues都是对开源项目的促进,
如果框架帮助到了你,欢迎你把框架推荐给你的朋友、同事使用,为sa-token的推广做一份贡献
## 🌱 建议贡献的地方
- 修复源码现有bug,或增加新的实用功能
- 完善在线文档,或者修复现有错误之处
- 更多demo示例:比如SSM版搭建步骤
## 建议贡献的地方
- 修复源码现有bug,或优化代码架构,或增加新的实用功能
- 完善在线文档,或者修复现有描述错误之处
- 更多的第三方框架集成方案,更多的demo示例:比如SSM版搭建步骤
- 您可以参考项目issues与需求墙进行贡献
- 如果更新实用功能,可在文档友情链接处留下自己的推广链接
## 🚀 友情链接
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
## 😎 交流群
## 交流群
QQ交流群:[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) ,欢迎你的加入
+2
View File
@@ -14,9 +14,11 @@
- [无Cookie模式(前后台分离)](/use/not-cookie)
- [模拟他人](/use/mock-person)
- [多账号验证](/use/many-account)
- [同端互斥登录](/use/mutex-login)
- [注解式鉴权](/use/at-check)
- [花式token](/use/token-style)
- [框架配置](/use/config)
- [会话治理](/use/search-session)
- **其它**
- [常见问题](/more/common-questions)
+10 -7
View File
@@ -6,9 +6,9 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<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在线文档|权限认证框架">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架,功能全面,上手简单,登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<link rel="stylesheet" href="https://unpkg.com/docsify@4.11.3/lib/themes/vue.css">
<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">
</head>
@@ -22,6 +22,9 @@
<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.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>
<option value="http://sa-token.dev33.cn/v/v1.7.0/doc/index.html">v1.7.0</option>
<option value="http://sa-token.dev33.cn/v/v1.6.0/doc/index.html">v1.6.0</option>
<option value="http://sa-token.dev33.cn/v/v1.5.1/doc/index.html">v1.5.1</option>
@@ -37,7 +40,7 @@
</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.8.0</sub>'
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.11.0</sub>'
window.$docsify = {
name: name, // 名字
repo: 'https://github.com/click33/sa-token', // github地址
@@ -75,9 +78,9 @@
]
}
</script>
<script src="https://unpkg.com/docsify@4.9.4/lib/docsify.min.js"></script>
<script src="https://unpkg.com/docsify-copy-code@2.1.0/dist/docsify-copy-code.min.js"></script>
<script src="https://unpkg.com/prismjs@1.19.0/components/prism-java.min.js"></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>
+17 -1
View File
@@ -1,7 +1,23 @@
# 更新日志
### 2020-12-24 @v1.8.0
### 2021-1-9 @v1.11.0
- 新增:提供AOP注解鉴权方案 **[重要]**
- 优化自动生成token的算法
### 2021-1-9 @v1.10.0
- 新增:提供查询所有会话方案 **[重要]**
- 修复:修复token设置为永不过期时无法正常被顶下线的bug,感谢github用户 @zjh599245299 提出的bug
### 2021-1-6 @v1.9.0
- 优化:`spring-boot-starter-data-redis``2.3.7.RELEASE` 改为 `2.3.3.RELEASE`
- 修复:补上注解拦截器里漏掉验证`@SaCheckRole`的bug
- 新增:新增同端互斥登录,像QQ一样手机电脑同时在线,但是两个手机上互斥登录 **[重要]**
### 2021-1-2 @v1.8.0
- 优化:优化源码注释
- 修复:修复部分文档错别字
- 修复:修复项目文件夹名称错误
+1 -1
View File
@@ -9,7 +9,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
```
+1 -1
View File
@@ -18,7 +18,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
```
+45 -3
View File
@@ -73,9 +73,51 @@
```
- mode有两种取值:
- `SaMode.AND`, 标注一组权限,会话必须全部具有才可通过校验
- `SaMode.OR`, 标注一组权限,会话只要具有其一即可通过校验
mode有两种取值:
- `SaMode.AND`, 标注一组权限,会话必须全部具有才可通过校验
- `SaMode.OR`, 标注一组权限,会话只要具有其一即可通过校验
## 4、AOP模式使用注解
使用拦截器方式,只能把注解加到`Controller层`上,那么如果我想把注解写到项目的任意位置,比如`Service层`,应该怎么办? <br>
很简单,你只需要将拦截器模式更换为`SpringAOP模式`即可, 在`pom.xml`里添加
``` xml
<!-- sa-token整合SpringAOP实现注解鉴权 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.11.0</version>
</dependency>
```
然后你就可以在任意地方使用注解鉴权,例如:
``` java
@Service
public class UserService {
@SaCheckLogin
public List<String> getList() {
System.out.println("getList");
return new ArrayList<String>();
}
}
```
**注意:拦截器模式和AOP模式不可同时集成,否则会在Controller层发生一个注解校验两次的bug**
+2 -2
View File
@@ -11,7 +11,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
```
优点:兼容性好,缺点:session序列化后基本不可读,对开发者来讲等同于乱码
@@ -23,7 +23,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
```
优点:session序列化后可读性强,可灵活手动修改,缺点:兼容性稍差
+10 -6
View File
@@ -7,11 +7,13 @@
- 所谓权限验证,验证的核心就是当前账号是否拥有一个权限码
- 有:就让你通过、没有:那么禁止访问
- 再往底了说,就是每个账号都会拥有一个权限码集合,我来验证这个集合中是否包括我需要检测的那个权限码
- 例如:当前账号拥有权限码集合:`[101, 102, "user-add", "user-get"]`,这时候我去验证权限码:`201`,则结果就是验证失败,禁止访问
- 所以现在问题的核心就是,1、如何获取一个账号所拥有的的权限码集合,2、本次操作要验证的权限码是哪个
- 例如:当前账号拥有权限码集合:`["user:add", "user:delete", "user:get"]`,这时候我去验证权限码:`"user:update"`,则结果就是验证失败,禁止访问
- 所以现在问题的核心就是:
1. 如何获取一个账号所拥有的的权限码集合
2. 本次操作要验证的权限码是哪个
## 获取当前账号权限码集合
因为每个项目的需求不同,其权限设计也千变万化,所以【获取当前账号权限码集合】这一操作不可能内置到框架中,
因为每个项目的需求不同,其权限设计也千变万化,【获取当前账号权限码集合】这一操作不可能内置到框架中,
所以`sa-token`将此操作以接口的方式暴露给你,以方便的你根据自己的业务逻辑进行重写
- 你需要做的就是新建一个类,重写`StpInterface`接口,例如以下代码:
@@ -34,8 +36,9 @@ public class StpInterfaceImpl implements StpInterface {
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<Object> getPermissionCodeList(Object loginId, String loginKey) {
List<Object> list = new ArrayList<Object>(); // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
public List<String> getPermissionCodeList(Object loginId, String loginKey) {
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user-add");
list.add("user-delete");
@@ -50,7 +53,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;
+39
View File
@@ -0,0 +1,39 @@
# 同端互斥登录
如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号 <br/>
同端互斥登录,指的就是像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线
---
## 具体API
`sa-token`中如何做到同端互斥登录? <br/>
首先在配置文件中,将 `allowConcurrentLogin` 配置为false,然后调用登录等相关接口时声明设备标识即可:
#### 指定设备标识登录
``` java
StpUtil.setLoginId(10001, "PC"); // 指定`账号id`和`设备标识`进行登录
```
调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-4`
#### 指定设备标识强制注销
``` java
StpUtil.logoutByLoginId(10001, "PC"); // 指定`账号id`和`设备标识`进行强制注销 (踢人下线)
```
如果第二个参数填写null或不填,代表将这个账号id所有在线端踢下线,被踢出者再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-5`
#### 查询当前登录的设备标识
``` java
StpUtil.getLoginDevice(); // 指返回当前token的登录设备
```
#### id反查token
``` java
StpUtil.getTokenValueByLoginId(10001, "APP"); // 获取指定loginId指定设备端的tokenValue
```
+52
View File
@@ -0,0 +1,52 @@
# 会话治理
尽管框架将大部分操作提供了简易的封装,但在一些特殊场景下,我们仍需要绕过框架,直达数据底层进行一些操作 <br>
sa-token提供以下API助你直接操作会话列表
---
## 具体API
#### 查询所有token
``` java
StpUtil.searchTokenValue(String keyword, int start, int size);
```
#### 查询所有账号Session会话
``` java
StpUtil.searchSessionId(String keyword, int start, int size);
```
#### 查询所有令牌Session会话
``` java
StpUtil.searchTokenSessionId(String keyword, int start, int size);
```
#### 参数详解:
- `keyword`: 查询关键字,只有包括这个字符串的token值才会被查询出来
- `start`: 数据开始处索引, 值为-1时代表一次性取出所有数据
- `size`: 要获取的数据条数
使用示例:
``` java
// 查询value包括1000的所有token,结果集从第0条开始,返回10条
List<String> tokenList = StpUtil.searchTokenValue("1000", 0, 10);
for (String token : tokenList) {
System.out.println(token);
}
```
<br/>
#### 注意事项:
由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据:
- 单机模式下:百万会话取出10条token平均耗时 `0.255s`
- Redis模式下:百万会话取出10条token平均耗时 `3.322s`
请根据业务实际水平合理调用API
+1
View File
@@ -14,6 +14,7 @@ StpUtil.getSession(); // 获取当前账号id的Session (
StpUtil.getSession(true); // 获取当前账号id的Session, 并决定在Session尚未创建时,是否新建并返回
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getSessionByLoginId(10001, true); // 获取账号id为10001的Session, 并决定在Session尚未创建时,是否新建并返回
StpUtil.getSessionBySessionId("xxxx-xxxx"); // 获取SessionId为xxxx-xxxx的Session, 在Session尚未创建时, 返回null
```
+7 -7
View File
@@ -2,13 +2,13 @@
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>sa-token</title>
<title>sa-token 官方文档</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<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在线文档|权限认证框架">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架,功能全面,上手简单,登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<link rel="stylesheet" href="https://unpkg.com/docsify@4.11.3/lib/themes/vue.css">
<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="shortcut icon" type="image/x-icon" href="doc/logo.png">
<link rel="stylesheet" href="index.css">
@@ -43,16 +43,16 @@
<!-- 内容部分 -->
<div class="main-box">
<div class="content-box">
<h1>sa-token<small>v1.8.0</small></h1>
<h1>sa-token<small>v1.11.0</small></h1>
<div class="sub-title">一个JavaWeb轻量级权限认证框架,功能全面,上手简单</div>
<!-- <p>0配置开箱即用,低学习成本</p> -->
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、Spring集成...</p>
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...</p>
<p>零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有</p>
<div class="btn-box">
<a href="https://github.com/click33/sa-token" target="_blank">GitHub</a>
<a href="https://gitee.com/sz6/sa-token" target="_blank">码云</a>
<a href="http://sa-app.dev33.cn/wall.html?name=sa-token" target="_blank">需求墙</a>
<a href="doc/" target="_self">开发文档</a>
<a href="doc/index.html" target="_self">开发文档</a>
</div>
</div>
</div>
@@ -60,7 +60,7 @@
<footer>
<p>
<span class="copyright">Copyright © 2020 &nbsp;</span>
<a href="http://www.miitbeian.gov.cn/" target="_blank">鲁ICP备18046274号-2</a> &nbsp;
<a href="https://beian.miit.gov.cn/" target="_blank">鲁ICP备18046274号-2</a> &nbsp;
QQ交流群:<a href="https://jq.qq.com/?_wv=1027&k=45H977HM" target="_blank">1002350610</a>
<!-- QQ交流群:<a href="https://jq.qq.com/?_wv=1027&k=5DHN5Ib" target="_blank">782974737</a> -->
</p>
+12
View File
@@ -0,0 +1,12 @@
target/
node_modules/
bin/
.settings/
unpackage/
.classpath
.project
.factorypath
.idea/
+35
View File
@@ -0,0 +1,35 @@
<?xml version='1.0' encoding='utf-8'?>
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
</parent>
<packaging>jar</packaging>
<name>sa-token-spring-aop</name>
<artifactId>sa-token-spring-aop</artifactId>
<description>sa-token authentication by spring-aop</description>
<dependencies>
<!-- sa-token-spring-boot-starter -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
</dependency>
<!-- spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,67 @@
package cn.dev33.satoken.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
/**
* sa-token 基于 Spring Aop 的注解鉴权
* @author kong
*/
@Aspect
@Component
public class SaCheckAspect {
/**
* 底层的 StpLogic 对象
*/
public StpLogic stpLogic = null;
/**
* 创建,并指定一个默认的 StpLogic
*/
public SaCheckAspect() {
this.stpLogic = StpUtil.stpLogic;
}
/**
* 定义AOP签名 (切入所有使用sa-token鉴权注解的方法)
*/
public static final String POINTCUT_SIGN = "@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || "
+ "@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || "
+ "@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission)";
/**
* 声明AOP签名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut() {
}
/**
* 环绕切入
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
stpLogic.checkMethodAnnotation(signature.getMethod());
try {
// 执行原有逻辑
Object obj = joinPoint.proceed();
return obj;
} catch (Throwable e) {
throw e;
}
}
}
@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.aop.SaCheckAspect
+2 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</parent>
<packaging>jar</packaging>
@@ -19,7 +19,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>1.8.0</version>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -1,14 +1,13 @@
package cn.dev33.satoken.interceptor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
@@ -51,34 +50,10 @@ public class SaCheckInterceptor implements HandlerInterceptor {
if (handler instanceof HandlerMethod == false) {
return true;
}
HandlerMethod method = (HandlerMethod ) handler;
Method method = ((HandlerMethod) handler).getMethod();
// ----------- 验证登录
if(method.hasMethodAnnotation(SaCheckLogin.class) || method.getBeanType().isAnnotationPresent(SaCheckLogin.class)) {
stpLogic.checkLogin();
}
// ----------- 验证权限
// 验证方法上的
SaCheckPermission scp = method.getMethodAnnotation(SaCheckPermission.class);
if(scp != null) {
String[] permissionCodeArray = scp.value();
if(scp.mode() == SaMode.AND) {
stpLogic.checkPermissionAnd(permissionCodeArray); // 必须全部都有
} else {
stpLogic.checkPermissionOr(permissionCodeArray); // 有一个就行了
}
}
// 验证类上的
scp = method.getBeanType().getAnnotation(SaCheckPermission.class);
if(scp != null) {
String[] permissionCodeArray = scp.value();
if(scp.mode() == SaMode.AND) {
stpLogic.checkPermissionAnd(permissionCodeArray); // 必须全部都有
} else {
stpLogic.checkPermissionOr(permissionCodeArray); // 有一个就行了
}
}
// 进行验证
stpLogic.checkMethodAnnotation(method);
// 通过验证
return true;