我正在尝试使用spring security和一个简单的home(root)控制器在spring- boot中运行单元测试,该控制器使用thymeleaf进行模板处理。我正在尝试编写一些单元测试,以验证我的安全权限是否正常工作,并且正确的数据是否已从我的模板(使用thymeleaf spring安全集成)中隐藏或显示。当我运行该应用程序时,它本身可以正常运行。我只想验证它是否正在与一组集成测试一起使用。您可以在此处找到所有代码,但我还将在下面提供相关的摘要:
https://github.com/azeckoski/lti_starter
控制器真的很简单,除了呈现模板(在根处-即“ /”)外,什么也不做。
@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 } }
模板中有很多东西,但是测试的相关内容是:
<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>
最后是测试:
@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)")); } }
我在以上测试中都得到的erorr是:
testLoadRoot(ltistarter.controllers.AppControllersTest)经过的时间:0.648秒<<<错误!org.springframework.web.util.NestedServletException:请求处理失败;嵌套的异常是org.thymeleaf.exceptions.TemplateProcessingException:执行处理器org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor’(home:33)时在org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext发生错误(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。
但是,只有在同时启用两个测试并且包括springSecurityFilter的情况下,这种情况才会发生。如果我禁用其中一项测试并删除springSecurityFilter代码(.addFilter(springSecurityFilter, "/*")),则不会再出现该错误。我怀疑可能是某种原因弄乱了WebApplicationContext或使安全性处于某种故障状态,但是我不确定需要重置或更改的内容。
.addFilter(springSecurityFilter, "/*")
因此,如果我取出第二个测试并删除springSecurityFilter,则我的第一个测试仍将失败(尤其是该测试assertTrue(content.contains("Form Login endpoint"))),但是我不再遇到任何错误。当我查看生成的HTML时,没有看到使用该sec:authorize属性的任何标签内容。
assertTrue(content.contains("Form Login endpoint"))
sec:authorize
因此,我四处寻找并发现了一个建议,我需要添加一个建议springSecurityFilter(我在上面的代码示例中已完成此建议),但是,一旦我这样做,我马上就会遇到失败(甚至没有得到解决)没有它就失败了)。关于什么原因导致该异常以及如何解决该异常的任何建议?
springSecurityFilter
我有一个解决方法,似乎可以完全解决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); } ...
这里的魔力是将强制为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE实际的Web应用程序上下文(我注入了该上下文)。这允许实际的sec:属性起作用,但是在我尝试设置权限以使用户登录未通过的第二次测试中(看起来用户仍然匿名)。
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
缺少了一些东西(我认为这是弹簧安全性工作方式上的空白),但是很幸运,它很容易解决(尽管有点麻烦)。请参阅此以获取有关该问题的更多详细信息:SpringTest&Security:如何模拟身份验证?
我需要添加一个为测试创建模拟会话的方法。此方法将设置安全性Principal/ Authentication并强制将/ SecurityContext插入HttpSession其中,然后可以将其添加到测试请求中(请参见下面的测试代码段和NamedOAuthPrincipal类示例)。
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; }
创建类的类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; } }
然后像这样修改测试(以创建会话,然后在模拟请求上进行设置):
@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")); }