Configuring Spring + Security +Tiles + Thymeleaf using JavaConfig

Timothy Tuti picture Timothy Tuti · Jul 4, 2013 · Viewed 7.8k times · Source

I am trying to configure a web app to use Spring 3.2 + Spring Security + Apache Tiles + Apache Thymeleaf but i keep getting 404 error even though the logs (catalina.out) show no errors. What could be wrong with my configuration?? My configuration is as follows:


Thymeleaf configuration

@Configuration
public class ThymeleafConfig {

    @Bean
    public BeanNameViewResolver beanViewResolver() {
        BeanNameViewResolver resolver = new BeanNameViewResolver();
        resolver.setOrder(1);
        return resolver;
    }

    @Bean
    public MarshallingHttpMessageConverter marshallingMessageConverter() {
        MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter(
                jaxb2Marshaller(),
                jaxb2Marshaller()
        );
        List<MediaType> mediaTypes = new ArrayList<MediaType>();
        mediaTypes.add(MediaType.APPLICATION_XML);
        converter.setSupportedMediaTypes(mediaTypes);
        return converter;
    }

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();

        converters.add(stringHttpMessageConverter());
        converters.add(marshallingJsonConverter());
        converters.add(marshallingMessageConverter());
        restTemplate.setMessageConverters(converters);

        return restTemplate;
    }

    @Bean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        return new StringHttpMessageConverter();
    }

    @Bean
    public MappingJacksonHttpMessageConverter marshallingJsonConverter() {
        List<MediaType> mediaTypes = new ArrayList<MediaType>();
        mediaTypes.add(MediaType.APPLICATION_JSON);
        MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
        converter.setSupportedMediaTypes(mediaTypes);
        return converter;
    }

    @Bean
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setPackagesToScan(new String[]{
                "org.tutiworks.orm.*"
        });
        return marshaller;
    }

    @Bean
    public ThymeleafTilesConfigurer tilesConfigurer() {
        ThymeleafTilesConfigurer tilesConfigurer = new ThymeleafTilesConfigurer();
        tilesConfigurer.setDefinitions(new String[]{"/WEB-INF/tiles.xml"});
        return tilesConfigurer;
    }

    @Bean
    public TilesDialect tilesDialect() {
        TilesDialect dialect = new TilesDialect();
        return dialect;
    }

    @Bean
    public SpringSecurityDialect springSecurityDialect() {
        SpringSecurityDialect dialect = new SpringSecurityDialect();
        return dialect;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        Set<IDialect> dialects = new HashSet<IDialect>();
        dialects.add(springSecurityDialect());
        dialects.add(tilesDialect());
        engine.setAdditionalDialects(dialects);
        return engine;
    }

    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setViewClass(ThymeleafTilesView.class);
        resolver.setOrder(2);
        resolver.setCharacterEncoding("UTF-8");
        String[] views = {"*.html", "*.xhtml"};
        resolver.setViewNames(views);
        return resolver;
    }

    /**
     * Create the CNVR. Get Spring to inject the ContentNegotiationManager
     * created by the configurer (see previous method).
     */
    @Bean
    public ViewResolver contentNegotiatingViewResolver(
            ContentNegotiationManager manager) {
        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
        resolver.setContentNegotiationManager(manager);
        List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>();
        viewResolvers.add(beanViewResolver());
        viewResolvers.add(thymeleafViewResolver());
        resolver.setViewResolvers(viewResolvers);
        return resolver;
    }

}

Application Context

@Configuration
@ComponentScan(basePackages = {"org.tutiworks"})
@EnableWebMvc
@Import({SpringDataConfig.class, ThymeleafConfig.class, SecurityConfig.class})
@PropertySource("classpath:spring.properties")
public class ApplicationContext extends WebMvcConfigurerAdapter {

