Springboot中配置SSL证书

一、为什么要部署SSL证书:

目前互联网采取的是全网强制SSL,尤其是微信开发、APP开发、谷歌等都是强制的,为确保数据安全性,把http请求改成HTTPS(URL
https://而不是http://)请求确保客户端与站点传输数据的加密作用,所有操作系统都可以部署。

二、SSL证书是什么?

SSL(Secure socket
layer)对用户和服务器进行认证,对传输数据进行加密的和隐藏的全球化标准的的安全协议,保证在互联网交易中,双方传递信息的安全性。

作为文件形式存在的证书一般有这几种格式:

  **1.**带有私钥的证书

  由Public Key Cryptography Standards #12,PKCS#12标准定义,包含了公钥和私钥的二进制格式的证书形式,以pfx作为证书文件后缀名。

  **2.**二进制编码的证书

  证书中没有私钥,DER 编码二进制格式的证书文件,以cer作为证书文件后缀名。

  3.Base64编码的证书

证书中没有私钥,BASE64 编码格式的证书文件,也是以cer作为证书文件后缀名。

由定义可以看出,只有pfx**格式的数字证书是包含有私钥的,cer**格式的数字证书里面只有公钥没有私钥。

  在pfx证书的导入过程中有一项是“标志此密钥是可导出的。这将您在稍候备份或传输密钥”。一般是不选中的,如果选中,别人就有机会备份你的密钥了。如果是不选中,其实密钥也导入了,只是不能再次被导出。这就保证了密钥的安全。

  如果导入过程中没有选中这一项,做证书备份时“导出私钥”这一项是灰色的,不能选。只能导出cer格式的公钥。如果导入时选中该项,则在导出时“导出私钥”这一项就是可选的。

  如果要导出私钥(pfx),是需要输入密码的,这个密码就是对私钥再次加密,这样就保证了私钥的安全,别人即使拿到了你的证书备份(pfx),不知道加密私钥的密码,也是无法导入证书的。相反,如果只是导入导出cer格式的证书,是不会提示你输入密码的。因为公钥一般来说是对外公开的,不用加密

三. 配置

  1. 将.pfx文件放到项目的resources目录下

  2. 在properties或yml中如下配置

#https加密端口号 8002
server.port=8002
#SSL证书路径 一定要加上classpath: 3824167.jks
server.ssl.key-store=classpath:yunservice.ltd.pfx
#SSL证书密码6cj3QrTo
server.ssl.key-store-password=7K8UBVe5
#证书类型
server.ssl.key-store-type=PKCS12
#证书别名
server.ssl.key-alias=alias
  1. 在项目中添加如下配置
@Configuration
public class TomcatConfig {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
// TODO Auto-generated method stub
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};

factory.addAdditionalTomcatConnectors(createTomcatConnector());
return factory;

}

private Connector createTomcatConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8001);
connector.setSecure(false);
connector.setRedirectPort(8002);
return connector;
}
}

这里首先配置一个TomcatServletWebServerFactory,然后添加一个Tomcat中的Connector(监听80端口),并将请求转发到8080上去.

配置完成后,在浏览器中输入:”http://localhost:8001/hello",就会自动重定向到https://localhost:8002/hello上。

或者直接输入https://localhost:8002/hello也是能访问的

问题: post请求重定向后变成了GET请求

配置之后,确实生效。http get 请求 都能被重定向到https get 上,但是 http post 请求 死活不行,后台老是报requestMethod GET not supported,我访问的是post 方法呀,怎么就变成了get,百思不得其解,后来看到一篇博客讲到nginx做转发的时候也有这种问题,看了一下他的解决方法。

server {
listen 80;
server_name *.snsprj.cn;
return 307 https://request_uri;
}

我首先注意到了307 状态码,于是我想看一下我用tomcat 配置的它默认返回的状态码是多少
这里写图片描述
从上图中可以看出状态码是302,由于本人知识面比较窄,并不知道302,307的代表含义,于是就去查询相关资料,摘自wikipedia

302 Found
要求客户端执行临时重定向(原始描述短语为“Moved Temporarily”)。[20]由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。
如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。因此状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应。

看到这里就明白了为什么http post 请求重定向时会被变成http get 请求。

307 Temporary Redirect
在这种情况下,请求应该与另一个URI重复,但后续的请求应仍使用原始的URI。 与302相反,当重新发出原始请求时,不允许更改请求方法。 例如,应该使用另一个POST请求来重复POST请求

解决办法

由于我使用的是tomcat 进行重定向的,从上文中也可以看出tomcat 默认进行的是302重定向,不符合我们的需求,那又什么办法可以解决呢?办法就是不用tomcat提供的配置,那就是我们自己后台程序处理,毕竟用人家的受限制,自己搞,想怎么弄就怎么弄。自己做的话很明显需要一个过滤器,在请求到达sevlet 之前进行处理,代码如下:

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@WebFilter(urlPatterns="/*",filterName="HttpsFilter")
/**
* 过滤器,将http 请求转发到https请求上来
* 重定向类型:307
* @author FrankYuan
*
*/
public class HttpsFilter implements Filter{
private Logger logger = LoggerFactory.getLogger(HttpsFilter.class);
private static final String HTTPS ="https";
private static final int HTTPS_PORT = 8443;
@Override
public void destroy() {
logger.info("------------destroy HttpsFilter --------------");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
URL newUrl = null;
if(request.getScheme().equals(HTTPS)) {
chain.doFilter(request, response);
}else {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
String queryString = httpRequest.getQueryString()==null ? "":"?"+httpRequest.getQueryString();
httpResponse.setStatus(307);
String requestUrl = httpRequest.getRequestURL().toString();
URL reqUrl = new URL(requestUrl+queryString);
logger.info("【original request-】 "+reqUrl.toString());
newUrl = new URL(HTTPS,reqUrl.getHost(),HTTPS_PORT,reqUrl.getFile());
//进行重定向
logger.info("【new request-】 "+newUrl.toString());
httpResponse.setHeader("Location", newUrl.toString());
httpResponse.setHeader("Connection", "close");
//允许所有跨域请求
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
}

}

@Override
public void init(FilterConfig arg0) throws ServletException {
logger.info("------------init HttpsFilter --------------");

}

}

注意:这里又个坑,就是重定向后需要也需要解决跨域问题,不然页面ajax请求 http 地址会出现跨域问题。

关键代码

httpResponse.setStatus(307);

将响应码换成307