私の会社は、Spring MVCを評価して、次のプロジェクトの1つで使用すべきかどうかを判断しています。これまでのところ、私はこれまで見てきたものが大好きで、今はSpring Securityモジュールを見て、それが使用できる/すべきかどうかを判断しています。
セキュリティ要件は非常に基本的なものです。ユーザーは、ユーザー名とパスワードを入力するだけで、サイトの特定の部分にアクセスできます(アカウントに関する情報を取得するなど)。また、匿名ユーザーにアクセスを許可する必要があるサイト(FAQ、サポートなど)には少数のページがあります。
私が作成しているプロトタイプでは、認証済みユーザーのSessionに「LoginCredentials」オブジェクト(ユーザー名とパスワードのみを含む)を保存しています。たとえば、一部のコントローラーは、このオブジェクトがセッション中かどうかを確認して、ログインしているユーザー名への参照を取得します。代わりに、この自家製のロジックをSpring Securityに置き換えたいと考えています。これにより、「ログインしているユーザーをどのように追跡するのか」という種類を削除できるという利点があります。 「ユーザーをどのように認証するのですか?」私のコントローラー/ビジネスコードから。
Spring Securityは(スレッドごとの)「コンテキスト」オブジェクトを提供して、アプリのどこからでもユーザー名/主要情報にアクセスできるようです...
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
...このオブジェクトは、ある意味では(グローバル)シングルトンであるように、非常に春のように見えます。
私の質問はこれです:これがSpring Securityで認証されたユーザーに関する情報にアクセスする標準的な方法である場合、ユニットテストが認証されたユーザー?
各テストケースの初期化メソッドでこれを接続する必要がありますか?
protected void setUp() throws Exception {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
...
}
これは過度に冗長に思えます。もっと簡単な方法はありますか?
SecurityContextHolder
オブジェクト自体は非常に春のように見えます...
問題は、Spring Securityがコンテナ内の認証オブジェクトをBeanとして利用できないため、簡単にインジェクトまたは自動配線する方法がないことです。
Spring Securityの使用を開始する前に、コンテナにセッションスコープのBeanを作成してプリンシパルを格納し、これを「AuthenticationService」(シングルトン)に挿入してから、現在のプリンシパルの知識が必要な他のサービスにこのBeanを挿入します。
独自の認証サービスを実装する場合、基本的に同じことを行うことができます。「プリンシパル」プロパティを使用してセッションスコープBeanを作成し、これを認証サービスに挿入し、認証サービスに認証成功時にプロパティを設定させます。必要に応じて他のBeanで認証サービスを利用できるようにします。
SecurityContextHolderを使用しても、それほど悪くは感じません。しかし。私はそれが静的/シングルトンであり、Springがそのようなものを使用することを推奨していないことを知っていますが、その実装は環境に応じて適切に動作するよう注意します:サーブレットスコープのセッションスコープ、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つに言及しました。独自の「プリンシパル」タイプを作成し、インスタンスをコンシューマに注入します。 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);
}
お役に立てれば!
認証オブジェクトの作成方法と注入方法に関する質問に答えずに、Spring Security 4.0はテストに関して歓迎すべき代替手段を提供します。 @WithMockUser
注釈を使用すると、開発者は(オプションの権限、ユーザー名、パスワード、およびロールを持つ)模擬ユーザーをきちんと指定できます。
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
String message = messageService.getMessage();
...
}
UserDetails
から返されたUserDetailsService
をエミュレートするために@WithUserDetails
を使用するオプションもあります。
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
詳細については、Spring Securityリファレンスドキュメントの @ WithMockUser および @ WithUserDetails の章をご覧ください(上記の例のコピー元)
個人的には、MockitoまたはEasymockとともにPowermockを使用して、ユニット/統合テストで静的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);
...
}
}
確かにボイラープレートコードがかなりあります。つまり、認証オブジェクトをモックし、SecurityContextをモックして認証を返し、最後にSecurityContextHolderをモックしてSecurityContextを取得しますが、非常に柔軟で、null認証オブジェクトのようなシナリオの単体テストを可能にしますなど(テスト以外の)コードを変更する必要なし
この場合、静的なコードを使用することが、安全なコードを記述するための最良の方法です。
はい、静的は一般に悪いです-一般に、しかしこの場合、静的はあなたが望むものです。セキュリティコンテキストはプリンシパルを現在実行中のスレッドに関連付けるため、最も安全なコードは、可能な限り直接スレッドから静的にアクセスします。挿入されたラッパークラスの背後にアクセスを非表示にすると、攻撃者は攻撃するポイントが増えます。コードにアクセスする必要はなく(jarが署名されている場合は変更に苦労します)、構成をオーバーライドする方法が必要です。これは、実行時に行うか、XMLをクラスパスにスリップすることができます。アノテーションインジェクションを使用しても、外部XMLでオーバーライドできます。このようなXMLは、実行中のシステムに不正なプリンシパルを挿入する可能性があります。
here で同じ質問を自分で尋ね、最近見つけた答えを投稿しました。簡単な答えは:SecurityContext
を挿入し、Spring構成でのみSecurityContextHolder
を参照してSecurityContext
を取得することです。
それまでの間(バージョン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;
}
};
}
_
here について説明されているSpringの抽象テストクラスとモックオブジェクトを見てみましょう。 Spring管理対象オブジェクトを自動配線する強力な方法を提供し、単体テストと統合テストを容易にします。
認証は、OSのプロセスのプロパティと同様に、サーバー環境のスレッドのプロパティです。認証情報にアクセスするためのBeanインスタンスがあると、設定や配線のオーバーヘッドが不便になり、メリットがありません。
テスト認証に関しては、生活を楽にする方法がいくつかあります。私のお気に入りは、カスタムアノテーションを作成することです@Authenticated
およびそれを管理するテスト実行リスナー。インスピレーションについては、DirtiesContextTestExecutionListener
を確認してください。
かなりの作業の後、私は望ましい動作を再現することができました。 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
}