配套 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 的场景:
- 移动端应用(iOS/Android):
client_secret 嵌入在客户端代码中,容易被反编译获取
- SPA(单页应用):前端代码完全暴露,无法保护
client_secret
- 桌面应用:同样无法安全存储
client_secret
- 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 后:
- 验证签名(用授权服务器的公钥)
- 验证
iss 是否可信
- 验证
aud 是否是自己的 client_id
- 验证
exp 是否过期
- 验证
nonce 是否和自己发出的一致
- 通过后,从 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 中
安全隐患:
- Token 在浏览器中暴露:URL fragment(
#access_token=xxx)会被记录在浏览器历史、书签中
- Referer 泄露:如果页面跳转到其他网站,URL fragment 可能通过 Referer 头泄露
- 无法使用
client_secret:Implicit 模式专为公共客户端设计,没有后端保护
- 无授权码交换环节:省去了 Token 交换步骤,意味着少了后端加密通信的保护
- 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 的核心职责:
- URL 匹配:判断请求是否是 OAuth2 回调请求(默认
/login/oauth2/code/*)
- 提取授权响应:从 URL 参数中提取
code 和 state
- State 验证:验证返回的
state 与 Session 中存储的是否一致
- 构建认证 Token:创建
OAuth2LoginAuthenticationToken
- 委托认证:将 Token 交给
AuthenticationManager 处理
- 结果处理:认证成功则调用
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
参考资料
- OAuth2与 OIDC全解析 — 本面试自测的配套原文
- The OAuth 2.0 Authorization Framework - RFC 6749
- Proof Key for Code Exchange (PKCE) - RFC 7636
- OpenID Connect Core 1.0 Specification
- OAuth 2.1 Draft Specification
- Spring Security - OAuth2 Login
- Keycloak Documentation
以上 10 道题覆盖了 OAuth2 四角色模型、PKCE、OIDC、OAuth 2.1、Spring Security 集成以及企业级 SSO 选型等高频面试考点。建议把每道题的代码流程亲手画一遍,理解 Token 的流转链路,比死记硬背更有效。