找回密码
立即注册
搜索
发回帖 发新帖

6374

积分

1

好友

795

主题
发表于 2 小时前 | 查看: 3| 回复: 0

配套 OAuth2 与 OIDC 全解析 使用,涵盖 10 道面试自测题与参考答案。建议先阅读原文,再进行自测思考。

面试自测

# 题目 对应章节
Q1 OAuth2 的四个角色分别是什么?用你自己的话解释它们之间的关系 第二节
Q2 授权码模式和 PKCE 模式的核心区别是什么?什么场景下必须使用 PKCE? 第六节
Q3 ID Token 和 Access Token 有什么区别?为什么 OIDC 需要 ID Token? 第七节
Q4 OAuth 2.1 相比 OAuth 2.0 有哪些主要变化?为什么要移除 Implicit 模式? 第三节
Q5 如何防止 OAuth2 中的 CSRF 攻击和授权码拦截攻击? 第六节、第十三节
Q6 JWT Token 和 Reference Token 各有什么优缺点?在实际项目中如何选择? 第三节、第十二节
Q7 Spring Security OAuth2 Client 的认证流程是怎样的?OAuth2LoginAuthenticationFilter 做了什么? 第九节
Q8 Access Token 和 Refresh Token 的有效期应该如何设置?为什么? 第十三节
Q9 OIDC 的 Discovery Document 是什么?它解决了什么问题? 第七节
Q10 企业级 SSO 方案中,Keycloak 和 CAS 各有什么优缺点?如何选择? 第八节、第十二节

参考答案

Q1:OAuth2 的四个角色分别是什么?用你自己的话解释它们之间的关系

参考答案:

OAuth2 定义了四个核心角色:

1. Resource Owner(资源所有者)

即用户本人。资源的所有者,有权力决定是否授权第三方应用访问自己的数据。例如微信用户授权某个应用获取自己的昵称和头像。

2. Client(客户端)

即第三方应用,代表资源所有者发起请求。客户端需要先在授权服务器注册,获得 client_id 和 client_secret。例如某个电商平台上的"用微信登录"功能。

3. Authorization Server(授权服务器)

负责验证资源所有者身份、确认授权范围,然后颁发 Access Token 的服务器。例如微信开放平台的授权服务器。

4. Resource Server(资源服务器)

持有受保护资源的服务器,接收并验证 Access Token,验证通过后返回资源。例如微信的"获取用户信息" API。

四者之间的关系:

  Resource Owner  ──授权──>  Client
       │                         │
       │                         │  ① 带 Token 请求资源
       │                         v
       │                  Resource Server
       │                         ▲
       │                         │  ③ 验证 Token
       v                         │
  Authorization Server ──颁发 Token──┘
       ▲
       │ ② 用户认证 + 确认授权
       │
  Resource Owner

关键理解点:

  • Resource Server 不关心用户是谁,它只验证 Token 是否有效
  • Authorization Server 和用户直接交互,完成认证和授权
  • Client 是桥梁,连接用户和两个服务器
  • 这就是为什么 OAuth2 是授权协议而非认证协议——资源服务器拿到的是令牌,不是用户身份

参考:OAuth2与 OIDC全解析 - 四角色模型


Q2:授权码模式和 PKCE 模式的核心区别是什么?什么场景下必须使用 PKCE?

参考答案:

核心区别:

对比维度 Authorization Code Authorization Code + PKCE
code_verifier 不需要 必须,客户端生成的随机字符串
code_challenge 不需要 必须,code_verifier 的 SHA256 哈希值
client_secret 需要(Confidential Client) 不需要(Public Client)
防授权码拦截 不防护 防护——攻击者拿到 code 但没有 code_verifier,无法换 Token

PKCE 的防护原理:

  正常流程(有 PKCE):
  ① 客户端生成 code_verifier(随机串)
  ② 计算 code_challenge = SHA256(code_verifier)
  ③ 授权请求中发送 code_challenge
  ④ Token 请求中发送 code_verifier
  ⑤ 授权服务器验证: SHA256(code_verifier) == code_challenge?

  攻击场景(code 被截获):
  ① 攻击者截获了 authorization_code
  ② 攻击者没有 code_verifier
  ③ 攻击者去换 Token 时,授权服务器要求提供 code_verifier
  ④ 攻击者无法提供 → 换 Token 失败

