Next.js 14 认证与授权:从 Session 到 OAuth 的完整实现

在现代 Web 应用中,认证与授权是不可或缺的一部分。Next.js 14 提供了多种方式来实现用户认证和权限控制。今天,我们就来深入探讨如何在 Next.js 14 中实现完整的认证授权系统。

NextAuth.js 集成

1. 基础配置

首先安装 NextAuth.js:

npm install next-auth@beta

配置认证提供者:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

const handler = NextAuth(authConfig);
export { handler as GET, handler as POST };

// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';

export const authConfig: NextAuthConfig = {
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await prisma.user.findUnique({
            where: { email }
          });

          if (!user) return null;

          const passwordsMatch = await bcrypt.compare(password, user.password);

          if (passwordsMatch) return user;
        }

        return null;
      }
    })
  ],
  pages: {
    signIn: '/login',
    error: '/error',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');

      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false;
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    }
  }
};

2. 中间件配置

// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export const { auth: middleware } = NextAuth(authConfig);

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

3. 客户端集成

// app/providers.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

JWT 实现

1. JWT 工具函数

// lib/jwt.ts
import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function sign(payload: any): Promise<string> {
  const jwt = await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('24h')
    .sign(secret);

  return jwt;
}

export async function verify(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch (error) {
    return null;
  }
}

2. JWT 中间件

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verify } from './lib/jwt';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    const payload = await verify(token);
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('user', JSON.stringify(payload));

    return NextResponse.next({
      request: {
        headers: requestHeaders,
      },
    });
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

基于角色的访问控制(RBAC)

1. 角色定义

// types/auth.ts
export enum Role {
  USER = 'USER',
  ADMIN = 'ADMIN',
  EDITOR = 'EDITOR'
}

export interface User {
  id: string;
  email: string;
  role: Role;
  permissions: string[];
}

// lib/permissions.ts
export const PERMISSIONS = {
  'posts:read': 'Read posts',
  'posts:write': 'Write posts',
  'users:manage': 'Manage users',
} as const;

export const ROLE_PERMISSIONS: Record<Role, string[]> = {
  [Role.USER]: ['posts:read'],
  [Role.EDITOR]: ['posts:read', 'posts:write'],
  [Role.ADMIN]: ['posts:read', 'posts:write', 'users:manage'],
};

2. 权限检查组件

// components/PermissionGate.tsx
'use client';

import { useSession } from 'next-auth/react';

interface PermissionGateProps {
  children: React.ReactNode;
  permission: string;
  fallback?: React.ReactNode;
}

export function PermissionGate({
  children,
  permission,
  fallback = null
}: PermissionGateProps) {
  const { data: session } = useSession();
  const userPermissions = session?.user?.permissions ?? [];

  if (!userPermissions.includes(permission)) {
    return fallback;
  }

  return children;
}

// 使用示例
function AdminPanel() {
  return (
    <PermissionGate
      permission="users:manage"
      fallback={<p>无权访问</p>}
    >
      <div>管理员面板内容</div>
    </PermissionGate>
  );
}

3. 服务端权限检查

// lib/auth.ts
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

export async function checkPermission(permission: string) {
  const session = await getServerSession();

  if (!session?.user) {
    redirect('/login');
  }

  const hasPermission = session.user.permissions.includes(permission);

  if (!hasPermission) {
    throw new Error('Unauthorized');
  }
}

// app/admin/page.tsx
export default async function AdminPage() {
  await checkPermission('users:manage');

  return (
    <div>
      <h1>管理员页面</h1>
      {/* 管理员内容 */}
    </div>
  );
}

OAuth 2.0 集成

1. 配置 OAuth 提供者

// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';

export const authConfig: NextAuthConfig = {
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      // 在用户首次登录时创建用户记录
      if (account?.provider === 'github' || account?.provider === 'google') {
        try {
          const existingUser = await prisma.user.findUnique({
            where: { email: user.email }
          });

          if (!existingUser) {
            await prisma.user.create({
              data: {
                email: user.email,
                name: user.name,
                avatar: user.image,
                provider: account.provider,
              },
            });
          }

          return true;
        } catch (error) {
          console.error('Error during OAuth sign in:', error);
          return false;
        }
      }

      return true;
    },
    async session({ session, user }) {
      // 添加额外的用户信息到 session
      if (session.user) {
        session.user.id = user.id;
        session.user.role = user.role;
        session.user.permissions = user.permissions;
      }
      return session;
    },
  },
};

2. OAuth 登录按钮

// components/OAuthButtons.tsx
'use client';

import { signIn } from 'next-auth/react';

export function OAuthButtons() {
  return (
    <div className="space-y-4">
      <button
        onClick={() => signIn('github', { callbackUrl: '/dashboard' })}
        className="w-full flex items-center justify-center gap-2 bg-gray-900 text-white p-4 rounded-lg"
      >
        <GithubIcon />
        使用 GitHub 登录
      </button>

      <button
        onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
        className="w-full flex items-center justify-center gap-2 bg-white border border-gray-300 p-4 rounded-lg"
      >
        <GoogleIcon />
        使用 Google 登录
      </button>
    </div>
  );
}

