实现限制同一个账号最多只能在3个客户端(有电脑、手机等)登录(附关键源码)
如上图,我的百度网盘已登录设备列表,有一个手机,2个windows客户端。手机设备有型号、最后登录时间、IP等。windows客户端信息有最后登录时间、操作系统类型、IP地址等。这些具体是如何实现的?下面分别给出android APP中采集手机信息,VUE3中采集电脑信息的实现思路和关键代码。
一、思路
通常是通过 Session 管理+Token机制+数据库存储 组合实现的。以下是可能的实现方式:
1. Session 或 Token 机制
- 服务器为每个客户端生成唯一的 session 或 token(JWT、OAuth等)。
- 这些 token 会存储在数据库或缓存系统中(如 Redis)。
2. 记录登录状态
- 当用户成功登录后,服务器会在数据库中记录 当前设备信息(如 IP、User-Agent、设备 ID)。
- 还可以给每个 session 生成一个唯一 ID,并记录 设备唯一标识(如浏览器指纹、手机唯一 ID)。
3. 检查并限制登录数量
- 登录时检查:当用户登录时,服务器会查询数据库或缓存,看当前账号已登录的设备数量是否已达到上限(如 3 个)。
- 超限处理:
- 方案1:阻止新登录:如果已达上限,则拒绝新的登录请求,并提示“已达到最大设备数量”。
- 方案2:踢掉旧设备:可以让用户选择踢掉最早登录的设备(删除最早的 session/token)。
- 方案3:手动管理:提供用户后台,让用户手动管理登录设备,选择登出某个设备。
4. 定期清理过期/无效的 Session
- 服务器可以设置 session 过期时间(如 7 天无操作自动登出)。
- 或者在用户 主动登出 时,删除对应的 session/token。
超出最大登录数量时,百度网盘的实现方案是方案1:拒绝新的登录请求,并提示“已达到最大设备数量”。
二、上代码
1. Android APP 采集设备信息(Java/Kotlin)
Android 端可以使用 Build
类、TelephonyManager
、WifiManager
采集设备信息,例如 设备型号、系统版本、IP 地址、MAC 地址 等。
示例代码 (Kotlin)
import android.content.Context
import android.net.wifi.WifiManager
import android.os.Build
import android.telephony.TelephonyManager
import java.net.NetworkInterface
import java.net.SocketException
import java.util.*
fun getDeviceInfo(context: Context): Map<String, String> {
val deviceInfo = mutableMapOf<String, String>()
// 设备型号
deviceInfo["deviceModel"] = Build.MODEL
// 设备品牌
deviceInfo["deviceBrand"] = Build.BRAND
// Android 版本
deviceInfo["androidVersion"] = Build.VERSION.RELEASE
// 设备唯一 ID
deviceInfo["deviceId"] = Build.SERIAL
// 获取 IP 地址(WIFI 或移动网络)
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiInfo = wifiManager.connectionInfo
val ip = android.text.format.Formatter.formatIpAddress(wifiInfo.ipAddress)
deviceInfo["ipAddress"] = ip
// 获取 MAC 地址
deviceInfo["macAddress"] = getMacAddress()
return deviceInfo
}
// 获取 MAC 地址
fun getMacAddress(): String {
try {
val interfaces = Collections.list(NetworkInterface.getNetworkInterfaces())
for (networkInterface in interfaces) {
if (!networkInterface.name.equals("wlan0", ignoreCase = true)) continue
val macBytes = networkInterface.hardwareAddress ?: return ""
return macBytes.joinToString(":") { "%02X".format(it) }
}
} catch (ex: SocketException) {
ex.printStackTrace()
}
return "02:00:00:00:00:00" // 默认 MAC 地址
}
输出示例
2. Vue3 获取电脑信息
在 Vue3 Web 端,可以获取 IP、浏览器类型、操作系统等,但无法获取 MAC 地址(受浏览器安全限制)。需要配合后端来获取客户端 IP。
示例代码
<script setup>
import { onMounted, ref } from "vue";
const deviceInfo = ref({
userAgent: "",
platform: "",
ipAddress: "",
});
// 获取本地设备信息
const getDeviceInfo = async () => {
deviceInfo.value.userAgent = navigator.userAgent; // 浏览器信息
deviceInfo.value.platform = navigator.platform; // 操作系统
// 获取公网 IP(需要后端支持)
try {
const response = await fetch("https://api.ipify.org?format=json");
const data = await response.json();
deviceInfo.value.ipAddress = data.ip;
} catch (error) {
console.error("IP 获取失败", error);
}
};
onMounted(() => {
getDeviceInfo();
});
</script>
<template>
<div>
<h3>设备信息</h3>
<p>操作系统: {{ deviceInfo.platform }}</p>
<p>浏览器信息: {{ deviceInfo.userAgent }}</p>
<p>IP 地址: {{ deviceInfo.ipAddress }}</p>
</div>
</template>
输出示例
![](https://i-blog.csdnimg.cn/direct/576d014111274ffbaf0bb7ce723c8ee5.png)
3. 将 session 存入 Redis的示例代码
首先,在 Spring Boot 项目的 pom.xml
中引入 Redis 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
在 application.yml
中配置 Redis 连接信息:
spring:
redis:
host: localhost
port: 6379
password: ""
timeout: 5000
session:
store-type: redis
timeout: 86400 # 1天(可根据需求调整)
在 Java 代码中,我们使用 RedisTemplate
来存储 session,每次用户登录时:
- 检查是否超出设备数量(例如最多 3 个)
- 如果超出,踢掉最早的设备
- 存入 Redis 并设置过期时间
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class SessionService {
private final RedisTemplate<String, Object> redisTemplate;
private static final int MAX_SESSIONS = 3; // 限制最大设备登录数
public SessionService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 记录新的登录 session
* @param userId 用户 ID
* @param sessionId 新的 session ID
* @param deviceInfo 设备信息(IP、设备类型)
*/
public void addSession(String userId, String sessionId, String deviceInfo) {
String key = "user_sessions:" + userId;
// 获取当前已存储的 session 列表
List<Object> sessions = redisTemplate.opsForList().range(key, 0, -1);
if (sessions != null && sessions.size() >= MAX_SESSIONS) {
// 超过最大限制,移除最旧的 session(列表最左侧的)
redisTemplate.opsForList().leftPop(key);
}
// 添加新的 session
redisTemplate.opsForList().rightPush(key, sessionId + ":" + deviceInfo);
// 设置 session 过期时间(7 天)
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
/**
* 获取用户已登录的设备列表
*/
public List<Object> getSessions(String userId) {
String key = "user_sessions:" + userId;
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 删除某个 session(用于手动登出)
*/
public void removeSession(String userId, String sessionId) {
String key = "user_sessions:" + userId;
redisTemplate.opsForList().remove(key, 1, sessionId);
}
}
测试 API
创建一个 RestController 让前端可以调用:
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/session")
public class SessionController {
private final SessionService sessionService;
public SessionController(SessionService sessionService) {
this.sessionService = sessionService;
}
// 添加新的登录 session
@PostMapping("/add")
public String addSession(@RequestParam String userId, @RequestParam String sessionId, @RequestParam String deviceInfo) {
sessionService.addSession(userId, sessionId, deviceInfo);
return "Session added successfully!";
}
// 获取用户的 session 列表
@GetMapping("/list")
public List<Object> getSessions(@RequestParam String userId) {
return sessionService.getSessions(userId);
}
// 移除指定 session(登出)
@PostMapping("/remove")
public String removeSession(@RequestParam String userId, @RequestParam String sessionId) {
sessionService.removeSession(userId, sessionId);
return "Session removed!";
}
}
测试示例
启动 Spring Boot 服务器后,可以用 Postman 或 curl
测试:
① 添加新 session
curl -X POST "http://localhost:8080/session/add?userId=123&sessionId=abcd123&deviceInfo=Windows_192.168.1.10"
返回
Session added successfully!
② 获取已登录设备
curl -X GET "http://localhost:8080/session/list?userId=123"
返回
③ 手动登出某个设备
curl -X POST "http://localhost:8080/session/remove?userId=123&sessionId=session1"
返回:Session removed!
总结
功能 | 实现方式 |
---|---|
存储 session | Redis List 数据结构 (opsForList() ) |
限制最多 3 个设备 | 超过 3 个时 leftPop() 删除最旧 session |
查询设备 | range(0, -1) 获取当前 session |
登出设备 | remove() 删除指定 session |
Session 过期 | expire(7, TimeUnit.DAYS) 设置 7 天过期 |
这样就可以限制用户最多只能在3台设备上登录,并支持手动踢出设备。