Next.js 与 Kinde Auth 集成实战教程

232 阅读9分钟

Next.js 与 Kinde Auth 集成实战教程

前言

随着 Web 应用的安全需求不断提高,优秀的身份验证与授权方案变得越来越重要。在本文中,我将分享如何将 Kinde Auth(一个强大的身份验证服务)与 Next.js 无缝集成,打造安全可靠的用户认证系统。通过实际项目案例,我们将深入探讨从基础配置到高级权限控制的完整流程。

为什么选择 Kinde Auth?

Kinde Auth 是一个现代化的身份即服务 (IDaaS) 解决方案,提供了丰富的认证功能:

  • 简单易用的 API 和 SDK
  • 支持多种登录方式(邮箱、社交账号等)
  • 强大的用户管理功能
  • 细粒度的权限控制
  • 全面的安全功能(MFA、密码策略等)

相比自建认证系统,使用 Kinde Auth 能帮助我们节省大量开发时间,同时获得更专业的安全保障。下面让我们开始实战之旅!

一、Kinde 开发者账户设置

1.1 注册 Kinde 账户

首先,我们需要在 Kinde 官网 注册一个开发者账户:

  1. 访问 Kinde 官网并点击"Sign Up"
  2. 填写邮箱、密码等基本信息
  3. 验证邮箱完成注册

1.2 创建组织和应用

注册完成后,我们需要创建一个应用:

  1. 登录后创建一个新组织或使用默认组织
  2. 导航到"Applications"并点击"Create Application"
  3. 选择"Regular Web Application"类型(适用于 Next.js)
  4. 为应用命名(例如"Computer Repair Shop")

1.3 配置应用设置

