Spring Securityを使用した単体テスト


140

私の会社では、Spring MVCを評価して、次のプロジェクトの1つで使用するかどうかを決定しています。これまでのところ、私は今まで見たことを気に入っています。今は、Spring Securityモジュールを調べて、それが私たちが使用できる/すべきかどうかを判断しています。

私たちのセキュリティ要件はかなり基本的なものです。ユーザーは、サイトの特定の部分にアクセスできるように、ユーザー名とパスワードを提供できる必要があります(アカウントに関する情報を取得するなど)。サイトには、匿名ユーザーにアクセスを許可するページ(FAQ、サポートなど)がいくつかあります。

私が作成しているプロトタイプでは、認証されたユーザーのSessionに "LoginCredentials"オブジェクト(ユーザー名とパスワードのみが含まれています)を格納しています。一部のコントローラは、このオブジェクトがセッションにあるかどうかを確認して、たとえばログインしたユーザー名への参照を取得します。私はこの自家製ロジックを代わりにSpring Securityで置き換えようとしています。これは、「ログインしたユーザーをどのように追跡するのか」を削除するという素晴らしい利点があります。そして「ユーザーをどのように認証するのか?」私のコントローラー/ビジネスコードから。

Spring Securityは、アプリ内のどこからでもユーザー名/プリンシパル情報にアクセスできるようにする(スレッドごとの)「コンテキスト」オブジェクトを提供しているようです...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

ある意味、このオブジェクトは(グローバル)シングルトンなので、これは非常に非Springのようです。

私の質問はこれです:これがSpring Securityで認証されたユーザーに関する情報にアクセスする標準的な方法である場合、ユニットテストで必要なときにユニットテストで利用できるように、AuthenticationオブジェクトをSecurityContextに注入するための受け入れられた方法は何ですか?認証済みユーザー?

これを各テストケースの初期化メソッドに関連付ける必要がありますか?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

これは過度に冗長に見えます。もっと簡単な方法はありますか?

SecurityContextHolderオブジェクト自体は、非常に非春のように思えます...

回答:


48

問題は、Spring Securityがコンテナ内の認証オブジェクトをBeanとして使用できるようにしないため、ボックスから簡単に挿入または自動配線する方法がないことです。

Spring Securityの使用を開始する前に、コンテナにセッションスコープのBeanを作成してプリンシパルを格納し、これを「AuthenticationService」(シングルトン)に注入してから、このBeanを現在のプリンシパルの知識が必要な他のサービスに注入します。

独自の認証サービスを実装している場合は、基本的に同じことを実行できます。「プリンシパル」プロパティを使用してセッションスコープのBeanを作成し、これを認証サービスに挿入し、認証サービスで認証が成功したときにプロパティを設定します。必要に応じて、認証サービスを他のBeanで使用できるようにします。

SecurityContextHolderの使用についてはそれほど悪くはありません。でも。私はそれが静的/シングルトンであり、Springがそのようなものの使用を推奨しないことを知っていますが、それらの実装は、環境に応じて適切に動作するように注意します:サーブレットコンテナーでのセッションスコープ、JUnitテストでのスレッドスコープなど。実際の制限要因シングルトンとは、さまざまな環境に柔軟性のない実装を提供する場合です。


ありがとう、これは役に立つアドバイスです。これまでに行ったことは、基本的にはSecurityContextHolder.getContext()を呼び出すことです(独自のラッパーメソッドをいくつか使用しているため、少なくとも1つのクラスからのみ呼び出されます)。
matt b

2
ただ1つのメモですが、ServletContextHolderにはHttpSessionの概念や、Webサーバー環境で動作しているかどうかを知る方法はないと思います-他のものを使用するように構成しない限り、ThreadLocalを使用します(他の2つの組み込みモードはInheritableThreadLocalのみです)およびグローバル)
matt b