    // Maps resources path to webapp/resources
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations(
                "/resources/");
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }

    /**
     * Setup a simple strategy: 1. Only path extension is taken into account,
     * Accept headers are ignored. 2. Return HTML by default when not sure.
     */
    @Override
    public void configureContentNegotiation(
            ContentNegotiationConfigurer configurer) {
        configurer.ignoreAcceptHeader(true);
        Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
        mediaTypes.put("xml2", MediaType.TEXT_XML);
        mediaTypes.put("xhtml", MediaType.APPLICATION_XHTML_XML);
        mediaTypes.put("html", MediaType.TEXT_HTML);
        configurer.mediaTypes(mediaTypes);
        configurer.defaultContentType(MediaType.TEXT_HTML);
    }


    // Only needed if we are using @Value and ${...} when referencing properties
    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer propertySources =
                new PropertySourcesPlaceholderConfigurer();
        Resource[] resources = new ClassPathResource[]{new ClassPathResource(
                "spring.properties")};
        propertySources.setLocations(resources);
        propertySources.setIgnoreUnresolvablePlaceholders(true);
        return propertySources;
    }

    // Provides internationalization of messages
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
        source.setBasename("messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }
}

Application initializer

public class ApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        // Load application context
        AnnotationConfigWebApplicationContext rootContext =
                new AnnotationConfigWebApplicationContext();
        rootContext.register(ApplicationContext.class);
        rootContext.setDisplayName("Citizen Police");
        rootContext.setConfigLocation("org.tutiworks.config");

        FilterRegistration.Dynamic securityFilter = servletContext.addFilter("securityFilter",
                new DelegatingFilterProxy("springSecurityFilterChain"));
        securityFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");

        FilterRegistration.Dynamic characterEncodingFilter = servletContext.addFilter
                ("characterEncodingFilter", new CharacterEncodingFilter());
        characterEncodingFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class),
                true, "/*");
        characterEncodingFilter.setInitParameter("encoding", "UTF-8");
        characterEncodingFilter.setInitParameter("forceEncoding", "true");

        servletContext.setInitParameter("defaultHtmlEscape", "true");
        // Context loader listener
        servletContext.addListener(new ContextLoaderListener(rootContext));

        // Dispatcher servlet
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet(
                "dispatcher", new DispatcherServlet(rootContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.setAsyncSupported(true);
    }
}

Spring Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeUrls()
                .antMatchers("/**").authenticated()
                .antMatchers("/resources/**", "/forgot.html", "/signup.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .permitAll()
                .defaultSuccessUrl("/home.html")
                .failureUrl("/login.html?authfailed=true")
                .and()
                .logout()
                .invalidateHttpSession(true)
                .logoutUrl("/logout.html")
                .deleteCookies("JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE")
                .logoutSuccessUrl("/");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        return encoder;
    }

    @Bean
    public UserDetailsService loginService() {
        UserDetailsService service = new LoginService();
        return service;
    }

    @Override
    protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication().and()
                .eraseCredentials(true)
                .userDetailsService(loginService())
                .passwordEncoder(passwordEncoder());
    }
}

Tiles 2 Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
    "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
    "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">

<tiles-definitions>

    <definition name="indexLayout" extends="standardLayout" template="/WEB-INF/templates/layouts/template.html" templateType="thymeleaf">
        <put-attribute name="title" value="Afrikana - Sign in ::title" type="thymeleaf" />
        <put-attribute name="head" value="/WEB-INF/templates/fragments/head.html ::head" type="thymeleaf" />        
        <put-attribute name="footer" value="/WEB-INF/templates/fragments/footer.html ::footer" type="thymeleaf" />
    </definition>

    <!-- Index page -->
    <definition name="login" extends="indexLayout">
        <put-attribute name="content" value="/WEB-INF/templates/pages/login.html :: content" type="thymeleaf" />
    </definition>   

    <!--  
    <definition name="configure" 
        template="/WEB-INF/pages/configuration/configureDhis2.jsp" />
    --> 

</tiles-definitions>

Template layout

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:tiles="http://www.thymeleaf.org">
    <head tiles:fragment="head">
        <title tiles:string="title">
        </title>
    </head>
    <body>
        <div tiles:fragment="content"></div>
        <div tiles:fragment="footer"></div>
    </body>
</html>

What could be wrong? Why am I getting the 404 error when the app tries to load login.html?

Answer

Sotirios Delimanolis picture Sotirios Delimanolis · Jul 4, 2013

You haven't set a url mapping for your Servlet.

// Dispatcher servlet
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.setAsyncSupported(true);   
// add this
dispatcher.addMapping("/"); // or whatever you want/need