Skip to content

8.3 Route Protection and Access Control

Goal of this section: Understand why hiding entry points on the frontend does not equal security, and learn how to use Middleware and access control to protect every route.


Hiding Things on the Frontend Is Not Security

Xiaoming added an admin panel at /admin to his "Personal Douban" app, where he could bulk delete movies and manage user comments. He didn’t put a link to /admin in the frontend navigation bar, and figured that made it secure—regular users couldn’t see the entry point, so naturally they couldn’t get in.

Until one day, his friend Lao Wang said in the group chat, "Your admin panel is pretty handy. I helped you delete a few duplicate movies."

Xiaoming was startled: "How did you get in?"

Lao Wang: "I just typed localhost:3000/admin directly into the browser address bar, and it opened."

That’s when Xiaoming realized: hiding an entry point on the frontend is just security through obscurity. Leaving a link out of the navigation only makes users "not see" the entry point, but anyone can still manually type the URL into the address bar. Worse, an attacker can send requests with curl or write scripts to call your API in bulk—frontend "hiding" is meaningless against those methods. It’s like hiding your house key under a flowerpot and then covering the flowerpot with a cloth. You think people won’t find the key if they can’t see the pot, but anyone can just lift the cloth and take it. Real protection has to happen on the server side—not "hide the entrance," but "without the key, the door won’t open."

Middleware—Next.js's Gatekeeper

In Next.js, you don’t need to write permission checks on every page. You just need to place a middleware.ts file in the project root—it acts like the site’s gatekeeper, and every request must pass through it before reaching a page or API. A user requests /admin, Middleware intercepts it and checks whether they’re logged in and whether they’re an admin. If they’re not logged in, redirect them to /login; if they’re not an admin, return 403—403 means "I know who you are, but you don’t have permission," which is different from 401: "Who are you? Log in first." Only if the check passes does the request continue to the /admin page. This check happens on the server side, so the user’s browser never receives the /admin page content at all—it’s not "the page loaded and then redirected," it’s "the page was never sent to you in the first place."

Tell AI: "Create a middleware.ts that intercepts all paths starting with /admin. If the user is not logged in or their role is not admin, redirect them to the login page. Also protect all APIs starting with /api/admin."

Curious what Middleware looks like? Expand to take a look
typescript
// middleware.ts (project root)
import { betterFetch } from '@better-fetch/fetch'
import { NextRequest, NextResponse } from 'next/server'

export async function middleware(request: NextRequest) {
  // Check if the user is logged in
  const { data: session } = await betterFetch('/api/auth/get-session', {
    baseURL: request.nextUrl.origin,
    headers: { cookie: request.headers.get('cookie') || '' },
  })

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

  return NextResponse.next()
}

// Specify which paths need protection
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
}

Middleware doesn’t just protect pages—it can protect APIs too. With the matcher configuration, you can precisely control which paths need checking—/dashboard/:path* protects all dashboard pages, /admin/:path* protects the admin panel, and /api/admin/:path* protects admin APIs. :path* is a wildcard that means "all subpaths under this path," so /admin/users and /admin/settings will both be protected. Any path you don’t want to protect (home page, login page, signup page, public APIs) can simply be left out of matcher—Middleware only intercepts the paths you specify, and lets all others pass through directly.

Middleware 拦截流程
📨
请求到达
🛡️
Middleware 检查
有 Session?
Yes
放行
📄
页面 / API
No
🚫
拦截
🔒
/login
RBAC 角色权限
管理员 (Admin)所有权限
创建编辑删除查看管理用户系统设置
编辑者 (Editor)
创建编辑查看
普通用户 (User)
查看

Page Protection and API Protection Are Both Required

Xiaoming added Middleware to protect the /admin page. He thought things were secure now—users trying to visit /admin in a browser would be blocked. But then an experienced developer asked him: "Have you also protected your movie deletion endpoint at /api/admin/delete-movie?"

Xiaoming said, "Doesn’t Middleware already block paths starting with /admin?"

