6

春のセキュリティと、テンプレート処理にタイムリーフを使用する単純なホーム (ルート) コントローラーを使用して、春のブートで単体テストを実行しようとしています。セキュリティ権限が正しく機能していること、および適切なデータがテンプレートから非表示または表示されていることを確認するために、いくつかの単体テストを作成しようとしています (これは thymeleaf スプリング セキュリティ統合を使用します)。アプリ自体は、実行すると正しく動作します。一連の統合テストで機能していることを確認したいだけです。ここですべてのコードを見つけることができますが、関連するスニペットも以下に含めます。

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

上記のテストの両方で得られるエラーは次のとおりです。

testLoadRoot(ltistarter.controllers.AppControllersTest) 経過時間: 0.648 秒 <<< エラー! org.springframework.web.util.NestedServletException: リクエストの処理に失敗しました。ネストされた例外は org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor' (home:33) at 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 が含まれている場合にのみ発生します。テストの 1 つを無効にして springSecurityFilter コード ( .addFilter(springSecurityFilter, "/*")) を削除すると、そのエラーは発生しなくなります。何かが WebApplicationContext を台無しにしているか、セキュリティを何らかの障害状態のままにしている可能性があると思われますが、何をリセットまたは変更する必要があるかわかりません。

したがって、2 番目のテストを実行して springSecurityFilter を削除すると、最初のテストは失敗しますが (特にこれはassertTrue(content.contains("Form Login endpoint")))、エラーは発生しなくなります。生成された HTML を見ると、sec:authorize属性を使用するタグのコンテンツがまったく表示されません。

だから私は周りを検索し、追加する必要があるという提案を見つけましたspringSecurityFilter(上記のコードサンプルで実行しました)が、それを行うとすぐに失敗します(要点にさえ達しません)それなしでは失敗します)。その例外の原因とその修正方法に関する提案はありますか?

4

2 に答える 2

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

ここでの魔法はWebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE、 を実際の Web アプリ コンテキストにすることです (これは私が注入したものです)。これにより、実際の sec: 属性が機能するようになりますが、ユーザーがログインしているように権限を設定しようとする 2 番目のテストに合格しませんでした (ユーザーはまだ匿名のようです)。

アップデート

何かが欠けていました (これは、Spring Security の仕組みのギャップだと思います) が、幸運なことに、かなり簡単に解決できます (ちょっとしたハックですが)。問題の詳細については、Spring Test & Security: How to mock authentication? を参照してください。

テスト用のモック セッションを作成するメソッドを追加する必要がありました。このメソッドはセキュリティPrincipal/を設定し、テスト リクエストに追加できる/Authenticationを強制しSecurityContextます(以下のテスト スニペットとクラスの例を参照してください)。HttpSessionNamedOAuthPrincipal

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 部分をスキップして、プリンシパルを実装するだけです (ただし、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"));
}
于 2014-07-29T00:38:55.350 に答える