从单体到亿级流量:登录功能全场景设计指南,踩过的坑全给你填平了

0 阅读18分钟

登录功能是所有业务系统的第一道门户,它不仅承担着用户身份核验的核心职责,更是整个系统权限体系、数据安全的基石。很多开发者对登录功能的认知停留在“查库比对账号密码”的层面,却忽略了一个核心问题:登录功能的设计,必须与项目的业务复杂度、流量规模、部署架构完全匹配。小项目硬套分布式方案会徒增维护成本,大项目沿用单体设计会埋下安全与性能的双重隐患。

本文将从登录功能的底层逻辑出发,按照项目复杂度分级拆解全场景设计方案,从极简单体到高并发分布式,每一套方案都配套完整的落地实现,同时讲透每一个设计决策背后的原因,帮你彻底搞懂登录功能该怎么设计,再也不会出现过度设计或设计缺失的问题。

一、登录功能的底层核心逻辑

不管项目复杂度如何变化,登录功能的本质永远是解决三个核心问题,所有的设计都是围绕这三个问题展开的:

  1. 身份核验:确认操作者是其声称的合法用户,核心是“凭证校验”,解决“你是谁”的问题。
  2. 会话保持:在用户的一次访问周期内,无需重复核验身份,解决“你一直是你”的问题。
  3. 安全兜底:防范身份伪造、暴力破解、信息泄露等风险,保障用户身份的不可篡改性。

1.1 身份核验的核心凭证分类

身份核验的本质,是验证用户提供的“只有本人能提供的凭证”,主流分为三类:

  • 知识凭证:密码、验证码、安全问题等,只有用户知道的信息
  • 持有凭证:手机、U盾、硬件令牌等,只有用户持有的设备
  • 生物凭证:指纹、人脸、声纹等,用户独有的生物特征

1.2 会话保持的两种核心实现思路

HTTP协议本身是无状态的,会话保持的本质,是在无状态的协议上建立有状态的用户上下文,主流分为两种实现路线:

  • 服务端存储:服务端保存完整的会话状态,客户端仅存储一个无意义的会话ID,主流方案包括Tomcat原生Session、Redis分布式会话
  • 客户端存储:会话状态全部加密存储在客户端,服务端仅做合法性校验,主流方案为JWT

1.3 安全兜底的三大核心原则

所有登录相关的安全设计,都不能违背这三个原则:

  • 最小权限原则:登录后的会话仅授予用户必要的操作权限,避免过度授权
  • 纵深防御原则:从参数校验、身份核验、会话管理到异常审计,建立多层防护,避免单点失效导致整体安全崩溃
  • 全程可审计原则:所有登录相关的操作都必须留下可追溯的日志,方便异常排查与安全审计

二、按项目复杂度分级的登录设计方案

等级一:极简单体场景(适配个人项目、小型内部工具,日活<1w,单节点部署)

这个场景的核心设计原则:够用就好,最小化复杂度,同时守住安全底线。无需引入Redis、分布式组件,用最基础的技术栈实现,降低维护成本。

核心设计要点

  1. 基于Tomcat原生Session的会话保持,单节点部署无需考虑会话共享
  2. 密码采用BCrypt不可逆加密存储,杜绝明文、MD5、SHA1等弱加密方案
  3. 基础参数校验与连续登录失败次数限制,防范暴力破解
  4. 逻辑删除用户数据,避免物理删除导致的业务回溯问题
  5. 完整的异常处理与日志记录,守住基础安全底线

配套SQL表结构(MySQL 8.0)

CREATE TABLE `user_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户主键ID',
  `username` varchar(64NOT NULL COMMENT '登录用户名',
  `password` varchar(128NOT NULL COMMENT '加密后的密码',
  `phone` varchar(11DEFAULT NULL COMMENT '手机号',
  `email` varchar(64DEFAULT NULL COMMENT '邮箱',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '用户状态 1-正常 0-禁用',
  `login_fail_count` int NOT NULL DEFAULT '0' COMMENT '连续登录失败次数',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标志 0-未删除 1-已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`),
  KEY `idx_phone` (`phone`),
  KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';

核心Maven依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.4</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
        <version>3.2.4</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.6</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.0.36</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.49</version>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>33.1.0-jre</version>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.5.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-crypto</artifactId>
        <version>6.2.3</version>
    </dependency>
</dependencies>

核心代码实现

1. 用户信息实体类
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户信息实体类
 * @author ken
 */
