为什么配置的 servlet 路径被 REST 控制器正确使用,但在 Spring 安全功能中被忽略?

Why is the configured servlet path correctly used by the REST controllers, but ignored in Spring Security features?

我是 Spring 的新手,我尝试使用 Spring 启动和 Spring 安全创建安全的休息应用程序。我现在正在寻找数周的解决方案...

我在我的 pom 中使用 Spring Boots 嵌入式 Web 容器(Tomcat)和 spring-boot-starter-parent 1.2.6.RELEASE。

我的终点:

我在 application.properties 中配置了我的 servlet 路径,如下所示:

server.servletPath: /embedded

所以我期待我的服务,例如在 //localhost/embedded/login

好的,现在问题来了:如果我 运行 应用程序没有安全性,一切都很好,我可以调用 http//localhost/embedded/application 并得到答案。 如果我现在像这样添加我的安全配置:

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();
    }

}

当 运行 应用程序 //localhost/application/{id} 是安全的而不是 //localhost/embedded/application/{id} 如我所料。 由于某种原因,那里忽略了 servlet 路径。我 tought "ok so I just add the servlet path manually" 让它看起来像这样:

...antMatchers(servletPath+"/application/**").authenticated()...

这适用于我的应用程序。但是,我也使用 MockMvc 来测试我的服务,并且出于某种原因 servlet 路径已正确添加 到匹配器。因此,如果我开始测试,安全过滤器将映射到 //localhost/embedded/embedded/application/{id},而控制器本身仍将映射到 //localhost/embedded/application/{id},这非常烦人...... 我在这里查看 http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ 并认为我可以通过使用 AbstractSecurityWebApplicationInitializer 而不是 SpringBootServletInitializer 来解决问题,但它没有任何改变。 顺便说一句,这是我的申请class:

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;

}

application.properties 我认为没有包含任何更有趣的信息。为了完整起见,这是我的 MockMvc 测试 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);
    }
}

也许我误解了什么。希望你能告诉我。

总结

总之你需要映射Spring安全使用include servlet路径。此外,您需要在 MockMvc 请求中包含 servlet 路径。为此,您可以执行以下操作:

@Before
public void setup() throws Exception {
    this.mockMvc = webAppContextSetup(webApplicationContext)
           // ADD LINE BELOW!!!
           .defaultRequest(get("/").servletPath(servletPath))
           .addFilter(filterChainProxy)
           .build();
}

详细回复

Spring 基于上下文根的安全匹配

Spring 安全匹配器与应用程序的上下文根相关。它与 servlet 路径无关。这是故意的,因为它应该保护所有的 servlet(而不仅仅是 Spring MVC)。如果它与 servlet 相关,请考虑以下内容:

servlet1-path/abc -> Only users with role ROLE_ADMIN can access

servlet2-path/abc -> Only users with role ROLE_USER can access

如果 Spring 安全性与 servlet 路径相关,您将如何区分这两个映射?

在 Mock MVC 中工作

Spring 安全性在 MockMvc 中起作用的原因是因为当您使用 MockMvc 时,不再考虑 servlet 路径。您的请求被发送到 Spring Security 和 Spring MVC,就像 servlet 路径是“”一样。要解决此问题,您需要在请求中包含 servlet 路径。

@Before
public void setup() throws Exception {
    this.mockMvc = webAppContextSetup(webApplicationContext)
           // ADD LINE BELOW!!!
           .defaultRequest(get("/").servletPath(servletPath))
           .addFilter(filterChainProxy)
           .build();
}