Spring Test&Security:認証をモックする方法は?


124

コントローラのURLが適切に保護されているかどうかを単体テストする方法を見つけようとしていました。誰かが物事を変更し、誤ってセキュリティ設定を削除した場合に備えて。

私のコントローラーメソッドは次のようになります。

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

私は次のようにWebTestEnvironmentを設定しました:

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

私の実際のテストでは、次のようなことを試みました:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

私はこれをここで拾いました:

しかし、よく見ると、これは実際のリクエストをURLに送信しない場合にのみ役立ちますが、関数レベルでサービスをテストする場合にのみ役立ちます。私の場合、「アクセス拒否」例外がスローされました:

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

次の2つのログメッセージは、基本的に、ユーザーが認証されPrincipalなかったため、設定が機能しなかったこと、または上書きされたことを示しています。

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS

インポートには、会社名eu.ubiconが表示されます。これはセキュリティリスクではありませんか?
カイルブリデンスティン2017年

2
こんにちは、コメントありがとうございます!理由はわかりませんが。とにかくそれはオープンソースソフトウェアです。興味がある場合は、bitbucket.org / ubicon / ubicon(または最新のフォークについてはbitbucket.org/dmir_wue/everyaware)を参照してください。何かを見逃した場合はお知らせください。
マーティンベッカー

このソリューションをチェックしてください(答えは、ばね4のためです):stackoverflow.com/questions/14308341/...
ナジアッティラ

回答:


101

答えを求めて、簡単で柔軟なものを同時に見つけることができませんでした。それから、Spring Security Referenceを見つけました。完璧なソリューションに近いことがわかりました。多くの場合、AOPソリューションはテストに最適なソリューションであり、Springはこのアーティファクトで@WithMockUser@WithUserDetailsおよびを提供し@WithSecurityContextます。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

ほとんどの場合、@WithUserDetails私が必要とする柔軟性とパワーを集めます。

@WithUserDetailsはどのように機能しますか?

基本的にはUserDetailsService、テストしたいすべてのユーザープロファイルでカスタムを作成する必要があります。例えば

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "user@company.com", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "manager@company.com", "password");
        UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_MANAGER"),
                new SimpleGrantedAuthority("PERM_FOO_READ"),
                new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                new SimpleGrantedAuthority("PERM_FOO_MANAGE")
        ));

        return new InMemoryUserDetailsManager(Arrays.asList(
                basicActiveUser, managerActiveUser
        ));
    }
}

これでユーザーの準備が整ったので、このコントローラー関数へのアクセス制御をテストするとします。

@RestController
@RequestMapping("/foo")
public class FooController {

    @Secured("ROLE_MANAGER")
    @GetMapping("/salute")
    public String saluteYourManager(@AuthenticationPrincipal User activeUser)
    {
        return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
    }
}

ここでは、ルート/ foo / saluteにマップされget関数があり、アノテーションを使用してロールベースのセキュリティをテストしていますが、テストすることもできます。2つのテストを作成してみましょう。1つは有効なユーザーがこの敬礼応答を見ることができるかどうかをチェックするもので、もう1つは実際に禁止されているかどうかをチェックするものです。@Secured@PreAuthorize@PostAuthorize

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("manager@company.com")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("manager@company.com")));
    }

    @Test
    @WithUserDetails("user@company.com")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

ご覧のとおりSpringSecurityWebAuxTestConfig、ユーザーがテスト用にインポートできるようにインポートしました。単純な注釈を使用するだけで、それぞれが対応するテストケースで使用され、コードと複雑さが軽減されます。

@WithMockUserを使用してロールベースのセキュリティをシンプルにする

ご覧のとおり@WithUserDetails、ほとんどのアプリケーションに必要なすべての柔軟性があります。役割や権限など、GrantedAuthorityでカスタムユーザーを使用できます。ただし、役割を操作するだけの場合は、テストがさらに簡単になり、カスタムの作成を避けることができますUserDetailsService。このような場合は、@ WithMockUserを使用して、ユーザー、パスワード、およびロールの単純な組み合わせを指定します。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
    factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
    String value() default "user";

    String username() default "";

    String[] roles() default {"USER"};

    String password() default "password";
}

