新增 maxLoginCount 配置,指定同一账号可同时在线的最大数量

This commit is contained in:
click33 2022-04-24 19:19:20 +08:00
parent cfc11d0ba8
commit 969deb9470
5 changed files with 122 additions and 56 deletions

View File

@ -3,7 +3,7 @@ package cn.dev33.satoken.config;
import java.io.Serializable;
/**
* Sa-Token 配置类 Model
* Sa-Token 配置类 Model
* <p>
* 你可以通过ymlpropertiesjava代码等形式配置本类参数具体请查阅官方文档: http://sa-token.dev33.cn/
*
@ -32,6 +32,11 @@ public class SaTokenConfig implements Serializable {
/** 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
private Boolean isShare = true;
/**
* 同一账号最大登录数量-1代表不限 只有在 isConcurrent=true, isShare=false 时此配置才有效
*/
private int maxLoginCount = 10;
/** 是否尝试从请求体里读取token */
private Boolean isReadBody = true;
@ -176,6 +181,22 @@ public class SaTokenConfig implements Serializable {
return this;
}
/**
* @return 同一账号最大登录数量-1代表不限 只有在 isConcurrent=true, isShare=false 时此配置才有效
*/
public int getMaxLoginCount() {
return maxLoginCount;
}
/**
* @param maxLoginCount 同一账号最大登录数量-1代表不限 只有在 isConcurrent=true, isShare=false 时此配置才有效
* @return 对象自身
*/
public SaTokenConfig setMaxLoginCount(int maxLoginCount) {
this.maxLoginCount = maxLoginCount;
return this;
}
/**
* @return 是否尝试从请求体里读取token
*/
@ -458,6 +479,7 @@ public class SaTokenConfig implements Serializable {
+ ", activityTimeout=" + activityTimeout
+ ", isConcurrent=" + isConcurrent
+ ", isShare=" + isShare
+ ", maxLoginCount=" + maxLoginCount
+ ", isReadBody=" + isReadBody
+ ", isReadHead=" + isReadHead
+ ", isReadCookie=" + isReadCookie

View File

@ -1,6 +1,7 @@
package cn.dev33.satoken.session;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -110,12 +111,37 @@ public class SaSession implements Serializable {
private final List<TokenSign> tokenSignList = new Vector<>();
/**
* 返回token签名列表的拷贝副本
* 此Session绑定的token签名列表
*
* @return token签名列表
*/
public List<TokenSign> getTokenSignList() {
return new Vector<>(tokenSignList);
return tokenSignList;
}
/**
* 返回token签名列表的拷贝副本
*
* @return token签名列表
*/
public List<TokenSign> tokenSignListCopy() {
return new ArrayList<>(tokenSignList);
}
/**
* 返回token签名列表的拷贝副本根据 device 筛选
*
* @param device 设备类型 null 代表不限设备类型
* @return token签名列表
*/
public List<TokenSign> tokenSignListCopyByDevice(String device) {
List<TokenSign> list = new ArrayList<>();
for (TokenSign tokenSign : tokenSignListCopy()) {
if(device == null || tokenSign.getDevice().equals(device)) {
list.add(tokenSign);
}
}
return list;
}
/**
@ -125,7 +151,7 @@ public class SaSession implements Serializable {
* @return 查找到的tokenSign
*/
public TokenSign getTokenSign(String tokenValue) {
for (TokenSign tokenSign : getTokenSignList()) {
for (TokenSign tokenSign : tokenSignListCopy()) {
if (tokenSign.getValue().equals(tokenValue)) {
return tokenSign;
}
@ -140,7 +166,7 @@ public class SaSession implements Serializable {
*/
public void addTokenSign(TokenSign tokenSign) {
// 如果已经存在于列表中则无需再次添加
for (TokenSign tokenSign2 : getTokenSignList()) {
for (TokenSign tokenSign2 : tokenSignListCopy()) {
if (tokenSign2.getValue().equals(tokenSign.getValue())) {
return;
}

View File

@ -1,7 +1,10 @@
package cn.dev33.satoken.stp;
import java.util.*;
import java.util.function.Consumer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.annotation.SaCheckLogin;
@ -316,6 +319,11 @@ public class StpLogic {
// 如果配置为共享token, 则尝试从Session签名记录里取出token
if(getConfigOfIsShare()) {
tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
} else {
// 如果配置为不共享token需要检查会话是否超出 max-login-count
if(config.getMaxLoginCount() != -1) {
logoutByRetainCount(id, null, config.getMaxLoginCount() - 1);
}
}
} else {
// --- 如果不允许并发登录则将这个账号的历史登录标记为被顶下线
@ -383,7 +391,7 @@ public class StpLogic {
public void logout(Object loginId) {
logout(loginId, null);
}
/**
* 会话注销根据账号id 设备类型
*
@ -391,12 +399,38 @@ public class StpLogic {
* @param device 设备类型 (填null代表注销所有设备类型)
*/
public void logout(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> {
// 删除Token-Id映射 & 清除Token-Session
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue);
SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);
}, true);
logoutByRetainCount(loginId, device, 0);
}
/**
* 会话注销根据账号id 设备类型 保留数量
*
* @param loginId 账号id
* @param device 设备类型 (填null代表注销所有设备类型)
* @param retainCount 保留最近的n次登录
*/
public void logoutByRetainCount(Object loginId, String device, int retainCount) {
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
List<TokenSign> list = session.tokenSignListCopyByDevice(device);
// 遍历操作
for (int i = 0; i < list.size(); i++) {
// 只操作前n条
if(i >= list.size() - retainCount) {
continue;
}
// 清理 token签名token最后活跃时间
String tokenValue = list.get(i).getValue();
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// 删除Token-Id映射 & 清除Token-Session
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue);
SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);
}
// 注销 Session
session.logoutByTokenSignCountToZero();
}
}
/**
@ -451,11 +485,20 @@ public class StpLogic {
* @param device 设备类型 (填null代表踢出所有设备类型)
*/
public void kickout(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> {
// 将此 token 标记为已被踢下线
updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue);
}, true);
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
for (TokenSign tokenSign: session.tokenSignListCopyByDevice(device)) {
// 清理 token签名token最后活跃时间
String tokenValue = tokenSign.getValue();
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// 将此 token 标记为已被踢下线
updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue);
}
// 注销 Session
session.logoutByTokenSignCountToZero();
}
}
/**
@ -498,44 +541,18 @@ public class StpLogic {
* @param device 设备类型 (填null代表顶替所有设备类型)
*/
public void replaced(Object loginId, String device) {
clearTokenCommonMethod(loginId, device, tokenValue -> {
// 将此 token 标记为已被顶替
updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
SaManager.getSaTokenListener().doReplaced(loginType, loginId, tokenValue);
}, false);
}
/**
* 封装 注销踢人顶人 三个动作的相同代码无API含义方法
* @param loginId 账号id
* @param device 设备类型
* @param appendFun 追加操作
* @param isLogoutSession 是否注销 User-Session
*/
protected void clearTokenCommonMethod(Object loginId, String device, Consumer<String> appendFun, boolean isLogoutSession) {
// 1. 如果此账号尚未登录则不执行任何操作
SaSession session = getSessionByLoginId(loginId, false);
if(session == null) {
return;
}
// 2. 循环token签名列表开始删除相关信息
for (TokenSign tokenSign : session.getTokenSignList()) {
if(device == null || tokenSign.getDevice().equals(device)) {
// -------- 共有操作
// s1. 获取token
if(session != null) {
for (TokenSign tokenSign: session.tokenSignListCopyByDevice(device)) {
// 清理 token签名token最后活跃时间
String tokenValue = tokenSign.getValue();
// s2. 清理掉[token-last-activity]
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// s3. 从token签名列表移除
session.removeTokenSign(tokenValue);
// -------- 追加操作
appendFun.accept(tokenValue);
// 将此 token 标记为已被顶替
updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
SaManager.getSaTokenListener().doReplaced(loginType, loginId, tokenValue);
}
}
// 3. 尝试注销session
if(isLogoutSession) {
session.logoutByTokenSignCountToZero();
}
}
// ---- 会话查询
@ -1381,7 +1398,7 @@ public class StpLogic {
return Collections.emptyList();
}
// 遍历解析
List<TokenSign> tokenSignList = session.getTokenSignList();
List<TokenSign> tokenSignList = session.tokenSignListCopy();
List<String> tokenValueList = new ArrayList<>();
for (TokenSign tokenSign : tokenSignList) {
if(device == null || tokenSign.getDevice().equals(device)) {
@ -1411,7 +1428,7 @@ public class StpLogic {
return null;
}
// 遍历解析
List<TokenSign> tokenSignList = session.getTokenSignList();
List<TokenSign> tokenSignList = session.tokenSignListCopy();
for (TokenSign tokenSign : tokenSignList) {
if(tokenSign.getValue().equals(tokenValue)) {
return tokenSign.getDevice();

View File

@ -79,6 +79,7 @@ PS两者的区别在于**`模式1会覆盖yml中的配置模式2会与y
| activityTimeout | long | -1 | token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒, 默认-1 代表不限制 (例如可以设置为1800代表30分钟内无操作就过期) [参考token有效期详解](/fun/token-timeout) |
| isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) |
| isShare | Boolean | true | 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) |
| maxLoginCount | int | 10 | 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置才有效) |
| isReadBody | Boolean | true | 是否尝试从 请求体 里读取 Token |
| isReadHead | Boolean | true | 是否尝试从 header 里读取 Token |
| isReadCookie | Boolean | true | 是否尝试从 cookie 里读取 Token |

View File

@ -5,12 +5,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import cn.dev33.satoken.session.SaSession;
/**
* Jackson定制版SaSession忽略 timeout 属性的序列化
* Jackson定制版SaSession忽略 timeout 属性的序列化
*
* @author kong
*
*/
@JsonIgnoreProperties("timeout")
@JsonIgnoreProperties({"timeout"})
public class SaSessionForJacksonCustomized extends SaSession {
/**