@Data
@TableName("user_info")
@Schema(description = "用户信息实体")
public class UserInfo {

    @Schema(description = "用户主键ID")
    @TableId(type = IdType.AUTO)
    private Long id;

    @Schema(description = "登录用户名")
    private String username;

    @Schema(description = "加密后的密码")
    private String password;

    @Schema(description = "手机号")
    private String phone;

    @Schema(description = "邮箱")
    private String email;

    @Schema(description = "用户状态 1-正常 0-禁用")
    private Integer status;

    @Schema(description = "连续登录失败次数")
    private Integer loginFailCount;

    @Schema(description = "最后登录时间")
    private LocalDateTime lastLoginTime;

    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除标志")
    @TableLogic
    private Integer delFlag;
}
2. 持久层Mapper接口
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户信息Mapper接口
 * @author ken
 */
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
3. 登录请求参数DTO
package com.jam.demo.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * 登录请求参数DTO
 * @author ken
 */
@Data
@Schema(description = "登录请求参数")
public class LoginRequestDTO {

    @NotBlank(message = "用户名不能为空")
    @Schema(description = "登录用户名", requiredMode = Schema.RequiredMode.REQUIRED)
    private String username;

    @NotBlank(message = "密码不能为空")
    @Schema(description = "登录密码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String password;
}
4. 统一响应结果类
package com.jam.demo.common;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 统一响应结果类
 * @author ken
 */
@Data
@Schema(description = "统一响应结果")
public class Result<T> {

    @Schema(description = "响应码 200-成功 其他-失败")
    private Integer code;

    @Schema(description = "响应消息")
    private String msg;

    @Schema(description = "响应数据")
    private T data;

    private Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(200"操作成功", data);
    }

    public static <T> Result<T> success() {
        return new Result<>(200"操作成功"null);
    }

    public static <T> Result<T> fail(String msg) {
        return new Result<>(500, msg, null);
    }

    public static <T> Result<T> fail(Integer code, String msg) {
        return new Result<>(code, msg, null);
    }
}
5. 登录服务接口
package com.jam.demo.service;

import com.jam.demo.common.Result;
import com.jam.demo.dto.LoginRequestDTO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 登录服务接口
 * @author ken
 */
public interface LoginService {

    /**
     * 用户名密码登录
     * @param requestDTO 登录请求参数
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @return 登录结果
     */
    Result<Void> login(LoginRequestDTO requestDTO, HttpServletRequest request, HttpServletResponse response);

    /**
     * 退出登录
     * @param request HTTP请求对象
     * @return 退出结果
     */
    Result<Void> logout(HttpServletRequest request);
}
6. 登录服务实现类
package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.common.Result;
import com.jam.demo.dto.LoginRequestDTO;
import com.jam.demo.entity.UserInfo;
import com.jam.demo.mapper.UserInfoMapper;
import com.jam.demo.service.LoginService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;

