簡體   English   中英

使用Spring Security進行單元測試

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM