Samy is my Hero and Hacking the Magic of Spring Boot
0 CommentsLast Updated on January 13, 2016 by jt
Samy is my Hero
A few months ago Tim Ferriss interviewed Samy Kamkar on his podcast. Samy’s large claim to fame is being the author of the MySpace Samy worm. This is a worm that infected over a million MySpace accounts in just 20 hours. MySpace actually shut down because of the worm.
Hearing Samy’s version of the story is absolutely hilarious. Samy is a hacker. He loves to see how things work. Samy tells Tim that he didn’t set out to create the fastest spreading virus of all time. He saw an exploit in the MySpace code that would allow him to add Javascript code to his profile to add the string “but most of all, Samy is my Hero” to anyone’s MySpace profile that visited his MySpace page, and have them add Samy as their friend.
But Samy was bored with that. He wanted more friends on MySpace. Through his hacking skills, he found a way to add that same script to anyone’s MySpace page that visited his MySpace page. Now anyone who visited someone who had been to Samy’s MySpace page was infected. Anyone visiting an infected MySpace profile would add Samy as their friend on MySpace, add “but most of all, Samy is my Hero” to their profile, and they would also be infected with the Samy worm.
The results were exponential. 5, 10, 30, 80, 1,000, 5,000, 10,000, etc. Every time Samy refreshed his MySpace page he had more friends, and the rate was growing. Before MySpace crashed, I think Samy said the rate was 10’s of thousands – per second!
While I think the exploit was hilarious and relatively harmless, the government did not. Eight months later Samy was raided by the US Secret Service and he was charged with crimes under the Patriot Act. Samy’s punishment was for 3 years, he was not allowed to use a computer.
Since then Samy has continued hacking. But in a good way. He’s more of white hat hacker. Samy is also the author of Evercookie – an impossible to delete cookie for tracking internet users. A technology that the NSA is a fan of. Both of these exploits drove awareness and changes. The Samy Worm was a XSS exploit, which is now common to defend against. And Evercookie drove privacy changes in all major browsers.
I love Samy’s passion for hacking. Today he’s hacking keyless car FOBs and consumer drones. The more expensive the car, the easier it is to hack. And did you know you can take over someone else’s drone?
Hacking Spring Boot Autoconfiguration
All programmers are hackers to some degree. We love to figure out how stuff works. So, whenever I start hacking something, often I start thinking about Samy is my hero.
This week I’ve been hacking the Spring Boot autoconfiguration. I developed a Spring Boot web application for my Spring Core course, and in my Spring Core Advanced course, I’m un-doing all the magic of Spring Boot autoconfiguration. I’ve been spending hours going through the Spring Boot autoconfiguration code developed by the Spring Boot team: , , , , , , , ,
The Spring Boot documentation is fairly decent about explaining what is being autoconfigured at a high level. But the documentation does not get down to specifics. My goal is to un-do all the Spring Boot autoconfiguration magic. Ultimately I’m going to remove Spring Boot entirely from my project. It’s not because I don’t like Spring Boot. I’m a total Spring Boot fanboy. Spring Boot is the most exciting thing to happen to Spring since Java annotations. (Really, who misses the XML hell of configuring a Spring / Hibernate project??? Anyone? Buller? Buller?)
No, I’m going through this exercise to show my students the “old” days of Spring application development. To be honest, I’m also gaining a better appreciation for all the things Spring Boot is doing for us. I’ve been using Spring Boot long enough that I was blissfully forgetting what Spring application development was like before Spring Boot.
But not everyone is lucky as me. I know there are many Spring developers out there wishing they could be using Spring Boot. And a fair amount that are scared of Spring Boot.
And if you’re a Spring developer wondering what Spring Boot is –
After peeking under the covers of Spring Boot Autoconfiguration, I do have to give kudos to the Spring development team. They’ve been doing a real nice job. Overall, I’m impressed. There is a lot going on with Spring Boot autoconfiguration. There is a lot of conditional stuff happening. Much of it isn’t trivial either. Autoconfiguration of Hibernate, popular databases, and Spring Security? Yep, it’s in there.
Much of the Spring Boot is conditional. It only kicks in when the appropriate jars are on your classpath. And typically, key properties can be easily overridden via properties files.
I thought I’d share a bit of what I’ve found in my hacking adventures with Spring Boot. After all, Samy is my hero.
Hitchhiker’s Guide to Spring Boot Autoconfiguration
Spring Boot Autoconfiguration Classes
To my knowledge all the Spring Boot Autoconfiguration classes are in a single jar. Below is the Maven dependency for Spring Boot Autoconfiguration. Don’t worry, this jar is automatically included as a dependency of Spring Boot. I’m just pointing it out so you can easily hack it with your tool of choice. (IntelliJ for me)
spring-boot-autoconfigure
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>1.3.1.RELEASE</version> </dependency>
Inside this jar is a collection of Spring Java configuration classes. These are the classes behind the autoconfiguration in Spring Boot.
Key Spring Boot Autoconfiguration Annotations
@ConditionalOnClass
There’s a Java annotation in the Spring configuration which I was not familiar with called @ConditionalOnClass . In a nutshell this is what will kick in the Spring Boot autoconfiguration. If the specified classes are found, do then do the auto configuration.
@ConditionalOnProperty
This is an annotation to specify properties. If you remember, Spring Boot Autoconfiguration allows you to override properties via Spring Boot properties files. Through this annotation, if a property has not been set in the environment, one can be specified.
@ConditionalOnMissingBean
In Spring Boot, you can supply a bean through normal Spring configuration. I have an example of this in my post on Configuring Spring Boot for Oracle. In this post I show you how to override the Spring Boot data source by just properties, or by creating a DataSource bean in a Spring Java configuration class. First is by properties, ie @ConditionalOnProperty . Second is by bean type, ie @ConditionalOnMissingBean .
With the @ConditionalOnMissingBean annotation, the configuration option will only kick in if the bean is not already contained in the Spring bean factory.
Hacking the Spring Boot Autoconfiguration for Thymeleaf
Spring Boot Default
As an example, lets take a look at hacking the Thymeleaf autoconfiguration of Spring Boot.
Here we can see the above Spring Boot autoconfiguration annotations in use as it applies to the autoconfiguration of Thymeleaf.
ThymeleafAutoConfiguration.class
As of version 1.3.1 of Spring Boot.
/** * {@link EnableAutoConfiguration Auto-configuration} for Thymeleaf. * * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll * @author Brian Clozel */ @Configuration @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass(SpringTemplateEngine.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class ThymeleafAutoConfiguration { private static final Log logger = LogFactory.getLog(ThymeleafAutoConfiguration.class); @Configuration @ConditionalOnMissingBean(name = "defaultTemplateResolver") public static class DefaultTemplateResolverConfiguration { @Autowired private ThymeleafProperties properties; @Autowired private ApplicationContext applicationContext; @PostConstruct public void checkTemplateLocationExists() { boolean checkTemplateLocation = this.properties.isCheckTemplateLocation(); if (checkTemplateLocation) { TemplateLocation location = new TemplateLocation( this.properties.getPrefix()); if (!location.exists(this.applicationContext)) { logger.warn("Cannot find template location: " + location + " (please add some templates or check " + "your Thymeleaf configuration)"); } } } @Bean public TemplateResolver defaultTemplateResolver() { TemplateResolver resolver = new TemplateResolver(); resolver.setResourceResolver(thymeleafResourceResolver()); resolver.setPrefix(this.properties.getPrefix()); resolver.setSuffix(this.properties.getSuffix()); resolver.setTemplateMode(this.properties.getMode()); if (this.properties.getEncoding() != null) { resolver.setCharacterEncoding(this.properties.getEncoding().name()); } resolver.setCacheable(this.properties.isCache()); Integer order = this.properties.getTemplateResolverOrder(); if (order != null) { resolver.setOrder(order); } return resolver; } @Bean public SpringResourceResourceResolver thymeleafResourceResolver() { return new SpringResourceResourceResolver(); } } @Configuration @ConditionalOnMissingBean(SpringTemplateEngine.class) protected static class ThymeleafDefaultConfiguration { @Autowired private final Collection<ITemplateResolver> templateResolvers = Collections .emptySet(); @Autowired(required = false) private final Collection<IDialect> dialects = Collections.emptySet(); @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine engine = new SpringTemplateEngine(); for (ITemplateResolver templateResolver : this.templateResolvers) { engine.addTemplateResolver(templateResolver); } for (IDialect dialect : this.dialects) { engine.addDialect(dialect); } return engine; } } @Configuration @ConditionalOnClass(name = "nz.net.ultraq.thymeleaf.LayoutDialect") protected static class ThymeleafWebLayoutConfiguration { @Bean public LayoutDialect layoutDialect() { return new LayoutDialect(); } } @Configuration @ConditionalOnClass(DataAttributeDialect.class) protected static class DataAttributeDialectConfiguration { @Bean @ConditionalOnMissingBean public DataAttributeDialect dialect() { return new DataAttributeDialect(); } } @Configuration @ConditionalOnClass({ SpringSecurityDialect.class }) protected static class ThymeleafSecurityDialectConfiguration { @Bean @ConditionalOnMissingBean public SpringSecurityDialect securityDialect() { return new SpringSecurityDialect(); } } @Configuration @ConditionalOnClass(ConditionalCommentsDialect.class) protected static class ThymeleafConditionalCommentsDialectConfiguration { @Bean @ConditionalOnMissingBean public ConditionalCommentsDialect conditionalCommentsDialect() { return new ConditionalCommentsDialect(); } } @Configuration @ConditionalOnClass({ Servlet.class }) @ConditionalOnWebApplication protected static class ThymeleafViewResolverConfiguration { @Autowired private ThymeleafProperties properties; @Autowired private SpringTemplateEngine templateEngine; @Bean @ConditionalOnMissingBean(name = "thymeleafViewResolver") @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) public ThymeleafViewResolver thymeleafViewResolver() { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(this.templateEngine); resolver.setCharacterEncoding(this.properties.getEncoding().name()); resolver.setContentType(appendCharset(this.properties.getContentType(), resolver.getCharacterEncoding())); resolver.setExcludedViewNames(this.properties.getExcludedViewNames()); resolver.setViewNames(this.properties.getViewNames()); // This resolver acts as a fallback resolver (e.g. like a // InternalResourceViewResolver) so it needs to have low precedence resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); return resolver; } private String appendCharset(MimeType type, String charset) { if (type.getCharSet() != null) { return type.toString(); } LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>(); parameters.put("charset", charset); parameters.putAll(type.getParameters()); return new MimeType(type, parameters).toString(); } } @Configuration @ConditionalOnWebApplication protected static class ThymeleafResourceHandlingConfig { @Bean @ConditionalOnMissingBean @ConditionalOnEnabledResourceChain public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { return new ResourceUrlEncodingFilter(); } } }
/** * {@link EnableAutoConfiguration Auto-configuration} for Thymeleaf. * * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll * @author Brian Clozel */ @Configuration @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass(SpringTemplateEngine.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class ThymeleafAutoConfiguration { private static final Log logger = LogFactory.getLog(ThymeleafAutoConfiguration.class); @Configuration @ConditionalOnMissingBean(name = "defaultTemplateResolver") public static class DefaultTemplateResolverConfiguration { @Autowired private ThymeleafProperties properties; @Autowired private ApplicationContext applicationContext; @PostConstruct public void checkTemplateLocationExists() { boolean checkTemplateLocation = this.properties.isCheckTemplateLocation(); if (checkTemplateLocation) { TemplateLocation location = new TemplateLocation( this.properties.getPrefix()); if (!location.exists(this.applicationContext)) { logger.warn("Cannot find template location: " + location + " (please add some templates or check " + "your Thymeleaf configuration)"); } } } @Bean public TemplateResolver defaultTemplateResolver() { TemplateResolver resolver = new TemplateResolver(); resolver.setResourceResolver(thymeleafResourceResolver()); resolver.setPrefix(this.properties.getPrefix()); resolver.setSuffix(this.properties.getSuffix()); resolver.setTemplateMode(this.properties.getMode()); if (this.properties.getEncoding() != null) { resolver.setCharacterEncoding(this.properties.getEncoding().name()); } resolver.setCacheable(this.properties.isCache()); Integer order = this.properties.getTemplateResolverOrder(); if (order != null) { resolver.setOrder(order); } return resolver; } @Bean public SpringResourceResourceResolver thymeleafResourceResolver() { return new SpringResourceResourceResolver(); } } @Configuration @ConditionalOnMissingBean(SpringTemplateEngine.class) protected static class ThymeleafDefaultConfiguration { @Autowired private final Collection<ITemplateResolver> templateResolvers = Collections .emptySet(); @Autowired(required = false) private final Collection<IDialect> dialects = Collections.emptySet(); @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine engine = new SpringTemplateEngine(); for (ITemplateResolver templateResolver : this.templateResolvers) { engine.addTemplateResolver(templateResolver); } for (IDialect dialect : this.dialects) { engine.addDialect(dialect); } return engine; } } @Configuration @ConditionalOnClass(name = "nz.net.ultraq.thymeleaf.LayoutDialect") protected static class ThymeleafWebLayoutConfiguration { @Bean public LayoutDialect layoutDialect() { return new LayoutDialect(); } } @Configuration @ConditionalOnClass(DataAttributeDialect.class) protected static class DataAttributeDialectConfiguration { @Bean @ConditionalOnMissingBean public DataAttributeDialect dialect() { return new DataAttributeDialect(); } } @Configuration @ConditionalOnClass({ SpringSecurityDialect.class }) protected static class ThymeleafSecurityDialectConfiguration { @Bean @ConditionalOnMissingBean public SpringSecurityDialect securityDialect() { return new SpringSecurityDialect(); } } @Configuration @ConditionalOnClass(ConditionalCommentsDialect.class) protected static class ThymeleafConditionalCommentsDialectConfiguration { @Bean @ConditionalOnMissingBean public ConditionalCommentsDialect conditionalCommentsDialect() { return new ConditionalCommentsDialect(); } } @Configuration @ConditionalOnClass({ Servlet.class }) @ConditionalOnWebApplication protected static class ThymeleafViewResolverConfiguration { @Autowired private ThymeleafProperties properties; @Autowired private SpringTemplateEngine templateEngine; @Bean @ConditionalOnMissingBean(name = "thymeleafViewResolver") @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) public ThymeleafViewResolver thymeleafViewResolver() { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(this.templateEngine); resolver.setCharacterEncoding(this.properties.getEncoding().name()); resolver.setContentType(appendCharset(this.properties.getContentType(), resolver.getCharacterEncoding())); resolver.setExcludedViewNames(this.properties.getExcludedViewNames()); resolver.setViewNames(this.properties.getViewNames()); // This resolver acts as a fallback resolver (e.g. like a // InternalResourceViewResolver) so it needs to have low precedence resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); return resolver; } private String appendCharset(MimeType type, String charset) { if (type.getCharSet() != null) { return type.toString(); } LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>(); parameters.put("charset", charset); parameters.putAll(type.getParameters()); return new MimeType(type, parameters).toString(); } } @Configuration @ConditionalOnWebApplication protected static class ThymeleafResourceHandlingConfig { @Bean @ConditionalOnMissingBean @ConditionalOnEnabledResourceChain public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { return new ResourceUrlEncodingFilter(); } } }
Overriding Spring Boot
Now, here’s the implementation I did for my Spring Core Advanced class.
@Configuration public class ThymeleafConfig { @Bean public TemplateResolver defaultTemplateResolver() { TemplateResolver resolver = new TemplateResolver(); resolver.setResourceResolver(thymeleafResourceResolver()); resolver.setPrefix("classpath:/templates/"); resolver.setSuffix(".html"); resolver.setTemplateMode("HTML5"); resolver.setCharacterEncoding("UTF-8"); resolver.setCacheable(true); return resolver; } @Bean public SpringResourceResourceResolver thymeleafResourceResolver() { return new SpringResourceResourceResolver(); } @Bean public SpringTemplateEngine templateEngine(TemplateResolver defaultTemplateResolver) { SpringTemplateEngine engine = new SpringTemplateEngine(); engine.addTemplateResolver(defaultTemplateResolver); return engine; } @Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine); resolver.setCharacterEncoding("UTF-8"); resolver.setContentType("text/html"); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); return resolver; } @Bean public LayoutDialect layoutDialect() { return new LayoutDialect(); } }
Now, don’t get on me about hard coding properties. I know, BAD DEVELOPER! My students haven’t learned about externalizing properties – yet.
In a nutshell, I’ve provided the Thymeleaf objects needed to configure Thymeleaf for use with Spring MVC. In doing so, the Spring Boot autoconfiguration won’t kick in (due to the @ConditionalOnMissingBean in the Spring Boot default autoconfiguration class).
Conclusion
Spring Boot autoconfiguration is a very cool feature of Spring Boot. As Spring developers, it saves us ton of time configuring our Spring projects. But the Spring Boot autoconfiguration is a double edge sword. Through sensible defaults, come defaults. Which open the door to hacking. I remember in the early days of Oracle, every Oracle database came with the account SCOTT, with the password TIGER. You also had the equivalent of a root account (aka god account) of SYSTEM, default password manager. Production Oracle databases were getting hacked, because someone forgot to change the password of SYSTEM from ‘manager’.
Spring Boot autoconfiguration is saving us Spring developers a TON of time. But don’t use that as an excuse to be lazy. Put your hacker hat on. Take a peek at what Spring Boot autoconfiguration is doing for you. Get familiar with it. Spring Boot should not be magical. Spring Boot should not be a black box. This is exactly why I’m going through the exercise of removing Spring Boot from a project for my students. I feel they will be better Spring developers if Spring Boot is not a mystery to them.
I encourage you to hack the Spring Boot autoconfiguration. And when you do, say to yourself –
“but most of all, Samy is my hero”