使用 Next.js 和 Supabase 实现细粒度的访问控制系统
- Published on
- ...
- Authors

- Name
- Huashan
- @herohuashan
概述
在开发个人博客和健康追踪应用时,我需要实现一个既安全又灵活的访问控制系统。经过技术选型和实践,我基于 Next.js 15 和 Supabase Auth 构建了一套完整的权限管理方案,具有以下特性:
- 🔐 多种认证方式:支持 Email Magic Link(无密码)、GitHub OAuth、Google OAuth
- 🛡️ Middleware 路由保护:使用 Next.js 中间件拦截未授权访问
- 👥 白名单机制:基于 Supabase 数据库的用户授权表
- 🎯 细粒度权限控制:四级访问权限(Public/Family/Work/Private)
- ⚡ 性能优化:Cookie 缓存减少数据库查询
- 🚀 开发体验:TypeScript 全栈类型安全
在本文中,我将详细介绍整个系统的架构设计和实现细节。
系统架构
整体流程
┌─────────────────────────────────────────────────────────────────┐
│ 用户访问流程 │
└─────────────────────────────────────────────────────────────────┘
1. 用户访问 /tracking 或受保护的博客文章
↓
2. Next.js Middleware 拦截请求
↓
3. 检查用户是否已登录 (Supabase Session)
↓
┌──────────┴──────────┐
│ 未登录 │ 已登录
↓ ↓
重定向到 /login 检查缓存
↓
┌─────────┴─────────┐
│ 缓存命中 │ 缓存未命中
↓ ↓
允许访问 查询 allowed_users 表
↓
┌─────────┴─────────┐
│ 在白名单中 │ 不在白名单中
↓ ↓
检查访问级别 重定向到 /unauthorized
↓
┌──────────┴──────────┐
│ 权限足够 │ 权限不足
↓ ↓
允许访问 显示权限不足
技术栈
| 层级 | 技术 | 用途 |
|---|---|---|
| 前端框架 | Next.js 15 (App Router) | 服务端渲染、路由、中间件 |
| 认证服务 | Supabase Auth | 用户认证、Session 管理 |
| 数据库 | Supabase (PostgreSQL) | 存储白名单、访问级别 |
| UI 组件 | Tailwind CSS | 响应式登录页面 |
| 类型安全 | TypeScript | 全栈类型定义 |
核心实现
1. 认证层:多种登录方式
1.1 登录页面实现
我实现了三种认证方式,满足不同用户的需求:
文件: app/login/page.tsx
'use client'
import { useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { createClient } from '@/lib/supabase-auth'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [emailSent, setEmailSent] = useState(false)
const searchParams = useSearchParams()
const redirect = searchParams.get('redirect') || '/'
const supabase = createClient()
// 方式 1: Email Magic Link(无密码登录)
const handleEmailLogin = async () => {
setLoading(true)
const origin = window.location.origin
const { error } = await supabase.auth.signInWithOtp({
email: email,
options: {
emailRedirectTo: `${origin}/auth/callback?redirect=${encodeURIComponent(redirect)}`,
},
})
if (error) {
alert('发送失败:' + error.message)
} else {
setEmailSent(true)
}
setLoading(false)
}
// 方式 2: GitHub OAuth
const handleGithubLogin = async () => {
const origin = window.location.origin
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${origin}/auth/callback?redirect=${encodeURIComponent(redirect)}`,
},
})
}
// 方式 3: Google OAuth
const handleGoogleLogin = async () => {
const origin = window.location.origin
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${origin}/auth/callback?redirect=${encodeURIComponent(redirect)}`,
},
})
}
return (
<div className="flex min-h-screen items-center justify-center">
{/* 登录表单 UI */}
</div>
)
}
三种认证方式的对比:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Email Magic Link | 无需密码、安全、简单 | 需要邮箱访问 | 个人博客、轻量应用 |
| GitHub OAuth | 快速、开发者友好 | 仅限 GitHub 用户 | 技术博客、开源项目 |
| Google OAuth | 用户基数大、信任度高 | 需要配置 OAuth | 通用应用 |
1.2 认证回调处理
文件: app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const redirect = requestUrl.searchParams.get('redirect') || '/'
if (code) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
)
},
},
}
)
// 交换 code 为 session
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (error) {
return NextResponse.redirect(new URL('/login?error=auth_failed', request.url))
}
// 重定向到原始请求的页面
return NextResponse.redirect(new URL(redirect, request.url))
}
return NextResponse.redirect(new URL('/login?error=no_code', request.url))
}
关键点:
- 使用
exchangeCodeForSession将授权码交换为会话 - 通过 Server-Side Cookie 管理保持会话安全
- 支持
redirect参数返回用户原始目标页面
2. 授权层:Middleware + 白名单
2.1 Middleware 路由保护
文件: middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'
// 需要保护的路径
const PROTECTED_PATHS = ['/tracking']
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
let response = NextResponse.next()
// 检查是否是受保护路径
const isProtectedPath = PROTECTED_PATHS.some((path) => pathname.startsWith(path))
if (isProtectedPath) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
)
},
},
}
)
// 获取用户 session
const { data: { session } } = await supabase.auth.getSession()
// 未登录 → 重定向到登录页
if (!session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
// ⚡ 性能优化:检查缓存的白名单状态
const cachedAllowlist = request.cookies.get('auth_allowed')?.value
const userEmail = session.user.email
// 缓存命中 → 跳过数据库查询
if (cachedAllowlist === userEmail) {
return response
}
// 查询白名单表
const { data: allowedUser } = await supabase
.from('allowed_users')
.select('email')
.eq('email', userEmail)
.maybeSingle()
if (!allowedUser) {
response.cookies.delete('auth_allowed')
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 缓存验证结果(5 分钟)
response.cookies.set('auth_allowed', userEmail!, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 5, // 5 minutes
})
}
return response
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|static).*)'],
}
Middleware 的优势:
- ✅ 早期拦截:在请求到达页面组件之前执行
- ✅ 统一管理:所有路由保护逻辑集中在一处
- ✅ 性能优化:支持缓存机制减少数据库查询
- ✅ 灵活配置:通过
PROTECTED_PATHS数组轻松管理
2.2 数据库白名单表
表结构:
CREATE TABLE allowed_users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
access_level TEXT DEFAULT 'public'
CHECK (access_level IN ('public', 'family', 'work', 'private')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 索引优化
CREATE INDEX idx_allowed_users_email ON allowed_users(email);
访问级别说明:
| 级别 | 权限范围 | 应用场景 |
|---|---|---|
| public | 仅可访问公开内容 | 普通访客、试用用户 |
| family | 公开 + 家庭内容 | 家庭成员、亲密朋友 |
| work | 公开 + 工作内容 | 同事、业务伙伴 |
| private | 全部内容 | 博客所有者 |
3. 细粒度权限控制
3.1 博客文章访问控制
文件: app/blog/[...slug]/page.tsx
import { notFound, redirect } from 'next/navigation'
import { allBlogs } from 'contentlayer/generated'
import { createServerComponentClient } from '@/lib/supabase-auth'
type AccessLevel = 'public' | 'family' | 'work' | 'private'
// 权限级别映射
const ACCESS_HIERARCHY: Record<AccessLevel, number> = {
public: 0,
family: 1,
work: 1,
private: 2,
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string[] }
}) {
const post = allBlogs.find(/* ... */)
if (!post) notFound()
// 获取文章的访问级别
const contentAccessLevel = (post as any).accessLevel as AccessLevel || 'public'
// 如果文章需要权限控制
if (contentAccessLevel !== 'public' || post.private) {
const supabase = await createServerComponentClient()
const { data: { session } } = await supabase.auth.getSession()
// 未登录 → 重定向登录
if (!session) {
redirect(`/login?redirect=/blog/${params.slug.join('/')}`)
}
// 查询用户访问级别
const { data: allowedUser } = await supabase
.from('allowed_users')
.select('email, access_level')
.eq('email', session.user.email)
.single()
if (!allowedUser) {
redirect('/unauthorized')
}
const userAccessLevel = allowedUser.access_level as AccessLevel
// 检查权限是否足够
if (ACCESS_HIERARCHY[userAccessLevel] < ACCESS_HIERARCHY[contentAccessLevel]) {
return (
<div className="container py-12">
<div className="rounded-lg border-2 border-yellow-400 bg-yellow-50 p-6">
<h2 className="text-xl font-bold">🔒 权限不足</h2>
<p className="mt-2 text-gray-700">
此文章需要 <strong>{contentAccessLevel}</strong> 级别权限,
您当前的权限为 <strong>{userAccessLevel}</strong>。
</p>
</div>
</div>
)
}
}
// 渲染文章内容
return <BlogLayout post={post} />
}
3.2 在 MDX 文件中设置访问级别
---
title: '我的私密博客文章'
date: '2025-11-21'
tags: ['个人']
accessLevel: 'private' # 设置访问级别
---
这是一篇只有我自己能看到的文章...
4. 性能优化
4.1 Middleware 缓存机制
优化前: 每次访问都查询数据库
请求 1 → 查询 DB (150ms)
请求 2 → 查询 DB (150ms)
请求 3 → 查询 DB (150ms)
总耗时:450ms
优化后: Cookie 缓存(5分钟)
请求 1 → 查询 DB (150ms) → 缓存
请求 2 → 读缓存 (1ms)
请求 3 → 读缓存 (1ms)
总耗时:152ms (↓66%)
4.2 数据库索引
文件: scripts/add_performance_indexes.sql
-- 为 allowed_users.email 添加索引
CREATE INDEX IF NOT EXISTS idx_allowed_users_email
ON allowed_users(email);
-- 分析表以更新查询优化器统计信息
ANALYZE allowed_users;
效果: 白名单查询速度提升 80-90%
安全考虑
1. Session 安全
- ✅ 使用 HttpOnly Cookie 存储 session,防止 XSS 攻击
- ✅ 设置 SameSite=lax,防止 CSRF 攻击
- ✅ 生产环境强制 Secure flag,仅 HTTPS 传输
2. SQL 注入防护
- ✅ 使用 Supabase SDK 的参数化查询
- ✅ 所有用户输入都经过验证和转义
3. 权限检查
- ✅ 双重验证:Middleware + 页面组件都检查权限
- ✅ 最小权限原则:默认
public级别 - ✅ 服务端验证:所有权限检查在服务端执行
部署配置
1. 环境变量
.env.local:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
# 可选:生产环境配置
NODE_ENV=production
2. Supabase 配置
2.1 启用认证提供商
在 Supabase Dashboard → Authentication → Providers 中:
- ✅ Email (Magic Link)
- ✅ GitHub OAuth
- ✅ Google OAuth
2.2 配置回调 URL
https://your-domain.com/auth/callback
2.3 初始化白名单表
-- 添加自己的邮箱到白名单
INSERT INTO allowed_users (email, access_level)
VALUES ('[email protected]', 'private');
-- 添加家人邮箱
INSERT INTO allowed_users (email, access_level)
VALUES ('[email protected]', 'family');
3. Next.js 部署
支持部署到:
- ✅ Vercel
- ✅ Cloudflare Pages
- ✅ 自托管 Node.js
最佳实践
1. 分层设计
┌─────────────────────┐
│ UI Layer (前端) │ 登录表单、错误提示
├─────────────────────┤
│ Auth Layer (认证) │ Supabase Auth
├─────────────────────┤
│ Middleware (授权) │ 路由保护、缓存
├─────────────────────┤
│ Access Control │ 细粒度权限检查
├─────────────────────┤
│ Database (数据) │ 白名单、访问级别
└─────────────────────┘
2. 错误处理
// 友好的错误提示
if (!allowedUser) {
return (
<div className="container py-12">
<h2>⛔ 访问被拒绝</h2>
<p>您的账号未被授权访问此内容。</p>
<p>如需访问权限,请联系管理员。</p>
<button onClick={() => signOut()}>退出登录</button>
</div>
)
}
3. 性能监控
// 添加性能监控
console.time('auth-check')
const { data: allowedUser } = await supabase.from('allowed_users')...
console.timeEnd('auth-check')
// 监控缓存命中率
const cacheHit = cachedAllowlist === userEmail
analytics.track('auth_cache', { hit: cacheHit })
总结
通过这套访问控制系统,我实现了:
- 🔐 安全性:多层防护,Session 管理,SQL 注入防护
- ⚡ 高性能:缓存优化,数据库索引,减少 66% 查询时间
- 🎯 灵活性:四级权限,支持文章级别控制
- 👥 易用性:三种登录方式,友好的错误提示
- 🚀 可扩展:清晰的架构,易于添加新功能
完整代码
所有代码已开源,详见我的 GitHub 仓库:blog-nextjs
相关资源
如果你觉得这篇文章有帮助,欢迎分享和讨论!
标签: #Next.js #Supabase #权限管理 #TypeScript #全栈开发