登录的过程中,后端发生了什么?

1. 概览:一次登录请求的后端旅程

用户在前端输入账号密码点下「登录」,请求进入后端,大致经过以下阶段:

flowchart TD
    A[前端提交账号密码] --> B[网关层 参数校验]
    B --> C[业务层 身份验证]
    C --> D{验证通过?}
    D -->|否| E[返回错误]
    D -->|是| F[生成 Session/Token]
    F --> G[返回凭证给客户端]
    G --> H[后续请求 携带凭证做鉴权]

下面逐阶段拆解。

2. 参数校验(网关层 / 入口层)

请求进入后端,最先由网关层或入口中间件校验其是否符合格式要求,不符合直接返回,不让无效请求消耗业务层资源。这是「快速失败(fail fast)」原则的体现。

典型校验项:

  • 必填校验:用户名、密码字段是否为空
  • 格式校验:用户名长度、是否含非法字符;密码强度(长度、是否含数字字母)
  • 类型校验:手机号是否符合 11 位数字格式、邮箱是否符合邮箱正则

注:参数校验只负责「长得对不对」,不负责「这个用户存不存在」。后者属于业务层范畴。

3. 密码验证(业务层)

业务层处理由网关层传入的请求,根据用户名从数据库取出该用户的哈希值(hash),再用用户输入的明文密码实时计算哈希值,比对两者是否一致。

3.1 为什么用用户输入算哈希,而不是反过来?

在现代安全体系中,数据库里保存的密码绝对不是「加密」的(可逆),而是通过单向哈希函数(One-Way Hash Function)处理过的。因为是单向的,从技术和数学原理上讲,根本无法从数据库里的值反向计算出原始密码

所以验证只能正向进行:把用户输入也哈希一遍,看两个哈希值是否相等。

3.2 只哈希就够了吗?——盐值(Salt)

只做一次单纯的哈希(如 MD5(password)SHA256(password))是不安全的:

  • 彩虹表攻击:攻击者预先计算大量常见密码的哈希值建表,拿到库里的哈希后反查即可得到明文
  • 相同密码哈希相同:两个用户用同一密码,库里的哈希一模一样,一处泄露即可关联识别

解决办法是加盐(Salt):为每个用户生成一段随机串,与密码拼接后再哈希:

stored = hash(password + salt)

盐值不需要保密,和哈希一起存在数据库里即可。它的作用是:

  • 让相同密码产生不同的哈希,破坏彩虹表
  • 迫使攻击者只能逐个用户暴力破解,无法批量复用

3.3 还不够:用慢哈希函数

MD5 / SHA-256 设计目标是「快」,这对密码存储反而是缺点——攻击者每秒能算上亿次,暴力破解成本低。

密码存储应使用故意变慢的哈希函数:

函数 特点
bcrypt 经典选择,内置盐,可调 cost 因子
scrypt 内存硬,抗 GPU/ASIC 并行破解
argon2 现代推荐(密码哈希竞赛冠军),抗多硬件

它们通过可调的计算/内存成本,让单次哈希耗时在几十到几百毫秒,对正常登录无感,却让暴力破解成本高到不可行。

一句话总结密码存储:单向哈希 + 随机盐 + 慢哈希函数,缺一不可。

4. 手机号 + 验证码如何验证?

验证码登录不依赖密码哈希,流程如下:

sequenceDiagram
    participant U as 用户
    participant S as 后端
    participant R as Redis
    participant G as 短信网关
    U->>S: 请求发送验证码(手机号)
    S->>S: 校验手机号格式 + 频率限制
    S->>S: 生成随机验证码(4-6位)
    S->>R: 存 code=123456 TTL=5min
    S->>G: 发送短信
    G->>U: 收到验证码
    U->>S: 提交手机号+验证码
    S->>R: 取出 code 比对
    R-->>S: 一致
    S->>R: 删除 code(一次性)
    S-->>U: 登录成功,颁发凭证

关键点:

  • 验证码存缓存(如 Redis):带 TTL(如 5 分钟)自动过期,不落库
  • 一次性:验证通过后立即删除,防止重放
  • 频率限制:同一手机号发送间隔限制、每日次数限制,防短信轰炸与刷量
  • 错误次数限制:连续输错 N 次锁定,防暴力枚举验证码

5. 生成 Session / Token

身份验证通过后,系统需颁发一个「凭证」给客户端,后续请求携带它即可证明身份,不必每次重输密码。主流有两种方案:

5.1 Session(有状态)

  • 服务器在**服务端内存或共享存储(如 Redis)**里保存一份会话状态,生成一个 sessionId
  • sessionId 通过 Set-Cookie 返回客户端
  • 后续请求浏览器自动带上 Cookie,服务器用 sessionId 查到对应用户

特点:服务端有状态,可随时主动失效(删 session 即可);但多实例部署需共享 session 存储,否则要做会话粘滞。

5.2 Token / JWT(无状态)

  • 服务器签发一个 JWT(JSON Web Token),载荷(payload)里含用户 ID、角色等,并用密钥签名
  • 客户端把 Token 存在 localStorage / cookie,请求时放在 Authorization: Bearer <token> 头里
  • 服务器收到后只验签(无需查库),验签通过即信任载荷内容

特点:服务端无状态、易水平扩展;代价是难以主动吊销(Token 未过期前一直有效),常配合短有效期 + 刷新令牌(Refresh Token)或黑名单机制。

维度 Session JWT
状态 服务端有状态 服务端无状态
扩展性 需共享存储 天然易扩展
主动失效 简单(删 session) 较难(需黑名单/短有效期)
凭证大小 sessionId 很小 较大(含载荷+签名)

6. 认证与鉴权

常被混用,其实是两件事:

  • 认证(Authentication,AuthN):你是谁——验证身份
  • 鉴权(Authorization,AuthZ):你能做什么——验证权限

登录阶段(认证):你输入密码通过了验证,系统颁发给你一个身份凭证(比如 JWT Token)。

请求阶段(鉴权)

  • 你发起了请求:DELETE /api/v1/users/123(删除某个用户)
  • 系统拦截器拿到你的 Token,解析出你的角色是 普通用户
  • 系统比对权限规则,发现只有 管理员 才能执行删除操作
  • 系统拒绝请求,返回 403 Forbidden

在这个过程中,系统完全没有再次校验你的密码,但它完成了一次标准的鉴权

HTTP 状态码层面也对应得很清楚:401 Unauthorized 其实是「未认证」(没登录或凭证失效);403 Forbidden 才是「已认证但无权限」。

7. 串联与小结

阶段 做什么 关键点
参数校验 拦截格式错误的请求 fail fast,不入业务层
密码验证 比对哈希 单向哈希 + 盐 + 慢哈希函数
验证码登录 比对缓存中的码 TTL、一次性、频率限制
生成凭证 颁发 Session / Token 有状态 vs 无状态的权衡
认证 确认你是谁 登录时完成
鉴权 确认你能做什么 每次请求都做

一次「登录」看似简单,背后其实是校验、加密学、状态管理、权限模型四块知识的交汇。理解了这条链路,再去看注册改密、单点登录(SSO)、OAuth 第三方登录,都是在这套骨架上的延伸。