单页应用的登陆验证方式

前端流行的单页应用(SPA),带来了很多体验上的优化,也带来了很多问题。以往我们每点击一次鼠标,页面就会在后台生成一次,很多事情都会很简单。但是到了单页应用,相当于访问页面的时候只请求了页面一次,后面的数据、表单等都是通过 AJAX 的,页面的渲染由前端完成,给后端带来了很多挑战。

比如说登陆验证,传统的页面每次请求都会经过后端判断一下 Cookie 中携带的信息,如果用户没有登陆,就将用户重定向到一个登陆页面。

但是单页应用一般是页面先渲染出来,然后通过 API 访问后端,虽然鉴权方式还是通过访问 API 的时候携带 Cookie,但是用户将会先看到页面加载出来,然后访问 API 得到 403,跳转到登陆页面,体验就不太友好了。(应该是页面都渲染不出来,直接跳到登陆)。

今天用了一个方法,Nginx 配合后端的应用,来解决了这个问题。这篇文章来分享一下原理。

Before:

  1. 用户访问页面;
  2. 用户访问 API;
  3. API 返回了 403;
  4. 用户跳转到登陆;

After:

  1. 用户访问页面;
  2. 页面判断用户没有登陆,返回302;
  3. 用户跳转到登陆;

之所以出现这个问题,因为采用了传统的单页应用部署方式,即前后端完全分离。Nginx 负责返回前端页面,应用只是一个 API 服务器。渲染页面这一步应用感知不到。

部署方式

要判断用户是否登录的话,就要让“返回前端HTML”这一步交给应用来做。(假如我们想保持 Nginx 不涉及业务逻辑的话。)有人可能认为这样会很慢,实际上返回HTML这一步是很快的,因为这个 HTML 很小,我们写的前端应用都作为 <link> <script> 等引用外部资源异步加载,而且这些静态资源一般都是有 CDN 的。

真正可能慢的地方是 URL 匹配。我们知道在单页应用中,你访问 /m/foo 和 /m/bar 都是完全相同的一个 HTML,只是前端应用通过 URL 的不同来帮你路由了。但是访问 /api/foo.json 的时候可能真正访问到了服务器。一般来说,什么时候是访问静态页面,什么时候是请求 API,我们是交给 Nginx 配置来处理的,因为 Nginx 是一个专业的 Web 服务器,URL 匹配的效率很高。虽然我没测试过,但是我觉得 Django/Spring 这种 Web 框架,匹配 URL 的速度肯定要比 Nginx 慢很多。

所以这里需要:

  1. 让应用判断用户是否登录,选择是返回 HTML 还是重定向到登录页面;
  2. URL 匹配还是在 Nginx 做,如果是前端页面,那么 Nginx 就去掉 Path,直接 proxy_pass / 就可以了。那么应用这边只需要对 / 这个 Path 来返回一个 HTML;

Nginx 相关的配置如下:

表示正则 match 到 / 开头的话,就用 rewrite 指令去掉 / 后面的内容,将 URL 改写成 / 。然后 break 参数告诉 Nginx 停止处理其他的 rewrite,传给后端的 appserver 。注意这里我们的 rewrite 只是改写了传给应用的 Path,此时 Chrome 浏览器的 Path 还是完整的,所以我们这么做并不影响前端的路由。

后端的应用只要对 / 这个 Path 返回一个 HTML 就好啦。比如说用 Spring 的话,就这么写:

这个配置其实很灵活。如果你要 /a 是一个 app,/b 是另一个 app 的话,只要将 Nginx 配置替换成西面这样:

然后应用只渲染 /a 这个 Path 就好啦。

总结下原理就是:Nginx 负责去掉单页应用的 Path,替换成根目录,然后 App 负责判断用户是否登录,如果登录就返回根目录的 HTML,浏览器渲染出来页面。

登陆跳转问题

这里有一个小小的问题,就是后端应用将用户重定向到登陆的回收,会带上一个用户登陆完成之后回到的URL。比如用户访问的是 /hello/bar,那么我们希望用户登陆完成之后,直接跳转到 /hello/bar 。

但是因为 Nginx 将用户访问的 Path 给去掉了,应用认为用户访问的是 / ,那么用户登陆之后就看到的是首页,这样体验就不太好了。

解决这个问题,一开始我想用 cookie 记录下用户去登陆之前想访问的 URL (也是在传统网页时代,我们经常用的方法)。这样一来就要区分用户什么时候是想访问的 cookie 的 URL(应该重定向),什么时候就是想访问用户输入的 URL (不需要重定向)。还要注意什么时候应该设置 Cookie,什么时候不要设置 Cookie,什么时候清除Cookie。所以这个方案比较复杂。

搜索了一通之后,我发现 Nginx 是可以对代理后面的服务器返回的 Location Header 进行修改的。

比如我们 proxy_pass 到 appserver,appserver 返回了 302,带有 Location: /abc/de 的 Header,Nginx 可以将 Header 修改成 /foo/bar ,再返回给浏览器。

这个功能就是 proxy_redirect 指令。

配置如下:

注意这里的第7行,如果应用返回了 Location Header,那么这里就会将应用返回的重定向目标,修改成第二个参数所表示的重定向目标。其中,$http_host 是用户请求的 URL 里面的 Host,$request_uri 是用户请求的原始的 Path。

 

这样,我们就有了和传统网页一致的登陆体验了。



单页应用的登陆验证方式”已经有6条评论

  1. 我知道在vue里有beforeRoute的钩子,可以在这里去请求后端判断登录。它会在路由之前就执行,在渲染之前。

    提供另一个可能性

    • Cool!这样用户就不必看到渲染页面了。

      不过感觉有个坏处,就是重定向之前页面需要请求下来,然后再请求一次API。相当于请求了两次。

      就算已经登录的情况下,也需要先请求一次API然后才能进行后续的渲染,感觉会拖慢所有页面的打开速度。

  2. 所以楼主是想解决:但是单页应用一般是页面先渲染出来,然后通过 API 访问后端,虽然鉴权方式还是通过访问 API 的时候携带 Cookie,但是用户将会先看到页面加载出来,然后访问 API 得到 403,跳转到登陆页面,体验就不太友好了。(应该是页面都渲染不出来,直接跳到登陆)。这个问题,Vue已经有非常成熟方案了,看到你的解决方案,我觉得有点杀鸡用牛刀,还烦请了Nginx,docker化部署会带来很多麻烦。方案:Vue路由导航beforeRoute,登录使用jwt这类token,token存放在localStorage,在beforeRoute先检查token中的过期时间,过期了就跳转行了,只需要2个接口:登录接口返回token、权限获取接口返回过期时间等等。如果有token并且不过期,就直接正常挂载路由,根本不用动nginx。话说文中nginx滥用了,在前后端分离开发模式下,前端和后端耦合越少越好。

    • 是的,是想解决这个问题。也看过 vue 这个方案,我觉得这个是最好的,登陆之后跳回原来的 URL,vue 也可以一并解决,实现起来非常直白,除了每次打开页面多发送一次HTTP请求token(会有点慢?)之外没有任何缺点。奈何我们公司的前端框架是自研的,只能靠 Nginx + 后端应用来解决这个问题了。

      我们公司的研发环境和框架结合了,你必须用它这个框架,才能在研发环境中自动帮你 build,部署各个测试环境等。所以我想用 vue 也用不了啊~~

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注