/**
 * 登录服务实现类
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

    private final UserInfoMapper userInfoMapper;
    private final PlatformTransactionManager transactionManager;
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    private static final int MAX_LOGIN_FAIL_COUNT = 5;
    private static final String SESSION_USER_KEY = "LOGIN_USER_INFO";

    @Override
    public Result<Void> login(LoginRequestDTO requestDTO, HttpServletRequest request, HttpServletResponse response) {
        if (!StringUtils.hasText(requestDTO.getUsername())) {
            return Result.fail("用户名不能为空");
        }
        if (!StringUtils.hasText(requestDTO.getPassword())) {
            return Result.fail("密码不能为空");
        }

        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getUsername, requestDTO.getUsername());
        UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
        if (ObjectUtils.isEmpty(userInfo)) {
            log.warn("登录失败,用户不存在:{}", requestDTO.getUsername());
            return Result.fail("用户名或密码错误");
        }

        if (userInfo.getStatus() != 1) {
            log.warn("登录失败,账号已禁用:{}", requestDTO.getUsername());
            return Result.fail("账号已被禁用,请联系管理员");
        }

        if (userInfo.getLoginFailCount() >= MAX_LOGIN_FAIL_COUNT) {
            log.warn("登录失败,账号已锁定:{},连续失败次数:{}", requestDTO.getUsername(), userInfo.getLoginFailCount());
            return Result.fail("账号已被锁定,请1小时后再试或联系管理员");
        }

        boolean passwordMatch = passwordEncoder.matches(requestDTO.getPassword(), userInfo.getPassword());
        if (!passwordMatch) {
            DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
            TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
            try {
                userInfo.setLoginFailCount(userInfo.getLoginFailCount() + 1);
                userInfoMapper.updateById(userInfo);
                transactionManager.commit(status);
            } catch (Exception e) {
                transactionManager.rollback(status);
                log.error("更新登录失败次数异常", e);
            }
            log.warn("登录失败,密码错误:{},当前失败次数:{}", requestDTO.getUsername(), userInfo.getLoginFailCount() + 1);
            return Result.fail("用户名或密码错误");
        }

        DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
        try {
            userInfo.setLoginFailCount(0);
            userInfo.setLastLoginTime(LocalDateTime.now());
            userInfoMapper.updateById(userInfo);
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            log.error("更新用户登录信息异常", e);
            return Result.fail("登录失败,请稍后重试");
        }

        HttpSession session = request.getSession(true);
        session.setAttribute(SESSION_USER_KEY, userInfo);
        session.setMaxInactiveInterval(1800);

        log.info("用户登录成功:{}", requestDTO.getUsername());
        return Result.success();
    }

    @Override
    public Result<Void> logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (!ObjectUtils.isEmpty(session)) {
            session.removeAttribute(SESSION_USER_KEY);
            session.invalidate();
        }
        log.info("用户退出登录成功");
        return Result.success();
    }
}
7. 登录接口控制器
package com.jam.demo.controller;

import com.jam.demo.common.Result;
import com.jam.demo.dto.LoginRequestDTO;
import com.jam.demo.service.LoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 * 登录控制器
 * @author ken
 */
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Tag(name = "登录认证接口", description = "用户登录、退出相关接口")
public class LoginController {

    private final LoginService loginService;

    @PostMapping("/login")
    @Operation(summary = "用户名密码登录", description = "用户通过用户名和密码进行登录")
    public Result<Void> login(@RequestBody @Valid LoginRequestDTO requestDTO,
                               HttpServletRequest request,
                               HttpServletResponse response) {
        return loginService.login(requestDTO, request, response);
    }

    @PostMapping("/logout")
    @Operation(summary = "退出登录", description = "用户退出当前登录会话")
    public Result<Void> logout(HttpServletRequest request) {
        return loginService.logout(request);
    }
}
8. 登录状态拦截器
package com.jam.demo.interceptor;

import com.alibaba.fastjson2.JSON;
import com.jam.demo.common.Result;
import com.jam.demo.entity.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 登录状态拦截器
 * @author ken
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    private static final String SESSION_USER_KEY = "LOGIN_USER_INFO";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (ObjectUtils.isEmpty(session)) {
            return handleUnauthorized(response);
        }
        UserInfo userInfo = (UserInfo) session.getAttribute(SESSION_USER_KEY);
        if (ObjectUtils.isEmpty(userInfo)) {
            return handleUnauthorized(response);
        }
        return true;
    }

    private boolean handleUnauthorized(HttpServletResponse response) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write(JSON.toJSONString(Result.fail(401"请先登录")));
        return false;
    }
}
9. Web MVC配置类
package com.jam.demo.config;

import com.jam.demo.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web MVC配置类
 * @author ken
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/auth/login""/auth/logout""/v3/api-docs/**""/swagger-ui/**""/swagger-ui.html");
    }
}
10. 密码加密配置类
package com.jam.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * 密码加密配置类
 * @author ken
 */
@Configuration
public class PasswordEncoderConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
11. MyBatis Plus配置类
package com.jam.demo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;

/**
 * MyBatis Plus配置类
 * @author ken
 */
@Configuration
public class MybatisPlusConfig implements MetaObjectHandler {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

单体场景登录核心流程

单体场景核心注意事项

  • BCrypt是单向不可逆加密算法,每次加密生成的密文均不相同,无法通过彩虹表破解,绝对禁止在数据库中存储明文密码
  • 登录失败时统一返回模糊提示,不要区分“用户不存在”和“密码错误”,防止恶意用户枚举合法账号
  • 必须限制连续登录失败次数,超过阈值后锁定账号,防范暴力破解攻击
  • Session超时时间需设置合理值,建议不超过30分钟,降低会话劫持风险
  • 所有用户输入的参数必须做合法性校验,防范SQL注入、XSS等常见Web攻击

等级二:标准集群场景(适配中大型企业级项目,日活1w-100w,多节点集群部署,多端接入)

这个场景的核心痛点:单节点的Session存储在本地内存中,集群部署时,用户请求被负载均衡到其他节点,会出现会话丢失、需要重复登录的问题。同时,APP、小程序等非浏览器场景无法原生支持Cookie+Session机制,需要更通用的凭证方案。

核心设计要点

