【架构实战】OAuth2.0授权框架第三方登录的安全设计一、一键登录引发的安全漏洞2022年我们平台接入了微信和支付宝的第三方登录。上线两周后安全扫描报告了一个严重漏洞通过截获授权码攻击者可以在另一台设备上登录用户的账号。排查发现我们错误地实现了授权码流程Authorization Code Grant没有绑定PKCEProof Key for Code Exchange导致授权码被中间人截获后直接换取Token。更可怕的是Token的过期时间设了30天权限范围是readwrite——这意味着攻击者一旦获取Token可以冒充用户在30天内操作所有功能。OAuth2.0看似简单——调几个API就实现微信登录但安全实践中到处都是坑。二、OAuth2.0核心概念2.1 四个角色┌──────────┐ ┌──────────────┐ │ 用户 │ │ 资源服务器 │ │ Resource │ │ Resource │ │ Owner │ │ Server │ └────┬─────┘ └──────┬───────┘ │ │ │ 我同意授权 │ 这是用户的数据 │ │ ┌────▼────────────────────────▼───────┐ │ 授权服务器 │ │ Authorization Server │ │ ┌─────────────┐ ┌───────────────┐ │ │ │ /authorize │ │ /token │ │ │ │ 获取授权码 │ │ 换取Token │ │ │ └─────────────┘ └───────────────┘ │ └──────────────────┬─────────────────┘ │ ┌─────▼─────┐ │ 客户端 │ │ Client │ │ 我们的应用 │ └───────────┘2.2 四种授权模式授权模式使用场景安全等级是否需要前端Authorization Code PKCESPA/移动端/普通Web高推荐是Client Credentials服务间调用高否Implicit已废弃不安全不推荐低是Resource Owner Password遗留系统兼容低否Device CodeTV/物联网设备中否关键建议所有涉及用户的授权都用 Authorization Code PKCE不要用Implicit模式。三、Authorization Code PKCE 完整流程3.1 流程图用户 客户端(SPA) 授权服务器 资源服务器 │ │ │ │ │ 点击微信登录│ │ │ ├──────────────│ │ │ │ │ 1.生成code_verifier challenge │ │ │ 2.重定向 /authorize │ │ ├──────────────────│ │ │ │ │ │ │ │ 3.用户授权页面 │ │ │─────────────────────────────────┤ │ │ │ │ │ │ 4.输入账号密码 │ │ ├──────────────────────────────────│ │ │ │ │ │ │ │ 5.返回授权码(code) │ │ │ │──────────────────┤ │ │ │ │ │ │ │ 6.POST /token │ │ │ │ (code code_verifier) │ │ ├──────────────────│ │ │ │ │ │ │ │ 7.返回access_token│ │ │ │──────────────────┤ │ │ │ │ │ │ │ 8.API请求(带Token)│ │ │ ├─────────────────────────────────────│ │ │ │ │ │ │ 9.返回用户数据 │ │ │ │─────────────────────────────────────┤3.2 代码实现// 1. 前端生成 PKCE 参数publicclassPKCEGenerator{publicstaticPKCEParamsgenerate(){// 生成 code_verifier43-128位随机字符串byte[]codeVerifierBytesnewbyte[32];newSecureRandom().nextBytes(codeVerifierBytes);StringcodeVerifierBase64.getUrlEncoder().withoutPadding().encodeToString(codeVerifierBytes);// 生成 code_challengeSHA256(code_verifier)MessageDigestmdMessageDigest.getInstance(SHA-256);byte[]digestmd.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));StringcodeChallengeBase64.getUrlEncoder().withoutPadding().encodeToString(digest);returnnewPKCEParams(codeVerifier,codeChallenge);}}// 2. 构建授权请求URLpublicStringbuildAuthorizationUrl(){PKCEParamspkcePKCEGenerator.generate();// 存储 code_verifier用于后续换Token不能暴露给任何人sessionStorage.set(code_verifier,pkce.getCodeVerifier());MapString,StringparamsnewLinkedHashMap();params.put(response_type,code);params.put(client_id,CLIENT_ID);params.put(redirect_uri,REDIRECT_URI);params.put(scope,read_user_info);params.put(state,UUID.randomUUID().toString());// 防CSRFparams.put(code_challenge,pkce.getCodeChallenge());params.put(code_challenge_method,S256);returnAUTHORIZE_URL?buildQueryString(params);}// 3. 后端用授权码换取TokenPostMapping(/oauth/callback)publicTokenResponsehandleCallback(RequestParamStringcode,RequestParamStringstate){// 验证state防止CSRF攻击StringsavedState(String)request.getSession().getAttribute(oauth_state);if(!state.equals(savedState)){thrownewSecurityException(State mismatch - possible CSRF attack);}// 构建 Token 请求HttpHeadersheadersnewHttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);MultiValueMapString,StringbodynewLinkedMultiValueMap();body.add(grant_type,authorization_code);body.add(code,code);body.add(redirect_uri,REDIRECT_URI);body.add(client_id,CLIENT_ID);body.add(client_secret,CLIENT_SECRET);// 仅在服务端使用body.add(code_verifier,codeVerifier);// PKCE验证HttpEntityMultiValueMapString,StringrequestnewHttpEntity(body,headers);ResponseEntityTokenResponseresponserestTemplate.postForEntity(TOKEN_URL,request,TokenResponse.class);returnresponse.getBody();}四、安全设计最佳实践4.1 Token安全// JWT Token 设计{iss:https://auth.example.com,// 签发者sub:user_12345,// 用户标识aud:api.example.com,// 受众限制Token使用范围exp:1625097600,// 过期时间绝对时间iat:1625000000,// 签发时间jti:unique-token-id-xxxxx,// Token唯一ID用于撤销scope:read:profile write:orders,// 权限范围client_id:spa-app// 使用的客户端}// 安全配置accessTokenExpiry:15分钟// 短期即便泄露窗口也小refreshTokenExpiry:7天// 可撤销refreshTokenRotation:true// 每次刷新换新的refreshToken4.2 安全威胁与防御威胁攻击方式防御措施CSRF伪造授权请求state参数验证授权码拦截中间人截获codePKCE code一次性使用Token泄露XSS获取TokenHttpOnly Cookie short TTL重定向劫持伪造redirect_uri服务端白名单校验暴力破解穷举client_secret速率限制 强密钥权限膨胀客户端请求过多scope最小权限原则4.3 服务端关键校验ServicepublicclassOAuth2SecurityService{publicAuthorizationCodevalidateAuthorizationRequest(AuthorizationRequestrequest){// 1. 校验redirect_uri白名单if(!isValidRedirectUri(request.getRedirectUri())){thrownewInvalidGrantException(redirect_uri not whitelisted);}// 2. 校验scope在允许范围内SetStringrequestedScopesrequest.getScopes();SetStringallowedScopesclientService.getAllowedScopes(request.getClientId());if(!allowedScopes.containsAll(requestedScopes)){thrownewInvalidScopeException(Requested scope exceeds allowed scope);}// 3. 校验client_idClientclientclientService.findByClientId(request.getClientId());if(clientnull){thrownewInvalidClientException(Unknown client);}// 4. 生成一次性授权码5分钟过期用后即删AuthorizationCodecodeAuthorizationCode.builder().code(UUID.randomUUID().toString()).clientId(request.getClientId()).userId(currentUser.getId()).redirectUri(request.getRedirectUri()).scopes(requestedScopes).codeChallenge(request.getCodeChallenge()).codeChallengeMethod(request.getCodeChallengeMethod()).expiresAt(Instant.now().plus(5,ChronoUnit.MINUTES)).used(false).build();returnauthorizationCodeRepository.save(code);}}五、微服务中的OAuth2.05.1 API网关统一鉴权请求流程 Client ──Authorization: Bearer xxx── API Gateway │ 1. 解析JWT 2. 验证签名 3. 检查过期 4. 提取用户信息 5. 构造内部Header │ ┌────▼────┐ │ 订单服务 │ │ (不感知 │ │ OAuth) │ └─────────┘ // Gateway配置Spring Cloud Gateway Bean public GlobalFilter authFilter() { return (exchange, chain) - { String token exchange.getRequest() .getHeaders() .getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { return unauthorized(exchange); } // 验证Token Claims claims tokenService.validateToken(token.substring(7)); // 构造内部请求头传递给下游服务 ServerHttpRequest request exchange.getRequest().mutate() .header(X-User-Id, claims.getSubject()) .header(X-User-Roles, String.join(,, claims.getRoles())) .build(); return chain.filter(exchange.mutate().request(request).build()); }; }六、总结OAuth2.0是一个授权框架而非认证协议——它告诉API这个客户端有权代表用户做什么而不是这个用户是谁。很多团队混淆了这两个概念。核心安全原则所有用户授权场景必须使用 Authorization Code PKCEstate参数是防CSRF的唯一防线必须验证redirect_uri必须做白名单严格匹配access_token有效期不超过15分钟配合refresh_token轮换授权码一次性使用一旦换取Token立即失效scope最小权限原则 —— 宁少勿多Token在服务端验证不依赖客户端传来的用户信息血的教训OAuth2.0的规范只定义了流程安全细节需要自己实现。别把client_secret写在客户端代码里别用Implicit模式别把Token有效期设成30天。个人观点仅供参考