创建应用后,需要进行一些基本配置:

  1. 配置重定向URL(非常关键):

    • 开发环境:http://localhost:3000/api/auth/kinde_callback
    • 生产环境:https://yourdomain.com/api/auth/kinde_callback
  2. 配置登录后/注销后跳转页面

    • 登录后跳转:http://localhost:3000/dashboard
    • 注销后跳转:http://localhost:3000
  3. 记录重要凭据

    • Client IDClient Secret
    • Issuer URL(通常是 https://your-org.kinde.com

二、项目初始化与配置

2.1 创建 Next.js 项目

如果你还没有 Next.js 项目,可以使用以下命令创建一个:

npx create-next-app@latest my-kinde-app

2.2 安装 Kinde Auth 包

在项目根目录下运行:

npm i @kinde-oss/kinde-auth-nextjs --legacy-peer-deps

注意:目前可能需要使用 --legacy-peer-deps 来解决一些依赖问题

2.3 环境变量配置

在项目根目录创建 .env.local 文件,添加以下配置:

# Kinde Auth 基本配置
KINDE_CLIENT_ID=你的客户端ID
KINDE_CLIENT_SECRET=你的客户端密钥
KINDE_ISSUER_URL=https://你的组织名.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard

# 可选配置
KINDE_AUDIENCE=你的API端点 (如果需要API授权)
KINDE_DEFAULT_SCOPES=openid profile email

2.4 创建 API 路由

Kinde Auth 需要一个专用的 API 路由来处理认证回调。创建以下文件:

// src/app/api/auth/[kindeAuth]/route.ts
import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";

export const GET = handleAuth();

这个路由将处理所有与 Kinde 相关的认证请求,包括登录、注销和回调处理。

三、身份验证组件实现

3.1 创建登录与注册按钮

首先,让我们创建基本的认证按钮组件:

// src/components/auth/AuthButtons.tsx
'use client'
import { 
  LoginLink, 
  RegisterLink, 
  LogoutLink 
} from "@kinde-oss/kinde-auth-nextjs";
import { Button } from "@/components/ui/button";  // 假设使用 shadcn/ui

export function LoginButton() {
  return (
    <Button asChild variant="default">
      <LoginLink>登录</LoginLink>
    </Button>
  );
}

export function RegisterButton() {
  return (
    <Button asChild variant="outline">
      <RegisterLink>注册</RegisterLink>
    </Button>
  );
}

export function LogoutButton() {
  return (
    <Button asChild variant="ghost" size="sm">
      <LogoutLink>退出</LogoutLink>
    </Button>
  );
}

3.2 创建登录/注册页面

接下来,创建一个吸引人的登录注册页面:

// src/app/login/page.tsx
import { LoginButton, RegisterButton } from "@/components/auth/AuthButtons";

export default function AuthPage() {
  return (
    <main className="h-dvh flex flex-col items-center justify-center gap-6 p-4">
      <div className="max-w-md w-full p-8 border rounded-lg shadow-md">
        <h1 className="text-2xl font-bold mb-6 text-center">Computer Repair Shop</h1>
        
        <div className="space-y-4">
          <div className="flex flex-col gap-2">
            <LoginButton />
            <RegisterButton />
          </div>
          
          <div className="relative my-4">
            <div className="absolute inset-0 flex items-center">
              <span className="w-full border-t"></span>
            </div>
            <div className="relative flex justify-center text-xs uppercase">
              <span className="bg-background px-2 text-muted-foreground">
                或使用社交账号
              </span>
            </div>
          </div>
          
          {/* 社交登录按钮可以在这里添加 */}
        </div>
      </div>
    </main>
  );
}

3.3 创建用户仪表盘

现在让我们创建一个简单的仪表盘页面,展示用户信息并实现权限控制:

// src/app/dashboard/page.tsx
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { redirect } from "next/navigation";
import { LogoutButton } from "@/components/auth/AuthButtons";

export default async function Dashboard() {
  const { getUser, isAuthenticated } = getKindeServerSession();
  const user = await getUser();
  const authenticated = await isAuthenticated();
  
  // 未登录用户重定向到登录页
  if (!authenticated) {
    redirect("/login");
  }
  
  return (
    <div className="p-6">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">仪表盘</h1>
        <LogoutButton />
      </div>
      
      <div className="bg-card p-4 rounded-lg shadow">
        <h2 className="font-medium mb-2">欢迎, {user?.given_name || user?.email}</h2>
        <p className="text-muted-foreground">
          邮箱: {user?.email || "未提供"}
        </p>
        {user?.picture && (
          <div className="mt-4">
            <img 
              src={user.picture} 
              alt="Profile" 
              className="w-16 h-16 rounded-full"
            />
          </div>
        )}
      </div>
    </div>
  );
}

现在,只有已登录用户才能访问仪表盘页面,未登录用户会被重定向到登录页。

四、权限控制与用户管理

4.1 添加权限检查中间件

Next.js 中间件是实现全局权限控制的理想选择。创建以下文件:

// src/middleware.ts
import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";
import { NextRequest } from "next/server";

// 定义需要保护的路由
const protectedRoutes = [
  "/dashboard",
  "/tickets",
  "/admin",
];

// 定义需要特定权限的路由
const permissionRoutes = {
  "/admin": "read:admin",  // 需要admin权限才能访问
};

export default withAuth({
  // 在认证逻辑执行前的钩子
  beforeAuth: async (req: NextRequest) => {
    // 可以在这里添加额外的前置逻辑
    return;
  },
  
  // 在认证逻辑执行后的钩子
  afterAuth: async (auth, req) => {
    const path = req.nextUrl.pathname;
    
    // 检查是否为受保护路由
    const isProtectedRoute = protectedRoutes.some(route => 
      path === route || path.startsWith(`${route}/`)
    );
    
    // 未认证用户试图访问受保护路由时重定向到登录页
    if (!auth.isAuthenticated && isProtectedRoute) {
      return auth.redirectToLogin();
    }
    
    // 检查是否有访问特定路由的权限
    if (auth.isAuthenticated) {
      for (const [route, permission] of Object.entries(permissionRoutes)) {
        if ((path === route || path.startsWith(`${route}/`)) && 
            !auth.permissions.includes(permission)) {
          // 没有权限时重定向到无权限页面
          return Response.redirect(new URL('/unauthorized', req.url));
        }
      }
    }
    
    return;
  }
});

// 配置路由匹配规则,排除静态资源和认证API路由
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|api/auth).*)"],
};

