如何对使用百日咳的安全控制器进行单元测试(没有得到TemplateProcessingException)?

Aar*_*ski 6 spring-mvc spring-security spring-test-mvc thymeleaf spring-boot

我正在尝试使用spring security和一个简单的home(root)控制器在spring-boot中运行单元测试,该控制器使用百日咳进行模板处理.我正在尝试编写一些单元测试来验证我的安全权限是否正常工作以及正确的数据是否隐藏或显示在我的模板中(使用百万富翁弹簧安全集成).应用程序本身在运行时可以正常工作.我只是想验证它是否正在使用一组集成测试.您可以在此处找到所有代码,但我还将在下面包含相关的代码段:

https://github.com/azeckoski/lti_starter
Run Code Online (Sandbox Code Playgroud)

控制器非常简单,只能渲染模板(在根目录 - 即"/").

@Controller
public class HomeController extends BaseController {
    @RequestMapping(method = RequestMethod.GET)
    public String index(HttpServletRequest req, Principal principal, Model model) {
        log.info("HOME: " + req);
        model.addAttribute("name", "HOME");
        return "home"; // name of the template
    }
}
Run Code Online (Sandbox Code Playgroud)

模板中有很多,但测试的相关位是:

<p>Hello Spring Boot User <span th:text="${username}"/>! (<span th:text="${name}"/>)</p>
<div sec:authorize="hasRole('ROLE_USER')">
    This content is only shown to users (ROLE_USER).
</div>
<div sec:authorize="isAnonymous()"><!-- only show this when user is NOT logged in -->
    <h2>Form Login endpoint</h2>
    ...
</div>
Run Code Online (Sandbox Code Playgroud)

最后测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class AppControllersTest extends BaseApplicationTest {

    @Autowired
    WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilter;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        // Process mock annotations
        MockitoAnnotations.initMocks(this);
        // Setup Spring test in webapp-mode (same config as spring-boot)
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
                .addFilter(springSecurityFilter, "/*")
                .build();
    }

    @Test
    public void testLoadRoot() throws Exception {
        // Test basic home controller request
        MvcResult result = this.mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
                .andReturn();
        String content = result.getResponse().getContentAsString();
        assertNotNull(content);
        assertTrue(content.contains("Hello Spring Boot"));
        assertTrue(content.contains("Form Login endpoint"));
    }

    @Test
    public void testLoadRootWithAuth() throws Exception {
        Collection<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        Authentication authToken = new UsernamePasswordAuthenticationToken("azeckoski", "password", authorities);
        SecurityContextHolder.getContext().setAuthentication(authToken);
        // Test basic home controller request
        MvcResult result = this.mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
                .andReturn();
        String content = result.getResponse().getContentAsString();
        assertNotNull(content);
        assertTrue(content.contains("Hello Spring Boot"));
        assertTrue(content.contains("only shown to users (ROLE_USER)"));
    }
}
Run Code Online (Sandbox Code Playgroud)

我同时接受上述测试的是:

testLoadRoot(ltistarter.controllers.AppControllersTest)经过的时间:0.648秒<<<错误!org.springframework.web.util.NestedServletException:请求处理失败; 嵌套异常是org.thymeleaf.exceptions.TemplateProcessingException:执行org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext中处理器'org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor'(主页:33)时出错(WebApplicationContextUtils.java:84)在org.thymeleaf.extras.springsecurity3.auth.AuthUtils.getExpressionHandler(AuthUtils.java:260)在org.thymeleaf.extras.springsecurity3.auth.AuthUtils.authorizeUsingAccessExpression(AuthUtils.java:182)在org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor.isVisible(AuthorizeAttrProcessor.java:100)位于org.thymeleaf.processor.attr的org.thymeleaf.processor.attr.AbstractConditionalVisibilityAttrProcessor.processAttribute(AbstractConditionalVisibilityAttrProcessor.java:58).在org.thymelea的org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)的AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)org.thymeleaf.dom.Node.processNode(Node.java:971)的f.dom.Node.applyNextProcessor(Node.java:1016)org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:672)at org位于org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:672)org.thymeleaf.dom.Node.processNode(Node.java:990)的.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655) org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655)org.thymeleaf.dom.Node.processNode(Node.java:990)atg.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:672) org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655)org.thymeleaf.dom.Node.processNode(Node.java:990)atg.thymeleaf.dom.Document.process(Document.java:93 )在org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1155)在org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1060)在org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1011)在org.thymeleaf .spring4.view.Thyme leafView.renderFragment(ThymeleafView.java:335)在org.thymeleaf.spring4.view.ThymeleafView.render(ThymeleafView.java:190)在org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1221)在组织.springframework.test.web.servlet.TestDispatcherServlet.render(TestDispatcherServlet.java:102)位于org.springframework.web.servlet.DispatcherServlet.doDispatch的org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1005) (DispatcherServlet.java:952)在org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)在org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)在org.springframework. web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)在javax.servlet.http.HttpServlet.service(HttpServlet.java:735)在org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)在org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:62)在javax.servlet.http.HttpServlet.service(HttpServlet.java:848)在org.springframework.mock.web.MockFilterChain $ ServletFilterProxy.doFilter(MockFilterChain.java:170)在org.springframework.mock位于org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke的org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:330)中的.web.MockFilterChain.doFilter(MockFilterChain.java:137) (FilterSecurityInterceptor.java:118)在org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)在org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342 )在org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)在org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)在org.springframework.security.卷筒纸 .session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter) .java:113)org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)at org. springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)at org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)位于org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110))在org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)在org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:85)在org.springframework.web. filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter. Java的:57)在org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)在org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)在org.springframework.security .web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)在org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)在org.springframework.security.web.context.request.位于org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java)的org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)中的async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50): 342)在org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)在org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)在org.springframework.mock.web.MockFilterChain .doFilter(MockFilterChain.java:137)在org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:141)在ltistarter.controllers.AppControllersTest.testLoadRoot(AppControllersTest.java:70)