  1. 基于Redis的分布式会话方案,替代Tomcat原生Session,解决集群节点间的会话共享问题
  2. 令牌机制:生成全局唯一的access_token作为会话凭证,客户端存储,服务端将用户会话信息存储在Redis集群
  3. 多端登录隔离:不同终端的令牌相互独立,支持单端登录互踢、多端同时登录等灵活控制
  4. 双令牌机制:access_token短期有效(30分钟),refresh_token长期有效(7天),实现用户无感刷新令牌,兼顾安全性与用户体验
  5. 分布式限流:基于Redis实现登录接口全局限流,防范暴力破解与恶意流量攻击
  6. 兼容多种登录方式:用户名密码、短信验证码、邮箱验证码等,适配多端接入需求

集群场景核心架构

核心方案说明

  1. 分布式会话实现:登录成功后,服务端生成UUID作为access_token,以login:token:{access_token}为key,将用户信息序列化后存储在Redis中,设置过期时间。客户端将token存储在请求头的Authorization字段中,每次请求携带。
  2. 拦截器改造:登录拦截器从请求头中提取token,去Redis中查询对应的用户信息。如果存在,刷新token的过期时间,放行请求;如果不存在,返回401未授权提示。
  3. 双令牌机制:登录成功时同时返回access_token和refresh_token。access_token过期后,客户端使用refresh_token向服务端申请新的令牌,无需用户重新输入账号密码。refresh_token仅可用于刷新令牌,不可用于访问业务接口,且一次使用后立即作废,降低泄露风险。
  4. 分布式限流:基于Redis的令牌桶算法,对登录接口按IP、账号维度进行限流,例如单IP每分钟最多请求10次登录接口,单账号每分钟最多请求5次,防范暴力破解。

等级三:分布式微服务场景(适配日活100w-1000w,多服务、多租户、多端统一登录)

这个场景的核心痛点:微服务架构下,存在数十上百个业务服务,若每个服务都独立实现登录校验,会出现代码冗余、权限不统一、维护成本极高的问题。同时,多个互信的业务系统需要实现一次登录、全系统访问,需要标准化的统一认证方案。

核心设计要点

  1. 基于OAuth2.0协议的统一认证中心,实现SSO单点登录,一次登录即可访问所有互信的业务系统
  2. 认证中心与业务服务完全解耦,所有业务服务通过统一的组件完成令牌合法性校验
  3. 全量支持OAuth2.0四种标准授权模式:授权码模式(PC网页端)、密码模式(自有APP端)、客户端模式(服务间调用)、简化模式(小程序/单页应用)
  4. 多租户隔离设计,支持不同租户的用户体系、权限体系完全隔离,适配SaaS类业务场景
  5. 基于RBAC模型的统一权限管理,实现细粒度的接口级、数据级权限控制
  6. 标准化第三方登录适配,支持微信、支付宝、GitHub等主流第三方平台登录,基于OAuth2.0协议统一封装

微服务场景核心架构

OAuth2.0授权码模式核心流程


等级四:高并发极致场景(适配日活千万+,亿级流量,全球化部署的互联网项目)

这个场景的核心痛点:亿级流量下,登录接口成为系统的核心瓶颈,常规的Redis+数据库方案无法支撑超高并发;全球化部署带来的跨地域访问延迟问题;同时,超大规模用户体系下,账号安全、风控成为核心诉求。

核心设计要点

  1. 多级缓存架构:本地缓存Caffeine + Redis集群,热点用户的会话信息存储在本地缓存,过期时间1分钟,大幅减少Redis访问压力,将令牌校验的RT控制在10ms以内
  2. 全链路分布式限流:基于Redis实现网关层、认证中心层、接口层三级限流,采用令牌桶+漏桶算法结合的方式,应对突发流量与恶意攻击
  3. 异地多活部署:认证中心、Redis集群、数据库均采用多地域部署,用户就近接入,将跨地域访问延迟降低90%以上
  4. 智能风控体系:实时采集登录行为数据,包括IP地址、设备指纹、登录时间、常用地点等,通过规则引擎+机器学习模型实时检测异常登录行为,触发二次身份校验
  5. 读写分离与冷热数据分离:用户信息读请求走从库,写请求走主库;活跃用户的会话信息存储在Redis,非活跃用户的会话信息持久化到数据库,大幅节省Redis内存开销
  6. 熔断降级机制:基于Sentinel实现登录接口的熔断降级,当认证中心出现故障时,降级为本地缓存校验核心用户的会话,保证核心业务的可用性

三、登录功能的核心安全红线(所有场景必须严格遵守)