アノテーションは、非常に基本的なユーザーのデフォルト値を定義します。私たちの場合、テストしているルートでは認証されたユーザーがマネージャーである必要があるだけなので、使用SpringSecurityWebAuxTestConfigを中止してこれを行うことができます。

@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
    mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
            .accept(MediaType.ALL))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("user")));
}

今の代わりにユーザーのことに注意してくださいmanager@company.com私たちが提供するデフォルトになっている@WithMockUserユーザーが、しかし、私たちが本当に気にしているのは彼の役割なので、それは問題になりませんROLE_MANAGER

結論

のような注釈でわかるように@WithUserDetails、単純なテストを行うためだけに、私たちのアーキテクチャーから離れたクラスを構築することなく、さまざまな認証済みユーザーのシナリオ切り替える@WithMockUserことができます。また、@ WithSecurityContextがさらに柔軟に機能する方法を確認することもお勧めします。


複数のユーザーをモックする方法は?たとえば、最初のリクエストはによって送信されtom、2番目のリクエストはjerry?によって送信されます。
ch271828n

テストをトムで行う関数を作成し、同じロジックで別のテストを作成して、ジェリーでテストできます。各テストには特定の結果があるため、異なるアサーションが存在し、テストが失敗した場合、機能しなかったユーザー/ロールがその名前で通知されます。リクエストではユーザーは1人しか指定できないので、リクエストで複数のユーザーを指定しても意味がありません。
EliuX

申し訳ありませんが、このようなシナリオの例を示します。トムが秘密の記事を作成し、ジェリーがそれを読み込もうとすると、ジェリーはそれを見ることができません(秘密なので)。したがって、この場合には、それは1つのユニットテスト...
ch271828n

これは、回答に記載されているシナリオBasicUserとほとんど同じManager Userです。重要な概念は、ユーザーを気にする代わりに、実際にはユーザーの役割を気にしますが、同じ単体テスト内にあるこれらの各テストは、実際には異なるクエリを表すということです。同じエンドポイントに対して異なるユーザー(異なる役割を持つ)によって行われます。
EliuX

61

Spring 4.0以降、最善の解決策は@WithMockUserでテストメソッドに注釈を付けることです

@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
    mockMvc.perform(get("/someApi"))
        .andExpect(status().isOk());
}

次の依存関係をプロジェクトに追加することを忘れないでください

'org.springframework.security:spring-security-test:4.2.3.RELEASE'

1
春は素晴らしいです。ありがとう
TuGordoBello 2018年

いい答えだ。さらに、mockMvcを使用する必要はありませんが、たとえば、springframework.dataのPagingAndSortingRepositoryを使用している場合は、リポジトリから直接メソッドを呼び出すことができます(EL @PreAuthorize(......)で注釈されています)。
スーパー

50

これは、ことが判明SecurityContextPersistenceFilter春のセキュリティフィルター・チェーンの一部であるが、いつも私リセットしSecurityContext、私は呼び出し設定SecurityContextHolder.getContext().setAuthentication(principal)(または使用して.principal(principal)メソッドを)。このフィルタは設定SecurityContextSecurityContextHolderしてSecurityContextからSecurityContextRepository 上書きし、私が以前に設定1。リポジトリはHttpSessionSecurityContextRepositoryデフォルトでです。HttpSessionSecurityContextRepository与えられた検査HttpRequestと対応にアクセスしようとしますHttpSession。存在する場合は、SecurityContextからの読み取りを試みHttpSessionます。これが失敗した場合、リポジトリは空のを生成しますSecurityContext

したがって、私の解決策はHttpSession、を保持するリクエストとともにを渡すことですSecurityContext

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class Test extends WebappTestEnvironment {

    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        MockHttpSession session = new MockHttpSession();
        session.setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                new MockSecurityContext(principal));


        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
                    .session(session))
            .andExpect(status().isOk());
    }
}

2
Spring Securityの公式サポートはまだ追加されていません。jira.springsource.org/browse/SEC-2015を参照してください。どのようになるかについての概要は、github.com / SpringSource / spring
Rob Winch