但是,只有在启用了两个测试并且包含springSecurityFilter时才会发生这种情况.如果我禁用测试之一,取下springSecurityFilter代码(.addFilter(springSecurityFilter, "/*")),然后我不再得到这个错误.我怀疑某些东西可能搞砸了WebApplicationContext或者让安全资料处于某种故障状态,但我不确定我需要重置或更改什么.

因此,如果我取出第二个测试并删除springSecurityFilter,那么我的第一个测试仍将失败(特别是这个assertTrue(content.contains("Form Login endpoint"))),但我不再收到任何错误.当我查看生成的HTML时,我没有看到任何使用该sec:authorize属性的标签内容.

所以我四处搜索并找到了一个建议,我需要添加springSecurityFilter(我在上面的代码示例中已经完成),但是,一旦我这样做,我立即得到了失败(它甚至没有达到目的没有它就失败的地方).有关导致该异常的原因以及如何解决该问题的任何建议?

Aar*_*ski 9

我有一个解决方案似乎完全解决了spring-boot的这个问题:1.1.4,spring-security:3.2.4和thymeleaf:2.1.3(虽然它有点像黑客).

这是修改后的单元测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class AppControllersTest {

    @Autowired
    public WebApplicationContext context;

    @Autowired
    private FilterChainProxy springSecurityFilter;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        assertNotNull(context);
        assertNotNull(springSecurityFilter);
        // Process mock annotations
        MockitoAnnotations.initMocks(this);
        // Setup Spring test in webapp-mode (same config as spring-boot)
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .addFilters(springSecurityFilter)
                .build();
        context.getServletContext().setAttribute(
            WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
    }
...
Run Code Online (Sandbox Code Playgroud)

这里的魔力是强制WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE成为实际的Web应用程序上下文(我注入了).这允许实际的sec:属性工作但是我的第二个测试我尝试设置权限,因此用户登录时没有通过(看起来用户仍然是ANONYMOUS).

UPDATE

缺少一些东西(我认为这是弹簧安全性如何工作的一个空白)但幸运的是相当容易解决(尽管它有点像黑客).有关此问题的更多详细信息,请参阅此内容:Spring Test&Security:如何模拟身份验证?

我需要添加一个为测试创建模拟会话的方法.此方法将设置安全性Principal/ Authentication并强制SecurityContext进入HttpSession,然后可以将其添加到测试请求中(请参阅下面的测试片段和NamedOAuthPrincipal类示例).

public MockHttpSession makeAuthSession(String username, String... roles) {
    if (StringUtils.isEmpty(username)) {
        username = "azeckoski";
    }
    MockHttpSession session = new MockHttpSession();
    session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
    Collection<GrantedAuthority> authorities = new HashSet<>();
    if (roles != null && roles.length > 0) {
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
    }
    //Authentication authToken = new UsernamePasswordAuthenticationToken("azeckoski", "password", authorities); // causes a NPE when it tries to access the Principal
    Principal principal = new NamedOAuthPrincipal(username, authorities,
            "key", "signature", "HMAC-SHA-1", "signaturebase", "token");
    Authentication authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities);
    SecurityContextHolder.getContext().setAuthentication(authToken);
    return session;
}
Run Code Online (Sandbox Code Playgroud)

用于创建的类Principal(通过ConsumerCredentials提供OAuth支持).如果您不使用OAuth,则可以跳过ConsumerCredentials部分,只需实现Principal(但您应该返回GrantedAuthority的集合).

public static class NamedOAuthPrincipal extends ConsumerCredentials implements Principal {
    public String name;
    public Collection<GrantedAuthority> authorities;
    public NamedOAuthPrincipal(String name, Collection<GrantedAuthority> authorities, String consumerKey, String signature, String signatureMethod, String signatureBaseString, String token) {
        super(consumerKey, signature, signatureMethod, signatureBaseString, token);
        this.name = name;
        this.authorities = authorities;
    }
    @Override
    public String getName() {
        return name;
    }
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后像这样修改测试(创建会话然后在模拟请求上设置它):

@Test
public void testLoadRootWithAuth() throws Exception {
    // Test basic home controller request with a session and logged in user
    MockHttpSession session = makeAuthSession("azeckoski", "ROLE_USER");
    MvcResult result = this.mockMvc.perform(get("/").session(session))
            .andExpect(status().isOk())
            .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
            .andReturn();
    String content = result.getResponse().getContentAsString();
    assertNotNull(content);
    assertTrue(content.contains("Hello Spring Boot"));
}
Run Code Online (Sandbox Code Playgroud)