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

如何在React(NextJS)中无用户流调用Azure AD B2C API实现自定义UI登录注册

在Next.js中实现自定义UI调用Azure AD B2C API完成注册与登录

一、Azure AD B2C 前期配置

  • 启用ROPC(资源所有者密码凭据)流:登录Azure门户,进入你的B2C租户,在「应用注册」中选中目标Next.js应用,在「认证」模块添加ROPC流(注意该流仅支持本地账户,不兼容社交登录)。
  • 配置API权限:为应用添加User.ReadWrite.All(用于创建用户)、openid、offline_access(用于登录获取令牌)权限,并完成管理员同意授权。
  • 记录核心参数:租户ID、客户端ID、ROPC用户流名称(如B2C_1_ROPC_Auth)、Graph API端点(https://graph.microsoft.com/v1.0)、B2C令牌端点(https://<租户名>.b2clogin.com/<租户名>.onmicrosoft.com/<用户流名称>/oauth2/v2.0/token)。

二、注册功能实现(调用Graph API创建用户)

注册逻辑需通过Azure AD Graph API创建本地账户,注意敏感操作必须放在Next.js服务器端API路由,避免泄露客户端密钥:

// app/api/register/route.js
export async function POST(request) {
  const { email, password, firstName, lastName } = await request.json();
  
  // 第一步:获取Graph API应用权限令牌
  const tokenRes = await fetch('https://<租户名>.b2clogin.com/<租户名>.onmicrosoft.com/oauth2/v2.0/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: '<你的客户端ID>',
      scope: 'https://graph.microsoft.com/.default',
      client_secret: '<你的客户端密钥>',
      grant_type: 'client_credentials'
    })
  });
  const tokenData = await tokenRes.json();
  const graphToken = tokenData.access_token;

  // 第二步:调用Graph API创建B2C用户
  const createUserRes = await fetch('https://graph.microsoft.com/v1.0/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${graphToken}`
    },
    body: JSON.stringify({
      accountEnabled: true,
      displayName: `${firstName} ${lastName}`,
      mailNickname: email.split('@')[0],
      userPrincipalName: `${email}@<租户名>.onmicrosoft.com`,
      passwordProfile: {
        forceChangePasswordNextSignIn: false,
        password: password
      },
      passwordPolicies: "DisablePasswordExpiration"
    })
  });

  if (!createUserRes.ok) {
    const error = await createUserRes.json();
    return new Response(JSON.stringify({ error: error.error.message }), { status: 400 });
  }

  return new Response(JSON.stringify({ success: true }), { status: 201 });
}

三、登录功能实现(调用ROPC流获取令牌)

登录直接通过ROPC流调用B2C令牌端点,获取身份令牌与访问令牌:

// 前端组件中的登录逻辑
async function loginUser(email, password) {
  const tokenRes = await fetch('https://<租户名>.b2clogin.com/<租户名>.onmicrosoft.com/<ROPC用户流名称>/oauth2/v2.0/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: '<你的客户端ID>',
      scope: 'openid offline_access https://<租户名>.onmicrosoft.com/<你的API>/access_as_user',
      username: email,
      password: password,
      grant_type: 'password'
    })
  });

  const tokenData = await tokenRes.json();
  if (tokenData.error) throw new Error(tokenData.error_description);

  // 建议将令牌存储到HttpOnly Cookie(需配合服务器端处理),避免XSS风险
  localStorage.setItem('idToken', tokenData.id_token);
  localStorage.setItem('accessToken', tokenData.access_token);
  localStorage.setItem('refreshToken', tokenData.refresh_token);
  
  return tokenData;
}

注意:ROPC流不支持多因素认证(MFA),若租户启用MFA该流会失效;令牌存储优先选择HttpOnly Cookie,而非localStorage。

四、全局弹窗组件集成

创建可复用的登录/注册弹窗组件,嵌入任意页面:

'use client';

import { useState } from 'react';

export default function AuthModal({ isOpen, onClose }) {
  const [isRegisterMode, setIsRegisterMode] = useState(false);
  const [formData, setFormData] = useState({ email: '', password: '', firstName: '', lastName: '' });
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    try {
      if (isRegisterMode) {
        const res = await fetch('/api/register', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(formData)
        });
        if (!res.ok) {
          const errData = await res.json();
          throw new Error(errData.error);
        }
        setIsRegisterMode(false);
      } else {
        await loginUser(formData.email, formData.password);
        onClose();
        // 登录成功后更新全局用户状态(如通过Context或NextAuth)
      }
    } catch (err) {
      setError(err.message);
    }
  };

  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
      <div className="bg-white p-6 rounded-lg w-96">
        <button onClick={onClose} className="float-right text-gray-500 hover:text-gray-700">×</button>
        <h2 className="text-xl font-bold mb-4">{isRegisterMode ? '注册账户' : '登录'}</h2>
        {error && <p className="text-red-500 mb-4">{error}</p>}
        <form onSubmit={handleSubmit}>
          {isRegisterMode && (
            <>
              <input
                type="text"
                placeholder="名"
                value={formData.firstName}
                onChange={(e) => setFormData({...formData, firstName: e.target.value})}
                required
                className="block w-full mb-3 p-2 border rounded"
              />
              <input
                type="text"
                placeholder="姓"
                value={formData.lastName}
                onChange={(e) => setFormData({...formData, lastName: e.target.value})}
                required
                className="block w-full mb-3 p-2 border rounded"
              />
            </>
          )}
          <input
            type="email"
            placeholder="邮箱"
            value={formData.email}
            onChange={(e) => setFormData({...formData, email: e.target.value})}
            required
            className="block w-full mb-3 p-2 border rounded"
          />
          <input
            type="password"
            placeholder="密码"
            value={formData.password}
            onChange={(e) => setFormData({...formData, password: e.target.value})}
            required
            className="block w-full mb-3 p-2 border rounded"
          />
          <button type="submit" className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600">
            {isRegisterMode ? '完成注册' : '登录'}
          </button>
        </form>
        <p className="mt-4 text-sm">
          {isRegisterMode ? '已有账号?' : '还没有账号?'}
          <button onClick={() => setIsRegisterMode(!isRegisterMode)} className="text-blue-500 ml-1">
            {isRegisterMode ? '立即登录' : '去注册'}
          </button>
        </p>
      </div>
    </div>
  );
}

在任意页面中调用弹窗:

'use client';

import { useState } from 'react';
import AuthModal from '@/components/AuthModal';

export default function Home() {
  const [showAuthModal, setShowAuthModal] = useState(false);

  return (
    <div className="p-8">
      <button onClick={() => setShowAuthModal(true)} className="px-4 py-2 bg-blue-500 text-white rounded">
        打开登录/注册弹窗
      </button>
      <AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} />
    </div>
  );
}

五、后续优化与安全注意事项

  • 令牌刷新:使用Refresh Token定期调用B2C令牌端点获取新的Access Token,避免用户频繁登录。
  • 用户状态管理:结合NextAuth.js或自定义Context实现全局用户状态同步,服务器端组件可通过Cookie验证用户身份。
  • 安全加固:
    • 客户端密钥仅在服务器端使用,绝对不能暴露在前端代码中。
    • 令牌优先存储到HttpOnly Cookie,减少XSS攻击风险。
    • 对用户输入做严格校验,避免注入攻击。
    • ROPC流仅适用于信任的客户端,公开面向用户的应用需评估安全风险。

内容的提问来源于stack exchange,提问作者Nigel Thomas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 08:25:12