Springでセッション/リクエストスコープBeanを使用することの唯一の欠点は、JUnitテストで失敗することです。あなたができることは、可能であればセッション/リクエストを使用し、スレッドにフォールバックする必要があるカスタムスコープを実装することです。私の推測では、Spring Securityは同様のことをしています...
cliff.meyers 2008

私の目標は、セッションなしでRest APIを構築することです。おそらく、更新可能なトークンを使用します。これは私の質問に答えませんでしたが、役に立ちました。ありがとう
Pomagranite 2017

166

通常の方法で実行し、SecurityContextHolder.setContext()テストクラスで使用して挿入します。次に例を示します。

コントローラ:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

テスト:

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);

2
@LeonardoこれAuthentication aはコントローラーのどこに追加する必要がありますか?各メソッドの呼び出しで理解できるように?注入するのではなく、追加するだけで「春の道」は大丈夫ですか?
Oleg Kuts

しかしSecurityContextHolderは...そう、あなたはテストの間、この変数を共有するローカルスレッド変数を保持するため、そのはTestNGの持つ仕事に行くのではない覚えている
ルカシュWoźniczka

でそれを行う@BeforeEach(JUnit5)または@Before(JUnitの4)。良いとシンプル。
WesternGun、

30

認証オブジェクトを作成および挿入する方法についての質問に答えることなく、Spring Security 4.0はテストに関していくつかの歓迎すべき代替案を提供します。@WithMockUser注釈は、きちんとした方法でモックユーザー(オプション当局と、ユーザ名、パスワード、および役割)を指定する開発者を可能にします:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

からの戻り@WithUserDetailsをエミュレートするために使用するオプションもあります。たとえば、UserDetailsUserDetailsService

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

詳細については、Spring Securityリファレンスドキュメントの@WithMockUserおよび@WithUserDetailsの章を参照してください(上記の例のコピー元)。


29

心配するのは当然です。静的メソッド呼び出しは、依存関係を簡単にモックすることができないため、単体テストでは特に問題があります。ここでお見せするのは、Spring IoCコンテナにダーティな作業を任せて、きちんとしたテスト可能なコードを残す方法です。SecurityContextHolderはフレームワーククラスであり、低レベルのセキュリティコードを関連付けることは問題ないかもしれませんが、UIコンポーネント(つまりコントローラー)にすっきりしたインターフェイスを公開したい場合があります。

cliff.meyersはその周りの1つの方法について言及しました-独自の「プリンシパル」タイプを作成し、インスタンスをコンシューマーに注入します。2.xで導入されたSpring < aop:scoped-proxy />タグは、リクエストスコープBean定義と組み合わされており、ファクトリメソッドサポートは、最も読みやすいコードへのチケットである可能性があります。

次のように機能します:

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

今のところ複雑なことはありませんよね?実際、あなたはおそらくこれのほとんどをすでにやらなければなりませんでした。次に、Beanコンテキストで、プリンシパルを保持するリクエストスコープのBeanを定義します。

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
    <aop:scoped-proxy/>
</bean>

<bean id="controller" class="MyUserAwareController">
    <property name="currentUser" ref="userDetails"/>
    <!-- other props -->
</bean>

aop:scoped-proxyタグの魔法のおかげで、静的メソッドgetUserDetailsは、新しいHTTPリクエストが着信するたびに呼び出され、currentUserプロパティへの参照はすべて正しく解決されます。これでユニットテストは簡単になります:

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

お役に立てれば!


9

個人的には、PowermockをMockitoまたはEasymockと一緒に使用して、ユニット/統合テストで静的SecurityContextHolder.getSecurityContext()をモックします。

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

確かに、ここにはかなりのボイラープレートコードがあります。つまり、Authenticationオブジェクトをモックし、SecurityContextをモックして認証を返し、最後にSecurityContextHolderをモックしてSecurityContextを取得しますが、非常に柔軟で、null認証オブジェクトなどのシナリオの単体テストを行うことができます。 (非テスト)コードを変更する必要なしなど


