HTTP 通信安全

HTTP 通信安全主要从三个方面入手:

  1. 使用 HTTPS 代替 HTTP。

  2. Strict-Transport-Security 配置。

  3. 代理服务器配置。

其中第 2 点我们前面已经讲过,这里主要和天家分享第 1 点和第 3 点。

使用HTTPS

作为一个框架,Spring Security 不处理 HTTP 连接问题,因此不直接提供对 HTTPS 的支持。但是,它提供了许多有助于 HTTPS 使用的功能。

接下来我们通过一个简单的案例来演示其具体用法。

首先创建一个 Spring Boot 项目,引入 Spring Security 和 Web,然后参考 9.2.3 小节中的方式创建 HTTPS 证书,并配置到 Spring Boot 项目中。

配置完成后,我们再在 application.properties 中添加如下配置修改项目端口号:

spring.security.user.name=javaboy
spring.security.user.password=123
server.port=8443

此时我们的项目就支持 HTTPS 访问了,HTTPS 的访问端口是 8443。为了更好地演示 Spring Security 的功能,我们需要项目同时支持 HTTPS 和 HTTP,所以还需要在项目中添加如下配置:

@Configuration
public class TomcatConfig {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addAdditionalTomcatConnectors(createTomcatConnector());
        return factory;
    }
    private Connector createTomcatConnector() {
        Connector connector = new
                Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);
        return connector;
    }
}

这相当于又添加了一个 Connector,让 Tomcat 同时监听 8080 端口。

配置完成后,启动项目,控制台可以看到如下日志,表示项目现在同时支持 HTTP 和 HTTPS 了:

Tomcat started on port(s): 8443 (https) 8080 (http) with context path ''

接下来,我们创建两人测试接口,代码如下:

@RestController
public class HelloController {

    @GetMapping("/https")
    public String https() {
        return "https";
    }

    @GetMapping("/http")
    public String http() {
        return "http";
    }

}

/http 接口表示可以直接使用 HTTP 议访问,/https 接口表示只可以通过 HTTPS 协议访问。我们来看一下 SecurityConfig 中的配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .requiresChannel()
                .antMatchers("/https").requiresSecure()
                .antMatchers("/http").requiresInsecure()
                .and()
                .csrf().disable();
    }
}

通过 requiresChannel() 方法开启配置,requiresSecure() 表示该请求是 HTTPS 协议,如果不是,则重定向到 HTTPS 协议请求;requiresInsecure() 则要求请求是 HTTP 协议,如果不是,则重定到 HTTP 协议请求。没有列举出来的请求,则是两种协议都可以。

配置完成后,重启项目。使用如下两个地址都可以访问到登录页面:

现在我们使用 http://localhost:8080/login 地址登录成功后,访问 /http 没问题,访问 /https 则会自动重定向到 https://localhost:8443/https。之所以重定向到 8443 端口,并非因为我们项目端口是 8443,而是因为 8443 是 HTTPS 的默认监听端口,无论项目端口号是多少,这里都会重定向到 8443 端口。

现在修改 application.properties 中的配置,将 server.port 改为 8444,即将项目中 HTTPS 的监听端口改为 8444,那么如何让这里重定向到 8444 呢?配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .portMapper()
                .http(8080).mapsTo(8444)
                .and()
                .requiresChannel()
                .antMatchers("/https").requiresSecure()
                .antMatchers("/http").requiresInsecure()
                .and()
                .csrf().disable();
    }
}

通过 portMapper() 方法开启端口的映射配置,这里我们配置将 HTTP 端口8080 转发到 HTTPS 端口 8444。配置完成后,当用户再次访问 http://localhost:8080/https 时,就会自动重定向到 https://localhost:8444/https 地址。

在测试时有一个问题需要注意。如果一开始使用 HTTP 协议登录,则登录成功访问 /http、/https 都没有问题,都会自动进行重定向,但是如果一开始使用了 HTTPS 协议登录,则在登录成功后,从 HTTPS 协议重定向到 HTTP 协议时,会让用户重新登录。出现这一问题的原因在于,如果用户使用 HTTPS 协议登录,则返回的 Cookie 中包含了 Secure 标记(见图 9-14),该属性表示该 Cookie 只可以在安全环境下传输(即 HTTPS 协议中传输),当从 HTTPS 重定向到 HTTP 时,HTTP 请求并不会自动携带该 Cookie,所以就会让用户重新登录。反之,如果一开始登录使用了 HTTP 协议,则返回的 Cookie 中没有 Secure 标记,该 Cookie 在 HTTPS 和 HTTP 环境下都可以传输,因此可以无缝重定向。同时,由于我们的两个测试地址域名都是 localhost,而 Cookie 是不区分端口号的,如果 Cookie 名相同,会自动覆盖,并且读取的是相同的数据。所以,当从 HTTPS 协议重定向到 HTTP 协议时,浏览器上 HTTPS 的 JSESSIONID 还在,但是 HTTP 协议又用不了该 Cookie,就会导致 HTTP 协议一直登录失败,此时只要清除浏览器缓存即可。

image 2024 04 14 18 37 34 538
Figure 1. 图 9-14 HTTPS 环境下,Cookie 中含有 Secure 标记

这就是 Spring Security 中关于 HTTP 请求转发到 HTTPS 的配置,当然,HTTP 自动转发到 HTTPS 也可以在 Servlet 容器层面进行配置,这个不属于本书的范畴,这里不做过多介绍,感兴趣的读者可以自行查找资料学习。

代理服务器配置

在分布式环境下或者集群环境下,项目中可能会引入 Nginx 作为负载均衡服务器,这个时候需要确保自己的代理服务器和 Spring Security 的配置是正确的,以便 Spring Security 能够准确获取请求的真实 IP,避免各种潜在的威胁或应用程序错误。

开发者需要在代理服务器中配置 X-Forwarded-* 以便将客户端信息转发到真实的后端,后端收到请求后,不同的 Servlet 容器以及 Spring 框架都有各自的方式从请求头中获取客户端真实的 IP 地址、Host 以及请求协议等,并重新设置在相应的 request.getXXX 方法上,开发者调用这些方法就可以成功获取到客户端的真实信息,并不会感觉到有代理服务器存在。例如,在 Tomcat 中是通过 RemoteIpValve 类来处理,在 Jetty 中是通过 ForwardedRequestCustomizer 来处理,Spring 框架则是通过 ForwardedHeaderFilter 过滤器来处理。实际项目中,选择一种方式即可。

由于这里并不涉及 Spring Security 的相关知识,因此不做过多介绍,读者在实际项目开发中注意此问题即可。