深入解析:Spring Boot 实现单账号登录控制

在很多业务场景中,我们需要控制单个账号的登录数量。本文介绍一种轻量级实现方案,灵活支持单账号单登录和多登录两种模式。

付费系统场景:某在线教育平台销售VIP会员账号,如果不限制登录数量,一个账号可能被多个用户共享,导致收入损失。平台需要确保一个付费账号只能有一个用户同时在线。

企业办公系统:为了保证数据安全,企业OA系统通常要求员工账号只能在指定设备上登录。当员工在新设备登录时,需要自动踢出其他设备上的会话,防止账号被滥用。

传统方案的痛点

传统的账号登录控制方案存在各种问题:

Session方案的局限:使用HttpSession实现虽然简单,但在分布式环境下会面临Session共享的难题。引入Spring Session虽然可以解决问题,但增加了系统的复杂度和依赖,对于简单的需求来说有些"杀鸡用牛刀"。

Spring Security的复杂性:Spring Security提供了强大的安全框架,但对于仅仅需要登录控制的需求来说,学习成本过高,配置复杂,而且引入了很多不必要的功能。

数据库存储的性能问题:将登录状态存储在数据库中会导致每次请求都需要查询数据库,在高并发场景下会成为性能瓶颈。虽然可以通过缓存优化,但又增加了系统复杂度。

前端状态管理的麻烦:传统的页面跳转方式已经不适应现代前端框架的需求。前后端分离架构需要统一的API接口和状态管理机制。

二、方案概述

2.1 核心思路

本文提出的方案基于以下几个核心理念:

Token认证:使用自定义Token替代传统的Session机制。Token是无状态的,可以在分布式环境下轻松扩展,同时避免了Session共享的复杂性问题。

拦截器验证:通过Spring MVC的拦截器机制统一处理登录验证。这种方式的优点是不需要修改业务代码,通过配置即可实现对所有请求的拦截。

接口抽象:定义清晰的SessionManager接口,将存储逻辑与业务逻辑分离。这样既可以使用Map实现,也可以轻松切换到Redis,为系统扩展提供可能。

模式切换:通过简单的配置项即可在"单登录"和"多登录"模式之间切换,满足不同业务场景的需求。

2.2 技术选型

核心依赖

仅使用Spring Boot Web Starter,不引入任何额外的安全框架或中间件。

存储方案

单机部署:使用ConcurrentHashMap
分布式部署:通过接口抽象无缝切换到Redis

三、核心实现

3.1 会话管理接口

SessionManager接口是整个方案的核心,它定义了会话管理的所有基本操作:

public interface SessionManager {
// 登录:生成并返回Token
String login(String username, LoginInfo loginInfo);
// 登出:使指定Token失效
void logout(String token);
// 验证:检查Token是否有效
TokenInfo validateToken(String token);
// 查询:获取用户的所有活跃Token
List<String> getUserTokens(String username);
  // 管理:强制用户下线
  void kickoutUser(String username);
  }

3.2 Map实现方案

MapSessionManager是基于ConcurrentHashMap的具体实现,它使用两个Map来管理会话状态:

tokenMap:存储Token到TokenInfo的映射,用于快速验证Token的有效性。TokenInfo包含用户名、登录时间、过期时间等关键信息。

userTokenMap:存储用户名到Token集合的映射,用于管理用户的所有会话。这样设计的好处是,当需要踢出用户时,可以快速找到该用户的所有Token并使其失效。

@Component
public class MapSessionManager implements SessionManager {
// Token -> TokenInfo 的映射,用于验证
private final Map<String, TokenInfo> tokenMap = new ConcurrentHashMap<>();
  // Username -> Token集合的映射,用于管理
  private final Map<String, Set<String>> userTokenMap = new ConcurrentHashMap<>();
    @Override
    public String login(String username, LoginInfo loginInfo) {
    String token = generateToken();
    TokenInfo tokenInfo = new TokenInfo(token, username, loginInfo,
    System.currentTimeMillis() + properties.getTokenExpireTime() * 1000);
    // 单登录模式:先踢出旧会话
    if (properties.getMode() == LoginMode.SINGLE) {
    kickoutUser(username);
    }
    // 存储新Token
    tokenMap.put(token, tokenInfo);
    userTokenMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet())
    .add(token);
    return token;
    }
    }

3.3 拦截器设计

LoginInterceptor负责拦截所有需要认证的请求,验证Token的有效性。它的设计要点包括:

路径排除:静态资源、登录接口等不需要认证的路径需要被排除。

Token提取:从HTTP请求头中提取Token,支持自定义Header名称。

验证逻辑:调用SessionManager验证Token,将验证结果存入请求属性。

@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private SessionManager sessionManager;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = getTokenFromRequest(request);
if (token == null) {
return handleUnauthorized(response, "请先登录");
}
TokenInfo tokenInfo = sessionManager.validateToken(token);
if (tokenInfo == null) {
return handleUnauthorized(response, "登录已过期");
}
// 将用户信息存入请求上下文
request.setAttribute("username", tokenInfo.getUsername());
return true;
}
}

3.4 配置管理

