From 4199be84fa92157eeee96de798ce45a3c2fd6221 Mon Sep 17 00:00:00 2001 From: ctlove0523 <478309639@qq.com> Date: Sun, 6 Mar 2022 01:17:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0tls=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=A0=B7=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++ pom.xml | 1 + spring-boot-tls/pom.xml | 30 ++++++ .../github/ctlove0523/tls/AllowedFilter.java | 67 +++++++++++++ .../ctlove0523/tls/ConnectorConfigure.java | 69 +++++++++++++ .../tls/ConnectorConfigureRepository.java | 8 ++ .../ctlove0523/tls/ServerAllowedUrl.java | 24 +++++ .../github/ctlove0523/tls/TestController.java | 82 ++++++++++++++++ .../io/github/ctlove0523/tls/TlsServer.java | 13 +++ .../tls/YamlConnectorConfigureRepository.java | 67 +++++++++++++ .../tls/config/FilterConfigure.java | 42 ++++++++ .../tls/config/TlsServerConfigure.java | 92 ++++++++++++++++++ .../src/main/resources/application.yaml | 13 +++ .../src/main/resources/keystore.p12 | Bin 0 -> 2591 bytes 14 files changed, 514 insertions(+) create mode 100644 spring-boot-tls/pom.xml create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/AllowedFilter.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigure.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigureRepository.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ServerAllowedUrl.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TestController.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TlsServer.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/YamlConnectorConfigureRepository.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/FilterConfigure.java create mode 100644 spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/TlsServerConfigure.java create mode 100644 spring-boot-tls/src/main/resources/application.yaml create mode 100644 spring-boot-tls/src/main/resources/keystore.p12 diff --git a/README.md b/README.md index 7d87555..4d2aa7b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # spring-samples Samples of spring or spring boot + + + +## Spring-boot-tls + +该模块下的主要功能包括:Spring Boot配置证书提供HTTPS服务,Spring Boot监听多个端口,Spring Boot每个端口处理的资源支持配置。 diff --git a/pom.xml b/pom.xml index f482c91..358c43c 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ spring-cloud-gateway-nacos spring-boot-jwt jpa-mysql + spring-boot-tls diff --git a/spring-boot-tls/pom.xml b/spring-boot-tls/pom.xml new file mode 100644 index 0000000..fefb041 --- /dev/null +++ b/spring-boot-tls/pom.xml @@ -0,0 +1,30 @@ + + + + spring-samples + io.ctlove0523.spring + 0.0.1-SNAPSHO + + 4.0.0 + + spring-boot-tls + + + 11 + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.yaml + snakeyaml + + + + \ No newline at end of file diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/AllowedFilter.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/AllowedFilter.java new file mode 100644 index 0000000..5c12247 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/AllowedFilter.java @@ -0,0 +1,67 @@ +package io.github.ctlove0523.tls; + +import org.springframework.util.AntPathMatcher; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class AllowedFilter implements Filter { + private final Map> patternMap = new HashMap<>(); + private final AntPathMatcher antPathMatcher; + + public AllowedFilter(List urls) { + this.antPathMatcher = new AntPathMatcher(); + this.antPathMatcher.setCachePatterns(true); + this.antPathMatcher.setCaseSensitive(true); + + if (Objects.isNull(urls)) { + urls = new ArrayList<>(); + } + + for (ServerAllowedUrl url : urls) { + int port = url.getPort(); + if (url.getUrls() == null) { + patternMap.put(port, new ArrayList<>()); + } else { + patternMap.put(port, url.getUrls()); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + int serverPort = request.getServerPort(); + + if (!patternMap.containsKey(serverPort)) { + chain.doFilter(httpRequest, httpResponse); + return; + } + + List patterns = patternMap.get(serverPort); + for (String pattern : patterns) { + String uri = httpRequest.getRequestURI(); + if (antPathMatcher.match(pattern, uri)) { + chain.doFilter(httpRequest, httpResponse); + return; + } + + } + + httpResponse.setStatus(401); + + } +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigure.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigure.java new file mode 100644 index 0000000..c849796 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigure.java @@ -0,0 +1,69 @@ +package io.github.ctlove0523.tls; + +import java.util.List; + +public class ConnectorConfigure { + private int port; + private String scheme; + private boolean sslEnabled; + private String keystoreFile; + private String KeystorePass; + private String keyAlias; + private List allowedUrls; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public boolean isSslEnabled() { + return sslEnabled; + } + + public void setSslEnabled(boolean sslEnabled) { + this.sslEnabled = sslEnabled; + } + + public String getKeystoreFile() { + return keystoreFile; + } + + public void setKeystoreFile(String keystoreFile) { + this.keystoreFile = keystoreFile; + } + + public String getKeystorePass() { + return KeystorePass; + } + + public void setKeystorePass(String keystorePass) { + KeystorePass = keystorePass; + } + + public String getKeyAlias() { + return keyAlias; + } + + public void setKeyAlias(String keyAlias) { + this.keyAlias = keyAlias; + } + + public List getAllowedUrls() { + return allowedUrls; + } + + public void setAllowedUrls(List allowedUrls) { + this.allowedUrls = allowedUrls; + } +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigureRepository.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigureRepository.java new file mode 100644 index 0000000..2784331 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ConnectorConfigureRepository.java @@ -0,0 +1,8 @@ +package io.github.ctlove0523.tls; + +import java.util.List; + +public interface ConnectorConfigureRepository { + + List load(); +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ServerAllowedUrl.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ServerAllowedUrl.java new file mode 100644 index 0000000..ad7ea8c --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/ServerAllowedUrl.java @@ -0,0 +1,24 @@ +package io.github.ctlove0523.tls; + +import java.util.List; + +public class ServerAllowedUrl { + private int port; + private List urls; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public List getUrls() { + return urls; + } + + public void setUrls(List urls) { + this.urls = urls; + } +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TestController.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TestController.java new file mode 100644 index 0000000..ad11950 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TestController.java @@ -0,0 +1,82 @@ +package io.github.ctlove0523.tls; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import java.util.UUID; + +@Controller +public class TestController { + public static void main(String[] args) { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + boolean result= antPathMatcher.match("/**", "/api/hello"); + + System.out.println(result); + } + + @RequestMapping(value = "/api/apps/{appId}", method = RequestMethod.GET) + public ResponseEntity showApp(@PathVariable(name = "appId") String appId) { + App app = new App(); + app.setId(appId); + app.setName("hello app"); + return ResponseEntity.ok(app); + } + + @RequestMapping(value = "/api/health", method = RequestMethod.GET) + public ResponseEntity healthCheck() { + return ResponseEntity.ok("health"); + } + + @RequestMapping(value = "/api/users", method = RequestMethod.GET) + public ResponseEntity showUser() { + User user = new User(); + user.setId(UUID.randomUUID().toString()); + user.setName("hello app"); + return ResponseEntity.ok(user); + } +} + +class User { + private String id; + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +class App { + private String id; + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TlsServer.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TlsServer.java new file mode 100644 index 0000000..4a7e0c4 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/TlsServer.java @@ -0,0 +1,13 @@ +package io.github.ctlove0523.tls; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableAutoConfiguration +public class TlsServer { + public static void main(String[] args) { + SpringApplication.run(TlsServer.class, args); + } +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/YamlConnectorConfigureRepository.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/YamlConnectorConfigureRepository.java new file mode 100644 index 0000000..31b5db6 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/YamlConnectorConfigureRepository.java @@ -0,0 +1,67 @@ +package io.github.ctlove0523.tls; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Component +public class YamlConnectorConfigureRepository implements ConnectorConfigureRepository { + + @Autowired + private ResourceLoader resourceLoader; + + @SuppressWarnings("unchecked") + public List load() { + List connectorConfigures = new ArrayList<>(); + Yaml yaml = new Yaml(); + Resource resource = resourceLoader.getResource("classpath:application.yaml"); + try { + Map applicationConfig = yaml.load(resource.getInputStream()); + List> configs = (List>) applicationConfig.get("servers"); + for (Map config : configs) { + ConnectorConfigure connectorConfigure = createConnectorConfigure(config); + connectorConfigures.add(connectorConfigure); + } + + return connectorConfigures; + } catch (IOException e) { + e.printStackTrace(); + } + + return new ArrayList<>(); + } + + private ConnectorConfigure createConnectorConfigure(Map config) { + ConnectorConfigure connectorConfigure = new ConnectorConfigure(); + connectorConfigure.setScheme((String) config.get("scheme")); + connectorConfigure.setPort((int) config.get("port")); + + if (Objects.nonNull(config.get("sslEnabled"))) { + connectorConfigure.setSslEnabled((boolean) config.get("sslEnabled")); + } + if (Objects.nonNull(config.get("keystoreFile"))) { + connectorConfigure.setKeystoreFile((String) config.get("keystoreFile")); + } + if (Objects.nonNull(config.get("keystorePass"))) { + connectorConfigure.setKeystorePass(config.get("keystorePass").toString()); + } + + if (Objects.nonNull(config.get("keyAlias"))) { + connectorConfigure.setKeyAlias((String) config.get("keyAlias")); + } + + if (Objects.nonNull(config.get("allowedUrls"))) { + connectorConfigure.setAllowedUrls((List) config.get("allowedUrls")); + } + + return connectorConfigure; + } +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/FilterConfigure.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/FilterConfigure.java new file mode 100644 index 0000000..1f02c40 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/FilterConfigure.java @@ -0,0 +1,42 @@ +package io.github.ctlove0523.tls.config; + +import io.github.ctlove0523.tls.AllowedFilter; +import io.github.ctlove0523.tls.ConnectorConfigureRepository; +import io.github.ctlove0523.tls.ServerAllowedUrl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.ServletContext; +import java.util.List; +import java.util.stream.Collectors; + +@Configuration +public class FilterConfigure { + + @Autowired + private ServletContext servletContext; + + @Autowired + private ConnectorConfigureRepository repository; + + @Bean + public FilterRegistrationBean allowedFilter() { + List allowedUrls = repository.load() + .stream() + .map(connectorConfigure -> { + ServerAllowedUrl serverAllowedUrl = new ServerAllowedUrl(); + serverAllowedUrl.setPort(connectorConfigure.getPort()); + serverAllowedUrl.setUrls(connectorConfigure.getAllowedUrls()); + return serverAllowedUrl; + }).collect(Collectors.toList()); + AllowedFilter allowedFilter = new AllowedFilter(allowedUrls); + FilterRegistrationBean filter = new FilterRegistrationBean<>(); + filter.setFilter(allowedFilter); + filter.setOrder(0); + + return filter; + } + +} diff --git a/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/TlsServerConfigure.java b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/TlsServerConfigure.java new file mode 100644 index 0000000..6adcc90 --- /dev/null +++ b/spring-boot-tls/src/main/java/io/github/ctlove0523/tls/config/TlsServerConfigure.java @@ -0,0 +1,92 @@ +package io.github.ctlove0523.tls.config; + +import io.github.ctlove0523.tls.ConnectorConfigure; +import io.github.ctlove0523.tls.ConnectorConfigureRepository; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.Http11NioProtocol; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +@Component +public class TlsServerConfigure implements WebServerFactoryCustomizer { + + @Autowired + private ConnectorConfigureRepository configureRepository; + + @Override + public void customize(TomcatServletWebServerFactory factory) { + Connector[] connectors = createConnectors(); + TomcatConnectorCustomizer customizer = new TomcatConnectorCustomizer() { + @Override + public void customize(Connector connector) { + Connector copy = connectors[0]; + connector.setPort(copy.getPort()); + connector.setScheme(copy.getScheme()); + + Http11NioProtocol handler = (Http11NioProtocol)connector.getProtocolHandler(); + Http11NioProtocol copyHandler = (Http11NioProtocol)copy.getProtocolHandler(); + handler.setSSLEnabled(copyHandler.isSSLEnabled()); + handler.setKeystoreFile(copyHandler.getKeystoreFile()); + handler.setKeystorePass(copyHandler.getKeystorePass()); + handler.setKeyAlias(copyHandler.getKeyAlias()); + } + }; + factory.addConnectorCustomizers(customizer); + // used for health check + for (int i = 1; i < connectors.length; i++) { + factory.addAdditionalTomcatConnectors(connectors[i]); + } + } + + private Connector[] createConnectors() { + List configures = configureRepository.load(); + if (configures.isEmpty()) { + return new Connector[0]; + } + + Connector[] connectors = new Connector[configures.size()]; + for (int i = 0; i < configures.size(); i++) { + Connector connector = new Connector(); + connector.setPort(configures.get(i).getPort()); + connector.setScheme(configures.get(i).getScheme()); + if (configures.get(i).isSslEnabled()) { + Http11NioProtocol handler = (Http11NioProtocol) connector.getProtocolHandler(); + handler.setSSLEnabled(true); + handler.setKeystoreFile(configures.get(i).getKeystoreFile()); + handler.setKeystorePass(configures.get(i).getKeystorePass()); + handler.setKeyAlias(configures.get(i).getKeyAlias()); + } + connectors[i] = connector; + } + + return connectors; + } + + private Connector createHttpConnector() { + Connector connector = new Connector(); + connector.setPort(8080); + connector.setScheme("http"); + return connector; + } + + private InetAddress getBindHost() { + try { + return Inet4Address.getByName("localhost"); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return null; + } + + public String getKeystoreFile() { + return "D:\\codes\\GitHub\\spring-samples\\spring-boot-tls\\src\\main\\resources\\keystore.p12"; + } +} diff --git a/spring-boot-tls/src/main/resources/application.yaml b/spring-boot-tls/src/main/resources/application.yaml new file mode 100644 index 0000000..5e4d997 --- /dev/null +++ b/spring-boot-tls/src/main/resources/application.yaml @@ -0,0 +1,13 @@ +servers: + - port: 8090 + scheme: https + sslEnabled: true + keystoreFile: D:\codes\GitHub\spring-samples\spring-boot-tls\src\main\resources\keystore.p12 + keystorePass: 123456 + keyAlias: tls-server + allowedUrls: + - /** + - port: 8080 + scheme: http + allowedUrls: + - /api/health diff --git a/spring-boot-tls/src/main/resources/keystore.p12 b/spring-boot-tls/src/main/resources/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..fa52840c5de3be471a393c44d6a0534ab4f52846 GIT binary patch literal 2591 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