Session 管理

1. Session 存储

// lib/session.ts
import { getServerSession } from 'next-auth';
import { cache } from 'react';

export const getSession = cache(async () => {
  const session = await getServerSession();
  return session;
});

// 在服务端组件中使用
export default async function ProtectedPage() {
  const session = await getSession();

  if (!session?.user) {
    redirect('/login');
  }

  return (
    <div>
      <h1>Welcome {session.user.name}</h1>
    </div>
  );
}

2. Session 持久化

// lib/auth.ts
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const authConfig: NextAuthConfig = {
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: 'database',
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
  callbacks: {
    async session({ session, user }) {
      // 从数据库加载最新的用户数据
      const freshUser = await prisma.user.findUnique({
        where: { id: user.id },
        include: {
          permissions: true,
          settings: true,
        },
      });

      return {
        ...session,
        user: {
          ...session.user,
          ...freshUser,
        },
      };
    },
  },
};

3. Session 状态管理

// hooks/useAuth.ts
'use client';

import { useSession } from 'next-auth/react';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export function useAuth(requireAuth = true) {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (requireAuth && status === 'unauthenticated') {
      router.push('/login');
    }
  }, [requireAuth, status, router]);

  return {
    session,
    status,
    isAuthenticated: status === 'authenticated',
    isLoading: status === 'loading',
  };
}

// components/AuthGuard.tsx
'use client';

import { useAuth } from '@/hooks/useAuth';

