mirror of
https://gitee.com/dromara/sa-token.git
synced 2024-12-03 12:27:50 +08:00
新增 maxLoginCount 配置,指定同一账号可同时在线的最大数量
This commit is contained in:
parent
cfc11d0ba8
commit
969deb9470
@ -3,7 +3,7 @@ package cn.dev33.satoken.config;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置类 Model
|
||||
* Sa-Token 配置类 Model
|
||||
* <p>
|
||||
* 你可以通过yml、properties、java代码等形式配置本类参数,具体请查阅官方文档: 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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 |
|
||||
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user