wangpin34 / blog

个人博客, 博文写在 Issues 里
5 stars 0 forks source link

2023/03/03: 如何为 NextJS 静态页面做鉴权保护? #129

Open wangpin34 opened 1 year ago

wangpin34 commented 1 year ago

背景

我有一个文档网站,底层技术用的是 NextJS,所有页面都是 build time 生成,最终都是静态页面。我想要实现这样的效果:

  1. 当用户没有登录时,跳转到 sign in 页面
  2. 当登录成功时,跳转到之前请求的配置。

假设用户起初访问的是 /welcome,因为没有登录,所以跳转到 /sign_in?redirect=welcome。登录成功,则跳转到 /welcome 页面。

不太理想的尝试

对于 Auth ,NextJS 官网给了两种思路。第一种,页面load到浏览器后,用浏览器 js 判定登录状态和跳转。第二种,用 server page 判定登录状态和跳转。 第一种方案,实现比较简单,问题是 page props 会先 load 到页面,再判定登录状态和跳转。存在敏感数据泄露的可能,不能满足数据保护的要求。 第二种方案,实现稍微要复杂一些,也能达到数据保护的目的。但是相应的,会失去静态页面的便利性和安全性。得不偿失。

为什么不定制 NextJS Server

通过替换 NextJS 标准 server,加入拦截逻辑,也可以做到判定登录,以及跳转,官网中的示例表明这是一种可行的做法。https://nextjs.org/docs/advanced-features/custom-server。但这是有代价的,对我来说非常重要的 static gen 功能会得到削弱(具体数值没测试过)

Before deciding to use a custom server, please keep in mind that it should only be used when the integrated router of Next.js can't meet your app requirements. A custom server will remove important performance optimizations, like serverless functions and Automatic Static Optimization.

所以,思考再三,我决定跳出 NextJS ,寻找一种更加简便的办法:它应该足够解耦,不需要修改 NextJS 的 server,或者 page generation。这并不需要花费很多力气,现在毕竟不是互联网的蛮荒时期,啥都没有。我很快就找到了自己的目标: Nginx。

Nginx

首先,NextJS 这里准备一个 auth api,比如 /api/auth,用来判定 http 请求携带的 cookie 是否包含合法可用的 token。 然后,通过 Nginx 的 auth_request_module,拦截请求,转发给 /api/auth 处理,然后根据返回状态码,跳转到 sign_in 或者其他页面。 以下是 Nginx 的配置(忽略 server 的其他无关主题的配置)。 注:http://localhost:3030 是 NextJS App 的地址。

  # 静态内容请求直接转发给 NextJS,并且设定 1 year cache
  location ~* \.(gif|jpg|jpeg|svg|png|ico|woff|woff2|js|css|json|yaml)$ {
    expires 1y;
    add_header Cache-Control "public";
    proxy_pass              http://localhost:3030;
  }
  location ~* /(sign_in|sign_up) {
    proxy_pass              http://localhost:3030;
  }
  location /  {
    auth_request     /auth;
    auth_request_set $auth_status $upstream_status;
    error_page 401  =302 http://$http_host/signin?redirect=$request_uri;
    proxy_pass              http://localhost:3030;
  }
  location = /auth {
    internal;
    proxy_pass              http://localhost:3030/api/auth;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
    proxy_set_header        X-Original-URI $request_uri;
  }

简单解释一下:

  1. sign_in 和 sign_up 页面直接返回 NextJS App 的页面,不做鉴权。
  2. 其他页面进入鉴权模块 auth_request,这里 /auth 表示负责鉴权的 uri。
  3. /auth 这里,会将请求转交给 NextJS 的 /api/auth 接口处理。
  4. 当前一步结束, error_page 会检测之前(/auth)处理的结果 http status 是否为 401,是的话,返回 302 指向 sign_in 页面。注意这里使用了很多内置参数
  5. 如果 http status 不是 401,则继续由 Nginx App 提供页面内容。

结论

NextJS 本身无法兼顾静态和服务端鉴权,此时,可以将鉴权交由 Nginx 负责,通过它的 auth request 模块来转发请求,处理鉴权结果。 参考资料:

ps. Nginx 的 tag 居然是存在的,这证明之前也写过 Nginx 相关的东西。感觉每次都是 Nginx 救场,哈哈。