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接口是整个方案的核心,它定义了会话管理的所有基本操作:

java 复制代码
public interface SessionManager {
    // 登录:生成并返回Token
    String login(String username, LoginInfo loginInfo);

    // 登出:使指定Token失效
    void logout(String token);

    // 验证:检查Token是否有效
    TokenInfo validateToken(String token);

    // 查询:获取用户的所有活跃Token
    List getUserTokens(String username);

    // 管理:强制用户下线
    void kickoutUser(String username);
}

3.2 Map实现方案

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

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

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

java 复制代码
@Component
public class MapSessionManager implements SessionManager {
    // Token -> TokenInfo 的映射,用于验证
    private final Map tokenMap = new ConcurrentHashMap<>();

    // Username -> Token集合的映射,用于管理
    private final Map> 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,将验证结果存入请求属性。

java 复制代码
@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, &#34;请先登录&#34;);
        }

        TokenInfo tokenInfo = sessionManager.validateToken(token);
        if (tokenInfo == null) {
            return handleUnauthorized(response, &#34;登录已过期&#34;);
        }

        // 将用户信息存入请求上下文
        request.setAttribute(&#34;username&#34;, tokenInfo.getUsername());
        return true;
    }
}

3.4 配置管理

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

yaml 复制代码
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/login POST 用户登录 username, password Token信息
/api/auth/logout POST 用户退出 操作结果
/api/auth/current GET 当前用户 用户信息
/api/auth/online GET 在线列表 用户名列表
/api/auth/tokens GET 用户Tokens username Token列表
/api/auth/kickout POST 踢出用户 username 操作结果

4.2 统一响应格式

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

json 复制代码
{
    &#34;code&#34;: 200,        // 状态码
    &#34;message&#34;: &#34;成功&#34;,   // 提示信息
    &#34;data&#34;: {}          // 业务数据
}

状态码规范

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

4.3 登录认证流程

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

1. 用户提交凭据

javascript 复制代码
POST /api/auth/login
Content-Type: application/json

{
    &#34;username&#34;: &#34;admin&#34;,
    &#34;password&#34;: &#34;admin123&#34;
}

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

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

3. 返回Token给客户端

json 复制代码
{
    &#34;code&#34;: 200,
    &#34;message&#34;: &#34;登录成功&#34;,
    &#34;data&#34;: {
        &#34;token&#34;: &#34;TOKEN_550e8400-e29b-41d4-a716-446655440000&#34;,
        &#34;username&#34;: &#34;admin&#34;,
        &#34;expireTime&#34;: 1704067200000,
        &#34;loginMode&#34;: &#34;MULTIPLE&#34;
    }
}

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

5. 后续请求携带Token

javascript 复制代码
GET /api/auth/current
Authorization: TOKEN_550e8400-e29b-41d4-a716-446655440000

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

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

4.4 错误处理机制

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

json 复制代码
{
    &#34;code&#34;: 401,
    &#34;message&#34;: &#34;Token已过期,请重新登录&#34;,
    &#34;data&#34;: 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管理是前端的核心功能,实现了完整的生命周期管理:

javascript 复制代码
// 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);  // 用户选择&#34;记住我&#34;
    } else {
        sessionStorage.setItem('token', token);  // 仅本次会话有效
    }
}

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

5.3 请求拦截器

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

javascript 复制代码
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即可,业务代码无需任何修改。

ruby 复制代码
https://github.com/yuboon/java-examples/tree/master/springboot-single-login
相关推荐
Justin3go5 小时前
HUNT0 上线了——尽早发布,尽早发现
前端·后端·程序员
Tony Bai5 小时前
高并发后端:坚守 Go,还是拥抱 Rust?
开发语言·后端·golang·rust
一线大码6 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
weixin_425023006 小时前
Spring Boot 配置文件优先级详解
spring boot·后端·python
weixin_425023006 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端
一线大码6 小时前
Java 8-25 各个版本新特性总结
java·后端
VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
To Be Clean Coder7 小时前
【Spring源码】通过 Bean 工厂获取 Bean 的过程
java·后端·spring
weixin199701080168 小时前
闲鱼 item_get - 商品详情接口对接全攻略:从入门到精通
java·后端·spring
自己的九又四分之三站台9 小时前
导入数据到OG GraphQL以及创建graph
java·后端·graphql