I'm new to Spring and I try to create a secured rest application using Spring Boot and Spring Security. I'm searching for weeks for a solution now...
I'm using Spring Boots embedded web container (Tomcat) and the spring-boot-starter-parent 1.2.6.RELEASE in my pom.
My endpoints:
/login
(to authenticate)/application/{id}
(some service which I want to secure)
I configured my servlet path in my application.properties like this:
server.servletPath: /embedded
so I expect my services e.g. on //localhost/embedded/login
Ok so now the problem: If I run the application without security everything is fine, I can call http//localhost/embedded/application and get an answer. If I now add my security configuration like this:
import javax.servlet.ServletContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebMvcSecurity
@EnableScheduling
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
@Value("${server.servletPath}")
private String servletPath;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/hello/**", "/login").permitAll()
.antMatchers("/application/**").authenticated().and()
.addFilterBefore(new TokenAuthenticationFilter(tokenAuthenticationService), UsernamePasswordAuthenticationFilter.class);
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disable();
}
}
when running the application //localhost/application/{id} is secured instead of //localhost/embedded/application/{id} as I would have expected. For some reason the servlet path is ignored there. I tought "ok so I just add the servlet path manually" and make it look like this:
...antMatchers(servletPath+"/application/**").authenticated()...
This works in my application. However I also use MockMvc to test my services and for some reason there the servlet path is correctly added to the matchers. So if I start the tests the security filters are mapped to //localhost/embedded/embedded/application/{id}
while the controllers themselves still are mapped to //localhost/embedded/application/{id}
which is very annoying...
I took a look at here http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ and thought I could fix the issue by using AbstractSecurityWebApplicationInitializer
instead of SpringBootServletInitializer
but it changed nothing.
This is my application class by the way:
com.sebn.gsd.springservertemplate.service.security.WebSecurityConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
System.out.println("Run from main");
SpringApplication.run(applicationClass, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(applicationClass, WebSecurityConfig.class);
}
private static Class<Application> applicationClass = Application.class;
}
The application.properties
doesn't contain any more interesting information I think. To be complete this is my MockMvc testing class:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sebn.gsd.springservertemplate.service.api.LoginData;
import com.sebn.gsd.springservertemplate.service.security.Session_model;
import com.sebn.gsd.springservertemplate.service.security.WebSecurityConfig;
import java.util.Arrays;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.notNullValue;
import org.junit.Assert;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class, WebSecurityConfig.class })
@WebAppConfiguration
@ActiveProfiles(profiles = "development")
public class SecurityTests {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
private HttpMessageConverter mappingJackson2HttpMessageConverter;
private ObjectMapper o = new ObjectMapper();
@Autowired
private FilterChainProxy filterChainProxy;
@Value("${server.servletPath}")
private String servletPath;
@Before
public void setup() throws Exception {
this.mockMvc = webAppContextSetup(webApplicationContext).addFilter(filterChainProxy).build();
}
@Test
public void testLoginSecurity() throws Exception {
int applicationId = 1;
// Try to access secured api
ResultActions actions = mockMvc.perform(get("/application/" + applicationId))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isForbidden());
//login
String username = "user";
LoginData loginData = new LoginData();
loginData.setPasswordBase64("23j4235jk26=");
loginData.setUsername(username);
actions = mockMvc.perform(post("/login").content(o.writeValueAsString(loginData)).contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.login", Matchers.equalTo(username)))
.andExpect(jsonPath("$.token", notNullValue()))
.andExpect(jsonPath("$.expirationDate", notNullValue()));
Session_model session = getResponseContentAsJavaObject(actions.andReturn().getResponse(), Session_model.class);
Assert.assertNotNull(session);
// Try to access secured api again
actions = mockMvc.perform(get("/application/" + applicationId).header("X-AUTH-TOKEN", session.getToken()))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk());
}
private <T> T getResponseContentAsJavaObject(MockHttpServletResponse response, Class<T> returnType) throws Exception{
return o.readValue(response.getContentAsString(), returnType);
}
@Autowired
void setConverters(HttpMessageConverter<?>[] converters) {
this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
Assert.assertNotNull("the JSON message converter must not be null",
this.mappingJackson2HttpMessageConverter);
}
}
Maybe I misunderstood something. I hope you can tell me.