必须使用 PKCE 的场景:

  1. 移动端应用(iOS/Android):client_secret 嵌入在客户端代码中,容易被反编译获取
  2. SPA(单页应用):前端代码完全暴露,无法保护 client_secret
  3. 桌面应用:同样无法安全存储 client_secret
  4. OAuth 2.1:强制所有授权码模式使用 PKCE,不再区分 Confidential 和 Public Client

参考:OAuth2 与 OIDC全解析 - PKCE 增强


Q3:ID Token 和 Access Token 有什么区别?为什么 OIDC 需要 ID Token?

参考答案:

核心区别:

对比维度 Access Token ID Token
用途 授权——访问受保护资源 认证——证明用户身份
格式 可以是任意格式(JWT、不透明字符串) 必须是 JWT 格式
受众 资源服务器(Resource Server) 客户端(Client)
内容 权限范围(scope)、过期时间 用户身份(sub、name、email 等 claims)
验证方式 资源服务器验证签名或查询授权服务器 客户端验证签名 + 检查 claims
是否必需 OAuth2 必需 OIDC 必需(OAuth2 无此概念)

为什么 OIDC 需要 ID Token:

OAuth2 的 Access Token 解决的是"你有权限访问这个资源"的问题,但不解决"你是谁"的问题。

举个例子:

  • 你拿着 Access Token 去访问 /api/user/profile,资源服务器验证 Token 有效后返回数据
  • 但资源服务器不知道这个 Token 属于哪个用户(特别是 Reference Token 情况下)
  • 客户端也不知道用户究竟是谁——它只知道"用户授权我访问资源"

ID Token 补上了这个缺口:

  ID Token (JWT) 包含的认证信息:
  {
    "iss": "https://auth.example.com/",    // 谁签发的
    "sub": "user-12345",                    // 用户唯一标识
    "aud": "my-client-id",                  // 发给谁的
    "exp": 1516239022,                      // 什么时候过期
    "iat": 1516239022,                      // 什么时候签发的
    "nonce": "n-0S6_WzA2Mj",               // 防重放
    "name": "小A",                           // 用户名字
    "email": "xiaoa@example.com"            // 用户邮箱
  }

客户端收到 ID Token 后:

  1. 验证签名(用授权服务器的公钥)
  2. 验证 iss 是否可信
  3. 验证 aud 是否是自己的 client_id
  4. 验证 exp 是否过期
  5. 验证 nonce 是否和自己发出的一致
  6. 通过后,从 claims 中提取用户身份信息

总结:Access Token 是"通行证",ID Token 是"身份证"。OAuth2 只有通行证,OIDC 额外给了身份证。

参考:OAuth2与 OIDC全解析 - ID Token


Q4:OAuth 2.1 相比 OAuth 2.0 有哪些主要变化?为什么要移除 Implicit 模式?

参考答案:

OAuth 2.1 主要变化:

变化 说明
移除 Implicit 模式 隐式授权模式不再保留
强制 PKCE 所有授权码模式必须使用 PKCE
禁止 Token 在 URL 中传递 防止 Token 泄露到浏览器历史、日志、Referer
强制 state 参数 防止 CSRF 攻击
Refresh Token 轮换 每次刷新 Token 时必须颁发新的 Refresh Token,旧的立即失效

为什么要移除 Implicit 模式:

Implicit 模式(也称作"隐式授权")的工作流程:

  Implicit 模式流程:
  客户端 ──> 授权服务器: 请求授权
  授权服务器 ──> 重定向: redirect_uri#access_token=xxx
                                   ^^^^^^^^^^^^^^
                                   Token 在 URL fragment 中

