You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题排查与解决方案

核心问题分析

  1. 异步状态更新导致打印旧值:AuthProvider中调用setUserFromContext(user)后立刻打印userFromContext,React状态更新是异步的,此时拿到的还是初始的null。
  2. 回调页面未完成认证流程:认证跳转回/oidcCallback后,没有调用signinCallback完成认证并存储用户信息,导致getUser()始终返回null。
  3. UserManager实例重复创建:useAuthService每次渲染都会新建UserManager,无法共享用户状态。
  4. 上下文类型定义错误: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 10:42:01