[英]Unit testing with Spring Security
我的公司一直在評估Spring MVC,以確定我們是否應該在下一個項目中使用它。 到目前為止,我喜歡我所看到的內容,現在我正在查看Spring Security模塊,以確定它是否可以/應該使用。
我們的安全要求非常基本; 用戶只需提供用戶名和密碼即可訪問網站的某些部分(例如獲取有關其帳戶的信息); 並且網站上有一些頁面(常見問題解答,支持等),應該授予匿名用戶訪問權限。
在我創建的原型中,我一直在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
對象本身似乎非常像Spring一樣......
只需按常規方式執行,然后在測試類中使用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 Security不會將Authentication對象作為容器中的bean使用,因此無法輕易地將其注入或自動裝入盒中。
在我們開始使用Spring Security之前,我們將在容器中創建一個會話范圍的bean來存儲Principal,將其注入“AuthenticationService”(單例),然后將此bean注入需要了解當前Principal的其他服務。
如果您正在實現自己的身份驗證服務,您基本上可以做同樣的事情:創建一個具有“principal”屬性的會話范圍的bean,將其注入您的身份驗證服務,讓auth服務在成功的身份驗證中設置該屬性,然后根據需要將auth服務提供給其他bean。
使用SecurityContextHolder我不會感覺太糟糕。 雖然。 我知道它是一個靜態/ Singleton,並且Spring不鼓勵使用這些東西,但是它們的實現需要根據環境進行適當的操作:在Servlet容器中使用會話作用域,在JUnit測試中使用線程作用,等等。真正的限制因素Singleton的用途是它提供了一種對不同環境不靈活的實現。
你很關心 - 靜態方法調用對於單元測試尤其有問題,因為你不能輕易地模擬你的依賴項。 我要向您展示的是如何讓Spring IoC容器為您完成臟工作,為您提供整潔,可測試的代碼。 SecurityContextHolder是一個框架類,雖然您可以將低級安全代碼綁定到它,但您可能希望為UI組件(即控制器)公開更整潔的接口。
cliff.meyers提到了一種解決方法 - 創建自己的“主體”類型並向消費者注入實例。 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標記的神奇之處,每次有新的HTTP請求進入時都會調用靜態方法getUserDetails,並且正確解析對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();
...
}
還可以選擇使用@WithUserDetails
來模擬從UserDetails
返回的UserDetailsService
,例如
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
可以在Spring Security參考文檔中的@WithMockUser和@WithUserDetails章節中找到更多詳細信息(從中復制了上述示例)
就個人而言,我只會使用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來返回Authentication,最后模擬SecurityContextHolder來獲取SecurityContext,但它非常靈活,允許你對null認證對象等場景進行單元測試等,而無需更改您的(非測試)代碼
在這種情況下使用靜態是編寫安全代碼的最佳方法。
是的,靜態通常很糟糕 - 通常,但在這種情況下,靜態就是你想要的。 由於安全上下文將Principal與當前運行的線程相關聯,因此最安全的代碼將盡可能直接地從線程訪問靜態。 隱藏注入的包裝類后面的訪問權限會為攻擊者提供更多攻擊點。 他們不需要訪問代碼(如果jar被簽名,他們將很難改變它們),他們只需要一種覆蓋配置的方法,這可以在運行時完成或將一些XML滑入類路徑。 即使使用注釋注入也可以使用外部XML覆蓋。 這樣的XML可能會為正在運行的系統注入一個流氓主體。
我在這里問自己同樣的問題,剛剛發布了我最近發現的答案。 簡短的回答是:注入一個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;
}
};
}
我將看一下Spring的抽象測試類和模擬對象,這些都在這里討論。 它們為Spring管理對象提供了一種強大的自動連接方式,使單元和集成測試更加容易。
身份驗證是服務器環境中線程的屬性,其方式與操作系統中進程的屬性相同。 擁有用於訪問身份驗證信息的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
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.