阅读时间:40-50 分钟 | 分类:15 安全架构 | 难度:中高级认证(Authentication)回答"你是谁",授权(Authorization)回答"你能做什么"。这两个概念常被混为一谈,但在架构层面它们是完全不同的两个子系统。本文从 OAuth 2.0 协议栈、OIDC 身份层、JWT 令牌设计到 RBAC/ABAC 权限模型,帮你建立完整的认证授权知识体系。
一、认证与授权的本质区别
1.1 两个不同的问题域
| 维度 | 认证(Authentication) | 授权(Authorization) |
|---|---|---|
| 核心问题 | 你是谁? | 你能做什么? |
| 输入 | 凭证(密码、Token、证书) | 身份 + 资源 + 操作 |
| 输出 | 身份声明(Identity) | 访问决策(Allow/Deny) |
| 时机 | 请求入口 | 请求处理过程中 |
| 失败表现 | 401 Unauthorized | 403 Forbidden |
| 典型协议 | OAuth 2.0、OIDC、SAML、LDAP | RBAC、ABAC、ACL、PBAC |
一个关键认知:HTTP 状态码命名有历史误导。401 实际表示"未认证"(unauthenticated),403 才表示"未授权"(unauthorized)。面试中如果能指出这个细节,是非常加分的。
1.2 为什么两者必须分离?
假设一个电商系统,用户登录后既是买家又是卖家,还可能是管理员。如果把认证和授权耦合在一起,每次切换角色都需要重新登录——这显然不合理。分离认证和授权的好处:
- 单点登录(SSO):一个身份认证,多个系统共享
- 细粒度权限:认证后可以有不同的授权策略
- 独立演进:更换认证方式(从密码到生物识别)不影响授权逻辑
- 审计清晰:谁做了什么,认证和授权日志分开记录
二、OAuth 2.0:授权协议的核心

2.1 OAuth 2.0 不是认证协议
这是面试中最常见的纠正点:OAuth 2.0 本身是一个授权框架,不是认证协议。它解决的是"如何让第三方应用在用户不提供密码的情况下,访问用户在某服务上的受保护资源"。
OAuth 2.0 的核心角色:
| 角色 | 英文 | 职责 |
|---|---|---|
| 资源所有者 | Resource Owner | 用户本人,拥有数据 |
| 资源服务器 | Resource Server | 存储受保护资源的服务器 |
| 客户端 | Client | 第三方应用 |
| 授权服务器 | Authorization Server | 颁发令牌的服务器 |
2.2 四种授权模式(Grant Type)
Authorization Code Grant(授权码模式)— 最安全
用户 → 客户端 → 授权服务器(登录+授权)→ 授权码 → 客户端 → 授权服务器(换Token)→ Access Token
这是最主流的模式,配合 PKCE(Proof Key for Code Exchange)可以防止授权码拦截攻击。流程分两步:
第一步:获取授权码
GET /authorize?
response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://client.example.com/callback
&scope=read write
&state=random_string
&code_challenge=PKCE_CHALLENGE
&code_challenge_method=S256
第二步:用授权码换 Token
POST /token
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.example.com/callback
&code_verifier=PKCE_VERIFIER
PKCE 的工作原理
PKCE 解决了公共客户端(如 SPA、移动 App)无法安全保管 client_secret 的问题:
- 客户端生成
code_verifier(随机字符串,43-128 字符) - 客户端计算
code_challenge = BASE64URL(SHA256(code_verifier)) - 授权请求时发送
code_challenge - 换 Token 时发送
code_verifier - 授权服务器验证
SHA256(code_verifier) == code_challenge
即使攻击者截获了授权码,没有 code_verifier 也无法换取 Token。
Client Credentials Grant(客户端凭证模式)
客户端 → 授权服务器(client_id + client_secret)→ Access Token
用于服务间通信,没有用户参与。典型场景:后端服务调用另一个后端服务的 API。
Implicit Grant(隐式模式)— 已弃用
直接返回 Access Token,不再推荐使用。OAuth 2.1 草案已将其移除,推荐用 Authorization Code + PKCE 替代。
Resource Owner Password Credentials(密码模式)— 已弃用
用户直接把用户名密码交给客户端,客户端去换 Token。同样在 OAuth 2.1 中被移除。
2.3 Token 类型与生命周期
| Token 类型 | 用途 | 有效期 | 存储位置 |
|---|---|---|---|
| Access Token | 访问资源 | 短(分钟级) | 内存 |
| Refresh Token | 刷新 Access Token | 长(天/周级) | 安全存储 |
| ID Token(OIDC) | 用户身份信息 | 短 | 客户端 |
Refresh Token Rotation(刷新令牌轮换)是安全最佳实践:每次使用 Refresh Token 时,授权服务器同时颁发新的 Refresh Token 并使旧的失效。如果攻击者使用了已失效的 Refresh Token,说明令牌已被泄露,服务器可以撤销所有相关令牌。
三、OIDC:在 OAuth 2.0 之上构建身份层

