这是用户在 2024-4-3 14:33 为 https://pilcrowonpaper.com/blog/middleware-auth/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
profile picture
Github Twitter Donate

Please stop using middleware to protect your routes
请停止使用中间件来保护您的路由

March 31, 2024  2024 年 3 月 31 日

When talking about auth, there seems be a certain group that’s adamant on using middleware to handle authorization. Middleware here refers to functions that run before every request.
在谈论身份验证时,似乎有一个团体坚持使用中间件来处理授权。这里的中间件是指在每个请求之前运行的函数。

function isProtected(path: string) {
	return path !== "/login" && path !== "/signup";
}

app.middleware((req, res, next) => {
	if (!isProtected(req.path)) {
		return next();
	}
	const user = validateRequest(req);
	if (user) {
		return next();
	}
	res.writeHeader(401);
	res.write("Unauthorized");
});

app.get("/", (_, res) => {
	res.writeHeader(200);
	res.write("Secret message");
});

I do not like this approach at all.
我根本不喜欢这种方法。

I’m just confused at this point since you’re just re-implementing routing logic within middleware, an API provided by your routing library. And what do you do when you need to protect routes based on user roles?
我现在很困惑,因为您只是在中间件(路由库提供的 API)中重新实现路由逻辑。当您需要根据用户角色保护路由时该怎么办?

const adminOnlyRoutes = ["/admin/*"];

app.middleware((req, res, next) => {
	if (!isProtected(req.path)) {
		return next();
	}
	const user = validateRequest(req);
	if (user) {
		let requiresAdminRole = false;
		for (const route of adminOnlyRoutes) {
			requiresAdminRole = matchRoute(route, req.path);
		}
		if (requiresAdminRole && !user.admin) {
			res.writeHeader(401);
			return;
		}
		return next();
	}
	res.writeHeader(401);
});

While route-level middleware (middleware that only applies to certain routes) may help in this simple example, routes in real-world applications aren’t often organized by their required permissions. What happens if you have multiple roles? What if you need to implement different rate-limiting on each route based on user roles? How about API access token permissions and scopes?
虽然路由级中间件(仅适用于某些路由的中间件)可能会在这个简单的示例中有所帮助,但实际应用程序中的路由通常不会按其所需的权限进行组织。如果您有多个角色会发生什么?如果需要根据用户角色对每条路由实施不同的限速怎么办? API 访问令牌权限和范围如何?

Abstractions aren’t the problem here. The issue is that middleware is the wrong abstraction. It’s just the most obvious solution that seems to make sense in a smaller scale.
抽象不是这里的问题。问题在于中间件是错误的抽象。这只是最明显的解决方案,在较小的范围内似乎有意义。

But, we first have to answer: Do we need to abstract in the first place?
但是,我们首先必须回答:我们首先需要抽象吗?

This goes beyond this rant but I feel, at least in the JavaScript ecosystem, people seems to go too far on abstractions and “simplicity.” It isn’t surprising given how loosey-goosey powerful JS can be. Auth, which includes both authentication and authorization, seems to be particularly vulnerable to this since people are overtly scared of it. But auth is not an independent system from your application. It’s an integral part of it that affects and is affected by everything else. This makes it extra-hard to abstract without introducing unwanted complexity since it any abstraction that’s useful require some level of flexibility.
这超出了这种咆哮,但我觉得,至少在 JavaScript 生态系统中,人们似乎在抽象和“简单性”方面走得太远了。考虑到 JS 的强大功能,这并不奇怪。 Auth(包括身份验证和授权)似乎特别容易受到此影响,因为人们明显害怕它。但 auth 并不是一个独立于您的应用程序的系统。它是它不可分割的一部分,影响着其他一切,也受到其他一切的影响。这使得在不引入不必要的复杂性的情况下进行抽象变得异常困难,因为任何有用的抽象都需要一定程度的灵活性。

Getting back to the middleware discussion, why not just add the auth check on each route?
回到中间件讨论,为什么不在每个路由上添加身份验证检查呢?

app.get("/", (req, res) => {
	// ...
	if (!user.admin) {
		res.writeHeader(401);
		return;
	}
	// ...
});

”B, b… but DRY! Abstractions!”
“B,b……但是干!抽象!”

If you’re too lazy to write some basic if checks, maybe that’s a you problem. But on a serious note, if you need to abstract, use wrapper functions. This is a much better approach than middleware since you don’t have to worry about routing. I also like that all the logic is defined in a single location instead of scattered across your project.
如果您懒得编写一些基本的 if 检查,也许这就是您的问题。但严肃地说,如果您需要抽象,请使用包装函数。这是比中间件更好的方法,因为您不必担心路由。我还喜欢所有逻辑都定义在一个位置,而不是分散在项目中。

app.get(
	"/",
	protectedRoute((req, res, user) => {
		// ...
	})
);

If you deal with multiple permission level (e.g. roles, scopes), you can just create a helper function for checking them. Again, abstractions themselves aren’t bad. You just need to implement them at the right level.
如果您处理多个权限级别(例如角色、范围),您只需创建一个辅助函数来检查它们。再说一次,抽象本身并不坏。您只需要在正确的级别实施它们即可。

app.get("/", (req, res) => {
	// ...
	if (!hasPermission(user.role, ["moderator", "admin"])) {
		res.writeHeader(403);
		return;
	}
});

This doesn’t mean middleware is useless. It works for global-level stuff like CSRF protection and providing data to each route. Actually, authenticating requests and passing the user object to each route is a great use of middleware (but letting each route handle authorization). But even then, you should probably replace it once you need to deal with exceptions and multiple patterns.
这并不意味着中间件毫无用处。它适用于全局级别的内容,例如 CSRF 保护和向每个路由提供数据。实际上,对请求进行身份验证并将用户对象传递给每个路由是中间件的一个很好的用途(但让每个路由处理授权)。但即便如此,一旦您需要处理异常和多种模式,您可能应该替换它。

One common response I get to this opinion is that using middleware prevents developers from accidentally forgetting to add an auth check. That’s why you test your code. You should be testing your auth logic regardless of your implementation. Given that, adding auth checks to each route is less bug-prone and easier to debug than forcing an abstraction with middleware.
我对这一观点的一个常见反应是,使用中间件可以防止开发人员意外忘记添加身份验证检查。这就是您测试代码的原因。无论您的实现如何,您都应该测试您的身份验证逻辑。鉴于此,与使用中间件强制进行抽象相比,向每个路由添加身份验证检查更不容易出现错误,并且更易于调试。