如何用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
相关产品推荐
相关产品推荐