3.1 OIDC 解决什么问题
OAuth 2.0 只做授权,不告诉客户端"用户是谁"。OpenID Connect(OIDC)在 OAuth 2.0 之上增加了一个身份层,通过 ID Token(JWT 格式)传递用户身份信息。
OIDC = OAuth 2.0 + 身份认证 + JWT
3.2 ID Token 与 Access Token 的区别
| 维度 | ID Token | Access Token |
|---|---|---|
| 目的 | 证明用户身份 | 访问受保护资源 |
| 受众(aud) | 客户端 | 资源服务器 |
| 格式 | 必须是 JWT | 可以是任意格式 |
| 是否可解析 | 客户端可以解析 | 客户端不应解析 |
| 包含信息 | 用户身份声明 | 授权范围 |
关键原则:客户端不应该解析 Access Token 的内部结构。Access Token 对客户端来说是不透明的,客户端只负责传递它。如果需要用户信息,使用 ID Token 或调用 UserInfo 端点。
3.3 OIDC 三大认证流程
Authorization Code Flow(授权码流程):最常用,适用于有后端的 Web 应用。
Implicit Flow(隐式流程):已弃用,不推荐。
Hybrid Flow(混合流程):授权码 + ID Token 同时返回,适用于需要立即获取用户信息的场景。
3.4 OIDC 核心端点
# 发现端点(获取所有端点地址)
GET /.well-known/openid-configuration
# 授权端点
GET /authorize?response_type=code&scope=openid+profile+email&...
# Token 端点
POST /token
# UserInfo 端点(用 Access Token 获取用户信息)
GET /userinfo
Authorization: Bearer {access_token}
# 登出端点
GET /end_session?id_token_hint=
3.5 OIDC Scope 与 Claims
openid(必须):标识这是一个 OIDC 请求profile:姓名、头像、地区等email:邮箱、邮箱是否已验证address:地址信息phone:电话号码
四、JWT:令牌设计的工程实践
4.1 JWT 结构
JWT 由三部分组成,用 . 分隔:
Header.Payload.Signature
Header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-key-01"
}
Payload(Claims):
{
"iss": "https://auth.example.com",
"sub": "user_12345",
"aud": "api.example.com",
"exp": 1725600000,
"iat": 1725596400,
"nbf": 1725596400,
"jti": "unique-token-id-abc123",
"scope": "read write",
"roles": ["admin", "editor"]
}
签名:
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
private_key
)
4.2 关键 Claims 详解

