现象和问题:
有一个基于SpringBoot+Spring Security和CAS SSO的应用A,端口是8080,前端为Nginx,Nginx对外为https,即443端口,nginx内部反向代理到A就是常规的http协议了,应用A配置了正确的SSO login url和service url,历史原因,Nginx混乱的逻辑,没有配置80(http)强转443(https)。
问题来了:服务A本身运行正常,但是开启nginx前端代理时候,发现通过https进入系统A时,第一次(sso登录验证成功)通过url1总是跳转到 80端口(http)的服务,而不是443端口(https)的A应用,但是第二次再通过url1就能正常访问A应用。
怎么去解决这个问题呢?可能大部分人没看懂上文所述问题所在,也会猜到在Nginx上配置80强转443即可解决问题。
但本文希望探寻下问题的本质,以及有无其他解决办法。
先看简化且脱敏后的nginx配置
80端口为原生网页,443为A应用代理端口:
如果你对Spring Boot+Spring security+cas部分代码感兴趣,文末也附带脱敏后的代码了。
先透露下问题出在nginx的这行配置 “proxy_set_header Host $http_host”。
打开浏览器访问 https://dev.example.com/rule/index ,可以看到前面几个跳转 sso 服务器以及本地service url:https://dev.example.com/login/cas 都是正确的,即 SSO登录验证成功,访问https://dev.example.com/login/cas 也确实返回了 302 跳转链接: http://dev.example.com/rule/index ,问题就在302跳转这一步,返回302时的Location是http:// 而不是https://。
如何定位是哪一步出错?
如果把Spring Boot日志设置为debug level可能是可以的,幸而 Spring Security打印的日志足够详细,我们才能看到返回 302 条转链接相关一条log:
即上面A应用返回的是http,而不是htts,是否意味着A应用的spring security cas的bug?
下面我会针对这条日志,看几种不同的解决方案。
0.
先不看即决方案,先看 http:// 是怎么来的?通过 DefaultSavedRequest 源码,可以看到http其实是 从tomcat的Request的schema取得,tomcat的Request 解析/设置 有其本身的逻辑,当Nginx通过http://协议,就决定request只能获取http的schema。不过设置 “proxy_set_header Host $http_host” 就导致tomcat解析后,当发生302跳转时拼接的Host前半部分就是Host,即http://开头。
上面日志,即Spring Security返回http是对错?有的人认为可能是bug,其实不是。Nginx和A应用之间是 http协议,也就是说,nginx传给A应用时已经向其屏蔽了客户端的https信息,如果Spring Security解析出 https的schema,那Spring Security才是真正有bug了。
故而考虑下面方法。
1. 修改Nginx到tomcat的配置
修改Nginx到tomcat的配置,把客户端的 request 信息通过 proxy_set_header方式都传给 tomcat,从而让Spring security正确解析。
这个方法是可行的,可以参考这篇博文:SSO 无法获取正确的schema
简单摘录下:
如果你的应用复杂,多处使用到客户端原始的 request 信息里的header等(或不仅仅在sso登录这一步使用),那么 推荐该方式,虽然 使用/配置 起来较为复杂。
2. 修改 Spring Security CAS代码
能否修改Spring Security或者 CAS代码来实现?既然nginx传给应用A时已经丢失了schema信息,那么能否通过Spring的配置信息设置正确的Location?
让我们先看看 Spring Security 在哪里生成该Location。追寻 CasAuthenticationFilter 这个CAS的SSO实现filter,可以发现在 SavedRequestAwareAuthenticationSuccessHandler 通过 DefaultRedirectStrategy 生成了 302跳转的redirectUrl:
重写 sendRedirect 方法即可,即直接加个 如果redirect url以http://开头则替换为https://的逻辑。
不过由于Location是在 Spring Request里拼成的,有的同学可能会想到,那么是否可以通过只让这个 Location 以 // 开头,这样能适配http/https,即是否可以用下面方式?
这种方式是不可以的,org.apache.catalina.connector.Response.toAbsolute(String location) 这里实际上会对 redirectUrl 做一个schema的判断并修改为http或https。
但是考虑到,用https还是 http其实在配置 sso的 service url 时候已经可知了,所以可以根据 service url 来判断用http还是https,见下文MoreDefaultRedirectStrategy.java 部分代码。
3. 删除Nginx配置
上面两种方法都可解决问题,方案2较之方案1改动少,而且无需改nginx,但是他们其实都违背了系统设计之间单一性,增加了不必要的耦合。
如果 把 Nginx里 “proxy_set_header Host $http_host;” 这行去掉会发生什么呢?
还是开启应用A的Spring log level为debug会发现
你是否感到奇怪这里返回的跳转链接是 http://audit_config.short/rule/index ?更奇怪的是返回给客户端(浏览器)竟然是正确的Location,即 https://dev.example.com/rule/index 。
首先注意 host 为 audit_config.short ,即配置的nginx的 upstream名字,而不是大多数人认为的 dev.example.com,
其次Nginx把 Location中的 audit_config.short 重写了。
怎么去验证这个想法呢?
nginx debug 日志打开:
1)server配置添加 error_log /path/to/log; 这一行。
2)如果是Mac brew 安装,需要 “brew install nginx –cc –with-debug”指令,记住这里要加 –cc的参数,否则不对,至少目前版本的不对。
可以看到一下nginx日志:
也就是说 Nginx其实已经 rewrite 302的Location了,那么什么情况下会rewrite呢?
|
|
代码看下去略长,不过可以参考官方注释:
http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect
即,这里仅能进行简单的 proxy_pass 逆向替换。
附:相关代码
spring-boot.version:1.5.4.RELEASE
spring-security-cas:4.2.3.RELEASE
application.properties
下面是简单写的一段demo代码:
|
|
MoreDefaultRedirectStrategy部分更改:
outro
说一个不相干的感悟,为什么TCP必须三次握手?
在网上可以搜到答案,都很有道理,不过我想补充一下,这或许是很多人并不在乎的点,或者认为讨论三次以上意义不大。
但为什么是三次,五次不行吗?
三次其实就是请求确认->确认->对确认的确认,如果从严格的科学理论上讲,这可能是不够的,一个无限循环,但是从技术上讲,也就是涉及经验(当然也综合考虑了性能/效率等因素)。
超过三次就强制认为失败而已。
计算机技术并没有大家想象的那么严谨,甚至可能会有 0.3不等于0.3的情况,如果大学了解过一点数电和模电的知识,就会知道这种区别,如果再学过物理理论的对立面–物理实验,就会理解 误差/精确度 的含义。
同时也会知道,流行大众以及电影上的“蝴蝶效应”其实算是个笑话吧。但这并不表示误差就不重要,实际上,上个世纪最伟大的物理理论,跟人们对于极小误差的纠结源远很深呢。