225|认证与授权:OAuth 2.0/OIDC、RBAC/ABAC 权限模型与JWT 设计

安全架构约 18 分钟阅读10,314 字符
知识库文档 226 / 251

阅读时间: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 为什么两者必须分离?

假设一个电商系统,用户登录后既是买家又是卖家,还可能是管理员。如果把认证和授权耦合在一起,每次切换角色都需要重新登录——这显然不合理。分离认证和授权的好处:

  1. 单点登录(SSO):一个身份认证,多个系统共享
  2. 细粒度权限:认证后可以有不同的授权策略
  3. 独立演进:更换认证方式(从密码到生物识别)不影响授权逻辑
  4. 审计清晰:谁做了什么,认证和授权日志分开记录

二、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(授权码模式)— 最安全

code
用户 → 客户端 → 授权服务器(登录+授权)→ 授权码 → 客户端 → 授权服务器(换Token)→ Access Token

这是最主流的模式,配合 PKCE(Proof Key for Code Exchange)可以防止授权码拦截攻击。流程分两步:

第一步:获取授权码

code
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

code
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 的问题:

  1. 客户端生成 code_verifier(随机字符串,43-128 字符)
  2. 客户端计算 code_challenge = BASE64URL(SHA256(code_verifier))
  3. 授权请求时发送 code_challenge
  4. 换 Token 时发送 code_verifier
  5. 授权服务器验证 SHA256(code_verifier) == code_challenge

即使攻击者截获了授权码,没有 code_verifier 也无法换取 Token。

Client Credentials Grant(客户端凭证模式)

code
客户端 → 授权服务器(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 核心端点

code
# 发现端点(获取所有端点地址)
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 由三部分组成,用 . 分隔:

code

Header.Payload.Signature

Header:

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "2024-key-01"
}

Payload(Claims):

json
{
  "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"]
}

签名:

code
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. 确保算法不被降级

java
// 错误:允许算法降级
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 是最广泛使用的权限模型,核心思想是:权限不直接分配给用户,而是分配给角色,用户通过角色获得权限。

code
用户 → 角色 → 权限

5.2 RBAC 的三个层级

RBAC0(基础模型):

code
User --N:M--> Role --N:M--> Permission

# 示例
用户 Alice → 角色:编辑者 → 权限:article:read, article:write
用户 Bob   → 角色:读者   → 权限:article:read

RBAC1(角色继承):

code
上级角色自动继承下级角色的所有权限

       管理员
      /      \
   高级编辑  财务主管
    /           \
  编辑         会计

角色继承可以形成树结构或 DAG(有向无环图)。实现时注意避免循环继承。

RBAC2(约束模型):

  • 互斥角色(Separation of Duty):一个用户不能同时拥有"采购员"和"审批人"角色
  • 基数约束:一个角色最多分配 100 个用户
  • 前提角色:拥有"高级编辑"角色前必须先拥有"编辑"角色
  • 时间约束:某个角色仅在特定时间段可用

5.3 RBAC 数据库设计

sql
-- 核心六表设计
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 权限判断的性能优化

当权限数量很大时,每次请求都查数据库不现实。典型优化方案:

  1. 启动时加载到本地缓存:权限数据变更频率低,在应用启动时全量加载到内存
  2. 权限变更事件驱动刷新:权限变更时发送 MQ 消息,各服务消费后刷新本地缓存
  3. 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis),权限变更时广播失效
java
// 权限判断示例
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 通过评估主体属性、资源属性、环境属性和操作来做访问决策,不依赖预定义角色。

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

6.2 ABAC 策略语言(以 XACML 思想为例)

code
策略:允许部门经理在工作时间查看本部门员工的薪资
  - 主体属性: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 的工程实现

java
// 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 统一认证授权架构

code
┌───────────────────────────────────────────────┐
│                   API Gateway                 │
│  ┌─────────────┐  ┌──────────────────────┐    │
│  │ 认证过滤器   │  │ 授权过滤器            │    │
│  │ (JWT 校验)  │  │ (RBAC/ABAC 决策)      │    │
│  └─────────────┘  └──────────────────────┘    │
└───────────────────────────────────────────────┘
         │                    │
    ┌────▼────┐          ┌───▼────┐
    │ Auth    │          │ Policy │
    │ Service │          │ Service│
    │ (OIDC)  │          │ (ABAC) │
    └─────────┘          └────────┘

7.2 网关层认证拦截

java
@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 权限模型的演进路径

code

阶段一: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 级别的访问范围,无法控制数据行级别的权限。需要在资源服务器侧实现行级授权:

  1. OAuth 2.0 负责认证和粗粒度授权(scope = read:documents)
  2. 资源服务器在返回数据前,根据 Access Token 关联的 user_id 和请求的资源 ID,查询授权记录表(user_id, resource_id, grantee_id, permission, expire_at)
  3. 如果用户撤销授权,删除对应授权记录即可

这就是 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 分钟内):

  1. 立即轮换签名密钥对,新请求用新私钥签发 JWT
  2. 所有服务从配置中心(如 Nacos/Apollo)拉取最新的公钥列表,同时验证新旧公钥
  3. 将旧公钥标记为"已泄露",配置中心推送后,所有服务拒绝旧密钥签名的 JWT
  4. 所有用户强制下线,重新登录获取新密钥签发的 JWT

长期改进:

  1. 密钥轮换自动化:定期轮换密钥对,JWT Header 中通过 kid 标识密钥版本
  2. Token 版本号:用户表中维护 token_version,JWT 中包含此字段,紧急情况下递增版本号
  3. 缩短 Access Token 有效期至 5 分钟,牺牲一点性能换取应急响应速度