4.2 创建客户端权限检查组件

有时我们需要根据用户权限在前端条件性渲染组件,以下是一个实用的权限守卫组件:

// src/components/auth/PermissionGuard.tsx
'use client'
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
import { ReactNode } from "react";

interface PermissionGuardProps {
  permission: string;
  children: ReactNode;
  fallback?: ReactNode;
}

export function PermissionGuard({ permission, children, fallback }: PermissionGuardProps) {
  const { permissions } = useKindeBrowserClient();
  
  // 检查用户是否有指定权限
  const hasPermission = permissions?.includes(permission);
  
  // 无权限时显示备用内容
  if (!hasPermission) {
    return fallback || null;
  }
  
  // 有权限时显示原内容
  return <>{children}</>;
}

4.3 在组件中使用权限守卫

import { PermissionGuard } from "@/components/auth/PermissionGuard";

// 在组件中使用
<PermissionGuard 
  permission="read:admin" 
  fallback={<p>您没有查看此内容的权限</p>}
>
  <AdminPanel />
</PermissionGuard>

五、Kinde 管理后台配置

要充分利用 Kinde 的功能,还需要在 Kinde 管理后台进行一些配置:

5.1 创建自定义用户字段

  1. 登录 Kinde 管理控制台
  2. 导航至 "Users" → "Custom Fields"
  3. 点击 "Add Field" 创建自定义字段,例如:
    • phone_number(电话号码)
    • subscription_tier(订阅等级)
    • company_name(公司名称)

5.2 配置权限与角色

权限和角色是实现细粒度访问控制的基础:

  1. 创建权限:

    • 导航至 "User Management" → "Permissions"
    • 添加权限,如 create:ticketsedit:ticketsadmin:access
  2. 创建角色:

    • 导航至 "User Management" → "Roles"
    • 创建角色,如 "管理员"、"技术支持"、"普通用户" 等
    • 为每个角色分配相应的权限

5.3 定制注册表单

可以自定义用户注册时需要填写的信息:

  1. 导航至 "Authentication" → "Login & Registration"
  2. 配置注册表单,启用需要收集的字段
  3. 设置必填字段和选填字段

5.4 配置社交登录

添加社交帐号登录选项:

  1. 导航至 "Authentication" → "Social Connections"
  2. 启用并配置需要的社交登录提供商,如 Google、GitHub、微信等
  3. 根据各平台要求填写 Client ID 和 Secret

六、访问用户数据和权限

6.1 服务器端获取用户信息

在 Next.js 的服务器组件中,我们可以使用 getKindeServerSession 获取用户信息:

// 在服务器组件中使用
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";

export default async function ProfilePage() {
  const { getUser, getPermissions, getOrganization } = getKindeServerSession();
  
  // 获取用户基本信息
  const user = await getUser();
  
  // 获取用户权限
  const permissions = await getPermissions();
  
  // 获取组织信息(如果启用了组织功能)
  const organization = await getOrganization();
  
  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold">用户资料</h1>
      
      <div className="bg-card p-4 rounded-lg shadow mt-4">
        <h2 className="font-medium mb-2">基本信息</h2>
        <p>姓名: {user?.given_name} {user?.family_name}</p>
        <p>邮箱: {user?.email}</p>
        
        {permissions && permissions.length > 0 && (
          <div className="mt-4">
            <h2 className="font-medium mb-2">您的权限:</h2>
            <ul className="list-disc pl-5">
              {permissions.map((perm) => (
                <li key={perm.id}>{perm.name}</li>
              ))}
            </ul>
          </div>
        )}
        
        {organization && (
          <div className="mt-4">
            <h2 className="font-medium mb-2">组织信息:</h2>
            <p>组织名称: {organization.name}</p>
          </div>
        )}
      </div>
    </div>
  );
}

6.2 客户端检查认证状态