| Claim | 全称 | 含义 | 必须? |
|---|---|---|---|
| iss | Issuer | 签发者 | 推荐 |
| sub | Subject | 主体(用户ID) | 推荐 |
| aud | Audience | 接收方 | 推荐 |
| exp | Expiration | 过期时间 | 推荐 |
| nbf | Not Before | 生效时间 | 可选 |
| iat | Issued At | 签发时间 | 推荐 |
| jti | JWT ID | 唯一标识 | 推荐 |
面试要点:为什么需要 aud?防止 Token 被用于错误的接收方。例如,颁发给 payment-service 的 Token 不应该被 user-service 接受。如果服务端不验证 aud,就存在 Token 滥用的风险。
4.3 签名算法选择
| 算法 | 类型 | 密钥 | 适用场景 |
|---|---|---|---|
| HS256 | 对称 | 共享密钥 | 单体应用、内部服务 |
| RS256 | 非对称 | 公私钥对 | 微服务、多消费者 |
| ES256 | 非对称 | ECDSA 密钥对 | 移动端、IoT(密钥更短) |
| EdDSA | 非对称 | Ed25519 | 新一代推荐 |
推荐 RS256/ES256:在微服务架构中,只有授权服务器持有私钥(签名),其他服务持有公钥(验证)。这样即使某个服务被攻破,攻击者也无法伪造 Token。
4.4 JWT 最佳实践与常见陷阱
1. 不要在 JWT 中放敏感信息
JWT 的 Header 和 Payload 只是 Base64URL 编码,不是加密。任何人都可以解码查看。敏感数据应该只存在于服务端。
2. 设置合理的过期时间
Access Token 建议 5-15 分钟,配合 Refresh Token 使用。不要设置几小时甚至几天的 Access Token 过期时间。
3. 确保算法不被降级
// 错误:允许算法降级
Jwts.parser().setSigningKey(key).parseClaimsJws(token);
// 正确:强制指定算法
Jwts.parserBuilder()
.setSigningKey(key)
.require("alg", "RS256") // 拒绝 HS256 等
.build()
.parseClaimsJws(token);
攻击者可以将 RS256 签名的 JWT 的 Header 改为 "alg": "none",如果服务端不校验算法,就会接受未签名的 Token。
4. 实现 Token 撤销机制
JWT 是无状态的,一旦签发就无法单方面撤销。解决方案:
- Token 黑名单:维护一个已撤销 Token 的集合(Redis 存储,TTL = Token 过期时间)
- 短有效期 + Refresh Token:Access Token 很短,通过不续发 Refresh Token 来撤销
- 版本号:在用户记录中维护一个
token_version,JWT 中包含此版本号,撤销时递增版本号
5. 不要将 JWT 存储在 localStorage
localStorage 容易被 XSS 攻击读取。推荐方案:
- Web 应用:HttpOnly + Secure + SameSite Cookie
- 移动应用:Keychain(iOS)/ Keystore(Android)


五、RBAC:基于角色的访问控制
5.1 RBAC 核心模型
RBAC 是最广泛使用的权限模型,核心思想是:权限不直接分配给用户,而是分配给角色,用户通过角色获得权限。
用户 → 角色 → 权限
5.2 RBAC 的三个层级
RBAC0(基础模型):
User --N:M--> Role --N:M--> Permission
# 示例
用户 Alice → 角色:编辑者 → 权限:article:read, article:write
用户 Bob → 角色:读者 → 权限:article:read
RBAC1(角色继承):
上级角色自动继承下级角色的所有权限
管理员
/ \
高级编辑 财务主管
/ \
编辑 会计
角色继承可以形成树结构或 DAG(有向无环图)。实现时注意避免循环继承。
RBAC2(约束模型):
- 互斥角色(Separation of Duty):一个用户不能同时拥有"采购员"和"审批人"角色
- 基数约束:一个角色最多分配 100 个用户
- 前提角色:拥有"高级编辑"角色前必须先拥有"编辑"角色
- 时间约束:某个角色仅在特定时间段可用
5.3 RBAC 数据库设计
-- 核心六表设计
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE roles (
id BIGINT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
parent_id BIGINT, -- RBAC1 角色继承
FOREIGN KEY (parent_id) REFERENCES roles(id)
);
CREATE TABLE permissions (
id BIGINT PRIMARY KEY,
resource VARCHAR(100) NOT NULL, -- 如 'article', 'user'
action VARCHAR(50) NOT NULL, -- 如 'read', 'write', 'delete'
UNIQUE (resource, action)
);
CREATE TABLE user_roles (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id)
);
-- 约束表(RBAC2)
CREATE TABLE role_constraints (
id BIGINT PRIMARY KEY,
role_id BIGINT,
constraint_type VARCHAR(50), -- 'mutual_exclusion', 'prerequisite', 'cardinality'
constraint_value JSON
);
5.4 RBAC 权限判断的性能优化
当权限数量很大时,每次请求都查数据库不现实。典型优化方案:
- 启动时加载到本地缓存:权限数据变更频率低,在应用启动时全量加载到内存
- 权限变更事件驱动刷新:权限变更时发送 MQ 消息,各服务消费后刷新本地缓存
- 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis),权限变更时广播失效
// 权限判断示例
public boolean hasPermission(Long userId, String resource, String action) {
// 1. 从本地缓存获取用户角色
Set<String> roles = roleCache.get(userId);
// 2. 获取角色权限(含继承)
Set<String> permissions = roles.stream()
.flatMap(role -> permissionCache.getWithInheritance(role).stream())
.collect(Collectors.toSet());
// 3. 权限匹配
String required = resource + ":" + action;
return permissions.contains(required)
|| permissions.contains(resource + ":*")
|| permissions.contains("*:*");
}
5.5 RBAC 的局限性
- 角色爆炸:当权限组合过多时,需要定义大量角色
- 粒度不足:RBAC 只能控制"能不能访问",无法控制"能访问哪些数据"
- 跨租户困难:多租户场景下,同样角色在不同租户中权限不同
这些局限性引出了 ABAC。
六、ABAC:基于属性的访问控制