Authenticationオブジェクトを作成し、対応する属性を持つセッションを追加することは、それほど悪いことではないと思います。これは有効な「回避策」だと思いますか?一方、直接サポートはもちろん素晴らしいでしょう。かなりきれいに見えます。リンクをありがとう!
マーティンベッカー

素晴らしいソリューション。私のために働いた!getPrincipal()私の意見では少し誤解を招く可能性がある保護されたメソッドの命名に関するほんの小さな問題です。理想的には、名前が付けられているはずgetAuthentication()です。同様に、signedIn()テストでは、ローカル変数に名前を付けるauthか、authenticationその代わりにprincipal
Tanvir

?「getPrincipal(」TEST1" )¿??あなたはexplineでしたが、それはということです事前に感謝何ですか
user2992476

@ user2992476おそらくUsernamePasswordAuthenticationToken型のオブジェクトを返します。または、GrantedAuthorityを作成してこのオブジェクトを作成します。
bluelurker

31

pom.xmlに追加:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.0.RC2</version>
    </dependency>

org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors承認リクエストに使用します。https://github.com/rwinch/spring-security-test-blog(https://jira.spring.io/browse/SEC-2592 )で使用例を参照してください。)。

更新:

4.0.0.RC2は、spring-security 3.xで機能します。spring-security 4の場合、spring-security-testがspring-securityの一部になる(http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test、バージョンは同じ)。

設定が変更されました:http : //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())  
            .build();
}

基本認証のサンプル:http : //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication


これにより、ログインセキュリティフィルターを介してログインしようとしたときに404が表示される問題も修正されました。ありがとう!
Ian Newland 2016

こんにちは、GKislinによって言及されているテスト中。「認証に失敗しましたUserDetailsS​​erviceがnullを返しました。これはインターフェイスコントラクト違反です」というエラーメッセージが表示されます。どんな提案でもしてください。final AuthenticationRequest auth = new AuthenticationRequest(); auth.setUsername(userId); auth.setPassword(password); mockMvc.perform(post( "/ api / auth /")。content(json(auth))。contentType(MediaType.APPLICATION_JSON));
Sanjeev 2017

7

Base64基本認証を使用してSpring MockMvc Security Configをテストする場合の例を次に示します。

String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

Mavenの依存関係

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>

3

短い答え:

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
    final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
            .defaultRequest(defaultRequestBuilder)
            .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
            .apply(springSecurity(springSecurityFilterChain))
            .build();
}

private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                             final MockHttpServletRequest request) {
    requestBuilder.session((MockHttpSession) request.getSession());
    return request;
}

formLogin春のセキュリティテストから実行した後、各リクエストはログインしたユーザーとして自動的に呼び出されます。

長い答え:

このソリューションをチェックしてください(答えはSpring 4に対するものです):Spring 3.2の新しいMVCテストでユーザーをログインさせる方法


2

テストでSecurityContextHolderを使用しないようにするオプション:

  • オプション1:モックSecurityContextHolderを使用する-いくつかのモックライブラリを使用してモックを作成する-EasyMockするたとえば
  • オプション2SecurityContextHolder.get...一部のサービスでコード内の呼び出しをラップします。たとえば、インターフェイスを実装SecurityServiceImplするメソッドgetCurrentPrincipalSecurityService使用して、テストで、アクセスせずに目的のプリンシパルを返すこのインターフェイスのモック実装を作成できますSecurityContextHolder

ええと、おそらく全体像がわかりません。私の問題は、SecurityContextPersistenceFilterが、対応するHttpSessionからSecurityContextを読み取るHttpSessionSecurityContextRepositoryからSecurityContextを使用してSecurityContextを置き換えることでした。したがって、セッションを使用したソリューション。SecurityContextHolderの呼び出しについて:SecurityContextHolderの呼び出しを使用しないように、回答を編集しました。しかし、ラッピングライブラリや追加のモックライブラリを導入する必要もありません。これはより良い解決策だと思いますか?
Martin Becker

申し訳ありませんが、あなたが何を探しているのか正確に理解できませんでした。あなたが思いついた解決策よりも良い答えを提供することはできません-それは良いオプションのようです。
PavlaNováková2013年

了解、ありがとう。私の提案を今のところ解決策として受け入れます。
マーティンベッカー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.