在客户端组件中,我们使用 useKindeBrowserClient hook 获取用户信息:

'use client'
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";

export function UserProfileWidget() {
  const { user, isLoading, isAuthenticated } = useKindeBrowserClient();
  
  // 加载状态处理
  if (isLoading) {
    return <div className="animate-pulse bg-muted h-8 w-32 rounded"></div>;
  }
  
  // 未认证状态处理
  if (!isAuthenticated) {
    return <div>请登录查看个人资料</div>;
  }
  
  // 已认证状态
  return (
    <div className="flex items-center gap-2">
      {user?.picture ? (
        <img 
          src={user.picture} 
          alt="Profile" 
          className="w-8 h-8 rounded-full"
        />
      ) : (
        <div className="w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center">
          {user?.given_name?.[0] || user?.email?.[0] || '?'}
        </div>
      )}
      <span>{user?.given_name || user?.email}</span>
    </div>
  );
}

七、部署注意事项

7.1 环境变量

在生产环境部署前,确保设置了正确的环境变量:

KINDE_CLIENT_ID=生产环境的Client ID
KINDE_CLIENT_SECRET=生产环境的Client Secret
KINDE_ISSUER_URL=https://你的组织名.kinde.com
KINDE_SITE_URL=https://yourdomain.com
KINDE_POST_LOGOUT_REDIRECT_URL=https://yourdomain.com
KINDE_POST_LOGIN_REDIRECT_URL=https://yourdomain.com/dashboard

7.2 更新 Kinde 应用设置

在 Kinde 管理后台更新应用设置:

  1. 添加生产环境重定向 URL:https://yourdomain.com/api/auth/kinde_callback
  2. 更新登录后和退出后的重定向地址
  3. 将生产环境域名添加到允许的来源列表

7.3 CORS 配置

如果您的应用包含自定义 API 调用 Kinde,请确保正确配置 CORS 策略。

八、最佳实践和问题排查

8.1 认证状态管理

  • 在客户端组件中使用 useKindeBrowserClient() 获取和管理认证状态
  • 在服务器组件中使用 getKindeServerSession() 验证会话和获取用户信息
  • 避免在同一组件中混用两种方法,可能导致不一致

8.2 常见问题排查

  1. 404 API 路由错误

    • 检查 [kindeAuth] 动态路由配置是否正确
    • 确认 API 路由文件位置正确(Next.js 13+ 的 App Router 和 Pages Router 路径不同)
  2. 重定向循环

    • 检查中间件配置是否正确
    • 确认重定向 URL 配置无误
  3. 权限错误

    • 验证用户是否被分配了正确的角色和权限
    • 检查权限名称拼写是否一致
  4. 水合错误

    • 使用 suppressHydrationWarning 或修复客户端/服务器端渲染差异
    • 确保在客户端组件中正确使用 useKindeBrowserClient

8.3 认证刷新机制

Kinde Auth SDK 会自动处理令牌刷新,但需要注意:

  • 确保前端和后端共享状态的一致性
  • 避免在同一流程中混合使用客户端和服务器端认证方法
  • 处理令牌过期的场景,提供优雅的重新认证体验

8.4 安全最佳实践

提高应用安全性的一些建议:

  1. 启用双因素认证(2FA)选项
  2. 定期审核用户权限和角色分配
  3. 使用 HTTPS 保护所有通信
  4. 设置安全的密码策略
  5. 实现登录尝试限制
  6. 添加可疑登录检测和通知

总结

通过本文,我们详细介绍了如何在 Next.js 应用中集成 Kinde Auth,实现完整的身份验证和授权功能。从基础配置到高级权限控制,从本地开发到生产部署,我们涵盖了实际开发过程中的关键环节和最佳实践。

Kinde Auth 作为一个现代化的身份验证服务,不仅简化了开发过程,还提供了强大的安全保障。希望本文能帮助你在自己的 Next.js 项目中构建安全可靠的用户认证系统。

如果你有任何问题或建议,欢迎在评论区留言交流!

参考资源