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

如何用Next-Auth安全调用Laravel后端受保护路由并处理未授权登出?

解决方案:在Next.js中安全调用Laravel受保护API并处理未授权登出

一、核心思路

要安全调用Laravel的受保护API,核心是将Next-Auth获取到的OAuth访问令牌(access token)附加到API请求的Header中;同时统一处理API响应,捕获401/Unauthorized状态并触发Next-Auth的自动登出操作。

二、步骤1:完善Next-Auth配置,确保令牌可被获取

你的现有OAuth配置已能正常运行,但需要补充session和回调配置,将Laravel返回的access token存入Next-Auth的会话中,方便后续调用API时取用:

import NextAuth, { NextAuthOptions } from "next-auth"
export const authOptions: NextAuthOptions = {
  providers: [
    {
      id: "I4T",
      name: "I4T",
      type: "oauth",
      authorization: {
        url: `${process.env.I4T_BACKEND_URL}/oauth/authorize`,
        params: { scope: "" }
      },
      token: `${process.env.I4T_BACKEND_URL}/oauth/token`,
      userinfo: `${process.env.I4T_BACKEND_URL}/api/user`,
      clientId: process.env.I4T_CLIENT_ID,
      clientSecret: process.env.I4T_CLIENT_SECRET,
      profile: (profile) => profile,
    }
  ],
  // 使用JWT模式适配App Directory
  session: {
    strategy: "jwt",
  },
  callbacks: {
    // 将Laravel返回的access token存入JWT
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.expiresAt = account.expires_at;
      }
      return token;
    },
    // 将JWT中的令牌同步到session对象
    async session({ session, token }) {
      session.user.accessToken = token.accessToken as string;
      session.user.expiresAt = token.expiresAt as number;
      return session;
    },
  },
}
export default NextAuth(authOptions)

三、步骤2:封装API请求工具(统一处理令牌与错误)

创建utils/api.ts文件,分别封装客户端和服务端的API请求逻辑,统一处理令牌附加和401登出:

客户端组件专用请求函数

import { getSession, signOut } from "next-auth/react";

export async function clientApiRequest<T>(
  url: string,
  options: RequestInit = {}
): Promise<T> {
  // 从Next-Auth会话获取access token
  const session = await getSession();
  const accessToken = session?.user.accessToken;

  // 构造请求Header,附加Bearer令牌
  const headers = new Headers(options.headers);
  if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
  headers.set("Accept", "application/json");
  headers.set("Content-Type", "application/json");

  const response = await fetch(`${process.env.I4T_BACKEND_URL}${url}`, {
    ...options,
    headers,
  });

  // 捕获401状态,触发自动登出
  if (response.status === 401) {
    await signOut({ callbackUrl: "/" });
    throw new Error("未授权,已自动登出");
  }

  if (!response.ok) {
    throw new Error(`API请求失败: ${response.statusText}`);
  }

  return response.json() as Promise<T>;
}

服务端组件/路由专用请求函数

服务端组件可直接通过getServerSession获取令牌,401时直接重定向到登出页:

import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { redirect } from "next/navigation";

export async function serverApiRequest<T>(
  url: string,
  options: RequestInit = {}
): Promise<T> {
  const session = await getServerSession(authOptions);
  const accessToken = session?.user.accessToken;

  // 无令牌时直接跳转登录页
  if (!accessToken) redirect("/api/auth/signin");

  const headers = new Headers(options.headers);
  headers.set("Authorization", `Bearer ${accessToken}`);
  headers.set("Accept", "application/json");
  headers.set("Content-Type", "application/json");

  const response = await fetch(`${process.env.I4T_BACKEND_URL}${url}`, {
    ...options,
    headers,
    cache: "no-store", // 根据业务需求调整缓存策略
  });

  if (response.status === 401) redirect("/api/auth/signout");

  if (!response.ok) {
    throw new Error(`API请求失败: ${response.statusText}`);
  }

  return response.json() as Promise<T>;
}

四、步骤3:在组件中使用封装的请求工具

客户端组件示例

"use client";

import { clientApiRequest } from "@/utils/api";
import { useState, useEffect } from "react";

type Post = {
  id: number;
  title: string;
  content: string;
};

export default function ClientPosts() {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const data = await clientApiRequest<Post[]>("/api/posts");
        setPosts(data);
      } catch (error) {
        console.error(error);
      }
    };
    fetchPosts();
  }, []);

  return (
    <div>
      <h2>客户端组件获取的文章列表</h2>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

服务端组件示例

import { serverApiRequest } from "@/utils/api";

type Post = {
  id: number;
  title: string;
  content: string;
};

export default async function ServerPosts() {
  const posts = await serverApiRequest<Post[]>("/api/posts");

  return (
    <div>
      <h2>服务端组件获取的文章列表</h2>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

五、安全注意事项

  • 令牌存储:使用Next-Auth的JWT模式,令牌会加密存储在HttpOnly Cookie中,避免XSS攻击风险。
  • 服务端请求:服务端组件直接从会话获取令牌,不会暴露给前端,安全性更高。
  • 令牌过期处理:可在JWT回调中检查令牌过期时间,结合Laravel Passport的刷新令牌机制实现自动续期。
  • HTTPS强制:生产环境必须启用HTTPS,防止令牌被明文传输窃取。

内容的提问来源于stack exchange,提问作者Aniket Das

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 11:20:53