Spring Security を使用した単体テスト
-
21-08-2019 - |
質問
私の会社では Spring MVC を評価して、次のプロジェクトで Spring MVC を使用する必要があるかどうかを判断しています。これまでのところ、私はこれまで見てきたものが気に入っており、現在 Spring Security モジュールを調べて、それが使用できるかどうか、使用すべきかどうかを判断しています。
私たちのセキュリティ要件は非常に基本的なものです。ユーザーはユーザー名とパスワードを入力するだけで、サイトの特定の部分にアクセスできるようになります (アカウントに関する情報を取得するなど)。また、サイト上には匿名ユーザーにアクセスを許可する必要があるページ (FAQ、サポートなど) がいくつかあります。
私が作成しているプロトタイプでは、認証されたユーザーの「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
オブジェクト自体は非常に Spring らしくないように思えます...
解決
問題は、春のセキュリティは、コンテナ内のBeanとして認証オブジェクトを利用可能にしません、そう簡単に箱から出して、それを注入またはautowireする方法がないということです。
私たちは春のセキュリティを使用し始めた前に、我々は、校長を保存「AuthenticationService」(シングルトン)にこれを注入し、その後の知識を必要とする他のサービスにこのBeanを注入する容器にセッションスコープのBeanを作成します現在の校長ます。
独自の認証サービスを実装している場合は、、あなたは基本的に同じことを行うことができます:認証サービスが成功した認証のプロパティを設定している、あなたの認証サービスにこれを注入し、「校長」プロパティでセッションスコープのBeanを作成しますあなたがそれを必要として、他のBeanに利用できる認証サービスを行います。
私はSecurityContextHolderの使用についてあまりにも悪い感じではないでしょう。けれども。実際の制限要因をなど、サーブレットコンテナでセッションスコープ、JUnitテストにスレッドスコープ:私はそれは、静的/シングルトンだということを知っていて、その春には、このようなものを使用して阻止するが、その実装は、環境に応じて適切に動作するように注意を要しますそれは異なる環境に不撓性である実装を提供する場合シングルトンである。
他のヒント
ただ、例えば、それを通常の方法を行い、その後、あなたのテストクラスで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);
心配するのは当然です。依存関係を簡単にモックできないため、静的メソッドの呼び出しは単体テストの場合に特に問題になります。これから説明するのは、Spring IoC コンテナーに汚い仕事をさせて、きちんとしたテスト可能なコードを残す方法です。SecurityContextHolder はフレームワーク クラスであり、低レベルのセキュリティ コードを関連付けることは問題ありませんが、おそらく UI コンポーネント (つまり、コントローラー)。
cliff.meyers 氏は、これを回避する方法の 1 つとして、独自の「プリンシパル」タイプを作成し、コンシューマーにインスタンスを注入する方法について言及しました。春 <aop:スコープ指定されたプロキシ/> タグは 2.x で導入され、リクエスト スコープ 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 タグの魔法のおかげで、新しい HTTP リクエストが受信されるたびに静的メソッド getUserDetails が呼び出され、currentUser プロパティへの参照が正しく解決されます。これで、単体テストが簡単になります。
protected void setUp() {
// existing init code
MyUserDetails user = new MyUserDetails();
// set up user as you wish
controller.setCurrentUser(user);
}
お役に立てれば!
Authentication オブジェクトを作成して挿入する方法についての質問には答えずに、Spring Security 4.0 はテストに関していくつかの歓迎すべき代替手段を提供します。の @WithMockUser
アノテーションを使用すると、開発者は模擬ユーザー (オプションの権限、ユーザー名、パスワード、ロールを含む) を適切な方法で指定できます。
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
String message = messageService.getMessage();
...
}
を使用するオプションもあります @WithUserDetails
をエミュレートする UserDetails
から戻ってきました UserDetailsService
, 、例えば
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
詳細については、 @WithMockUser そしてその @WithUserDetails Spring Security リファレンス ドキュメントの章 (上記の例はそこからコピーされました)
個人的に私はちょうど例えばあなたのユニット/統合テストに()静的SecurityContextHolder.getSecurityContextを模擬するためにMockitoまたはEasymockとともにPowermockを使用することになります。
@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);
...
}
}
確かに、ここでボイラープレートコードのかなりは、すなわち認証対象が嘲笑され、認証を返却し、最終的にSecurityContextがを得るためにSecurityContextHolderを模擬したSecurityContextをあざけり、しかし、その非常に柔軟で、次のようなシナリオのためのユニットテストすることができますヌル認証オブジェクト等あなたの(非試験)コード
を変更することなくこの場合、静的を使用すると、安全なコードを書くための最良の方法です。
一般的に、しかし、この場合には、静的は何をしたいです -はい、静は、一般的に悪いです。セキュリティコンテキストは、現在実行中のスレッドで校長を関連付けているので、最も安全なコードはできるだけ直接スレッドから静的にアクセスします。注入されたラッパークラスの後ろのアクセスを非表示にする攻撃に対してより多くのポイントと、攻撃者が用意されています。彼らはただ、実行時に行うことができます設定、またはクラスパス上にいくつかのXMLを滑りをオーバーライドする方法を必要とする、(jarファイルが署名された場合、彼らは変化する苦労を持っていることになる)コードにアクセスする必要はありません。でも使って注釈注入は、外部のXMLとオーバーライドだろう。そのようなXMLは不正プリンシパルに実行しているシステムを注入することができます。
私は同じ質問に自分の上に<のhref = "https://stackoverflow.com/questions/248562/when-using-spring-security-what-is-the-proper-way-to-obtain-currentを尋ねました-username-I ">ここを、とだけは、私が最近見つけ答えを掲載。短い答えは次のとおりです。SecurityContext
を注入し、SecurityContextHolder
を得るために、あなたの春の設定でのみSecurityContext
を参照してください。
私は<のhref =「http://static.springframework.org/spring/docs/2.5.x/reference/testing.html」のrelについて話しているSpringの抽象テストクラスとモックオブジェクトを見てみましょう= "nofollowをnoreferrer">ここを。彼らは、ユニットと統合テストを作るより簡単に春の管理対象オブジェクトを自動配線の強力な方法を提供します。
一般的な
それまでの間 (2013 年のバージョン 3.2 以降、おかげさまで) SEC-2298) アノテーションを使用して認証を 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;
}
};
}
認証は、OSにおけるプロセスの財産であると同じようにサーバー環境でのスレッドのプロパティです。認証情報にアクセスするためのBeanインスタンスを有する任意の恩恵なしに不便構成および配線のオーバーヘッドであろう。
テスト認証に関しては、あなたがあなたの人生を容易にすることができますどのようにいくつかの方法があります。私のお気に入りは、それを管理するカスタム注釈@Authenticated
とテストの実行リスナーを作ることです。インスピレーションを得るためDirtiesContextTestExecutionListener
を確認します。
仕事のかなり多くの後、私は希望の動作を再現することができました。私はMockMvcを通じてログインをエミュレートしていました。それはあまりにも、ほとんどのユニットテストのために重いが、統合テストのために有用です。
もちろん私は、私たちのテストが容易になります春のセキュリティ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
}