7

この場合は、staticを使用することが、安全なコードを作成するための最良の方法です。

はい、静力学は一般的に悪いです-一般に、この場合、静力学はあなたが望むものです。セキュリティコンテキストはプリンシパルを現在実行中のスレッドに関連付けるため、最も安全なコードはスレッドからstaticにできるだけ直接アクセスします。挿入されたラッパークラスの背後にアクセスを隠すことで、攻撃者は攻撃するポイントを増やすことができます。コードにアクセスする必要はありません(jarが署名されている場合は変更が困難です)。構成をオーバーライドする方法が必要なだけです。これは、実行時に実行するか、XMLをクラスパスに入れることができます。アノテーション注入を使用しても、外部XMLでオーバーライドできます。このようなXMLは、実行中のシステムに不正なプリンシパルを挿入する可能性があります。


4

ここで同じ質問をし、最近見つけた回答を投稿しました。短い答えは:を挿入しSecurityContextSecurityContextHolderSpring構成でのみ参照して、SecurityContext


3

一般的な

その間(バージョン3.2以降、2013年にはSEC-2298のおかげ)、アノテーション@AuthenticationPrincipalを使用してMVCメソッドに認証を注入できます。

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

テスト

単体テストでは、明らかにこのメソッドを直接呼び出すことができます。を使用した統合テストでは、org.springframework.test.web.servlet.MockMvc次のorg.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()ようにしてユーザーを挿入できます。

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

ただし、これはSecurityContextを直接埋めるだけです。ユーザーがテストのセッションからロードされていることを確認したい場合は、これを使用できます。

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
    return new RequestPostProcessor() {
        @Override
        public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
            final SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
            );
            request.getSession().setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
            );
            return request;
        }
    };
}

2

私は、Springの抽象テストクラスとの話されているモックオブジェクトを見てかかるだろう、ここを。これらは、Springが管理するオブジェクトを自動接続する強力な方法を提供し、単体テストと統合テストを容易にします。


これらのテストクラスは役立ちますが、ここで適用できるかどうかはわかりません。私のテストにはApplicationContextの概念はありません-それらは必要ありません。私が必要なのは、テストメソッドが実行される前にSecurityContextが入力されていることを確認することだけです。最初にThreadLocalに設定する必要があるのは不愉快な感じです
matt b

1

認証は、OSのプロセスのプロパティと同じように、サーバー環境のスレッドのプロパティです。認証情報にアクセスするためのBeanインスタンスがあると、構成や配線のオーバーヘッドが不便になり、メリットがありません。

テスト認証に関しては、あなたの人生を楽にする方法がいくつかあります。私のお気に入りは、カスタムアノテーション@Authenticatedとそれを管理するテスト実行リスナーを作成することです。DirtiesContextTestExecutionListenerインスピレーションをチェックします。


0

かなりの作業の後、私は望ましい振る舞いを再現することができました。MockMvcを介してログインをエミュレートしました。ほとんどの単体テストには重すぎますが、統合テストには役立ちます。

もちろん、テストを容易にするSpring Security 4.0の新機能を喜んで紹介します。

package [myPackage]

import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.servlet.http.HttpSession;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
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;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{

    private MockMvc mockMvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private MockHttpServletRequest request;

    @Autowired
    private WebApplicationContext webappContext;

    @Before  
    public void init() {  
        mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
    }  


    @Test
    public void testTwoReads() throws Exception{                        

    HttpSession session  = mockMvc.perform(post("/j_spring_security_check")
                        .param("j_username", "admin_001")
                        .param("j_password", "secret007"))
                        .andDo(print())
                        .andExpect(status().isMovedTemporarily())
                        .andExpect(redirectedUrl("/index"))
                        .andReturn()
                        .getRequest()
                        .getSession();

    request.setSession(session);

    SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

    SecurityContextHolder.setContext(securityContext);

        // Your test goes here. User is logged with 
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.