LoginProperties集中管理所有可配置的参数,通过Spring Boot的配置机制,可以在application.yml中灵活调整:

app:
login:
# 登录模式控制
mode: MULTIPLE  # SINGLE-单登录, MULTIPLE-多登录
# Token相关配置
token-expire-time: 1800  # 有效期30分钟
token-header: Authorization  # 请求头名称
token-prefix: TOKEN_  # Token前缀
# 维护配置
enable-auto-clean: true  # 启用自动清理
clean-interval: 5  # 清理间隔(分钟)

四、API接口设计

4.1 接口定义

系统提供了一套RESTful API接口,支持完整的用户会话管理功能:

接口路径请求方法功能说明请求参数响应数据
/api/auth/loginPOST用户登录username, passwordToken信息
/api/auth/logoutPOST用户退出操作结果
/api/auth/currentGET当前用户用户信息
/api/auth/onlineGET在线列表用户名列表
/api/auth/tokensGET用户TokensusernameToken列表
/api/auth/kickoutPOST踢出用户username操作结果

4.2 统一响应格式

所有API接口都遵循统一的响应格式,便于前端处理:

{
"code": 200,        // 状态码
"message": "成功",   // 提示信息
"data": {}          // 业务数据
}

状态码规范

  • 200:请求成功
  • 401:未认证或Token失效
  • 403:权限不足
  • 500:服务器内部错误

4.3 登录认证流程

完整的登录流程包括以下几个步骤:

1. 用户提交凭据

POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}

2. 服务端验证并生成Token
服务端验证用户名密码后,生成一个唯一的Token,包含:

  • Token字符串(UUID)
  • 用户信息
  • 登录时间
  • 过期时间

3. 返回Token给客户端

{
"code": 200,
"message": "登录成功",
"data": {
"token": "TOKEN_550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"expireTime": 1704067200000,
"loginMode": "MULTIPLE"
}
}

4. 客户端存储Token
根据用户选择,Token可以存储在localStorage(长期有效)或sessionStorage(会话级别)。

5. 后续请求携带Token

GET /api/auth/current
Authorization: TOKEN_550e8400-e29b-41d4-a716-446655440000

6. 服务端验证Token
拦截器自动验证Token的有效性,包括:

  • Token是否存在
  • Token是否过期
  • Token是否被踢出

4.4 错误处理机制

当认证失败时,系统会返回明确的错误信息:

{
"code": 401,
"message": "Token已过期,请重新登录",
"data": null
}

前端收到401响应后,会自动清除本地存储的Token,并跳转到登录页面。

4.5 管理功能接口

管理员可以通过专门的接口管理系统用户:

获取在线用户:返回当前所有在线用户的用户名列表,可用于监控和统计。

踢出指定用户:强制指定用户下线,清除其所有Token,常用于处理异常情况。

查看用户会话:获取指定用户的所有活跃Token,了解用户的登录状态。

五、前端实现

5.1 核心实现

api.js:负责所有API请求的封装,包括Token管理、请求拦截、错误处理等。

页面模块:login.html、index.html、admin.html各自负责独立的功能,通过import导入api模块。

状态管理:使用localStorage和sessionStorage管理Token状态,支持"记住我"功能。

5.2 Token管理机制

Token管理是前端的核心功能,实现了完整的生命周期管理:

// Token获取策略
export function getToken() {
// 优先从localStorage获取(长期有效)
return localStorage.getItem('token')
|| sessionStorage.getItem('token');  // 再从sessionStorage获取
}
// Token存储策略
export function setToken(token, remember) {
if (remember) {
localStorage.setItem('token', token);  // 用户选择"记住我"
} else {
sessionStorage.setItem('token', token);  // 仅本次会话有效
}
}

这种设计的优势是给了用户选择权:隐私敏感的场景可以使用sessionStorage,确保关闭浏览器后自动清除;个人设备上可以使用localStorage,提供更好的用户体验。

5.3 请求拦截器

前端实现了一个轻量级的请求拦截器,自动处理认证相关的逻辑:

async function apiRequest(url, options = {}) {
// 自动添加Token
const token = getToken();
if (token) {
options.headers = {
...options.headers,
'Authorization': token
};
}
const response = await fetch(url, options);
// 统一处理401响应
if (response.status === 401) {
clearToken();
// 避免在登录页重复跳转
if (location.pathname !== '/login.html') {
location.href = '/login.html';
}
}
return response.json();
}

这个拦截器实现了:

  • 自动携带Token,减少重复代码
  • 统一的401处理,确保用户在Token失效时能够及时重新登录

六、总结

整个方案仅依赖Spring Boot Web,不需要引入任何额外的安全框架、缓存中间件或数据库。这使得项目的依赖保持最小化。

通过SessionManager接口抽象,实现了业务逻辑与存储逻辑的完全分离。当需要从单机扩展到分布式时,只需要实现一个基于Redis的SessionManager即可,业务代码无需任何修改。

https://github.com/yuboon/java-examples/tree/master/springboot-single-login
posted @ 2026-01-10 09:05  clnchanpin  阅读(5)  评论(0)    收藏  举报