安全隐患:

  1. Token 在浏览器中暴露:URL fragment(#access_token=xxx)会被记录在浏览器历史、书签中
  2. Referer 泄露:如果页面跳转到其他网站,URL fragment 可能通过 Referer 头泄露
  3. 无法使用 client_secret:Implicit 模式专为公共客户端设计,没有后端保护
  4. 无授权码交换环节:省去了 Token 交换步骤,意味着少了后端加密通信的保护
  5. CSRF 风险更大:Token 直接在浏览器中,攻击者更容易窃取

对比 Authorization Code 模式:

  Authorization Code 模式:
  ① 授权请求: 返回 code(一次性,有效期短)
  ② Token 请求: 后端 POST /token(HTTPS 加密传输)
  ③ 返回 Token: 在 POST 响应 body 中(不出现在 URL 中)

OAuth 2.1 选择直接移除 Implicit 模式,统一推荐使用 Authorization Code + PKCE——对公共客户端和 Confidential Client 都适用,安全性更高。

参考:OAuth2与 OIDC全解析 - OAuth2.1 规范的关键变化


Q5:如何防止 OAuth2 中的 CSRF 攻击和授权码拦截攻击?

参考答案:

CSRF 攻击(跨站请求伪造)防护:

CSRF 攻击场景:攻击者构造恶意链接,诱使用户点击,自动完成 OAuth2 授权,将攻击者的第三方账号与用户的系统账号绑定。

防护方式:state 参数

  ① 客户端生成随机 state 值,存储在用户 Session 中
  ② 授权请求中附带 state 参数
  ③ 授权回调时,客户端验证返回的 state 是否与 Session 中的一致
  ④ 不一致则拒绝,一致则继续
// Spring Security 自动处理 state 验证
// 手动实现示例:
String state = UUID.randomUUID().toString();
request.getSession().setAttribute("oauth2_state", state);

String authUrl = authorizationUri
    + "?response_type=code"
    + "&client_id=" + clientId
    + "&redirect_uri=" + redirectUri
    + "&state=" + state;  // 携带 state

// 回调时验证
String returnedState = request.getParameter("state");
String savedState = (String) request.getSession().getAttribute("oauth2_state");
if (!returnedState.equals(savedState)) {
    throw new OAuth2AuthenticationException("Invalid state parameter");
}

授权码拦截攻击防护:

攻击场景:攻击者通过注入恶意 redirect_uri 或其他方式截获 authorization_code,然后用 code 换取 Token。

防护方式:PKCE

  PKCE 防护原理:
  ① 客户端生成 code_verifier(43-128 位随机字符串)
  ② 计算 code_challenge = BASE64URL(SHA256(code_verifier))
  ③ 授权请求中发送 code_challenge(不发送 code_verifier)
  ④ 授权回调后,Token 请求中发送 code_verifier
  ⑤ 授权服务器验证: SHA256(code_verifier) == code_challenge

即使攻击者截获了 authorization_code,没有 code_verifier 也无法通过验证换取 Token。

两种攻击的对比防护:

攻击类型 攻击目标 防护方式 防护原理
CSRF 攻击 伪造用户授权 state 参数 绑定用户 Session,验证来源
授权码拦截 截获 code 换 Token PKCE 只有合法客户端有 code_verifier

参考:OAuth2 与 OIDC全解析 - PKCE 增强 和 OAuth2 与 OIDC全解析 - 跨域登录安全


Q6:JWT Token 和 Reference Token 各有什么优缺点?在实际项目中如何选择?

参考答案:

JWT Token:

  结构: header.payload.signature
  示例: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMyJ9.XYZ...
优点 缺点
自包含,资源服务器本地验证签名,无需额外请求 Token 较大(几百到几千字节)
性能好,适合高并发场景 无法主动吊销(需额外机制如黑名单)
携带用户信息(claims),减少数据库查询 信息泄露风险—— payload 可被解码(但未加密)
适合微服务架构 密钥管理复杂(需要密钥轮换机制)

Reference Token:

  结构: 不透明字符串
  示例: 5f3d8a9b2c1e7f4a6b8d0e9c3f2a1b4c
优点 缺点
短小精悍,网络开销小 资源服务器每次需要向授权服务器查询有效性
支持实时吊销,授权服务器标记失效后立即生效 增加网络开销,存在单点故障风险
不携带用户信息,更安全 需要额外的 introspection endpoint
密钥管理简单 高并发下授权服务器压力大

实际项目选择建议:

  选择 JWT Token 的场景:
  ├── 微服务架构,服务间调用频繁
  ├── 高并发场景,性能要求高
  ├── 资源服务器与授权服务器不在同一网络
  └── Token 不需要实时吊销

  选择 Reference Token 的场景:
  ├── 安全要求极高,需要实时吊销能力
  ├── 用户权限可能频繁变更
  ├── 授权服务器与资源服务器在同一内网
  └── 需要集中审计和监控 Token 使用情况

混合方案:

实际项目中可以混合使用:

  • Access Token 使用 JWT(资源服务器本地验证,性能好)
  • Refresh Token 使用不透明字符串(授权服务器管理,可随时吊销)
  • ID Token 必须是 JWT(OIDC 强制要求)

参考:OAuth2与 OIDC全解析 - JWT 在 OAuth2 中的角色 和 OAuth2 与 OIDC全解析 - JWT vs Reference Token


Q7:Spring Security OAuth2 Client 的认证流程是怎样的?OAuth2LoginAuthenticationFilter 做了什么?

参考答案:

Spring Security OAuth2 Client 认证流程:

  完整认证流程:

  ① 用户访问受保护资源
     │
     ├─ 未认证 → 重定向到 OAuth2 授权入口
     │          /oauth2/authorization/{registrationId}
     │
     └─ 已认证 → 直接放行

  ② 用户点击第三方登录(如微信)
     │
     └─ 客户端构造授权 URL,重定向到授权服务器

  ③ 用户在授权服务器登录并授权
     │
     └─ 授权服务器回调 redirect_uri?code=xxx&state=xxx

  ④ OAuth2LoginAuthenticationFilter 拦截回调请求
     │
     ├─ 提取 authorization_code 和 state
     ├─ 验证 state 参数(防 CSRF)
     └─ 委托给 AuthenticationManager

  ⑤ AuthorizationCodeAuthenticationProvider
     │
     ├─ 构造 Token 请求(code + redirect_uri + client_secret)
     ├─ 如果启用 PKCE,附加 code_verifier
     └─ POST /token 换取 access_token

  ⑥ OAuth2UserService.loadUser()
     │
     ├─ 请求 UserInfo Endpoint 获取用户信息
     └─ 构造 OAuth2User 对象

  ⑦ 创建 OAuth2LoginAuthenticationToken
     │
     ├─ 放入 SecurityContext
     └─ 后续请求自动携带认证信息

OAuth2LoginAuthenticationFilter 的核心职责:

  1. URL 匹配:判断请求是否是 OAuth2 回调请求(默认 /login/oauth2/code/*)
  2. 提取授权响应:从 URL 参数中提取 code 和 state
  3. State 验证:验证返回的 state 与 Session 中存储的是否一致
  4. 构建认证 Token:创建 OAuth2LoginAuthenticationToken
  5. 委托认证:将 Token 交给 AuthenticationManager 处理
  6. 结果处理:认证成功则调用 successHandler,失败则调用 failureHandler

简化源码:

public class OAuth2LoginAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain) {
        // 1. 判断是否为 OAuth2 回调
        if (!requiresAuthorizationCodeResponse(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            // 2. 尝试授权码模式认证
            OAuth2LoginAuthenticationToken result =
                attemptAuthorizationCodeGrant(request, response);

            // 3. 认证成功
            this.successHandler.onAuthenticationSuccess(
                request, response, result);
        } catch (Exception ex) {
            // 4. 认证失败
            this.failureHandler.onAuthenticationFailure(request, response, ex);
        }
    }

    private OAuth2LoginAuthenticationToken attemptAuthorizationCodeGrant(
            HttpServletRequest request, HttpServletResponse response) {
        // 提取 code 和 state
        Map<String, Object> authResponse = getAuthorizationResponse(request);
        String code = (String) authResponse.get("code");
        String state = (String) authResponse.get("state");

        // 验证 state
        if (!StringUtils.hasText(state) || !state.equals(loadState(request))) {
            throw new OAuth2AuthenticationException(
                new OAuth2Error("invalid_state"), "Invalid state");
        }

        // 构建认证 Token,委托给 AuthenticationManager
        OAuth2LoginAuthenticationToken auth =
            new OAuth2LoginAuthenticationToken(
                clientRegistration,
                new OAuth2AuthorizationExchange(authResponse));

        return (OAuth2LoginAuthenticationToken)
            this.authenticationManager.authenticate(auth);
    }
}

关键过滤器链:

  OAuth2LoginAuthenticationFilter    ← OAuth2 登录入口,处理回调
         ↓
  OAuth2AuthorizationCodeGrantFilter ← 授权码 Token 交换(Spring Security 6.x)
         ↓
  BearerTokenAuthenticationFilter    ← 资源服务器验证 Bearer Token

参考:OAuth2与 OIDC全解析 - Spring Security OAuth2LoginAuthenticationFilter


Q8:Access Token 和 Refresh Token 的有效期应该如何设置?为什么?

参考答案:

建议有效期设置:

Token 类型 建议有效期 原因
Access Token 5-15 分钟 缩短泄露后的攻击窗口;过期后可用 Refresh Token 自动续期,用户无感知
Refresh Token 7-30 天 平衡安全性和用户体验;超过此期限应要求用户重新登录
ID Token 5-15 分钟 与 Access Token 保持一致

为什么 Access Token 要短:

  安全风险分析:

  有效期 1 小时:
  ──────────────────────────────────────
  泄露后攻击窗口: 60 分钟
  攻击者可以: 在这段时间内用 Token 访问所有受保护资源

  有效期 5 分钟:
  ──────────────────────────────────────
  泄露后攻击窗口: 5 分钟
  攻击者可以: 在 5 分钟内访问资源
  之后: Token 过期,攻击者需要 Refresh Token(通常存储在服务端,更安全)

为什么 Refresh Token 可以较长:

  • Refresh Token 不随每个请求携带,泄露风险更低
  • Refresh Token 通常存储在服务端(内存/Redis),不像 Access Token 需要在客户端存储
  • Refresh Token 轮换机制(OAuth 2.1):每次刷新时颁发新 Refresh Token,旧 Token 立即失效,即使泄露也能被发现
  • 超过有效期后,用户需要重新登录——这是合理的,不是所有账号都需要"永远在线"

Refresh Token 轮换机制(OAuth 2.1):

  刷新流程:
  ① 客户端: POST /token?grant_type=refresh_token&refresh_token=old_token
  ② 授权服务器:
     - 验证 old_token 有效
     - 颁发新 access_token
     - 颁发新 refresh_token(轮换)
     - 标记 old_token 为已使用(立即失效)
  ③ 客户端: 保存新 token
  ④ 如果旧 token 再次被使用:
     - 授权服务器检测到重复使用
     - 吊销该用户的所有 token
     - 要求用户重新登录

参考:OAuth2 与 OIDC全解析 - Token 管理策略


Q9:OIDC 的 Discovery Document 是什么?它解决了什么问题?

参考答案:

Discovery Document 是什么:

OIDC 定义了标准化的自动发现机制。客户端通过访问授权服务器的 .well-known/openid-configuration 端点,即可获取该授权服务器的所有关键配置信息。

  GET https://auth.example.com/.well-known/openid-configuration

  返回示例:
  {
    "issuer": "https://auth.example.com/",
    "authorization_endpoint": "https://auth.example.com/authorize",
    "token_endpoint": "https://auth.example.com/token",
    "userinfo_endpoint": "https://auth.example.com/userinfo",
    "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
    "registration_endpoint": "https://auth.example.com/register",
    "scopes_supported": ["openid", "profile", "email"],
    "response_types_supported": ["code", "code id_token"],
    "grant_types_supported": ["authorization_code", "refresh_token"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "claims_supported": ["sub", "iss", "aud", "exp", "name", "email"]
  }

解决的问题:

问题 无 Discovery 有 Discovery
端点 URL 需要硬编码或查阅文档 自动获取,无需硬编码
密钥管理 需要单独获取公钥 通过 jwks_uri 自动获取
配置变更 需要手动更新客户端配置 客户端自动发现新配置
多 Provider 适配 每个 Provider 需要单独配置 统一格式,适配成本低
新 Provider 集成 需要阅读文档,手动配置 一个 URL 即可获取所有信息

工作流程:

  客户端启动时:
  ① 访问 issuer_url + "/.well-known/openid-configuration"
  ② 解析返回的 JSON,提取各端点 URL
  ③ 访问 jwks_uri 获取签名公钥(用于验证 ID Token)
  ④ 使用提取的端点 URL 进行后续操作

  优势:
  - 客户端配置只需一个 issuer URL
  - 授权服务器升级/迁移时,客户端无需修改代码
  - 新接入 OAuth2 Provider 时,只需配置 issuer URL

Spring Security 中的自动发现:

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            # Spring Security 会自动请求此 URL + /.well-known/openid-configuration
            # 自动提取 authorization_endpoint、token_endpoint 等信息
            issuer-uri: http://localhost:8080/realms/myrealm

参考:OAuth2与 OIDC全解析 - Discovery Document


Q10:企业级 SSO 方案中,Keycloak 和 CAS 各有什么优缺点?如何选择?

参考答案:

Keycloak:

优点 缺点
基于开放标准(OAuth2/OIDC/SAML) 配置相对复杂,学习曲线较陡
功能全面(IAM = 身份 + 访问管理) 资源消耗较大(基于 WildFly/Quarkus)
内置社交登录支持(Google/GitHub/微信等) 大规模部署需要额外的集群配置
活跃的社区,Red Hat 官方维护 管理界面功能丰富但操作略显繁琐
支持容器化部署,官方提供 Docker 镜像 文档虽然完整,但部分场景需要深入阅读
支持 LDAP/AD 集成
支持细粒度权限管理(Authorization Services)

CAS(Central Authentication Service):

优点 缺点
协议简单,学习成本低 自有协议,非开放标准(虽然后来支持了 OAuth2/OIDC 插件)
部署简单,适合快速搭建 功能相对单一,主要聚焦 SSO
社区成熟,文档完善 社交登录、用户联邦等高级功能需要插件
资源消耗较低 管理界面相对简陋
与 Java 生态集成良好 权限管理能力有限

选择建议:

  选 Keycloak 的场景:
  ├── 需要 OAuth2/OIDC 原生支持
  ├── 需要社交登录(微信/GitHub/Google 等)
  ├── 需要完整的 IAM 能力(权限管理、审计、联邦)
  ├── 微服务架构,需要服务间认证
  ├── 需要支持 SAML 协议
  └── 未来可能有复杂的多租户、多组织需求

  选 CAS 的场景:
  ├── 只需要基本的 SSO 功能
  ├── 校园网、企业内部网等传统场景
  ├── 技术栈以 Java/Spring 为主
  ├── 部署资源有限,需要轻量级方案
  └── 团队对 CAS 已有经验

对比总结:

维度 Keycloak CAS
协议标准化 开放标准 自有协议 + 插件
功能范围 全面 IAM 专注 SSO
社交登录 内置 需插件
学习曲线 中等 低
资源消耗 较高 低
适合场景 企业级、云原生 校园网、传统企业

参考:OAuth2 与 OIDC全解析 - Keycloak vs CAS 和 OAuth2与 OIDC全解析 - 企业内部 SSO


参考资料

  1. OAuth2与 OIDC全解析 — 本面试自测的配套原文
  2. The OAuth 2.0 Authorization Framework - RFC 6749
  3. Proof Key for Code Exchange (PKCE) - RFC 7636
  4. OpenID Connect Core 1.0 Specification
  5. OAuth 2.1 Draft Specification
  6. Spring Security - OAuth2 Login
  7. Keycloak Documentation

以上 10 道题覆盖了 OAuth2 四角色模型、PKCE、OIDC、OAuth 2.1、Spring Security 集成以及企业级 SSO 选型等高频面试考点。建议把每道题的代码流程亲手画一遍,理解 Token 的流转链路,比死记硬背更有效。




上一篇:自研 Redis 分布式 Session 实战:多端登录与滑动过期
下一篇:1337x 数据中心遭袭:全球第二大 BT 站服务器下线后恢复
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-4 06:59 , Processed in 0.088064 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表