相关文章推荐
成熟的馒头  ·  南北年俗大PK体现“和而不同”:汤圆VS饺子 ...·  11 月前    · 
冲动的八宝粥  ·  我校与白马湖实验室签署共建能源与碳中和科教融 ...·  1 年前    · 
开心的滑板  ·  我在末世有座黄金宫-🌈️包子漫畫·  2 年前    · 
腹黑的剪刀  ·  沙马兰执导惊悚片《拜访小屋》发海报,2月3日 ...·  2 年前    · 
酒量小的骆驼  ·  漫画解说女帝的后宫_漫画解说女帝的后宫生活_ ...·  2 年前    · 
Code  ›  单页应用的登陆验证方式 | 卡瓦邦噶!
nginx 重定向 cookie 单页面应用
https://www.kawabangga.com/posts/3729
奋斗的机器人
2 年前
Posted on 2019年10月28日 by laixintao 6 Comments

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

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

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

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

Before:

  • 用户访问页面;
  • 用户访问 API;
  • API 返回了 403;
  • 用户跳转到登陆;
  • After:

  • 用户访问页面;
  • 页面判断用户没有登陆,返回302;
  • 用户跳转到登陆;
  • 之所以出现这个问题,因为采用了传统的单页应用部署方式,即前后端完全分离。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 慢很多。

    所以这里需要:

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

    proxy_pass http://appserver; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-By $server_addr:$server_port; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Connection "";
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server {
    listen 80 default_server ;
    location ^ ~ / {
    rewrite / ( . * ) / break ;
    proxy_pass http : / / appserver ;
    proxy_set_header Host $http_host ;
    proxy_set_header X-Forwarded-By $server_addr : $server_port ;
    proxy_set_header X-Forwarded-For $remote_addr ;
    proxy_set_header Connection "" ;
    }
    }

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

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

    @RequestMapping(value = "/", method = RequestMethod.GET) public String doGet() { return "app.vm"; proxy_pass http://appserver; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-By $server_addr:$server_port; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Connection "";
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server {
    listen 80 default_server ;
    location ^ ~ / a {
    rewrite / a / ( . * ) / a break ;
    proxy_pass http : / / appserver ;
    proxy_set_header Host $http_host ;
    proxy_set_header X-Forwarded-By $server_addr : $server_port ;
    proxy_set_header X-Forwarded-For $remote_addr ;
    proxy_set_header Connection "" ;
    }
    }

    然后应用只渲染 /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 指令。

    配置如下:

    rewrite /m/(.*) /m break; proxy_pass http://appserver; proxy_redirect ~*^(https?://[^/]+/)(.+) $1sourceUrl=http://$http_host$request_uri; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-By $server_addr:$server_port; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Connection "";
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server {
    listen 80 default_server ;
    location ^ ~ / m {
    rewrite / m / ( . * ) / m break ;
    proxy_pass http : / / appserver ;
    proxy_redirect ~ * ^ ( https ? : / / [^/] + / ) ( . + ) $1sourceUrl = http : / / $http_host $request_uri ;
    proxy_set_header Host $http_host ;
    proxy_set_header X-Forwarded-By $server_addr : $server_port ;
    proxy_set_header X-Forwarded-For $remote_addr ;
    proxy_set_header Connection "" ;
    }
    }

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

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

    所以楼主是想解决:但是单页应用一般是页面先渲染出来,然后通过 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 也用不了啊~~

     
    推荐文章
    成熟的馒头  ·  南北年俗大PK体现“和而不同”:汤圆VS饺子 花街VS庙会
    11 月前
    冲动的八宝粥  ·  我校与白马湖实验室签署共建能源与碳中和科教融合学院协议
    1 年前
    开心的滑板  ·  我在末世有座黄金宫-🌈️包子漫畫
    2 年前
    腹黑的剪刀  ·  沙马兰执导惊悚片《拜访小屋》发海报,2月3日北美上映_影片_要求_一家人
    2 年前
    酒量小的骆驼  ·  漫画解说女帝的后宫_漫画解说女帝的后宫生活_女帝的后宫怎么画 - 抖音
    2 年前
    今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
    删除内容请联系邮箱 2879853325@qq.com
    Code - 代码工具平台
    © 2024 ~ 沪ICP备11025650号