ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微软OAuth 2.0授权流程实战:从PKCE到access_token全链路解析

微软OAuth 2.0授权流程实战:从PKCE到access_token全链路解析 1. 这不是“点一下授权就完事”的流程——微软登录OAuth 2.0到底在解决什么问题你有没有遇到过这样的场景在一款国产笔记App里点击“用微软账号登录”跳转到一个带微软Logo的页面输入邮箱密码、点同意、再跳回App——整个过程不到10秒。但就在你按下“同意”的那一瞬间背后其实完成了一整套精密的身份验证与权限交接动作。这不是简单的“账号复用”而是一次严格遵循RFC 6749标准的授权委托协议执行。核心关键词——微软、OAuth 2.0、授权流程、access_token、authorization code——每一个都不是虚词而是真实运行在Azure Active DirectoryAzure AD和Microsoft Identity Platform上的技术实体。我做过3个需要深度集成微软登录的SaaS项目从面向中小企业的CRM系统到给高校部署的教务平台再到为跨国律所定制的文档协作工具。所有项目都绕不开这个流程但绝大多数开发同学第一次接触时都会掉进同一个坑把“登录”当成“认证”把“授权码”当成“密码”把access_token当成“万能钥匙”。结果就是——前端能跳转、能拿到code后端一换token就400或者token拿回来了调Graph API查用户邮箱却返回Insufficient privileges更常见的是在Android App里用WebView做登录结果被微软判定为“不安全客户端”直接拒绝颁发code。这根本不是配置错了一个redirect_uri那么简单。它本质是三方应用如何在不接触用户密码的前提下获得有限、可撤销、有时效性的资源访问权。微软的实现特别强调两点一是强制要求authorization code流程不支持implicit flow二是所有token必须通过后端交换禁止前端JS直接换token。这意味着你不能靠一个curl命令就搞定也不能把client_secret写死在Android代码里——后者在2023年之后已被微软明确标记为高危行为并拦截。适合谁看如果你正在开发Web后台服务、Node.js微服务、Java Spring Boot应用或者需要在React/Vue前端Express后端架构中接入微软登录这篇就是为你写的。如果你只是想给Flutter App加个“微软登录按钮”也别跳过——我会告诉你为什么Flutter Web能跑通而Android原生WebView会失败以及怎么用PKCE机制绕过这个限制。这不是API文档的搬运而是我把过去三年踩过的每一个坑、抓包分析的每一帧HTTPS请求、反复调试的每一种错误响应码全部拆开揉碎后重新组装出来的实操路径。2. 整体设计逻辑为什么必须走Authorization Code Flow微软的三道安全防线2.1 不是“选一个流程”而是“微软只允许这一种”很多开发者第一反应是“OAuth 2.0不是有四种授权模式吗我选最简单的Implicit Flow不行吗”——不行。微软早在2020年就正式弃用Implicit Flow并在2022年全面禁用。现在所有新注册的应用App Registration默认只启用Authorization Code Flow且强制开启PKCEProof Key for Code Exchange。这不是微软“任性”而是针对移动/桌面客户端暴露client_secret的重大安全风险做出的硬性约束。我们来还原一次典型攻击链假设你在Android App里把client_id和client_secret明文写进APK黑客反编译后就能拿到这两个值。接着他构造一个伪造的redirect_uri诱导用户点击拿到authorization code后直接用抓包工具向https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token发起POST请求带上真实的client_id、client_secret和窃取的code——立刻就能换到access_token进而读取用户邮件、日历甚至OneDrive文件。而PKCE正是为堵住这个漏洞设计的它要求客户端在发起授权请求时先生成一个随机code_verifier长度43-128字符的base64url编码字符串再用SHA256哈希得到code_challenge把这个challenge发给微软等到换token时必须把原始的code_verifier一起提交微软会重新哈希比对。由于code_verifier只存在于用户设备本地黑客即使截获code也无法伪造verifier换token请求必然失败。提示微软官方文档里把PKCE称为“推荐”而非“强制”但实测中如果你在App Registration里关闭PKCE支持Android WebView登录会直接报错invalid_request: The provided code_challenge_method is not supported.——因为现代Android WebView默认启用PKCE你关不掉。2.2 微软的三道防线从租户隔离到令牌绑定微软的OAuth 2.0实现远不止RFC标准它叠加了三层企业级防护第一层租户粒度控制Tenant-bound Authorization你注册应用时选择的“支持的账户类型”决定了授权范围仅限此组织目录中的帐户 → 只能用{tenant-id}.onmicrosoft.com域名登录任何组织目录中的帐户 → 支持所有Azure AD租户需管理员同意个人Microsoft帐户和工作或学校帐户 → 公共云common endpoint但会触发额外的consent流程关键点在于authorization code本身是租户绑定的。比如你用https://login.microsoftonline.com/common/oauth2/v2.0/authorize发起请求用户用usercontoso.com登录微软返回的code只能用于向contoso.com租户换token不能拿去fabrikam.com换。这是防止跨租户令牌盗用的基础。第二层重定向URI白名单Redirect URI Strict Validation微软对redirect_uri的校验极其严格必须完全匹配包括末尾斜杠、大小写、协议http://localhost:3000/callback≠http://localhost:3000/callback/https://myapp.com/callback≠https://myapp.com/callback?stateabc移动端必须使用自定义scheme如msal://callback或Android App Linkshttps://myapp.com/redirect我曾因Nginx反向代理配置多加了一个/导致连续两天换token失败错误码是invalid_request日志里却只显示“redirect_uri mismatch”根本没说具体哪不匹配。最后用Wireshark抓包对比才发现浏览器实际跳转的URL带了尾部斜杠而Azure Portal里填的没有。第三层令牌绑定与设备指纹Token Binding Device Fingerprinting微软的access_token默认绑定以下信息发行时间iat与过期时间exp通常1小时客户端IP地址首次换token时记录后续请求若IP突变会触发风险评估User-Agent字符串Web端或Package Name Signature HashAndroid端如果启用了Conditional Access策略还会检查设备合规状态是否越狱、是否有MDM证书这意味着你不能把一个token复制到另一台设备上使用也不能用Postman模拟请求时随便改User-Agent——微软会返回invalid_token并附带error:invalid_token,error_description:The token has been revoked or is invalid.2.3 为什么不能跳过后端前端直换token的致命缺陷有些团队为了“快”尝试在Vue前端用axios直接调用token接口// ❌ 危险永远不要这样做 axios.post(https://login.microsoftonline.com/common/oauth2/v2.0/token, { client_id: your-client-id, client_secret: DANGEROUS! NEVER HARD-CODE THIS, code: authCode, redirect_uri: https://myapp.com/callback, grant_type: authorization_code })这违反了OAuth 2.0的核心原则client_secret必须由可信后端保管。一旦泄露攻击者就能无限次换token且无法通过Azure Portal“重置密钥”立即生效——因为旧密钥可能已被缓存或用于签发长期有效的refresh_token。更隐蔽的问题是微软对来自浏览器的token请求有额外限制。2023年起所有client_secret请求必须携带X-Client-SKU: MSAL.JS头且redirect_uri必须是HTTPSlocalhost除外。但即使满足这些微软仍会检测请求来源——如果发现Origin头是https://myapp.com而Referer却是https://login.microsoftonline.com就会判定为CSRF风险返回invalid_client。正确做法永远是前端只负责跳转授权页、接收code后端服务哪怕只是个轻量Express路由接收code用client_secret换token再把access_token安全地传回前端通过HTTP-only Cookie或短时效JWT。这样client_secret永远不会暴露在客户端。3. 核心细节解析从注册应用到获取access_token的七步实操3.1 第一步在Azure Portal创建应用注册App Registration这不是“填个名字点创建”那么简单。关键配置项必须精准应用名称建议用产品名环境如MyApp-Prod、MyApp-Staging。避免用test-app这类通用名否则后期审计时无法区分。支持的账户类型根据业务决定如果只服务内部员工 → 选“仅限此组织目录中的帐户”如果要支持客户用各自公司邮箱登录 → 选“任何组织目录中的帐户”如果要兼容Outlook.com个人账号 → 必须选“个人Microsoft帐户和工作或学校帐户”重定向URI这是最容易出错的地方按平台分类配置Web应用https://myapp.com/auth/callback必须HTTPS生产环境SPAReact/Vuehttps://myapp.com注意不是callback路径微软要求SPA用https://myapp.com作为重定向URI然后在前端路由里处理/auth/callbackAndroidmsauth://com.mycompany.myapp/3nZV...格式msauth://package_name/base64_url_encoded_signature签名需用keytool生成iOSmsauth.com.mycompany.myapp://authCFBundleURLSchemes注意Azure Portal里填的redirect_uri必须与代码中发起授权请求时传的redirect_uri参数逐字节一致。我见过最离谱的错误是前端代码里写https://myapp.com/callbackPortal里填https://myapp.com/callback/多一个斜杠结果code返回后后端收到的回调URL是https://myapp.com/callback/?codexxx而微软认为这个URI不在白名单里直接拒绝。API权限API permissions这是权限控制的核心。点击“添加权限”→“Microsoft Graph”→选择所需权限User.Read读取用户基本信息必需Mail.Read读取邮箱如需邮件同步Files.Read读取OneDrive文件如需云存储关键区别勾选“委派权限Delegated”还是“应用权限Application”前者代表“用户授权后应用以用户身份操作”后者代表“应用自身拥有权限无需用户登录”。绝大多数SaaS应用只需委派权限。3.2 第二步生成PKCE code_verifier与code_challenge移动端必备对于Android/iOS/桌面应用必须实现PKCE。步骤如下生成43字符随机字符串推荐用crypto库// Node.js示例 const crypto require(crypto); function generateCodeVerifier() { return crypto.randomBytes(32).toString(base64url); // 43字符 } const codeVerifier generateCodeVerifier(); // 如dBjftJeZ4CVP-mB927GiTW_5rS1234567890abcdefg计算SHA256哈希并base64url编码function generateCodeChallenge(codeVerifier) { const hash crypto.createHash(sha256).update(codeVerifier).digest(); return hash.toString(base64url); // 32字符 } const codeChallenge generateCodeChallenge(codeVerifier); // 如E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGecyFCyA在授权请求中带上这两个参数https://login.microsoftonline.com/common/oauth2/v2.0/authorize? client_idYOUR_CLIENT_ID response_typecode redirect_urihttps%3A%2F%2Fmyapp.com%2Fcallback scopeopenid%20profile%20User.Read code_challenge_methodS256 code_challengeE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGecyFCyA state123456789实操心得Android开发中很多同学用androidx.browser:browser库的CustomTabsIntent启动授权页但忘记在Intent里传递code_verifier。结果授权成功后onActivityResult收到code却无法换token——因为后端没拿到verifier。正确做法是把code_verifier存在SharedPreferences里换token时再读取。3.3 第三步前端发起授权请求Web与移动端差异Web端React/Vue标准流程// 1. 构建授权URL const authUrl new URL(https://login.microsoftonline.com/common/oauth2/v2.0/authorize); authUrl.searchParams.set(client_id, YOUR_CLIENT_ID); authUrl.searchParams.set(response_type, code); authUrl.searchParams.set(redirect_uri, https://myapp.com/auth/callback); authUrl.searchParams.set(scope, openid profile User.Read); authUrl.searchParams.set(state, random-state-string); // 防CSRF必须存储在sessionStorage // 2. 跳转 window.location.href authUrl.toString();Android WebView关键配置// 必须启用JavaScript和DOM存储 webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true); // 设置WebViewClient拦截重定向 webView.setWebViewClient(new WebViewClient() { Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(msauth://com.mycompany.myapp/)) { // 提取code Uri uri Uri.parse(url); String code uri.getQueryParameter(code); // 传给后端换token exchangeCodeForToken(code); return true; } return false; } });重要区别Web端redirect_uri是https://myapp.com/auth/callback而Android必须用自定义schememsauth://...。这是因为iOS/Android系统限制WebView无法监听HTTPS重定向只能通过自定义scheme触发App内回调。3.4 第四步后端接收code并换tokenNode.js Express示例这是整个流程中最关键的后端环节必须严格校验const express require(express); const axios require(axios); const app express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 接收前端跳转后的code app.get(/auth/callback, async (req, res) { const { code, state, session_state } req.query; // 1. 校验state防CSRF必须与发起时一致 const storedState req.session.state; // 存在session中 if (!storedState || storedState ! state) { return res.status(400).send(Invalid state); } try { // 2. 向微软token端点发起POST请求 const tokenResponse await axios.post( https://login.microsoftonline.com/common/oauth2/v2.0/token, new URLSearchParams({ client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, // 从环境变量读取 code: code, redirect_uri: https://myapp.com/auth/callback, grant_type: authorization_code, // 移动端必须加code_verifier code_verifier: req.session.codeVerifier // 从session读取 }), { headers: { Content-Type: application/x-www-form-urlencoded, }, } ); const { access_token, refresh_token, expires_in, id_token } tokenResponse.data; // 3. 解析id_token获取用户信息JWT const payload JSON.parse(Buffer.from(id_token.split(.)[1], base64url).toString()); // 4. 创建用户会话示例存入Redis const userId payload.oid; // 对象ID全局唯一 await redis.setex(user:${userId}:token, expires_in, access_token); // 5. 重定向回前端应用 res.redirect(/dashboard?user${userId}); } catch (error) { console.error(Token exchange failed:, error.response?.data); res.status(500).send(Authentication failed); } });关键参数说明client_secret必须从环境变量读取绝不可硬编码redirect_uri必须与App Registration中注册的完全一致code_verifier移动端必需Web端可省略但建议统一实现grant_typeauthorization_code固定值不可更改3.5 第五步解析access_token与id_tokenJWT结构详解微软返回的access_token和id_token都是JWTJSON Web Token结构为header.payload.signature三段base64url编码字符串。id_token解析用户身份凭证{ aud: YOUR_CLIENT_ID, iss: https://login.microsoftonline.com/{tenant-id}/v2.0, iat: 1712345678, nbf: 1712345678, exp: 1712349278, aio: ATQAy/8DAAA..., amr: [pwd], email: usercontoso.com, family_name: Smith, given_name: John, ipaddr: 192.168.1.100, name: John Smith, oid: 12345678-1234-1234-1234-1234567890ab, preferred_username: johncontoso.com, sub: 12345678-1234-1234-1234-1234567890ab, tid: 12345678-1234-1234-1234-1234567890ab, uti: abcd1234..., ver: 2.0 }oid用户对象ID租户内唯一适合作为数据库主键tid租户ID用于区分不同公司email仅当用户同意emailscope时才存在amr认证方法[pwd]表示密码登录[mfa]表示多因素认证access_token解析资源访问凭证{ aud: https://graph.microsoft.com, iss: https://login.microsoftonline.com/{tenant-id}/v2.0, iat: 1712345678, nbf: 1712345678, exp: 1712349278, acct: 0, acr: 1, aio: ATQAy/8DAAA..., amr: [pwd], app_displayname: MyApp, appid: YOUR_CLIENT_ID, appidacr: 1, idp: https://sts.windows.net/{tenant-id}/, ipaddr: 192.168.1.100, oid: 12345678-1234-1234-1234-1234567890ab, platf: 5, puid: 1003BFFD..., scp: User.Read profile openid email, signin_state: [kmsi], sub: 12345678-1234-1234-1234-1234567890ab, tid: 12345678-1234-1234-1234-1234567890ab, uti: abcd1234..., ver: 2.0 }aud受众Audience表明此token只能用于调用https://graph.microsoft.comAPIscp作用域Scopes列出用户授权的具体权限如User.Readoid/tid与id_token一致可用于关联用户与租户实操心得不要用第三方JWT库直接解析token微软的JWT signature使用RSA256算法但公钥需从https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys动态获取。生产环境必须实现JWKSJSON Web Key Set轮询机制否则公钥过期会导致验签失败。简单项目可用微软官方MSAL库自动处理。3.6 第六步调用Microsoft Graph API实战案例拿到access_token后即可调用Graph API。以获取用户邮箱为例curl -X GET \ https://graph.microsoft.com/v1.0/me?$selectdisplayName,mail,userPrincipalName \ -H Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtyaU1Q... \ -H Content-Type: application/json响应示例{ odata.context: https://graph.microsoft.com/v1.0/$metadata#users(displayName,mail,userPrincipalName)/$entity, displayName: John Smith, mail: johncontoso.com, userPrincipalName: johncontoso.com }关键注意事项所有Graph API请求必须带Authorization: Bearer access_token头access_token有效期默认1小时过期后需用refresh_token刷新见下节如果返回401 Unauthorized先检查token是否过期若未过期检查App Registration中是否已为该API权限“授予管理员同意”Admin consentrefresh_token有效期默认90天但每次用它换新token时微软会发放新的refresh_token旧的立即失效3.7 第七步refresh_token刷新机制避免用户重复登录access_token过期后不应让用户重新走一遍授权流程。正确做法是用refresh_token换新token// POST https://login.microsoftonline.com/common/oauth2/v2.0/token const refreshTokenResponse await axios.post( https://login.microsoftonline.com/common/oauth2/v2.0/token, new URLSearchParams({ client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, refresh_token: storedRefreshToken, grant_type: refresh_token, scope: User.Read // 必须与初始scope一致 }), { headers: { Content-Type: application/x-www-form-urlencoded } } );refresh_token生命周期规则每次成功刷新微软返回新的access_token和新的refresh_token旧refresh_token立即作废如果refresh_token超过90天未使用微软会自动使其失效如果用户在Azure Portal中“撤消应用访问权限”所有关联的refresh_token立即失效移动端应用应每7天至少刷新一次避免token链断裂常见问题为什么refresh_token换token失败90%的情况是scope不匹配。例如初始授权时请求scopeUser.Read Mail.Read但刷新时只传scopeUser.Read微软会返回invalid_grant。解决方案始终传入与初始授权完全相同的scope字符串。4. 实操过程全记录从零开始部署一个可运行的Web登录Demo4.1 环境准备本地开发必备工具链我用Node.js 18.x Express React搭建一个最小可行Demo全程在本地http://localhost:3000运行微软允许localhost作为重定向URI。依赖安装npm init -y npm install express axios cors dotenv npm install --save-dev nodemon项目结构microsoft-oauth-demo/ ├── server/ │ ├── index.js # Express后端 │ └── config.js # 配置文件 ├── client/ │ ├── public/ │ │ └── index.html # 前端入口 │ └── src/ │ ├── App.js # 主组件 │ └── auth.js # 认证逻辑 └── .env # 环境变量.env配置从Azure Portal复制CLIENT_ID12345678-1234-1234-1234-1234567890ab CLIENT_SECRETyour-client-secret-here TENANT_IDcommon # 或具体租户ID4.2 后端实现Express服务完整代码server/index.jsconst express require(express); const axios require(axios); const cors require(cors); require(dotenv).config(); const app express(); const PORT 5000; // 中间件 app.use(cors({ origin: http://localhost:3000, credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(../client/public)); // 内存存储生产环境替换为Redis const sessions new Map(); // 生成随机state function generateState() { return Math.random().toString(36).substring(2, 15) Math.random().toString(36).substring(2, 15); } // /auth/login 路由生成授权URL并重定向 app.get(/auth/login, (req, res) { const state generateState(); const authUrl new URL(https://login.microsoftonline.com/common/oauth2/v2.0/authorize); authUrl.searchParams.set(client_id, process.env.CLIENT_ID); authUrl.searchParams.set(response_type, code); authUrl.searchParams.set(redirect_uri, http://localhost:5000/auth/callback); authUrl.searchParams.set(scope, openid profile User.Read email); authUrl.searchParams.set(state, state); authUrl.searchParams.set(prompt, login); // 强制重新登录跳过SSO // 存储state到内存生产环境用Redis sessions.set(state, { createdAt: Date.now() }); res.redirect(authUrl.toString()); }); // /auth/callback 路由处理授权回调 app.get(/auth/callback, async (req, res) { const { code, state } req.query; // 校验state if (!sessions.has(state)) { return res.status(400).send(Invalid state); } sessions.delete(state); // 一次性使用 try { // 换token const tokenResponse await axios.post( https://login.microsoftonline.com/common/oauth2/v2.0/token, new URLSearchParams({ client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, code: code, redirect_uri: http://localhost:5000/auth/callback, grant_type: authorization_code, scope: openid profile User.Read email }), { headers: { Content-Type: application/x-www-form-urlencoded } } ); const { access_token, refresh_token, id_token, expires_in } tokenResponse.data; // 解析id_token获取用户信息 const idPayload JSON.parse(Buffer.from(id_token.split(.)[1], base64url).toString()); // 创建响应CookieHTTP-onlySecure res.cookie(access_token, access_token, { httpOnly: true, secure: false, // localhost不需HTTPS maxAge: expires_in * 1000, sameSite: lax }); res.cookie(user_info, JSON.stringify({ name: idPayload.name, email: idPayload.email || idPayload.preferred_username, oid: idPayload.oid }), { httpOnly: true, secure: false, maxAge: expires_in * 1000, sameSite: lax }); res.redirect(http://localhost:3000/dashboard); } catch (error) { console.error(Token exchange error:, error.response?.data); res.status(500).send(Login failed); } }); // /api/user 路由受保护的API需验证token app.get(/api/user, (req, res) { const token req.cookies.access_token; if (!token) { return res.status(401).json({ error: Unauthorized }); } // 这里应验证JWT签名Demo中简化处理 try { const payload JSON.parse(Buffer.from(token.split(.)[1], base64url).toString()); res.json({ name: payload.name, email: payload.email || payload.preferred_username, oid: payload.oid }); } catch (e) { res.status(401).json({ error: Invalid token }); } }); app.listen(PORT, () { console.log(Server running on http://localhost:${PORT}); });4.3 前端实现React登录按钮与用户展示client/src/App.jsimport React, { useState, useEffect } from react; import ./App.css; function App() { const [user, setUser] useState(null); const [loading, setLoading] useState(false); useEffect(() { // 页面加载时检查登录状态 const checkAuth async () { try { const response await fetch(http://localhost:5000/api/user); if (response.ok) { const userData await response.json(); setUser(userData); } } catch (error) { console.error(Check auth failed:, error); } }; checkAuth(); }, []); const handleLogin () { setLoading(true); window.location.href http://localhost:5000/auth/login; }; const handleLogout () { document.cookie access_token; expiresThu, 01 Jan 1970 00:00:00 UTC; path/;; document.cookie user_info; expiresThu, 01 Jan 1970 00:00:00 UTC; path/;; setUser(null); }; if (loading) return divLoading.../div; return ( div classNameApp header classNameApp-header h1Microsoft OAuth Demo/h1 {user ? ( div pWelcome, {user.name}!/p pEmail: {user.email}/p button onClick{handleLogout}Logout/button /div ) : ( button onClick{handleLogin}Login with Microsoft/button )} /header /div ); } export default App;4.4 启动与调试三步验证流程是否通畅启动服务# 终端1启动后端 cd server npm start # 终端2启动前端假设已用create-react-app cd client npm start首次访问打开http://localhost:3000点击“Login with Microsoft”跳转到微软登录页。抓包验证关键节点用Chrome DevTools Network标签过滤login.microsoftonline.com确认授权请求URL包含response_typecode和正确scope登录成功后观察重定向到http://localhost:5000/auth/callback?codexxxstateyyy确认code参数存在查看后端日志确认token exchange successful且返回的access_token长度约1200字符JWT标准长度验证token有效性复制access_token用curl调用Graph APIcurl -H Authorization: Bearer YOUR_TOKEN https://graph.microsoft.com/v1.0/me应返回用户基本信息而非401错误。实操心得如果卡在/auth/callback400错误90%是redirect_uri不匹配。打开Azure Portal → App Registration → Authentication → Redirect URIs确认填的是http://localhost:5000/auth/callback注意端口5000不是3000。前端跳转到后端路由不是直接到React路由。5. 常见问题与排查技巧实录那些让我熬过三个通宵的错误5.1 错误代码速查表按出现频率排序错误码错误消息根本原因解决方案invalid_requestThe provided code_challenge_method is not supported.Android WebView发起请求时未带code_challenge_methodS256检查授权URL是否包含code_challenge_methodS256unauthorized_clientAADSTS700016: Application with identifier xxx was not found in the directory common.client_id错误或应用注册在特定租户而非common确认App Registration的“
返回列表