关键认知:50 个服务的公钥更新不能靠手动。配置中心是这道题的隐藏考点——没有配置中心的话,5 分钟内根本做不到。

Q4:你的系统用了 RBAC,现在业务方提了一个需求:"同一个角色的人,不同部门只能看到自己部门的数据"。RBAC 能解决吗?如果不能,你会怎么扩展?

面试官视角:这道题考察候选人能否识别 RBAC 的边界,以及是否会自然地过渡到 ABAC 或混合模型。这是架构演进中非常真实的场景。

参考答案:纯 RBAC 无法解决,因为 RBAC 只控制"能不能访问某个功能",不控制"能访问哪些数据"。

方案一:RBAC + 数据过滤

java
// 在 DAO 层自动注入部门过滤条件
@Query("SELECT * FROM orders WHERE department = #{currentUser.department}")

简单直接,但逻辑散落在各 DAO 中,审计困难。

方案二:RBAC + ABAC 混合

code
功能权限:RBAC(角色 → 能否访问 /api/orders)
数据权限:ABAC(主体.department == 资源.department)

在网关或 AOP 层统一拦截,权限策略集中管理。

方案三:为每个部门创建角色(不推荐)

code
角色:财务部-订单查看者、市场部-订单查看者...

这会导致角色爆炸。一个有 20 个部门、10 种权限的系统需要 200 个角色,完全不可维护。

面试官想听到的:候选人能快速识别 RBAC 的粒度边界,知道"数据级权限"应该用 ABAC 或数据过滤层解决,而不是强行用 RBAC。

Q5:JWT 的 Payload 只是 Base64 编码,不是加密。但你说"不要在 JWT 中放敏感信息",那用户角色(如 admin)算不算敏感信息?放 JWT 里有风险吗?

面试官视角:这道题考察候选人对"敏感信息"定义的理解深度,以及安全威胁建模能力。很多人会机械地回答"不放敏感信息",但说不清楚什么算敏感信息。

参考答案:角色信息放在 JWT 中可以接受,但需要注意几点:

  1. 角色信息本身不是秘密:在前端判断是否显示"管理后台"按钮时,角色信息必然要暴露给前端。JWT 中放角色是合理的。
  2. 真正的风险是信任问题:前端可以根据 JWT 中的角色做 UI 展示优化,但后端必须独立验证权限。永远不要因为前端说"我是 admin"就相信。
  3. 不应放的内容:手机号、身份证号、邮箱地址等 PII(个人身份信息)。这些信息在 JWT 中裸奔,任何人都能解码查看。
  4. 最佳实践:JWT 中只放 sub(用户 ID)和 roles(角色列表),需要详细信息时通过 UserInfo 端点获取。

进阶讨论:如果角色信息确实需要保密(如内部系统不希望暴露完整的角色结构),可以使用不透明 Token(opaque token),网关持有 Token 对应的角色信息,下游服务通过网关获取。

Q6:OAuth 2.0 的 Authorization Code 流程中,为什么要分两步?先获取 code,再换 token,一步直接返回 token 不行吗?

面试官视角:这道题考察候选人对 OAuth 2.0 安全设计的深层理解。看似多此一举的设计,背后有明确的安全考量。

参考答案:分两步的核心原因是安全上下文隔离:

  1. 前端通道 vs 后端通道:
    • 第一步(获取 code):通过浏览器重定向(前端通道),code 暴露在 URL 中,但 code 是一次性的、有效期极短(通常 30 秒-1 分钟),且需要 client_secret 或 PKCE 才能兑换
    • 第二步(换 token):通过后端 HTTPS 直接请求(后端通道),token 不经过浏览器,不暴露在 URL 或 Referer 头中
  2. 如果一步返回 token:
    • Token 会出现在浏览器地址栏(重定向 URL 的 fragment 或 query 参数中)
    • 浏览器历史记录、服务器日志、Referer 头都会泄露 Token
    • 这就是 Implicit Grant 被弃用的原因之一
  3. 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:

code
限流维度设计:
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)。

在认证授权体系中的落地:

  1. 不做网络边界信任:即使请求来自内网 IP,也必须进行认证。去掉"内网 = 安全"的假设。
  2. 每次请求都验证:不使用 session 或"登录过一次就信任"的模式。每个 API 请求都携带并验证 Token。
  3. 最小权限原则:
    • 服务间调用:每个服务只授予调用它所需的最小 scope
    • 用户权限:默认无权限,按需授权
  4. mTLS 全覆盖:所有服务间通信使用 mTLS,不信任网络层的安全性
  5. 持续验证:
    • 设备态势:检测用户设备是否合规(是否越狱、是否安装安全软件)
    • 行为分析:异常登录地点、异常操作时间触发额外的 MFA 验证
    • 动态权限:根据风险评分动态调整权限级别

零信任不是单一技术,而是贯穿整个认证授权体系的架构思想。JWT 的短有效期、mTLS 的服务间认证、ABAC 的动态属性评估,都是零信任思想的具体工程实践。


面试准备建议:以上问题覆盖了设计决策、安全应急、权限模型演进、协议选型等维度。面试中如果遇到认证授权相关的问题,核心思路是:先明确问题域(认证还是授权?协议层还是数据层?),再选择合适的工具组合(OAuth 2.0/JWT/RBAC/ABAC),最后强调安全考量(不信任任何输入,默认拒绝)。