6.1 ABAC 核心思想
ABAC 通过评估主体属性、资源属性、环境属性和操作来做访问决策,不依赖预定义角色。
访问决策 = f(主体属性, 资源属性, 环境属性, 操作)
6.2 ABAC 策略语言(以 XACML 思想为例)
策略:允许部门经理在工作时间查看本部门员工的薪资
- 主体属性:role=manager, department={dept_id}
- 资源属性:type=salary, department={dept_id}
- 环境属性:time in 09:00-18:00, day in Mon-Fri
- 操作:read
- 规则:主体.department == 资源.department
- 结果:Allow
6.3 ABAC 的工程实现
// ABAC 策略评估引擎
public class AbacEngine {
public Decision evaluate(AccessRequest request, List<Policy> policies) {
Map<String, Object> subjectAttrs = subjectAttrProvider.getAttrs(request.getUserId());
Map<String, Object> resourceAttrs = resourceAttrProvider.getAttrs(request.getResourceId());
Map<String, Object> envAttrs = envAttrProvider.getAttrs();
Attributes attrs = new Attributes(subjectAttrs, resourceAttrs, envAttrs);
for (Policy policy : policies) {
if (policy.matches(request.getAction(), attrs)) {
return policy.evaluate(attrs);
}
}
return Decision.DENY; // 默认拒绝
}
}
// 策略示例
public class SameDepartmentPolicy implements Policy {
@Override
public boolean matches(String action, Attributes attrs) {
return "read".equals(action)
&& "salary".equals(attrs.getResourceAttr("type"));
}
@Override
public Decision evaluate(Attributes attrs) {
String subjectDept = attrs.getSubjectAttr("department");
String resourceDept = attrs.getResourceAttr("department");
return subjectDept.equals(resourceDept) ? Decision.ALLOW : Decision.DENY;
}
}
6.4 RBAC vs ABAC 对比
| 维度 | RBAC | ABAC |
|---|---|---|
| 权限粒度 | 粗(角色级) | 细(属性级) |
| 灵活性 | 低(需预定义角色) | 高(动态属性组合) |
| 实现复杂度 | 低 | 高 |
| 性能 | 高(简单的集合查找) | 中(属性获取+策略评估) |
| 适用场景 | 权限结构稳定 | 权限需要细粒度动态控制 |
| 审计友好度 | 中 | 高(可记录完整上下文) |
实际建议:大多数系统采用 RBAC 为主,ABAC 为补充 的混合方案。核心 CRUD 权限用 RBAC,数据级别的行级权限用 ABAC。
七、架构设计:认证授权体系的完整方案

7.1 统一认证授权架构
┌───────────────────────────────────────────────┐
│ API Gateway │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 认证过滤器 │ │ 授权过滤器 │ │
│ │ (JWT 校验) │ │ (RBAC/ABAC 决策) │ │
│ └─────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────────┘
│ │
┌────▼────┐ ┌───▼────┐
│ Auth │ │ Policy │
│ Service │ │ Service│
│ (OIDC) │ │ (ABAC) │
└─────────┘ └────────┘
7.2 网关层认证拦截
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
String token = extractToken(request);
// 1. 验证 JWT 签名和有效期
Claims claims = jwtService.validateAndParse(token);
// 2. 检查 Token 是否被撤销
if (tokenBlacklist.isRevoked(claims.getId())) {
throw new TokenRevokedException();
}
// 3. 构建安全上下文
SecurityContext context = new SecurityContext();
context.setUserId(claims.getSubject());
context.setRoles(claims.get("roles", List.class));
context.setPermissions(claims.get("permissions", List.class));
SecurityContextHolder.set(context);
chain.doFilter(request, response);
}
}
7.3 服务间认证
微服务之间调用也需要认证。常用方案:
方案一:Token 透传 将用户 Token 在服务调用链中透传,下游服务用同一 Token 验证。
方案二:服务间专用 Token 使用 Client Credentials 模式获取服务间 Token,Token 中携带调用方服务身份。
方案三:mTLS(双向 TLS) 通过证书进行服务间认证,零信任架构推荐。
生产环境推荐:方案二 + 方案三 组合使用。mTLS 保证传输层安全,服务间 Token 携带调用上下文。
7.4 权限模型的演进路径
阶段一:ACL(访问控制列表)
→ 每个资源直接挂用户列表,适用于用户少、资源少的场景
阶段二:RBAC(基于角色)
→ 用户量增长,引入角色中间层,降低管理复杂度
阶段三:RBAC \+ ABAC 混合
→ 业务复杂度上升,需要细粒度属性级控制
阶段四:PBAC(基于策略)
→ 权限策略统一管理、可视化编排,支持合规审计
八、安全注意事项