The experienced developer replied, "Middleware intercepts page requests. But if someone directly calls POST /api/admin/delete-movie with curl and includes a valid user Cookie, Middleware might not stop it—that depends on your matcher configuration. And even if Middleware does block it, the API itself should still have its own permission checks. That’s the principle of defense in depth."

Why do you need both layers? Because Middleware is like the "main entrance," but each room behind that entrance should still have its own lock. Even if the page is protected, an attacker can still bypass the browser and call the API directly with curl or Postman—it’s like skipping the mall’s front entrance and sneaking into the warehouse through the employee entrance. The security check you placed at the main entrance does nothing there. If the API has no permission checks of its own, then as long as the attacker gets a valid user Cookie, they can delete data, change settings, or even escalate privileges using curl. Page-level protection relies on Middleware interception plus server-side checks; API-level protection relies on validation inside the API Route itself—both layers are controlled on the server side, and neither can be bypassed. This is what security professionals mean by defense in depth: don’t put all your eggs in one basket, and make sure every layer has its own protection.

🛡️ 纵深防御架构

🛡️
边界防护
第一道防线,阻挡外部攻击
1
🌐
网络层
控制网络访问和流量
2
🔐
应用层
保护应用程序安全
3
💾
数据层
保护数据安全
4
👁️
监控层
实时监控和响应
5
🎯
纵深防御原则
多层防护,即使某一层被突破,其他层仍能提供保护
🔄
层层递进
从外到内,每一层都有独立的防护机制和监控
👁️
全面监控
实时监控所有层级,快速发现和响应安全威胁

Tell AI: "Make sure all APIs starting with /api/admin check user permissions internally in code, and do not rely only on Middleware."

CORS—The Browser’s Cross-Origin Security Mechanism

CORS 跨域机制演示

🌐
浏览器
https://example.com
HTTP 请求
HTTP 响应
🖥️
服务器
https://example.com/api
💡 关键知识点
  • CORS 是浏览器的安全机制,不是服务器的限制
  • 同源请求(协议、域名、端口都相同)不受 CORS 限制
  • 跨域请求需要服务器返回 Access-Control-Allow-Origin
  • 通配符 * 允许所有域名,但不能携带 Cookie(不安全)
  • 在浏览器地址栏直接访问不受 CORS 限制,只有 JavaScript 发起的请求才受限

Later, Xiaoming wanted to add a feature to "Personal Douban": fetching movie posters from Douban’s public API. He wrote a fetch('https://api.douban.com/...') call in the frontend, and the browser console immediately showed a red error: "Cross-origin request blocked (CORS policy)." He was confused—the API returned data just fine when opened directly in the browser address bar, so why didn’t it work when called from JavaScript?

This is not a bug—it’s the browser’s security mechanism. Imagine this: you’re logged into your bank’s website, and your browser is storing your login Cookie. Then you open a malicious website. Without cross-origin restrictions, the malicious site’s JavaScript could secretly send requests to the bank’s website—because the browser would automatically include your bank Cookie, and the bank server would think it was really you making the request. The attacker could then transfer money while impersonating you. CORS (Cross-Origin Resource Sharing) is the browser’s line of defense: by default, JavaScript running on a webpage is not allowed to make requests to a different domain unless the target server explicitly allows it. Directly visiting a URL in the browser address bar is unrestricted (that’s an intentional action by the user), but requests made by JavaScript code inside a webpage are blocked (because they could be malicious code acting in secret).

When do you run into CORS? If your frontend and backend are in the same Next.js project (as in this tutorial), and the frontend calls its own API Route, you won’t run into CORS because it’s same-origin. But if the frontend and backend are deployed separately—frontend at app.example.com and API at api.example.com—then you will. You’ll also hit it when calling third-party APIs directly from the frontend. If your frontend and backend are in the same Next.js project, you usually don’t need to configure CORS. If you really do need cross-origin access, tell AI: "My frontend is at https://app.example.com and needs to call APIs at https://api.example.com. Help me configure CORS so only this domain is allowed."

