FHIR沙箱获取Access Token时返回有效Token却报invalid_grant错误求助
单页应用请求FHIR Sandbox Access Token时,同时返回有效Token和
invalid_grant错误的原因分析 问题场景
我正在开发一个无Client Secret的独立Web应用,通过FHIR Sandbox的OAuth2授权流程获取Access Token。在授权重定向页面中,我从URL提取code和state参数,发起POST请求换取Token。
实现代码
export default function Redirect() { // const router = useRouter(); const searchParams = useSearchParams(); useEffect(() => { const code = searchParams.get("code"); const state = searchParams.get("state"); const clientId = "ea9b08eb-030c-41e5-b24b-e4b95ce068e5"; const redirectUri = "http://localhost:3000/redirect"; if (code) { console.log("code: ", code); } if (state) { console.log("state: ", state); } if (code && state) { const body = `grant_type=${encodeURIComponent( "authorization_code" )}&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent( redirectUri )}&client_id=${encodeURIComponent(clientId)}`; fetch("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body, }) .then((response) => { response.json().then((data) => { if (!response.ok) { console.error("Response Error Data: ", data); throw new Error(`HTTP error! Status: ${response.status}`); } console.log("Access Token Data: ", data); }); }) .catch((error) => { console.error("Error:", error); }); } }, [searchParams]); }
异常现象
控制台同时输出了有效的Access Token数据:
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1cm46b2lkOmZoaXIiLCJjbGllbnRfaWQiOiJlYTliMDhlYi0wMzBjLTQxZTUtYjI0Yi1lNGI5NWNlMDY4ZTUiLCJlcGljLmVjaSI6InVybjplcGljOk9wZW4uRXBpYy1jdXJyZW50IiwiZXBpYy5tZXRhZGF0YSI6InRiTjlBMk1iT090OXlaZXM5S2ZZY3ZfdFB4XzFNRWQwaktTRlU4YXhnUEdrX2owRzVvV0xibmlOWVg0TFNHT090YnN4OENUVl95d1NSeDhQckNyd2Z0WlZvb0d6cEhIWElfTVU5LUl2bEdoNldzNnhYVXE3RmFIZEEwSXVodDdmIiwiZXBpYy50b2tlbnR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3MjAwNDE2NDcsImlhdCI6MTcyMDAzODA0NywiaXNzIjoidXJuOm9pZDpmaGlyIiwianRpIjoiMmY0M2U2NDItMTc0OS00YjM4LWJlOGQtMjYxOGY4OGMwODgxIiwibmJmIjoxNzIwMDM4MDQ3LCJzdWIiOiJlM01CWENPbWNvTEtsN2F5TEQ1MUFXQTMifQ.OLeMi-B9QWR30fMLHJlZblEUKKYZe5UEnUgvD98Gmzq2B50m1Fofpz_ecVoS0sIGba75eFLw2JX6frEbhN1IPnQAybG0PV8aT1gM7RUSmERVRaw_ZOKJ3e6m8QtiBVbD8yjUH_UIRcMBhXJi3XHNf0KS0YqC1NcYFV8zfJ_xaUvYf4XrgUVAg-5CTNeElocYJ5cH67gn6TMxFOyxSgNEqBECNX-cMAHKAsSTLeUZLaiTOxOHLrWArSW__OuuxK5FGrlS6tHP0qKz1qX2SZUGnfoYDlR5BwDkx87W425aK_15r_7ZcMhxXCsom69gJvp46qAo8X8a1VYVP6ANKOetpg", "token_type": "Bearer", "expires_in": 3600, "scope": "system/DynamicClient.register user/Binary.read user/CarePlan.read user/CareTeam.read user/Communication.read user/Condition.read", "state": "abc123" }
以及**invalid_grant错误信息**:
{ "error": "invalid_grant", "error_description": null }
原因分析
这种矛盾现象的核心原因是:OAuth2授权码(code)是一次性凭证,一旦用它成功换取过Access Token,再次使用同一个code发起请求就会返回invalid_grant错误。
结合代码来看,问题出在useEffect的依赖项是searchParams:当页面加载时,searchParams会触发useEffect执行一次,但如果页面因为组件重渲染、路由监听触发等原因,导致useEffect重复执行,就会用同一个code发起多次Token请求。第一次请求成功拿到Token,第二次请求因为code已被消耗,就会返回错误。
解决方法
添加状态标记,确保同一个code只被使用一次:
export default function Redirect() { const searchParams = useSearchParams(); const [isTokenFetched, setIsTokenFetched] = useState(false); useEffect(() => { const code = searchParams.get("code"); const state = searchParams.get("state"); const clientId = "ea9b08eb-030c-41e5-b24b-e4b95ce068e5"; const redirectUri = "http://localhost:3000/redirect"; // 仅当code、state存在且未发起过请求时执行 if (code && state && !isTokenFetched) { setIsTokenFetched(true); // 标记已发起请求,避免重复执行 const body = `grant_type=${encodeURIComponent( "authorization_code" )}&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent( redirectUri )}&client_id=${encodeURIComponent(clientId)}`; fetch("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body, }) .then((response) => { return response.json().then((data) => { if (!response.ok) { console.error("Response Error Data: ", data); throw new Error(`HTTP error! Status: ${response.status}`); } console.log("Access Token Data: ", data); }); }) .catch((error) => { console.error("Error:", error); setIsTokenFetched(false); // 请求失败时重置标记,允许重试 }); } }, [searchParams, isTokenFetched]); }
同时建议检查页面是否存在不必要的重复渲染逻辑,比如多余的路由监听、状态更新触发的组件重挂载等,从根源避免重复请求。
内容的提问来源于stack exchange,提问作者Lance A. MD
相关产品推荐
相关产品推荐