8.1 OAuth 2.0 常见攻击与防御
| 攻击类型 | 描述 | 防御措施 |
|---|---|---|
| CSRF 攻击 | 攻击者诱导用户发起授权请求 | state 参数 |
| 授权码拦截 | 恶意应用拦截回调中的授权码 | PKCE |
| 重定向 URI 篡改 | 修改 redirect_uri 指向攻击者服务器 | 服务端白名单校验 |
| Token 泄露 | Access Token 被日志或前端泄露 | 短有效期、HTTPS 传输 |
| 混合攻击 | 将不同授权流程混合使用 | 严格按规范实现 |
8.2 JWT 安全检查清单
- [ ] 强制指定签名算法,不允许 "alg": "none"
- [ ] 验证 aud、iss、exp 等关键 Claims
- [ ] 使用非对称签名(RS256/ES256),私钥不离开授权服务器
- [ ] Access Token 有效期 ≤ 15 分钟
- [ ] 实现 Token 撤销机制(黑名单或版本号)
- [ ] 不在 JWT 中存储敏感信息
- [ ] 使用 HTTPS 传输所有 Token
- [ ] 实现 Refresh Token Rotation
本文要点回顾:认证解决"你是谁",授权解决"你能做什么"。OAuth 2.0 是授权框架,OIDC 在其上构建身份层。JWT 是令牌的工程实现,需要关注安全性(算法降级、过期时间、撤销机制)。RBAC 通过角色简化权限管理,ABAC 通过属性实现细粒度控制。实际架构中,两者结合使用是最佳实践。
下一篇预告:226|常见安全漏洞与防御:XSS、CSRF、SQL 注入、SSRF 与 API 安全
九、面试官视角提问
以下问题从面试官视角设计,考察候选人对认证授权体系的深度理解,而非简单复述概念。建议在阅读完正文后尝试独立回答,再对照参考答案。
Q1:如果让你设计一个支持"用户临时授权第三方应用访问自己某条数据"的机制,你会怎么设计?OAuth 2.0 的 scope 能解决吗?
面试官视角:这道题考察候选人能否将 OAuth 2.0 的授权模型映射到具体业务需求。scope 是粗粒度的(如 read、write),无法精确到"某条数据"。真正考察的是对授权粒度的理解。
参考答案:OAuth 2.0 的 scope 只能控制 API 级别的访问范围,无法控制数据行级别的权限。需要在资源服务器侧实现行级授权:
- OAuth 2.0 负责认证和粗粒度授权(scope =
read:documents) - 资源服务器在返回数据前,根据 Access Token 关联的 user_id 和请求的资源 ID,查询授权记录表(
user_id, resource_id, grantee_id, permission, expire_at) - 如果用户撤销授权,删除对应授权记录即可
这就是 OAuth 2.0(协议级授权) + ABAC(数据级授权)的混合模式。OAuth 管"能不能进这个 API",ABAC 管"能看哪些数据"。
Q2:你的系统同时有 Web 端和移动端,移动端没有 client_secret 的安全存储能力。你会为两端分别选择什么 OAuth 2.0 授权模式?为什么?
面试官视角:这道题考察候选人对不同客户端类型安全模型的认知。很多人会背"都用 Authorization Code + PKCE",但说不出为什么这是正确选择。
参考答案:
- Web 端(有后端):Authorization Code Grant。授权码在后端服务器换取 Token,client_secret 安全存储在服务器环境变量中,前端不接触任何密钥。
- 移动端:Authorization Code Grant + PKCE。移动端无法安全存储 client_secret(APK 可被反编译),PKCE 用动态生成的 code_verifier 替代了静态的 client_secret,即使授权码被截获也无法兑换 Token。
注意:OAuth 2.1 草案已经将 PKCE 设为所有 Authorization Code 流程的强制要求,无论客户端类型。所以生产环境统一用 Authorization Code + PKCE 是最稳妥的选择。
Q3:你设计的微服务系统有 50 个服务,每个服务都验证 JWT。某天你发现私钥可能泄露了,你需要在 5 分钟内让所有 JWT 失效。你的方案是什么?
面试官视角:这道题模拟真实应急场景,考察候选人对 JWT 无状态特性局限性的理解,以及应急响应能力。背过"JWT 无法撤销"的人很多,但能给出可落地方案的人少。
参考答案:
应急方案(5 分钟内):
- 立即轮换签名密钥对,新请求用新私钥签发 JWT
- 所有服务从配置中心(如 Nacos/Apollo)拉取最新的公钥列表,同时验证新旧公钥
- 将旧公钥标记为"已泄露",配置中心推送后,所有服务拒绝旧密钥签名的 JWT
- 所有用户强制下线,重新登录获取新密钥签发的 JWT
长期改进:
- 密钥轮换自动化:定期轮换密钥对,JWT Header 中通过
kid标识密钥版本 - Token 版本号:用户表中维护
token_version,JWT 中包含此字段,紧急情况下递增版本号 - 缩短 Access Token 有效期至 5 分钟,牺牲一点性能换取应急响应速度
关键认知:50 个服务的公钥更新不能靠手动。配置中心是这道题的隐藏考点——没有配置中心的话,5 分钟内根本做不到。
Q4:你的系统用了 RBAC,现在业务方提了一个需求:"同一个角色的人,不同部门只能看到自己部门的数据"。RBAC 能解决吗?如果不能,你会怎么扩展?
面试官视角:这道题考察候选人能否识别 RBAC 的边界,以及是否会自然地过渡到 ABAC 或混合模型。这是架构演进中非常真实的场景。
参考答案:纯 RBAC 无法解决,因为 RBAC 只控制"能不能访问某个功能",不控制"能访问哪些数据"。
方案一:RBAC + 数据过滤
// 在 DAO 层自动注入部门过滤条件
@Query("SELECT * FROM orders WHERE department = #{currentUser.department}")
简单直接,但逻辑散落在各 DAO 中,审计困难。
方案二:RBAC + ABAC 混合
功能权限:RBAC(角色 → 能否访问 /api/orders)
数据权限:ABAC(主体.department == 资源.department)
在网关或 AOP 层统一拦截,权限策略集中管理。
方案三:为每个部门创建角色(不推荐)
角色:财务部-订单查看者、市场部-订单查看者...
这会导致角色爆炸。一个有 20 个部门、10 种权限的系统需要 200 个角色,完全不可维护。
面试官想听到的:候选人能快速识别 RBAC 的粒度边界,知道"数据级权限"应该用 ABAC 或数据过滤层解决,而不是强行用 RBAC。
Q5:JWT 的 Payload 只是 Base64 编码,不是加密。但你说"不要在 JWT 中放敏感信息",那用户角色(如 admin)算不算敏感信息?放 JWT 里有风险吗?
面试官视角:这道题考察候选人对"敏感信息"定义的理解深度,以及安全威胁建模能力。很多人会机械地回答"不放敏感信息",但说不清楚什么算敏感信息。
参考答案:角色信息放在 JWT 中可以接受,但需要注意几点:
- 角色信息本身不是秘密:在前端判断是否显示"管理后台"按钮时,角色信息必然要暴露给前端。JWT 中放角色是合理的。
- 真正的风险是信任问题:前端可以根据 JWT 中的角色做 UI 展示优化,但后端必须独立验证权限。永远不要因为前端说"我是 admin"就相信。
- 不应放的内容:手机号、身份证号、邮箱地址等 PII(个人身份信息)。这些信息在 JWT 中裸奔,任何人都能解码查看。
- 最佳实践:JWT 中只放
sub(用户 ID)和roles(角色列表),需要详细信息时通过 UserInfo 端点获取。
进阶讨论:如果角色信息确实需要保密(如内部系统不希望暴露完整的角色结构),可以使用不透明 Token(opaque token),网关持有 Token 对应的角色信息,下游服务通过网关获取。
Q6:OAuth 2.0 的 Authorization Code 流程中,为什么要分两步?先获取 code,再换 token,一步直接返回 token 不行吗?
面试官视角:这道题考察候选人对 OAuth 2.0 安全设计的深层理解。看似多此一举的设计,背后有明确的安全考量。
参考答案:分两步的核心原因是安全上下文隔离:
- 前端通道 vs 后端通道:
- 第一步(获取 code):通过浏览器重定向(前端通道),code 暴露在 URL 中,但 code 是一次性的、有效期极短(通常 30 秒-1 分钟),且需要 client_secret 或 PKCE 才能兑换
- 第二步(换 token):通过后端 HTTPS 直接请求(后端通道),token 不经过浏览器,不暴露在 URL 或 Referer 头中
- 如果一步返回 token:
- Token 会出现在浏览器地址栏(重定向 URL 的 fragment 或 query 参数中)
- 浏览器历史记录、服务器日志、Referer 头都会泄露 Token
- 这就是 Implicit Grant 被弃用的原因之一
- PKCE 的加持:即使攻击者截获了 URL 中的 code,没有 code_verifier 也无法兑换 token。两步设计让 PKCE 这一安全机制成为可能。
类比:code 就像取号小票,丢了可以重新取;token 就像你的银行卡,不能随便丢在公开场合。
Q7:你设计了一个开放平台,第三方开发者接入后可以调用你的 API。你会如何设计 Rate Limiting 和 API 配额?JWT 能帮上忙吗?
面试官视角:这道题考察候选人将认证授权与 API 治理结合的能力。开放平台场景下,认证授权不是孤立存在的。
参考答案:JWT 可以辅助 Rate Limiting,但不是主力:
JWT 中的信息可以用于:
- 在
claims中携带client_id和tier(如free、pro、enterprise),网关解析后直接用于限流维度判断 - 避免每次请求都查数据库获取 client 信息
但限流的核心不在 JWT:
限流维度设计:
1. 按 client_id:每个应用的 QPS 上限
2. 按 client_id + API:每个应用每个接口的 QPS 上限
3. 按 client_id + user_id:每个应用下每个用户的 QPS 上限
4. 按 IP:防止单个 IP 恶意刷量
技术选型:
- 滑动窗口算法(Redis + Lua):精确控制 QPS
- 令牌桶(Token Bucket):允许突发流量
- 漏桶(Leaky Bucket):平滑流量
关键设计:
- 限流信息不要放在 JWT 中(JWT 过期前不会更新,无法反映实时的配额变化)
- 使用 Redis 做实时计数,JWT 只携带静态的 client 元信息
- 配额超限返回
429 Too Many Requests,并在响应头中返回X-RateLimit-Remaining
Q8:什么是"零信任架构"?在认证授权体系中如何落地?
面试官视角:零信任是近年来安全领域最热的概念之一,考察候选人是否能将概念落地到具体架构决策中。
参考答案:零信任的核心原则是"从不信任,始终验证"(Never Trust, Always Verify)。
在认证授权体系中的落地:
- 不做网络边界信任:即使请求来自内网 IP,也必须进行认证。去掉"内网 = 安全"的假设。
- 每次请求都验证:不使用 session 或"登录过一次就信任"的模式。每个 API 请求都携带并验证 Token。
- 最小权限原则:
- 服务间调用:每个服务只授予调用它所需的最小 scope
- 用户权限:默认无权限,按需授权
- mTLS 全覆盖:所有服务间通信使用 mTLS,不信任网络层的安全性
- 持续验证:
- 设备态势:检测用户设备是否合规(是否越狱、是否安装安全软件)
- 行为分析:异常登录地点、异常操作时间触发额外的 MFA 验证
- 动态权限:根据风险评分动态调整权限级别
零信任不是单一技术,而是贯穿整个认证授权体系的架构思想。JWT 的短有效期、mTLS 的服务间认证、ABAC 的动态属性评估,都是零信任思想的具体工程实践。
面试准备建议:以上问题覆盖了设计决策、安全应急、权限模型演进、协议选型等维度。面试中如果遇到认证授权相关的问题,核心思路是:先明确问题域(认证还是授权?协议层还是数据层?),再选择合适的工具组合(OAuth 2.0/JWT/RBAC/ABAC),最后强调安全考量(不信任任何输入,默认拒绝)。