export function AuthGuard({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (!isAuthenticated) {
    return null;
  }

  return children;
}

写在最后

Next.js 14 结合 NextAuth.js 提供了强大而灵活的认证授权解决方案。在实际应用中,需要注意以下几点:

  1. 选择合适的认证策略(JWT vs Session)
  2. 实现细粒度的权限控制
  3. 正确处理 OAuth 集成
  4. 注意安全性和性能优化
  5. 提供良好的用户体验

在下一篇文章中,我们将深入探讨 Next.js 14 的性能优化策略。如果你有任何问题或建议,欢迎在评论区讨论!

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/942609.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android——自定义按钮button

项目中经常高频使用按钮&#xff0c;要求&#xff1a;可设置颜色&#xff0c;有圆角且有按下效果的Button 一、自定义按钮button button的代码为 package com.fslihua.clickeffectimport android.annotation.SuppressLint import android.content.Context import android.gra…

黑龙江等保测评费用怎么收?

‌黑龙江二级等保测评费用‌&#xff1a;费用区间大致在3万至6万人民币之间&#xff0c;具体费用取决于测评机构的定价策略、所提供的服务内容以及企业的实际需求‌&#xff0c;服务内容包括防火墙、Web应用防火墙(WAF)、堡垒机、日志审计、漏洞扫描以及等保安全整改等‌。 ‌…

中文拼写检测纠正 Read, Listen, and See Leveraging Multimodal Information 论文

拼写纠正系列 NLP 中文拼写检测实现思路 NLP 中文拼写检测纠正算法整理 NLP 英文拼写算法&#xff0c;如果提升 100W 倍的性能&#xff1f; NLP 中文拼写检测纠正 Paper java 实现中英文拼写检查和错误纠正&#xff1f;可我只会写 CRUD 啊&#xff01; 一个提升英文单词拼…

vue2 elementui if导致的rules判断失效

优化目标 和 目标转化出价必填的 切换的时候还会隐藏掉 这时候的if语句会导致rules判断失效 我的办法是把判断拉到外面 别放在el-form-item里 <section v-if"unitForm.baseTarget OCPM && unitForm.cpaTargetOptions ! undefined && unitForm.cpaTa…

前端(Ajax)

1.客户端请求 向https://jsonplaceholder.typicode.com/users发送get请求 const xhr new XMLHttpRequest(); console.log(xhr.readyState); xhr.open(‘get’, ‘https://jsonplaceholder.typicode.com/users’) console.log(xhr.readyState); xhr.send(); console.log(xhr.…

uboot, s5pv210 , main_loop 分析(16)

main_loop 的代码如下&#xff1a; 4443 void main_loop (void)42 {41 #ifndef CONFIG_SYS_HUSH_PARSER E 40 ▎ static char lastcommand[CONFIG_SYS_CBSIZE] { 0, }; ■ Use of undeclared identifier CONFIG_SYS_CBSIZE39 ▎ int len;38 ▎ int rc 1;37 ▎ …

信号强劲,通信清晰:北斗三号多模对讲机TD70——专业通信解决方案

在边防海防等国家安全的关键领域&#xff0c;通信的稳定性和可靠性关乎着任务的成败和战士们的安全。北斗三号多模对讲机TD70&#xff0c;凭借其卓越的性能和全面的功能&#xff0c;成为了边防海防通信的利器&#xff0c;守护着国家安全的前沿哨兵。 一、三网融合&#xff0c;…

Arduino驱动DS18B20测量环境温度

DS18B20是一款高精度的单总线数字温度传感器&#xff0c;具体参数如下表所示&#xff1a; 参数名称 参数特性 测温范围 -55~125℃ 测量精度 在-10~85℃范围内的误差为0.5℃ 分辨率 9~12位数字信号&#xff0c;分辨率分别为0.5℃、0.25℃、0.125℃和0.0625℃ 通信方式 …

vector快慢指针+例题详解

1.快慢指针 例题 给定一个链表&#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;我们使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;索引从…

C++--------效率和表示

C 效率和表示 效率 时间效率&#xff1a;在 C 中&#xff0c;不同的数据结构和算法有着各异的时间复杂度。例如&#xff0c;访问数组元素的时间复杂度是 O ( 1 ) O(1) O(1)&#xff0c;而遍历链表查找元素的时间复杂度最坏情况下是 O ( n ) O(n) O(n)。选择合适的算法与数据…

【Mac】终端改色-让用户名和主机名有颜色

效果图 配置zsh 1.打开终端&#xff0c;进入.zshrc配置 cd ~ vim .zshrc2.添加如下配置并保存 # 启用命令行颜色显示 export CLICOLOR1 ## 加载颜色支持 autoload -U colors && colors # 配置 zsh 提示符 PROMPT"%{$fg_bold[red]%}%n%{$reset_color%}%{$fg_bol…

模拟——郑益慧_笔记1_绪论

B站视频链接 模电是数电的基础&#xff1b;参考书&#xff1a; 模拟电子技术基础&#xff08;第四版&#xff09;华成英、童诗白主编&#xff0c;高等教育出版社&#xff1b;电子技术基础 模拟部分 康华光主编&#xff0c;高等教育出版社&#xff1b; 电子技术的发展史 电子…

YOLOv11模型改进-模块-引入多尺度大核注意力Multi-scale Large Kernel Attention

MLKA 的提出源于图像超分辨率任务的挑战性&#xff0c;该任务需重建低质量图像缺失的高频信息&#xff0c;但因 LR 与 HR 图像对应关系复杂&#xff0c;寻找像素相关性困难。此前模型扩展容量的方法增加了训练负担和数据收集成本&#xff0c;而采用的注意力机制无法同时获取局部…

【gym】给定的强化学习环境简介(二)

文章目录 环境介绍一 box2dbipedal_walkercar_dynamicscar_racinglunar_lander 二、 classic_controlacrobotCartPolecontinuous_mountain_carmountain_carpendulum 三、toy_textblackjackcliffwalkingfrozentaxi 四、mujocoAnt&#xff1a;HalfCheetah&#xff1a;Hopper&…

基于支付宝百宝箱构建自己的Agent的基本简易流程(Datawhale AI冬令营)

一&#xff0c;使用支付宝百宝箱 官网地址&#xff1a;百宝箱 (alipay.com) 二&#xff0c;应用构建 点击左上角的新建应用 然后按自己的需求选择对应的模块 以下是我的示例 点击确认之后&#xff0c;进入模型设置界面 按需设计便可以&#xff0c;以下是我的设计 当你写好…

攻防世界 - Web - Level 1 unseping

关注这个靶场的其它相关笔记&#xff1a;攻防世界&#xff08;XCTF&#xff09; —— 靶场笔记合集-CSDN博客 0x01&#xff1a;Write UP 本关是一个 PHP 代码审计关卡&#xff0c;考察的是 PHP 反序列化漏洞以及命令执行的一些绕过手段&#xff0c;下面笔者将带你一步步过关。…

Java进阶学习笔记|面向对象

第一章.类和对象 1.面向对象的介绍 1.面向过程:自己的事情自己干,代表语言C语言洗衣服:每一步自己要亲力亲为 -> 找个盆,放点水,找个搓衣板,搓搓搓 2.面向对象:自己的事情别人帮忙去干,代表语言Java语言 洗衣服:自己的事情别人干 -> 全自动洗衣机3.为啥要使用面向对…

前端性能优化之大文件上传

大文件上传是前端开发中常见的需求之一&#xff0c;特别是在需要处理较大的Excel表格数据、高清图片、视频或其他大型文件时。优化大文件上传不仅可以提升用户体验&#xff0c;还能有效减轻服务器负担。本文将深入探讨大文件上传的几种常见优化技术&#xff0c;包括文件切片与并…

数据结构之线性表之顺序表

定义&#xff1a; 由n&#xff08;n>0&#xff09;个数据特性相同的元素构成的有限序列称为线性表 简单来说n个相同数据类型的数据组wsw合在一起的这么一个集合就是一个线性表 线性表包括顺序表和链表 1. 顺序表&#xff08;我们所有的代码实现都用函数来封装&#xff09…

Matlab 和 R 语言的数组索引都是从 1 开始,并且是左闭右闭的

文章目录 一、前言二、主要内容三、小结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 在早期的计算机科学中&#xff0c;数组索引从 1 开始是很常见的。例如&#xff0c;Fortran 和 Pascal 等编程语言也采用了从 1 开始的索引。 这种索引…