Do not configure Access-Control-Allow-Origin: *

Allowing all domains is effectively the same as having no CORS protection. Always specify the exact domain.

Role-Based Access Control

角色权限矩阵(RBAC)

普通用户
5 项权限
已登录用户,可以发布和管理自己的内容
允许
浏览公开内容
允许
查看个人资料
允许
发布评论
允许
编辑自己的内容
允许
删除自己的内容
拒绝
审核内容
拒绝
管理电影
拒绝
管理用户
拒绝
查看数据
拒绝
系统配置
查看完整权限矩阵
权限 / 角色访客普通用户版主管理员
浏览公开内容
查看首页、电影列表等公开页面
查看个人资料
访问自己的个人主页
×
发布评论
对电影发表评论和评分
×
编辑自己的内容
修改自己发布的评论
×
删除自己的内容
删除自己发布的评论
×
审核内容
删除违规评论、封禁用户
×
×
管理电影
添加、编辑、删除电影信息
×
×
×
管理用户
查看用户列表、修改用户权限
×
×
×
查看数据
访问后台数据统计
×
×
×
系统配置
修改系统设置、管理数据库
×
×
×
💡 RBAC 核心概念
  • 角色(Role):一组权限的集合,如 admin、user、moderator
  • 权限(Permission):具体的操作能力,如"删除评论"、"管理用户"
  • 用户-角色绑定:每个用户分配一个或多个角色
  • 最小权限原则:只给用户完成工作所需的最小权限
  • 纵深防御:前端隐藏入口 + Middleware 拦截 + API 内部校验

Middleware solves the "is the user logged in?" problem, but Xiaoming quickly ran into a new one: after logging in, Lao Wang could still access the /admin page—because Middleware only checked whether the user was logged in, not whether they were an administrator. Xiaoming’s app had two kinds of users: he himself was an admin who could delete movies and manage comments; his friends were regular users who could only browse movies and write comments. This is where role-based access control (RBAC) comes in—different roles can do different things.

The simplest implementation is to add a role field to the user table: Xiaoming’s role is admin, and Lao Wang’s role is user. Then in Middleware or the API, check session.user.role !== 'admin' and return 403 if true. This check can go in Middleware (for centralized interception), or in each API Route (for fine-grained control), but ideally it should be in both places—again, defense in depth.

As your application grows more complex, having only admin and user may not be enough. For example, if Xiaoming’s "Personal Douban" becomes popular and gets a few hundred users, he may want a few trusted friends to help moderate the comments section. At that point, you need more granular roles: guest can only browse public content, user can browse, post, and edit their own content, moderator has all user permissions plus the ability to remove inappropriate content, and admin has all permissions. Better Auth’s organization management plugin supports more advanced permission models, but for most personal projects, admin and user are enough. Don’t overengineer—expand only when you actually need to. When the time comes, tell AI what you need, and it can help you adjust the permission model.

Tell AI: "Help me implement a complete route protection solution: create middleware.ts to protect /dashboard and /admin paths, require users accessing /admin to also have the admin role, make all APIs starting with /api/admin check permissions internally in code as well, redirect unauthenticated users to /login, and redirect logged-in users away from /login to /dashboard."


Key takeaways from this section

  • Hiding entry points on the frontend is not a security measure; real protection happens on the server side
  • Middleware is Next.js’s unified interception layer, and matcher defines what gets protected
  • Page protection and API protection are both essential
  • CORS is a browser security mechanism, and usually doesn’t need configuration within the same project
  • Start role-based access control with simple admin/user roles, and expand only when needed

Next step

Now that your routes are protected, continue to Security Checks and Troubleshooting—a practical security checklist for every stage of development, plus a quick troubleshooting guide for when issues come up.

Alpha Preview:This is an early internal build. Some chapters are still incomplete and issues may exist. Feedback is very welcome on GitHub.