  1. 密码存储红线:绝对禁止明文存储密码,禁止使用MD5、SHA1等无盐弱哈希算法,必须使用BCrypt、Argon2等自带盐值的慢哈希算法,抗彩虹表攻击能力更强
  2. 凭证传输红线:登录接口必须使用HTTPS协议,禁止通过HTTP传输账号密码等敏感凭证,防范中间人攻击
  3. 错误提示红线:登录失败时必须返回模糊的统一提示,禁止区分“用户不存在”和“密码错误”,防止恶意用户枚举合法账号
  4. 暴力破解防护红线:必须实现连续登录失败次数限制,超过阈值后锁定账号或IP,同时对登录接口实现全局限流
  5. 会话安全红线:会话凭证必须具备足够的随机性,不可预测,禁止使用自增ID、手机号等可枚举信息作为凭证;所有会话必须设置过期时间,禁止永久有效
  6. CSRF防护红线:所有敏感操作(退出登录、修改密码、绑定手机号等)必须实现CSRF防护,防范跨站请求伪造攻击
  7. 日志审计红线:所有登录、退出、修改密码、异常登录行为必须记录完整日志,包括用户ID、IP地址、设备信息、操作时间、操作结果,实现全程可追溯
  8. 密码修改红线:修改密码必须校验原密码,修改成功后立即作废该用户所有历史会话,防止账号被盗后攻击者持续访问

四、易混淆技术点明确区分

1. Session vs Token

  • Session:会话状态完整存储在服务端,客户端仅存储无意义的会话ID,安全性高,支持主动作废会话,适合绝大多数Web浏览器场景
  • Token:广义上的令牌,SessionID本质上也是一种Token。通常所说的Token是指无Cookie依赖的分布式令牌,会话状态存储在服务端Redis,客户端仅存储令牌ID,适合APP、小程序等非浏览器场景

2. JWT vs 分布式Session

  • JWT:会话状态完整加密存储在客户端,服务端仅做签名验签,无需查询存储。核心缺点是无法主动作废,一旦签发,有效期内始终有效,即使账号被禁用,攻击者仍可使用;同时存储的信息不能过多,否则会导致请求头过大
  • 分布式Session:会话状态存储在服务端Redis,客户端仅存储令牌ID,支持主动作废会话,账号状态变更可实时生效,安全性可控,适合绝大多数分布式业务场景

3. OAuth2.0 vs SSO

  • SSO:单点登录,是一种业务目标,指用户一次登录后,即可访问多个互信的业务系统,无需重复输入账号密码
  • OAuth2.0:是一种标准化的授权协议,是实现SSO的主流技术方案之一,除此之外,CAS、SAML等协议也可实现SSO

4. 认证 vs 授权

  • 认证:解决“你是谁”的问题,核心是身份核验,就是完整的登录过程,确认用户的合法身份
  • 授权:解决“你能做什么”的问题,是在认证通过后,给用户分配对应的操作权限,控制用户可访问的资源范围

五、总结

登录功能看起来简单,实则是一个系统安全与性能的核心入口,它的设计没有绝对的标准答案,唯一的评判标准就是与项目的复杂度完全匹配

小型单体项目无需硬套微服务的分布式方案,过度设计只会徒增维护成本,够用、安全、易维护就是最好的方案;中大型集群项目需要解决会话共享、多端接入的问题,分布式会话是最成熟稳定的选择;微服务架构需要统一认证中心,基于标准化的OAuth2.0协议实现SSO单点登录,降低系统耦合度;高并发互联网项目需要聚焦性能优化、高可用与智能风控,通过多级缓存、异地多活等方案支撑亿级流量。

无论项目规模大小,所有登录功能的设计都必须守住安全红线,不要在安全问题上做任何妥协。一个小小的登录漏洞,就可能导致整个系统的用户数据泄露,造成不可挽回的损失。