多系统认证授权利器:OAuth2,究竟该如何使用?
朋友老周被一个需求卡住了新系统要访问老系统的用户数据两边的鉴权怎么搞总不能把老系统密码直接发给新系统吧我说你这场景有现成的实现方案——OAuth2核心思路就三句话用户不暴露密码第三方拿一张有权限、有时效的令牌去访问资源令牌过期了还能静默续上不用重新登录。老周一拍大腿懂了就是发张门禁卡门禁卡的比喻很贴切但除了架构师恐怕大多数开发者对 OAuth2 的认知也只是停在了这个比喻上。本文的目标很明确让你不仅会用还能把核心原理和最佳实践讲得清清楚楚。一、从门禁卡说起OAuth2 到底解决了什么问题1.1 一个你每天都遇到的场景假设你入住一家酒店前台给了你一张门禁卡。这张卡有以下几个特点只对特定房门有效你只能进你的房间不能进别人的有时间限制过了退房时间就打不开了能干什么是确定的开门可以但不能用它去餐厅签单随时可以挂失前台可以把卡作废这张门禁卡就是一种授权机制。现在把这个场景搬到互联网世界你打开一个叫咕咚笔记的第三方应用它说可以用微信账号登录。你点了授权微信给你弹了个确认框咕咚笔记申请获取你的昵称和头像。你点了同意然后咕咚笔记就能拿到你的微信昵称和头像了。这个过程是怎么实现的答案是OAuth2。1.2 传统方式的致命缺陷在没有 OAuth2 的年代第三方应用想要访问你在另一个服务上的数据只能这样做你 → 告诉咕咚笔记你的微信账号密码 → 咕咚笔记拿着你的密码去微信服务器登录 → 拿到你的数据问题在哪你把微信的密码给了咕咚笔记。咕咚笔记是好人还好说但如果它把密码存了明文呢如果它不止读取了昵称头像还偷偷翻了你的聊天记录呢如果你在 10 个应用里都用了这种给密码的方式改一次密码 10 个应用全部失效OAuth2 的解决思路非常朴素让用户在不暴露自己密码的情况下授权第三方应用访问自己在某个服务上的受保护资源。你 → 在微信授权页点同意 → 微信发给咕咚笔记一张临时门禁卡 → 咕咚笔记拿卡访问你的昵称头像你的密码从头到尾只有你知道微信服务器知道。咕咚笔记拿到的只是一个有时效性、有权限范围的令牌。1.3 OAuth2 到底是什么OAuth2Open Authorization 2.0是一个授权框架它允许第三方应用在资源拥有者的授权下获取对受保护资源的有限访问权限。注意这两个关键词——授权而非认证有限而非无限。概念通俗解释认证Authentication你是谁——出示身份证授权Authorization你能干什么——刷门禁卡进房间OAuth2 主要解决的是授权问题虽然在实际使用中它经常被用来做认证也就是用微信登录这类场景但那其实是基于 OAuth2 之上的扩展OpenID Connect也就是我们常说的 OIDC。二、四个人一台戏OAuth2 的核心角色在深入流程之前你必须先把这四个角色刻在脑子里。整个 OAuth2 就是这四个角色之间的交互。┌──────────────┐ ┌──────────────────┐ │ │ │ │ │ 资源拥有者 │ │ 授权服务器 │ │ (Resource │ │ (Authorization │ │ Owner) │ │ Server) │ │ │ │ │ │ 就是你,用户 │ │ 比如微信的OAuth │ │ │ │ 授权服务 │ └──────┬───────┘ └────────┬─────────┘ │ ┌──────────────────┐ │ │ │ │ │ └───────┤ 客户端 ├───────┘ │ (Client) │ │ │ │ 咕咚笔记这个应用 │ └────────┬─────────┘ │ ┌────────┴─────────┐ │ │ │ 资源服务器 │ │ (Resource │ │ Server) │ │ │ │ 微信的个人信息API │ │ │ └──────────────────┘资源拥有者Resource Owner用户本人。拥有数据的所有权。客户端Client第三方应用。想要访问用户数据的那一方。授权服务器Authorization Server负责认证用户并颁发令牌的服务。资源服务器Resource Server存放用户资源的服务验证令牌后提供数据。在实际的工程实现中授权服务器和资源服务器经常是同一个系统的不同模块但它们在 OAuth2 协议中是逻辑上独立的角色。小贴士面试的时候面试官经常问OAuth2 有哪几个角色这是一道送分题四个角色一个都不能少。三、令牌三兄弟Access Token、Refresh Token 和 JWT在正式进入授权流程之前我们先认识 OAuth2 中最核心的三个令牌概念。3.1 Access Token —— 门禁卡Access Token是 OAuth2 最核心的令牌。客户端拿着它去资源服务器请求数据资源服务器验证它有效就返回数据。它的特点短期有效通常是几十分钟到几小时包含权限信息能访问什么、不能访问什么对客户端不透明客户端不需要也不应该试图解析它的内容3.2 Refresh Token —— 换卡凭证Refresh Token用于在 Access Token 过期后换取新的 Access Token而不需要让用户重新授权。为什么要这样设计因为 Access Token 是频繁在网络上传输的泄露风险大所以设置较短的有效期。Refresh Token 只在与授权服务器通信时才使用暴露面小得多所以可以设置较长的有效期几天到几个月。3.3 JWT —— 自包含令牌JWTJSON Web Token不是 OAuth2 协议要求的但在实际落地中极其常见。它是一种自包含的令牌格式eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMDAxIn0.xxxxxxxxxxxxxxxxxx │ │ │ │ │ └── 签名 │ └── Payload载荷Base64解码后是JSON └── Header头Base64解码后是JSON为什么 JWT 和 OAuth2 常常一起出现因为传统的 Access Token 可能只是一个随机字符串不透明令牌资源服务器每次收到这种令牌都需要去授权服务器校验。而 JWT 本身携带了用户信息和签名资源服务器可以自己验证 JWT 的有效性不需要每次都远程调用授权服务器——这叫无状态验证性能更好。关键区分OAuth2 是授权协议定义的是怎么发令牌、怎么用令牌JWT 是令牌格式定义的是令牌里写什么、怎么签名。两者不是同一个层面的东西。四、四大授权模式选对武器才能打赢仗OAuth2 定义了四种授权模式Grant Type。这是整个 OAuth2 最核心的知识点也是面试中必考的。OAuth2 四种授权模式 │ ├── 授权码模式Authorization Code── ⭐ 最安全、最常用 │ 适用有后端服务器的 Web 应用 │ 比如咕咚笔记的后端服务器 │ ├── 简化模式Implicit── ⚠ OAuth 2.1 已不推荐 │ 适用纯前端应用SPA │ 比如没有后端、纯 JS 写的单页应用 │ ├── 密码模式Resource Owner Password── ⚠ 已废弃 │ 适用用户完全信任客户端比如官方App │ 比如微信自己的 App │ └── 客户端凭证模式Client Credentials 适用服务端到服务端的调用 比如后端微服务之间的调用4.1 授权码模式Authorization Code—— 王者这是最经典、最安全、使用最广泛的模式。如果你只记住一种就记这一种。完整流程图资源拥有者 客户端 授权服务器 资源服务器 (你) (咕咚笔记后端) (微信OAuth) (微信API) │ │ │ │ │1 点击微信登录 │ │ │ ├───────────────│ │ │ │ │ 2 重定向到授权页 │ │ │ ├───────────────────│ │ │ │ │ │ │ 3 显示授权确认页 │ │ │ │───────────────┼────────────────────┤ │ │ │ │ │ │ 4 用户点击同意 │ │ │ ├────────────────┼───────────────────│ │ │ │ │ │ │ │ 5 返回授权码(code) │ │ │ │───────────────────┤ │ │ │ │ │ │ │ 6 用code换token │ │ │ ├───────────────────│ │ │ │ │ │ │ │ 7 返回Access Token │ │ │ │ Refresh Token │ │ │ │───────────────────┤ │ │ │ │ │ │ │ 8 拿Access Token │ │ │ │ 请求用户数据 │ │ │ ├────────────────────┼─────────────────│ │ │ │ │ │ │ 9 返回用户数据 │ │ │ │───────────────────┼──────────────────┤ │ │ │ │ │10 登录成功,展示 │ │ │ │ 用户信息 │ │ │ │───────────────┤ │ │关键问题为什么叫授权码模式为什么要有 code 这一步这是面试中的高频追问。答案是安全——确保令牌只发给真正的后端服务器而不是浏览器。具体来说code是通过浏览器重定向传递的会暴露在 URL 中如果用code直接当令牌用任何一个看到 URL 的人都能拿它访问数据但code是一次性的而且换 token 时需要带上client_secret客户端密钥client_secret只存在后端服务器上浏览器里没有所以即使别人截获了code没有client_secret也换不到真正的 Access TokenAuthorization Code 流程的核心安全逻辑就是code 通过不安全的渠道浏览器传输但换 token 的操作只通过安全的渠道后端到后端完成。浏览器不安全通道 后端服务器 │ │ │ codexxxx ← 出现在URL里 │ │ │ └──── 后端收到 code ──────────────┘ │ code client_secret │ → 授权服务器 │ Access Token │ ← 授权服务器4.2 简化模式Implicit—— 被时代抛弃的快枪手简化模式省略了用 code 换 token这一步授权服务器直接把 Access Token 返回给了浏览器。为什么被抛弃因为 Access Token 直接暴露在浏览器端浏览器的 URL 片段#access_tokenxxx虽然不会发给服务器但可以被 JavaScript 读取浏览器的历史记录、第三方脚本、浏览器插件都可能泄露这个 token没有client_secret的保护任何一个拿到 token 的人都能用OAuth 2.1 已经明确不推荐使用 Implicit 模式改为推荐带 PKCE 的授权码模式。4.3 密码模式Resource Owner Password—— 已死用户直接把用户名和密码交给客户端客户端拿它们去授权服务器换 token。这又回到了我们最开始说的那个问题你把密码给了第三方。只有一个场景勉强可以用客户端和授权服务器是同一家公司的产品比如微信 App 和微信服务端。即便如此OAuth 2.1 也已经将其标记为废弃。4.4 客户端凭证模式Client Credentials—— 机器人的世界当客户端本身就是资源拥有者不涉及用户的时候使用。比如后端微服务 A 调用微服务 B 的 API定时任务访问自己的数据这种模式最简单客户端拿自己的client_idclient_secret直接去授权服务器换一个 Access Token。4.5 四种模式对比速查表模式适用场景安全性OAuth 2.1 状态是否涉及用户授权码模式Web 应用有后端⭐⭐⭐⭐⭐✅ 推荐是简化模式纯前端 SPA⭐⭐❌ 不推荐是密码模式高度可信应用⭐❌ 废弃是客户端凭证模式服务间调用⭐⭐⭐⭐✅ 推荐否五、PKCE给授权码模式加一层装甲如果你只了解了上面四种模式面试官很可能追问那 PKCE 是什么5.1 授权码模式的隐患即使是授权码模式也存在一个攻击场景攻击者在自己设备上发起授权流程拿到 code攻击者在受害者的应用回调 URL 中拦截到 code比如通过恶意 App 注册了相同的回调 scheme攻击者用自己的 code 替换掉受害者的 code受害者登录后攻击者能通过自己的 code 关联到受害者的 session这种攻击叫授权码拦截攻击Authorization Code Interception Attack。5.2 PKCE 做了什么PKCEProof Key for Code Exchange发音 pixy在授权码流程中增加了两个步骤客户端生成 code_verifier随机字符串 │ ├── 对其做 SHA-256 哈希 → code_challenge │ ├── ① 发起授权时把 code_challenge 传给授权服务器 │ ├── ② 用户授权后客户端拿到 code │ └── ③ 用 code 换 token 时把 code_verifier 也传过去 授权服务器验证SHA256(code_verifier) code_challenge ?关键安全点在于code_challenge是code_verifier的哈希值无法反推只有最初生成code_verifier的客户端才能在最后一步提供正确的code_verifier任何中间人截获了 code 也没用因为它没有code_verifier一句话总结 PKCE我先把锁给你code_challenge等我拿到授权码后来换 token 时我再证明我有钥匙code_verifier。换不了钥匙的人拿到授权码也没用。5.3 OAuth 2.1 的规定在 OAuth 2.1 中所有使用授权码模式的客户端都必须使用 PKCE。这不是可选增强而是硬性要求。六、Spring Security 实战10 分钟搭一个 OAuth2 授权服务器理论讲得差不多了手该上键盘了。我们来搭一个可以跑的 OAuth2 授权服务器。6.1 项目依赖Spring Boot 3.x Spring Security 6.x!-- pom.xml -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-authorization-server/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency6.2 授权服务器配置Configuration public class AuthorizationServerConfig { Bean Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain( HttpSecurity http) throws Exception { // Spring Security 6 的 OAuth2 授权服务器配置 OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.formLogin(Customizer.withDefaults()).build(); } Bean public RegisteredClientRepository registeredClientRepository() { // 注册一个客户端咕咚笔记 RegisteredClient client RegisteredClient .withId(UUID.randomUUID().toString()) .clientId(gudong-notes) // 客户端ID .clientSecret({noop}secret-123456) // 客户端密钥生产环境必须加密 .clientAuthenticationMethod( ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType( AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式 .authorizationGrantType( AuthorizationGrantType.REFRESH_TOKEN) // 支持刷新令牌 .redirectUri(http://localhost:8081/login/oauth2/code/gudong) .scope(read) // 读权限 .scope(write) // 写权限 .clientSettings( ClientSettings.builder() .requireAuthorizationConsent(true) // 需要用户确认 .requireProofKey(true) // 强制PKCE .build()) .build(); return new InMemoryRegisteredClientRepository(client); } Bean public JWKSourceSecurityContext jwkSource() { // 生成 RSA 密钥对用于签名 JWT KeyPair keyPair generateRsaKey(); RSAPrivateKey privateKey (RSAPrivateKey) keyPair.getPrivate(); RSAPublicKey publicKey (RSAPublicKey) keyPair.getPublic(); RSAKey rsaKey new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet new JWKSet(rsaKey); return new ImmutableJWKSet(jwkSet); } private static KeyPair generateRsaKey() { try { KeyPairGenerator generator KeyPairGenerator.getInstance(RSA); generator.initialize(2048); return generator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } } }6.3 资源服务器配置Configuration public class ResourceServerConfig { Bean Order(2) public SecurityFilterChain resourceServerSecurityFilterChain( HttpSecurity http) throws Exception { http .securityMatcher(/api/**) // 只保护 /api 路径 .authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 - oauth2 .jwt(Customizer.withDefaults()) // 使用 JWT 验证 ); return http.build(); } }6.4 资源接口RestController RequestMapping(/api) public class UserController { GetMapping(/public/hello) public String publicHello() { return 这是公开接口不需要 token; } GetMapping(/user/me) public MapString, Object currentUser( AuthenticationPrincipal Jwt jwt) { // jwt 里直接可以拿到用户信息和权限 MapString, Object result new HashMap(); result.put(subject, jwt.getSubject()); // 用户ID result.put(claims, jwt.getClaims()); // 所有声明 result.put(authorities, jwt.getClaimAsStringList(scope)); return result; } }6.5 配置 application.ymlserver: port: 8080 spring: security: user: name: zhangsan password: 123456 # 演示用生产环境绝不这样写 logging: level: org.springframework.security: DEBUG # 调试时可以打开6.6 验证流程启动项目后在浏览器里访问http://localhost:8080/oauth2/authorize ?response_typecode client_idgudong-notes redirect_urihttp://localhost:8081/login/oauth2/code/gudong scoperead code_challengePKCE_challenge code_challenge_methodS256你会依次看到Spring Security 默认的登录页面输入 zhangsan / 123456授权确认页面咕咚笔记申请获取你的 read 权限点击同意后浏览器重定向到http://localhost:8081/...?codexxxx拿到了code后端就可以用codeclient_secretcode_verifier去换 Access Token 了curl -X POST http://localhost:8080/oauth2/token \ -H Authorization: Basic $(echo -n gudong-notes:secret-123456 | base64) \ -d grant_typeauthorization_code \ -d code刚才拿到的code \ -d redirect_urihttp://localhost:8081/login/oauth2/code/gudong \ -d code_verifierPKCE_verifier响应{ access_token: eyJhbGciOiJSUzI1NiJ9..., refresh_token: abc123..., token_type: Bearer, expires_in: 3600, scope: read }七、细说 Token 安全Access Token 过期了怎么办7.1 Token 生命周期的设计哲学Access Token 有效期短15分钟 ~ 2小时 ↓ 过期后 Refresh Token 有效期长7天 ~ 30天 ↓ 过期后 重新授权 用户再次登录确认为什么要这样分层最小化暴露面。Access Token 在每次 API 请求中都会发送暴露在网络上所以让它短命Refresh Token 只在 token 过期时使用一次暴露次数极少即使 Access Token 泄露攻击者也只有十几分钟的窗口7.2 Refresh Token 轮转Rotation这是 OAuth 2.1 的最佳实践每次用 Refresh Token 换新 Access Token 时同时发放一个新的 Refresh Token并把旧的 Refresh Token 作废。请求refresh_token_123 → 换取新的 access_token 响应新的 access_token 新的 refresh_token_456 服务端refresh_token_123 标记为已使用好处是如果攻击者偷到了一个 Refresh Token但用户使用它会触发轮转那么当用户也用了一下这个 Refresh Token或攻击者用完之后用户再用服务端就会发现这个 token 已经被用过了——说明可能被泄露了可以立刻作废该用户的所有 token。7.3 Token 存储前端到底该放哪这也是面试和实际开发中都绕不过的问题。存储方式安全性XSS 风险CSRF 风险推荐度localStorage低❌ 可被 JS 读取无不推荐sessionStorage低❌ 可被 JS 读取无不推荐CookieHttpOnly高✅ JS 无法读取需防 CSRF⭐ 推荐内存变量高✅✅刷新即丢失BFFBackend For Frontend最高✅✅⭐⭐ 最推荐最佳实践Token 存在后端的 session 中前端只持有一个 session cookieHttpOnly Secure SameSiteStrict。这就是 BFF 模式——前端根本不碰 token。八、OAuth2 JWT 深度融合从原理到代码8.1 JWT 的结构深入一个 JWT 由三部分组成用.分隔Header.Payload.SignatureHeader算法信息{ alg: RS256, kid: key-id-001, typ: JWT }Payload声明信息{ iss: http://auth-server:8080, // 签发者 sub: zhangsan, // 主体用户ID aud: gudong-notes, // 受众谁在使用这个token exp: 1718000000, // 过期时间 iat: 1717996400, // 签发时间 scope: read write // 权限范围 }Signature签名RSASHA256( base64UrlEncode(header) . base64UrlEncode(payload), 私钥 )8.2 资源服务器如何验证 JWT资源服务器收到请求Authorization: Bearer jwt │ ├── 1. 解码 JWT Header获取 kidkey id │ ├── 2. 从授权服务器的 JWKS 端点获取公钥 │ GET http://auth-server:8080/oauth2/jwks │ ├── 3. 用公钥验证签名 │ 签名有效 → 内容未被篡改 │ ├── 4. 检查 Claims │ exp 是否过期 │ iss 是否是信任的签发者 │ aud 是否包含自己 │ └── 5. 全部通过 → 放行把 JWT 信息注入 SecurityContextSpring Security 的oauth2ResourceServer().jwt()配置自动完成了这一切。8.3 手动解析 JWT不用 Spring Securityimport io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import java.security.PublicKey; public class JwtValidator { public static Claims validateAndParse(String token, PublicKey publicKey) { return Jwts.parser() .verifyWith(publicKey) // 用公钥验证签名 .build() .parseSignedClaims(token) // 解析 .getPayload(); // 获取载荷 } }8.4 自定义 JWT 中的 Claims有时候你需要在 JWT 中加入业务相关的信息Bean public OAuth2TokenCustomizerJwtEncodingContext jwtTokenCustomizer() { return context - { if (context.getTokenType().getValue().equals( OAuth2TokenType.ACCESS_TOKEN.getValue())) { // 往 JWT 里加自定义字段 context.getClaims().claims(claims - { claims.put(department, engineering); // 部门 claims.put(role, admin); // 角色 }); } }; }资源服务器侧读取GetMapping(/admin/info) public MapString, Object adminInfo(AuthenticationPrincipal Jwt jwt) { String department jwt.getClaimAsString(department); // engineering String role jwt.getClaimAsString(role); // admin // ... }九、生产环境的七大安全最佳实践这一节不讲代码讲原则。每一条都是用血泪教训换来的。9.1 始终使用 HTTPS全文最重要的一条。如果授权请求走的是 HTTP中间人可以截获code如果 token 端点走的是 HTTP中间人可以截获client_secret。一旦泄漏全盘皆输。生产环境强制 HTTPS没有任何商量的余地。9.2 Token 不存前端我们在 7.3 节已经讨论过了。localStorage 存 token 等价于在大马路上贴银行卡密码。用 HttpOnly Cookie BFF 模式。9.3 最小权限原则申请 scope 时只申请你真正需要的。一个只展示用户昵称的应用不要申请读取好友列表发送私信的权限。9.4 验证 redirect_uri授权服务器必须严格校验redirect_uri只允许预先注册的地址。不校验的话攻击者可以诱导用户授权后把 code 发到自己的服务器。9.5 使用 state 参数防 CSRF在发起 OAuth2 授权请求时生成一个随机字符串作为state参数并在回调时验证。这可以防止攻击者诱导用户点击一个已预授权的链接。// 发起授权时 String state UUID.randomUUID().toString(); session.setAttribute(oauth2_state, state); // 回调时验证 String returnedState request.getParameter(state); if (!returnedState.equals(session.getAttribute(oauth2_state))) { throw new SecurityException(CSRF attack detected!); }9.6 启用 PKCE即便是机密客户端虽然 PKCE 最初是为公开客户端无法安全存储 client_secret 的应用设计的但 OAuth 2.1 要求所有客户端都使用。多一层防护总没错。9.7 监控异常 Token 使用同一个 Refresh Token 被使用了两次 → 可能是泄露同一个 Access Token 在短时间内从不同 IP 使用 → 可能被盗用大量 token 签发请求 → 可能是暴力攻击十、面试八股速通高频问题一网打尽10.1 OAuth2 的四种角色是什么资源拥有者、客户端、授权服务器、资源服务器。四个角色一个都别少。10.2 授权码模式为什么要先发 code 再换 token因为code通过浏览器传输不安全但换token需要client_secret只存在后端。这样确保了即使code泄露没有client_secret也拿不到真正的 Access Token。10.3 PKCE 解决了什么问题原理是什么解决了授权码拦截攻击。原理是客户端先生成code_verifier和它的哈希值code_challenge授权时传 challenge换 token 时传 verifier授权服务器验证两者匹配。只有最初发起授权请求的客户端才知道code_verifier。10.4 JWT 和 OAuth2 是什么关系OAuth2 是授权协议定义了怎么发、怎么用JWT 是令牌格式定义了长什么样。OAuth2 的 Access Token 可以用 JWT 格式也可以用随机字符串不透明令牌。10.5 Access Token 过期了怎么办用 Refresh Token 去换新的 Access Token不需要用户重新登录。如果 Refresh Token 也过期了才需要用户重新授权。10.6 Refresh Token 轮转是什么每次使用 Refresh Token 时授权服务器同时发放一个新的 Refresh Token旧 token 作废。用于检测 Refresh Token 是否被泄露。10.7 如何在微服务之间传递 token下游服务从请求头中获取 token透传或由网关统一处理用户 → Gateway → 服务A → 服务B │ │ │ │ 验证token │ │ │ │ │ └── 透传 ───────────┘ Authorization Header或者服务 A 使用客户端凭证模式以自己的身份去调用服务 B。10.8 OAuth2 和 SSO单点登录是什么关系OAuth2 本身是授权协议不能直接用于认证。OpenID Connect (OIDC)是基于 OAuth2 的身份认证层可以实现 SSO。简单理解OAuth2给你一张门禁卡你可以进房间OIDC给你一张门禁卡卡上还印了你的名字和照片可以证明你是谁10.9 Cookie 和 Token 两种认证方式的区别维度Cookie SessionJWT Token状态服务端有状态存 session服务端无状态token 自包含扩展性需要共享 session如 Redis天然支持分布式注销简单删 session复杂token 未过期仍有效需黑名单移动端不友好友好HTTP Header安全性需防 CSRF需防 XSS若存前端写在最后OAuth2 不是一个能一眼看懂的协议。它的规范 RFC 6749 是一份篇幅不小的文档而 OAuth 2.1 还在持续演进中。但好消息是对于绝大多数 Java 开发者来说你不需要通读 RFC你只需要搞懂本文覆盖的这些核心概念和原则。回顾一下你的学习路径理解问题OAuth2 是为了让第三方在不拿密码的情况下访问用户数据记住角色四个角色、四种模式吃透流程授权码模式为什么分两步、PKCE 做了什么动手实践Spring Security 搭一套授权/资源服务器守住安全七大实践条条都是真金白银的经验