背景
Nginx反向代理着Spring boot搭建的Java服务
正常情况下Nginx同时监听着80和443端口,SpringSecurity中的CAS用着也没毛病
突然有一天,80和443端口不让用了,还只能监听https端口,CAS流程用着就不行了,主要归结为两个问题
问题
问题1--重定向到http
由于Spring boot应用是http部署的,nginx和后端之间也是http交互,所以后端获取到的scheme都是http
再重定向的时候都重定向到http的地址上,但是现在nginx是没有监听http的,因此会报错400 Send http request to https server
问题2--重定向无端口号
再解决了上一个问题后,出现的新问题,重定向的时候地址里没有包括端口号,直接重定向到了443端口,导致连接不通
解决方案
先说解决方案,再细说踩坑过程
Nginx配置
location / {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_pass http://upstream;
}
首先是再Nginx的location
配置添加这两个Header
-
$scheme
对应的是协议http
或https
,取决于location
位于http的server
块中,还是https的server
快中 -
$server_port
对应的是当前server
块里监听的端口号
Springboot启动参数
- --server.tomcat.protocol-header=X-Forwarded-Proto
- --server.tomcat.port-header=X-Forwarded-Port
- --server.use-forward-headers=true
再Springboot的启动参数里我们需要添加这三个,用于接收这些Header信息
就这么两步就能解决问题了,springboot版本为2.1.6
,nginx版本为1.16.1
踩坑过程
踩坑1
最开始把Nginx的配置加在server
块里面(不是location
块里面),一直不生效,搞得都快怀疑人生了
但是为了解决问题,让Springboot在重定向的时候能够转到https,自定义了一个AuthenticationSuccessHandler
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("Into custom success handler");
setRequestCache(requestCache);
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
String refer = NetUtil.getSchemeFromRefer(request);
if (org.apache.commons.lang3.StringUtils.isNotEmpty(refer) && targetUrl.startsWith("http://")) {
log.info("Replace http with https");
targetUrl = targetUrl.replace("http://", "https://");
}
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
然后再CasAuthenticationFilter
中启用这个AuthenticationSuccessHandler
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(CAS_SERVICE_URL);
casAuthenticationFilter.setAuthenticationDetailsSource(myWebAuthenticationDetailsSource());
//casAuthenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
return casAuthenticationFilter;
}
最后是我们的登录接口/login
,这里原来重定向的时候不是完整的地址,而是只有请求路径和参数,这种路径在重定向的时候也会被重定向到http地址,而不是https地址,因此在这里把重定向的地址改为完成的https地址,避免出现重定向到http地址的问题
@GetMapping(value = "/login")
public void login(HttpServletRequest request, HttpServletResponse response) throws Exception {
String queryString = request.getQueryString();
String redirect = "/";
LOGGER.info("login redirect:{}", redirect);
redirect = NetUtil.constructCasUrl(request, redirect, null);
response.sendRedirect(redirect);
}
这个解决方案也能正常工作,在AuthenticationSuccessHandler
中主要是添加了参考Referer
这个Header信息来重新替换https,这样让它在登录成功后能够重定向到https
链接
String refer = NetUtil.getSchemeFromRefer(request);
if (org.apache.commons.lang3.StringUtils.isNotEmpty(refer) && targetUrl.startsWith("http://")) {
log.info("Replace http with https");
targetUrl = targetUrl.replace("http://", "https://");
}
为啥会想到这个解决方案呢?这就需要我们开启Springboot的debug级别日志,添加启动参数
- --logging.level.root=debug
开启debug日志后你可以看到这些日志
2023-09-21 07:36:38.246|DEBUG|http-nio-33442-exec-6 (HttpSessionRequestCache.java:61) - DefaultSavedRequest added to Session: DefaultSavedRequest[http://xx.xx.com:33443/login?path=/xxxxxxxxxxxxxxxxxxxxxxx]
2023-09-21 07:36:38.246|DEBUG|http-nio-33442-exec-6 (ExceptionTranslationFilter.java:212) - Calling Authentication entry point.
2023-09-21 07:37:20.277|DEBUG|http-nio-33442-exec-7 (DefaultRedirectStrategy.java:54) - Redirecting to 'http://xx.xx.com:33443/login?path=/xxxxxxxxxxxxxxxxxxxxxxx'
可以看到在登陆之前Spring Security就会把你之前的登录请求保存下来,在你CAS登录成功后直接读取之前保存的请求进行重定向;
但是问题出在这里保存的登录请求就已经是http了,所以重定向的时候就重定向到了http地址,导致请求400问题
所以上面的解决方案就是不管你之前保存的是http地址还是https地址,在你重定向的时候再查验一下Referer
这个Header信息,如果这个Header是https的,那就将重定向的地址改为https;
那为啥这个方案没有让我满意呢?是因为还有这么一行日志打印了Referer
这个Header信息
2023-09-21 07:37:20.277|INFO|http-nio-33442-exec-7 (CustomAuthenticationSuccessHandler.java:31) - Into custom success handler
2023-09-21 07:37:20.277|INFO|http-nio-33442-exec-7 (NetUtil.java:100) - Referer header:https://auth.xxx.com/
2023-09-21 07:37:20.277|INFO|http-nio-33442-exec-7 (CustomAuthenticationSuccessHandler.java:57) - Replace http with https
2023-09-21 07:37:20.277|DEBUG|http-nio-33442-exec-7 (DefaultRedirectStrategy.java:54) - Redirecting to 'https://xx.xx:33443/login?path=/xxxxxxxxxxxxxxxxxxxxxxx'
可以看到这里出现了Referer
这个Header信息为CAS的地址,这就意味着我们项目里是否替换https还需要依赖CAS的地址吗?
这显然是不科学的,如果CAS地址是http的,那我们就GG了;或者我们是http的,但是CAS是https的,那我们也GG了
所以还是没有采纳这个方案
踩坑2
病急乱投医,一顿瞎配置
最初把Nginx的X-Forwarded-*配置放在server块里,一直没觉得有啥问题,给Springboot一顿瞎配置
# Springboot 2.1.6不能用这两个配置
server.tomcat.remoteip.remote-ip-header=x-your-remote-ip-header
server.tomcat.remoteip.protocol-header=x-your-protocol-header
# 这个限制IP的也不需要
server.tomcat.remoteip.internal-proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3}
# Springboot 2.1.6 不能用这个配置
server.forward-headers-strategy=native
# 这个官网给的配置也不需要
server.tomcat.redirect-context-root=false
踩坑3
最开始只想要能够通过X-Forwarded-Proto拿到scheme信息,所以只加了这一个Header
但是再登录成功后重定向到了https://xx.xx/login?path=/xxxxxxxxxxxxxxxxxxxxxxx
地址,即重定向到了443端口
为啥会重定向到443端口呢?我们具体看下端口是怎么计算出来的
public int getServerPort(ServletRequest request) {
int serverPort = request.getServerPort();
...
return serverPort;
}
可以看到是ServletRequest里没有获取到Nginx中指定的端口,只获取到了默认的端口443
后来加上了X-Forwarded-Port这个Header后解决了,让Nginx把端口信息也传下来就可以了
网友评论