This project is based (a.k.a copied) from the Spring getting started guides: https://spring.io/guides/gs/securing-web/. In this post I´ll only talk about the classes I changed.
In order to complete the modification, we need to make several things:
- Configure the Spring Boot app to use the user certificate
- Configure the embedded tomcat container of the spring boot app to expose an AJP port
- Configure apache to serve as the front-end
Configure the Spring Boot app to use the user certificate
This is very easy. You only need to add the x509 config to the HttpSecurity element.In this example the authentication is doing with an in-memory repository, so you have to add the user certificate as a valid application user.
WebSecurityConfig.jave file:
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); http.x509().subjectPrincipalRegex("CN=(.*?),"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("sisifo").password("not_used").roles("USER"); } }
The x509 filter will search the user certificate in the request (in the 'javax.servlet.request.X509Certificate' attribute). When it exists, the filter will extract the "user name" and try to authenticate it against the userDetailsService, in this case the in-memory repository.
But the user will be able to loggin by using an username/password, so in the loggin HTML page you need to put a link to force him to send his certificate.
Login.html page:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security Example </title> </head> <body> <script type="text/javascript"> var copyFields = function() { document.getElementById('username').value = document.getElementById('usernameFake').value; document.getElementById('password').value = document.getElementById('passwordFake').value; } var submitRealForm = function() { copyFields(); document.getElementById('realLoginForm').submit(); return false; } var submitFormOnEnter = function(event) { if (event.keyCode == 13) { submitRealForm(); } } </script> <div th:if="${param.error}"> Invalid username and password. </div> <div th:if="${param.logout}"> You have been logged out. </div> <form id="realLoginForm" th:action="@{/login}" method="post" autocomplete="off" style="display:none"> <input id="username" type="hidden" name="username" readonly="readonly"/> <input id="password" type="hidden" name="password" readonly="readonly"/> </form> <div class="simulateForm"> <div><label> User Name : <input id="usernameFake" type="text" onkeydown="submitFormOnEnter(event)"/> </label></div> <div><label> Password: <input id="passwordFake" type="password" onkeydown="submitFormOnEnter(event)"/> </label></div> <div> <button type="button" onclick="submitRealForm()">Login user/pass</button></div> <div> <a th:href="@{/logincert}">Login certificate</a></div> </div> </body> </html>
When the user clicks in the Login certificate link he will be sent to https://serverhost/logincert. Apache will take care that in this URL the user will be asked for a valid certificate, and then Apache will sent this certificate to the app.
Configure the embedded tomcat container of the Spring Boot app to expose an AJP port
Apache will talk with the Spring Boot app by using the AJP protocol. The app is configured to use an embedded tomcat server, so you have to configure it.This part is copied from this post http://www.appsdev.is.ed.ac.uk/blog/?p=525.
Basically, you create your own Connector based on three config properties located in application.properties file.
Application.java file:
package hello; import org.apache.catalina.connector.Connector; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.context.annotation.Bean; @SpringBootApplication public class Application { @Value("${tomcat.ajp.port}") private int ajpPort; @Value("${tomcat.ajp.remoteauthentication}") private String remoteAuthentication; @Value("${tomcat.ajp.enabled}") private boolean tomcatAjpEnabled; @Bean public EmbeddedServletContainerFactory servletContainer() { TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory(); if (tomcatAjpEnabled) { Connector ajpConnector = new Connector("AJP/1.3"); ajpConnector.setProtocol("AJP/1.3"); ajpConnector.setPort(ajpPort); ajpConnector.setSecure(false); ajpConnector.setAllowTrace(false); ajpConnector.setScheme("http"); tomcat.addAdditionalTomcatConnectors(ajpConnector); } return tomcat; } public static void main(String[] args) throws Throwable { SpringApplication.run(Application.class, args); } }
And this is the properties file. Note that the server port was changed to 9080 instead 8080.
server.port = 9080 tomcat.ajp.port=9090 tomcat.ajp.remoteauthentication=false tomcat.ajp.enabled=true
Configure apache to serve as the front-end
The final step is to configure the web server. I created two virtual host:- The http virtual host will only redirect to the https virtual host.
- The https virtual host will configure the SSL and serve as proxy to the Spring Boot app.
<VirtualHost springBoot.local:80> ServerName springBoot.local:80 serverAlias springBoot.local.80 Redirect permanent / https://springBoot.local/ </VirtualHost> <VirtualHost springBoot.local:443> DocumentRoot "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/htdocs" ServerName springBoot.local:443 serverAlias springBoot.local.433 ErrorLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/error_SpringBoot.log" TransferLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/access_StpringBoot.log" SSLEngine on SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL SSLCertificateFile conf/keystores/desarr.local.cer SSLCertificateKeyFile "conf/keystores/desarr.local.key SSLCACertificateFile conf/keystores/desarr.local-cas.pem <FilesMatch "\.(cgi|shtml|phtml|php)$"> SSLOptions +StdEnvVars </FilesMatch> <Directory "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/cgi-bin"> SSLOptions +StdEnvVars </Directory> BrowserMatch ".*MSIE.*" \ nokeepalive ssl-unclean-shutdown \ downgrade-1.0 force-response-1.0 CustomLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/ssl_request_springBoot.log" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" ProxyRequests Off ProxyPreserveHost On SSLProxyEngine on <Location /logincert> SSLRequireSSL SSLVerifyClient require SSLVerifyDepth 2 SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate ProxyPass ajp://localhost:9090/ ProxyPassReverse ajp://localhost:9090/ </Location> <Location /> SSLRequireSSL ProxyPass http://localhost:9080/ ProxyPassReverse http://localhost:9080/ </Location> </VirtualHost>
The source code is in my github: https://github.com/evazquezma/JEE6/tree/master/gs-security-web
No hay comentarios:
Publicar un comentario