React应用对接Authority Server的PKCE认证无限刷新问题求助
React PKCE认证无限刷新问题排查
我正在为基础React模板对接权限服务器实现PKCE认证,虽对React不太熟悉,但已集成必要组件。启动应用后登录页正常显示,输入凭证点击登录后跳转至主页,但页面开始无限刷新,仅观察到URL参数中的code持续变化。
实现代码
1. 认证管理AuthWrapper组件
// index.js ... <AuthWrapper> <App /> </AuthWrapper> ...
AuthWrapper定义:
import { useNavigate } from 'react-router-dom'; import { AuthProvider } from './AuthContext'; import PropTypes from 'prop-types'; const AuthWrapper = ({ children }) => { const navigate = useNavigate(); return ( <AuthProvider navigate={navigate}> {children} </AuthProvider> ); }; export default AuthWrapper;
2. AuthProvider(code_verifier暂硬编码)
import React, { createContext, useContext, useState } from "react"; import {generateCodeChallenge} from "utils/authutils"; import PropTypes from 'prop-types'; const AuthContext = createContext(); export const useAuth = () => useContext(AuthContext); export const AuthProvider = ({ children, navigate }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const codeChallenge = generateCodeChallenge('dce35c1f-194d-48c4-bd90-6f14e9042023'); const login = () => { window.location.href = "https://localhost/Identity/authorize" + "?client_id=web_client" + "&redirect_uri=http://localhost:3000/authentication/callback" + "&response_type=code" + "&scope=web_scope offline_access" + "&state=123" + `&code_challenge=${codeChallenge}` + "&code_challenge_method=S256"; }; const handleLoginCallback = () => { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); setIsAuthenticated(true); navigate("/dashboard"); }; return ( <AuthContext.Provider value={{ isAuthenticated, login, handleLoginCallback }}> {children} </AuthContext.Provider> ); };
3. 回调页面CallbackPage
import React, { useEffect } from "react"; import { useLocation } from "react-router-dom"; import { useNavigate } from 'react-router-dom'; import { exchangeAuthorizationCode } from "services/api"; const CallbackPage = () => { const location = useLocation(); const navigate = useNavigate(); useEffect(() => { const searchParams = new URLSearchParams(location.search); const authorizationCode = searchParams.get("code"); if (authorizationCode) { exchangeAuthorizationCode(authorizationCode).then((accessToken) => { localStorage.setItem("accessToken", accessToken); navigate("/dashboard"); }); } else { navigate("/error"); } }, [location.search, navigate]); return ( <div> <p>Redirecting...</p> </div> ); }; export default CallbackPage;
4. API请求文件api.js
export const exchangeAuthorizationCode = async (authorizationCode, codeVerifier) => { const requestBody = { grant_type: "authorization_code", code: authorizationCode, redirect_uri: "http://localhost:3000/authentication/callback", code_verifier: 'dce35c1f-194d-48c4-bd90-6f14e9042023', client_id: "web_client" }; try { const response = await fetch("https://localhost/Identity/token", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error("Failed to exchange authorization code for access token"); } const data = await response.json(); return data.access_token; } catch (error) { console.error("Error exchanging authorization code:", error.message); throw error; } };
5. App页面重定向逻辑
... import { useAuth } from "AuthContext"; export default function App() { ... const { isAuthenticated, login } = useAuth(); const { pathname } = useLocation(); useEffect(() => { if (!isAuthenticated && pathname !== "/login") { login(); // 重定向到权限服务器的方法 } }, [isAuthenticated, pathname, login]); ... const getRoutes = (allRoutes) => allRoutes.map((route) => { if (route.collapse) { return getRoutes(route.collapse); } if (route.route) { return <Route exact path={route.route} element={route.component} key={route.key} />; } return null; }); ... return ( <ThemeProvider theme={darkMode ? themeDark : theme}> <CssBaseline /> <Routes> {getRoutes(routes)} <Route path="*" element={<Navigate to="/dashboard" />} /> </Routes> </ThemeProvider> ); }
问题原因及修复方案
核心问题
- 认证状态未同步更新:回调页获取到
accessToken后仅存入localStorage,未通知AuthProvider更新isAuthenticated状态,导致App组件检测到未认证,再次触发登录跳转,权限服务器返回新的code,形成循环。 - 回调页未排除在登录触发逻辑外:App组件的useEffect未排除回调页路径,在code兑换过程中仍会触发登录,重复发起认证请求。
- code_verifier硬编码:PKCE要求每次认证请求生成新的code_verifier,硬编码会导致重复使用失效的验证值,引发认证错误。
修复步骤
1. 同步更新认证状态
修改AuthProvider,添加状态更新方法并初始化时从localStorage读取状态:
export const AuthProvider = ({ children, navigate }) => { // 初始化时从localStorage读取,避免页面刷新后状态丢失 const [isAuthenticated, setIsAuthenticated] = useState(() => !!localStorage.getItem("accessToken")); const codeChallenge = generateCodeChallenge('dce35c1f-194d-48c4-bd90-6f14e9042023'); const login = () => { window.location.href = "https://localhost/Identity/authorize" + "?client_id=web_client" + "&redirect_uri=http://localhost:3000/authentication/callback" + "&response_type=code" + "&scope=web_scope offline_access" + "&state=123" + `&code_challenge=${codeChallenge}` + "&code_challenge_method=S256"; }; const handleLoginCallback = () => { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); setIsAuthenticated(true); navigate("/dashboard"); }; // 添加状态更新方法 const setAuthenticated = (status) => { setIsAuthenticated(status); }; return ( <AuthContext.Provider value={{ isAuthenticated, login, handleLoginCallback, setAuthenticated }}> {children} </AuthContext.Provider> ); };
在CallbackPage中调用状态更新方法:
import { useAuth } from "AuthContext"; const CallbackPage = () => { const location = useLocation(); const navigate = useNavigate(); const { setAuthenticated } = useAuth(); useEffect(() => { const searchParams = new URLSearchParams(location.search); const authorizationCode = searchParams.get("code"); if (authorizationCode) { exchangeAuthorizationCode(authorizationCode).then((accessToken) => { localStorage.setItem("accessToken", accessToken); setAuthenticated(true); // 更新认证状态 navigate("/dashboard"); }).catch(() => { // 处理code兑换失败,跳转回登录页 navigate("/login"); }); } else { navigate("/error"); } }, [location.search, navigate, setAuthenticated]); return ( <div> <p>Redirecting...</p> </div> ); };
2. 排除回调页的登录触发逻辑
修改App组件的useEffect,避免在回调流程中触发登录:
useEffect(() => { // 排除回调页,防止code兑换过程中重复触发登录 if (!isAuthenticated && pathname !== "/login" && pathname !== "/authentication/callback") { login(); } }, [isAuthenticated, pathname, login]);
3. 动态生成code_verifier
替换硬编码的code_verifier,每次登录时生成新值并存入sessionStorage,兑换时取出使用:
// AuthProvider中修改login方法 const login = () => { const codeVerifier = crypto.randomUUID(); // 生成随机code_verifier const codeChallenge = generateCodeChallenge(codeVerifier); sessionStorage.setItem("code_verifier", codeVerifier); // 存储到sessionStorage window.location.href = "https://localhost/Identity/authorize" + "?client_id=web_client" + "&redirect_uri=http://localhost:3000/authentication/callback" + "&response_type=code" + "&scope=web_scope offline_access" + "&state=123" + `&code_challenge=${codeChallenge}` + "&code_challenge_method=S256"; };
修改api.js中的兑换逻辑:
export const exchangeAuthorizationCode = async (authorizationCode) => { const codeVerifier = sessionStorage.getItem("code_verifier"); if (!codeVerifier) throw new Error("Code verifier not found"); const requestBody = { grant_type: "authorization_code", code: authorizationCode, redirect_uri: "http://localhost:3000/authentication/callback", code_verifier: codeVerifier, client_id: "web_client" }; try { const response = await fetch("https://localhost/Identity/token", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error("Failed to exchange authorization code for access token"); } const data = await response.json(); sessionStorage.removeItem("code_verifier"); // 兑换后删除 return data.access_token; } catch (error) { console.error("Error exchanging authorization code:", error.message); throw error; } };
内容的提问来源于stack exchange,提问作者Sergio Hernández
相关产品推荐
相关产品推荐

