SpringBoot + 登录设备管理 + 强制踢下线:用户可查看并登出其他设备会话
导语
你是否遇到过这样的场景:
- 在公司电脑登录后,回家想用家里电脑登录,却发现账号在别处登录
- 手机丢失后,担心账号被他人滥用,却无法远程踢出设备
- 发现账号有异常登录行为,却不知道如何处理
传统的登录系统往往只支持单设备登录,或者无法让用户主动管理登录设备。今天,我们就来聊聊如何通过SpringBoot实现一个完善的登录设备管理系统,让用户可以查看所有登录设备并强制踢下线。
一、为什么需要登录设备管理?
1.1 安全需求分析
账号安全威胁
在多设备时代,账号安全面临诸多挑战:
- 设备丢失:手机、电脑丢失后,账号可能被他人滥用
- 账号共享:多人共用账号,难以追踪登录行为
- 异常登录:黑客盗号后异地登录
- 忘记登出:在公共电脑登录后忘记退出
用户需求
- 查看所有已登录设备
- 识别异常登录行为
- 远程踢出可疑设备
- 保护账号安全
1.2 传统方案的局限
单设备登录
// 传统方案:只允许一个设备登录
if (isUserLoggedIn(userId)) {
// 强制踢出之前的登录
logoutPreviousSession(userId);
}
// 创建新会话
createSession(userId);
问题:
- 用户无法在多个设备同时使用
- 无法查看历史登录记录
- 无法主动管理登录设备
Session方案
// 传统Session方案
HttpSession session = request.getSession();
session.setAttribute("userId", userId);
问题:
- 依赖服务器内存,难以扩展
- 无法跨服务器共享
- 无法主动踢出指定设备
二、系统架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 登录设备管理系统 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 用户认证 │ │ 设备识别 │ │ 会话管理 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ JWT Token │ │ Redis缓存 │ │ 数据库持久化 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 登录日志 │ │ 异常检测 │ │ 安全告警 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 核心组件
1. JWT Token
- 无状态认证
- 包含用户ID、设备ID、会话ID
- 支持Token过期和刷新
2. Redis缓存
- 存储会话状态
- 快速验证Token有效性
- 支持分布式部署
3. 数据库持久化
- 存储设备信息
- 记录登录历史
- 支持查询统计
2.3 数据模型
用户表(users)
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
create_time DATETIME NOT NULL
);
设备会话表(device_sessions)
CREATE TABLE device_sessions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
session_id VARCHAR(255) UNIQUE NOT NULL,
device_id VARCHAR(255) NOT NULL,
device_name VARCHAR(255),
device_type VARCHAR(50),
ip_address VARCHAR(50),
login_time DATETIME NOT NULL,
last_active_time DATETIME,
status VARCHAR(50) NOT NULL
);
三、实战:实现核心功能
3.1 设备识别
核心思路: 通过User-Agent和IP地址识别设备信息
@Component
public class DeviceUtil {
public Map<String, String> getDeviceInfo() {
Map<String, String> deviceInfo = new HashMap<>();
HttpServletRequest request = getCurrentRequest();
if (request != null) {
String userAgent = request.getHeader("User-Agent");
deviceInfo.put("userAgent", userAgent);
deviceInfo.put("deviceName", parseDeviceName(userAgent));
deviceInfo.put("deviceType", parseDeviceType(userAgent));
deviceInfo.put("ipAddress", getClientIpAddress(request));
}
return deviceInfo;
}
private String parseDeviceName(String userAgent) {
if (userAgent.contains("iPhone")) {
return "iPhone";
} else if (userAgent.contains("iPad")) {
return "iPad";
} else if (userAgent.contains("Android")) {
return userAgent.contains("Mobile") ? "Android Phone" : "Android Tablet";
} else if (userAgent.contains("Windows")) {
return "Windows PC";
} else if (userAgent.contains("Mac")) {
return "Mac";
} else {
return "Unknown Device";
}
}
}
3.2 登录流程
核心逻辑:
- 验证用户名密码
- 识别设备信息
- 检查并发登录数
- 创建会话记录
- 生成JWT Token
- 缓存到Redis
@Service
public class AuthService {
@Transactional
public Map<String, Object> login(String username, String password) {
// 1. 验证用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户名或密码错误"));
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
// 2. 识别设备
Map<String, String> deviceInfo = deviceUtil.getDeviceInfo();
String deviceId = deviceUtil.generateDeviceId();
String sessionId = UUID.randomUUID().toString().replace("-", "");
// 3. 检查并发登录数
List<DeviceSession> activeSessions = sessionRepository
.findActiveSessionsByUserId(user.getId());
if (activeSessions.size() >= maxConcurrent) {
// 踢出最旧的会话
DeviceSession oldestSession = activeSessions.get(activeSessions.size() - 1);
kickOutSession(oldestSession.getSessionId());
}
// 4. 创建会话记录
DeviceSession session = new DeviceSession();
session.setUserId(user.getId());
session.setSessionId(sessionId);
session.setDeviceId(deviceId);
session.setDeviceName(deviceInfo.get("deviceName"));
session.setDeviceType(deviceInfo.get("deviceType"));
session.setIpAddress(deviceInfo.get("ipAddress"));
session.setStatus("ACTIVE");
sessionRepository.save(session);
// 5. 生成JWT Token
String token = jwtUtil.generateToken(user.getId(), username, deviceId, sessionId);
// 6. 缓存到Redis
String tokenKey = tokenPrefix + token;
redisTemplate.opsForValue().set(tokenKey, sessionId, timeout, TimeUnit.MILLISECONDS);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("sessionId", sessionId);
result.put("deviceName", deviceInfo.get("deviceName"));
return result;
}
}
3.3 踢出设备
核心逻辑:
- 验证用户权限
- 更新会话状态
- 清除Redis缓存
- 记录操作日志
@Service
public class DeviceSessionService {
@Transactional
public boolean kickOutDevice(Long userId, String sessionId) {
// 1. 查找会话
DeviceSession session = sessionRepository.findBySessionId(sessionId)
.orElseThrow(() -> new RuntimeException("会话不存在"));
// 2. 验证权限
if (!session.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此会话");
}
// 3. 更新状态
session.setStatus("KICKED");
session.setLastActiveTime(LocalDateTime.now());
sessionRepository.save(session);
// 4. 清除Redis缓存
String userKey = userPrefix + userId + ":" + sessionId;
Object tokenObj = redisTemplate.opsForValue().get(userKey);
if (tokenObj != null) {
String tokenKey = tokenPrefix + tokenObj.toString();
redisTemplate.delete(tokenKey);
}
redisTemplate.delete(userKey);
log.info("用户 {} 踢出设备: {}", userId, session.getDeviceName());
return true;
}
@Transactional
public void kickOutAllDevices(Long userId, String currentSessionId) {
List<DeviceSession> sessions = sessionRepository.findActiveSessionsByUserId(userId);
for (DeviceSession session : sessions) {
if (!session.getSessionId().equals(currentSessionId)) {
session.setStatus("KICKED");
session.setLastActiveTime(LocalDateTime.now());
sessionRepository.save(session);
}
}
log.info("用户 {} 踢出所有其他设备", userId);
}
}
3.4 设备列表查询
@Service
public class DeviceSessionService {
public List<Map<String, Object>> getUserDevices(Long userId) {
List<DeviceSession> sessions = sessionRepository.findActiveSessionsByUserId(userId);
return sessions.stream().map(session -> {
Map<String, Object> device = new HashMap<>();
device.put("sessionId", session.getSessionId());
device.put("deviceId", session.getDeviceId());
device.put("deviceName", session.getDeviceName());
device.put("deviceType", session.getDeviceType());
device.put("ipAddress", session.getIpAddress());
device.put("loginTime", session.getLoginTime());
device.put("lastActiveTime", session.getLastActiveTime());
device.put("status", session.getStatus());
return device;
}).collect(Collectors.toList());
}
}
四、前端界面实现
4.1 设备列表展示
<div class="device-list" id="deviceList">
<!-- 设备项 -->
<div class="device-item current">
<div class="device-header">
<span class="device-name">Windows PC (当前设备)</span>
<span class="device-status status-active">活跃</span>
</div>
<div class="device-info">
<div class="device-info-item">
<span>📱 设备类型:</span>
<span>DESKTOP</span>
</div>
<div class="device-info-item">
<span>🌐 IP地址:</span>
<span>192.168.1.100</span>
</div>
<div class="device-info-item">
<span>🕐 登录时间:</span>
<span>2026-02-28 10:00:00</span>
</div>
<div class="device-info-item">
<span>⏰ 最后活跃:</span>
<span>2026-02-28 10:05:00</span>
</div>
</div>
</div>
</div>
4.2 踢出设备操作
function kickOutDevice(sessionId) {
if (!confirm('确定要踢出此设备吗?')) {
return;
}
fetch(`/api/devices/${sessionId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('设备已踢下线');
loadDevices(); // 刷新设备列表
} else {
alert('踢出设备失败: ' + data.message);
}
});
}
五、安全增强措施
5.1 异常登录检测
@Service
public class LoginSecurityService {
public void checkLoginSecurity(Long userId, String ipAddress, String deviceId) {
// 1. 检查异地登录
List<DeviceSession> recentSessions = sessionRepository
.findRecentSessionsByUserId(userId, LocalDateTime.now().minusDays(7));
Set<String> recentIps = recentSessions.stream()
.map(DeviceSession::getIpAddress)
.collect(Collectors.toSet());
if (!recentIps.contains(ipAddress)) {
// 异地登录告警
alertService.sendLoginAlert(userId, "异地登录", ipAddress);
}
// 2. 检查频繁登录
long loginCount = sessionRepository.countTodayLogins(userId);
if (loginCount > 10) {
// 频繁登录告警
alertService.sendLoginAlert(userId, "频繁登录", ipAddress);
}
}
}
5.2 Token刷新机制
@Service
public class TokenRefreshService {
public String refreshToken(String oldToken) {
// 1. 验证旧Token
if (!jwtUtil.validateToken(oldToken)) {
throw new RuntimeException("Token无效");
}
// 2. 检查是否需要刷新
Claims claims = jwtUtil.parseToken(oldToken);
long expireTime = claims.getExpiration().getTime();
long currentTime = System.currentTimeMillis();
// 如果剩余时间小于1天,则刷新
if (expireTime - currentTime < 86400000) {
Long userId = claims.get("userId", Long.class);
String username = claims.getSubject();
String deviceId = claims.get("deviceId", String.class);
String sessionId = claims.get("sessionId", String.class);
return jwtUtil.generateToken(userId, username, deviceId, sessionId);
}
return oldToken;
}
}
5.3 登录日志记录
@Entity
@Table(name = "login_logs")
public class LoginLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private String username;
private String deviceId;
private String ipAddress;
private String location;
private String loginResult;
private LocalDateTime loginTime;
private String failReason;
}
六、性能优化
6.1 Redis缓存策略
// Token缓存
String tokenKey = "device:token:" + token;
redisTemplate.opsForValue().set(tokenKey, sessionId, 7, TimeUnit.DAYS);
// 用户会话列表缓存
String userKey = "device:user:" + userId;
redisTemplate.opsForHash().put(userKey, sessionId, token);
redisTemplate.expire(userKey, 7, TimeUnit.DAYS);
6.2 数据库索引优化
-- 会话表索引
CREATE INDEX idx_user_status ON device_sessions(user_id, status);
CREATE INDEX idx_session_id ON device_sessions(session_id);
CREATE INDEX idx_last_active ON device_sessions(last_active_time);
-- 登录日志表索引
CREATE INDEX idx_user_login_time ON login_logs(user_id, login_time);
6.3 定时清理过期会话
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanExpiredSessions() {
LocalDateTime expireTime = LocalDateTime.now().minusDays(30);
List<DeviceSession> expiredSessions = sessionRepository
.findByLastActiveTimeBeforeAndStatus(expireTime, "ACTIVE");
for (DeviceSession session : expiredSessions) {
session.setStatus("EXPIRED");
sessionRepository.save(session);
}
log.info("清理过期会话: {} 个", expiredSessions.size());
}
七、实战效果演示
7.1 用户登录流程
步骤1:用户登录
用户输入用户名密码 → 系统验证 → 识别设备 → 创建会话 → 返回Token
步骤2:查看设备列表
用户点击"我的设备" → 显示所有登录设备 → 标记当前设备
步骤3:踢出设备
用户点击"踢下线" → 确认操作 → 更新会话状态 → 清除缓存 → 设备被强制下线
7.2 界面效果
┌─────────────────────────────────────────────────────────────┐
│ 当前用户: testuser 当前设备: Windows PC 登录时间: 10:00 │
├─────────────────────────────────────────────────────────────┤
│ 会话统计 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 3 │ │ 2 │ │ 1 │ │ 0 │ │
│ │总会话│ │活跃 │ │已踢出│ │已登出│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────────────────────┤
│ 我的设备 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Windows PC (当前设备) [活跃] │ │
│ │ 📱 DESKTOP 🌐 192.168.1.100 │ │
│ │ 🕐 2026-02-28 10:00 ⏰ 2026-02-28 10:05 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ iPhone [活跃] │ │
│ │ 📱 MOBILE 🌐 192.168.1.101 │ │
│ │ 🕐 2026-02-28 09:00 ⏰ 2026-02-28 09:30 │ │
│ │ [踢下线] │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
八、最佳实践
8.1 配置建议
device:
session:
max-concurrent: 5 # 最大并发登录数
timeout: 604800000 # 会话超时时间(7天)
token-prefix: "device:token:"
user-prefix: "device:user:"
8.2 安全建议
- 强制强密码:要求用户设置复杂密码
- 二次验证:敏感操作进行短信或邮箱验证
- 登录告警:异地登录、频繁登录及时告警
- 定期清理:定期清理过期会话和日志
- 操作日志:记录所有关键操作日志
8.3 性能建议
- Redis缓存:使用Redis缓存会话状态
- 数据库索引:为常用查询字段创建索引
- 定时任务:定时清理过期数据
- 分页查询:大量数据时使用分页查询
九、总结
9.1 核心收益
- 用户体验提升:用户可以自主管理登录设备
- 账号安全增强:及时发现和处理异常登录
- 多设备支持:同一账号可在多个设备同时使用
- 操作透明:所有登录行为可追溯
9.2 适用场景
- 社交应用
- 电商系统
- 企业应用
- 在线教育
- 任何需要账号安全的系统
9.3 扩展方向
- 支持地理位置解析
- 支持设备指纹识别
- 支持登录行为分析
- 支持WebSocket实时通知
- 支持多因素认证
十、源码获取
完整项目代码已上传,包含:
- 完整的SpringBoot项目
- JWT认证机制
- Redis会话管理
- 可视化前端界面
- 详细的API文档
项目地址: 公众号"服务端技术精选",回复"登录设备管理"即可获取项目下载链接。
互动话题
- 你在项目中遇到过哪些账号安全问题?
- 除了踢下线,你觉得还有哪些安全措施是必要的?
- 你认为多设备登录应该如何平衡用户体验和安全性?
欢迎在评论区留言讨论!如果觉得文章对你有帮助,别忘了点赞、在看、转发三连支持一下~
关于作者
服务端技术精选,专注分享后端开发技术干货,包括微服务架构、分布式系统、性能优化等。欢迎关注,一起学习成长!
本文首发于公众号「服务端技术精选」,转载请注明出处。
标题:SpringBoot + 登录设备管理 + 强制踢下线:用户可查看并登出其他设备会话
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/02/1772277265194.html
公众号:服务端技术精选
- 导语
- 一、为什么需要登录设备管理?
- 1.1 安全需求分析
- 1.2 传统方案的局限
- 二、系统架构设计
- 2.1 整体架构
- 2.2 核心组件
- 2.3 数据模型
- 三、实战:实现核心功能
- 3.1 设备识别
- 3.2 登录流程
- 3.3 踢出设备
- 3.4 设备列表查询
- 四、前端界面实现
- 4.1 设备列表展示
- 4.2 踢出设备操作
- 五、安全增强措施
- 5.1 异常登录检测
- 5.2 Token刷新机制
- 5.3 登录日志记录
- 六、性能优化
- 6.1 Redis缓存策略
- 6.2 数据库索引优化
- 6.3 定时清理过期会话
- 七、实战效果演示
- 7.1 用户登录流程
- 7.2 界面效果
- 八、最佳实践
- 8.1 配置建议
- 8.2 安全建议
- 8.3 性能建议
- 九、总结
- 9.1 核心收益
- 9.2 适用场景
- 9.3 扩展方向
- 十、源码获取
- 互动话题
评论
0 评论