React/TS项目中如何用oidc-client-ts获取用户信息?
React/TS项目中oidc-client-ts无法获取用户信息问题
我正在开发一个React/TypeScript项目,使用oidc-client-ts库管理用户身份认证。目前已编写包含oidcSettings的AuthService服务,由AuthProvider上下文调用该服务。我能完成认证流程,但代码中的console.log始终输出null,无法获取用户的uid、access_token、email等信息。我是React新手,推测可能在上下文管理上出了问题,但找不到具体原因,希望得到帮助。
注:oidcSettings已和同事核对并通过Postman测试,配置无误。我尝试过多次修改代码,也在网上搜索并尝试示例,但都没解决。
相关组件源码
main.tsx
import { ThemeProvider } from '@emotion/react' import { CssBaseline } from '@mui/material' import React from 'react' import * as ReactDOMClient from 'react-dom/client' import theme from './theme' import { AuthProvider } from './hooks/AuthProvider' import { BrowserRouter } from 'react-router-dom' import Routes from './Routes' ReactDOMClient.createRoot( document.getElementById('root') as HTMLElement ).render( <AuthProvider> <BrowserRouter> <ThemeProvider theme={theme}> <CssBaseline /> <React.StrictMode> <Routes /> </React.StrictMode> </ThemeProvider> </BrowserRouter> </AuthProvider> )
Routes.tsx
import { Route, Routes as RouterRoutes } from 'react-router-dom' import PublicPage from './components/PublicPage' import ProtectedPage from './components/ProtectedPage' const Routes = () => { return ( <RouterRoutes> <Route path="/" element={<PublicPage />} /> // **包含LoginButton组件** <Route path="/oidcCallback" element={<ProtectedPage />} /> </RouterRoutes> ) } export default Routes
useAuth.tsx
import { useContext } from 'react' import { AuthContext } from './AuthProvider' const useAuth = () => { return useContext(AuthContext) } export default useAuth
AuthService.ts
import { User, UserManager, WebStorageStateStore } from 'oidc-client-ts' import { useEffect, useMemo, useState } from 'react' export interface AuthServiceProps { getUser(): Promise<User | null> login(): Promise<void> logout(): Promise<void> renewToken(): Promise<User | null> signinCallback(): Promise<User> } const useAuthService = (): AuthServiceProps => { const oidcSettings = { authority: 'https://...', client_id: 'myClientId', client_secret: 'mySecret', redirect_uri: 'http://...', response_type: 'code', scope: 'otherScope profile openid email', loadUserInfo: true, userStore: new WebStorageStateStore({ store: window.localStorage }), metadata: { authorization_endpoint: 'https://...', issuer: 'https://...', token_endpoint: 'https://...', userinfo_endpoint: 'https://...', }, } const userManager = new UserManager(oidcSettings) const getUser = (): Promise<User | null> => { return userManager.getUser() } const login = (): Promise<void> => { return userManager.signinRedirect() } const logout = (): Promise<void> => { return userManager.signoutRedirect() } const renewToken = (): Promise<User | null> => { return userManager.signinSilent() } const signinCallback = (): Promise<User> => { return userManager.signinRedirectCallback() } useEffect(() => { getUser().then(user => { console.log('通过AuthService.getUSer()获取的用户信息: ', user) // null }) }, []) return { getUser, login, logout, renewToken, signinCallback, } } export default useAuthService
AuthProvider.tsx
import { ReactElement, createContext, useEffect, useMemo, useState, } from 'react' import useAuthService from '../AuthService' import { User } from 'oidc-client-ts' interface AuthContextProps { userFromContext: User | null getUserFromContext: () => void loginFromContext: () => void logoutFromContext: () => void renewTokenFromContext: () => void signinCallbackFromContext: () => void } interface AuthProviderProps { children: ReactElement } const AuthContext = createContext<AuthContextProps>({ userFromContext: null, getUserFromContext: () => {}, loginFromContext: () => {}, logoutFromContext: () => {}, renewTokenFromContext: () => {}, signinCallbackFromContext: () => {}, }) const AuthProvider = ({ children }: AuthProviderProps) => { const [userFromContext, setUserFromContext] = useState<User | null>(null) const { getUser, login, logout, renewToken, signinCallback } = useAuthService() const getUserFromContext = (): Promise<User | null> => { return getUser() } const loginFromContext = (): Promise<void> => { return login() } const logoutFromContext = (): Promise<void> => { return logout() } const renewTokenFromContext = (): Promise<User | null> => { return renewToken() } const signinCallbackFromContext = (): Promise<User> => { return signinCallback() } useEffect(() => { getUserFromContext().then(user => { setUserFromContext(user) console.log('通过AuthProvider获取的userFromContext: ', userFromContext) // null }) }, []) return ( <AuthContext.Provider value={{ userFromContext, getUserFromContext, loginFromContext, logoutFromContext, renewTokenFromContext, signinCallbackFromContext, }} > {children} </AuthContext.Provider> ) } export { AuthContext, AuthProvider }
LoginButton.tsx
import useAuth from '../hooks/useAuth' const Login = () => { const { loginFromContext } = useAuth() const handleClick = () => { loginFromContext() } return <button onClick={handleClick}>登录</button> } export default Login
问题排查与解决方案
核心问题分析
- 异步状态更新导致打印旧值:AuthProvider中调用
setUserFromContext(user)后立刻打印userFromContext,React状态更新是异步的,此时拿到的还是初始的null。 - 回调页面未完成认证流程:认证跳转回
/oidcCallback后,没有调用signinCallback完成认证并存储用户信息,导致getUser()始终返回null。 - UserManager实例重复创建:
useAuthService每次渲染都会新建UserManager,无法共享用户状态。 - 上下文类型定义错误:
getUserFromContext的类型定义为() => void,但实际返回Promise,导致类型不匹配。
具体修复步骤
1. 修复状态更新后的打印问题
将打印逻辑改为直接输出返回的user,或新增监听状态变化的useEffect:
// AuthProvider.tsx useEffect(() => { getUserFromContext().then(user => { setUserFromContext(user) console.log('获取到的用户信息: ', user) // 直接打印接口返回的用户数据 }) }, []) // 新增监听状态变化的useEffect,打印最新状态 useEffect(() => { console.log('当前上下文用户信息: ', userFromContext) }, [userFromContext])
2. 在回调页面完成认证流程
修改ProtectedPage组件,挂载时调用signinCallback并更新上下文用户:
// ProtectedPage.tsx import useAuth from '../hooks/useAuth' import { useEffect } from 'react' const ProtectedPage = () => { const { signinCallbackFromContext, setUserFromContext } = useAuth() useEffect(() => { signinCallbackFromContext() .then(user => { setUserFromContext(user) window.location.href = '/' // 认证完成后跳转到主页 }) .catch(err => console.error('认证回调失败: ', err)) }, []) return <div>认证中...</div> } export default ProtectedPage
同时更新AuthContext的类型定义,添加状态更新方法并修正返回类型:
// AuthProvider.tsx interface AuthContextProps { userFromContext: User | null setUserFromContext: (user: User | null) => void getUserFromContext: () => Promise<User | null> loginFromContext: () => Promise<void> logoutFromContext: () => Promise<void> renewTokenFromContext: () => Promise<User | null> signinCallbackFromContext: () => Promise<User> } // 更新默认上下文值 const AuthContext = createContext<AuthContextProps>({ userFromContext: null, setUserFromContext: () => {}, getUserFromContext: async () => null, loginFromContext: async () => {}, logoutFromContext: async () => {}, renewTokenFromContext: async () => null, signinCallbackFromContext: async () => ({} as User), })
3. 缓存UserManager实例
用useMemo缓存UserManager,避免重复创建:
// AuthService.ts const userManager = useMemo(() => { return new UserManager(oidcSettings) }, [])
4. 监听用户状态变化
在useAuthService中添加事件监听,实时同步用户状态:
// AuthService.ts useEffect(() => { // 用户加载完成时触发 const loadedSubscription = userManager.events.addUserLoaded(user => { console.log('用户已加载: ', user) }) // 用户注销时触发 const unloadedSubscription = userManager.events.addUserUnloaded(() => { console.log('用户已注销') }) return () => { loadedSubscription.unsubscribe() unloadedSubscription.unsubscribe() } }, [userManager])
额外检查点
- 确认
redirect_uri与路由/oidcCallback完全一致。 - 检查浏览器localStorage中是否存在
oidc.user条目,认证成功后该条目应包含用户信息。
内容的提问来源于stack exchange,提问作者Guillaume
相关产品推荐
相关产品推荐

