單元測試
1. 前言
上節(jié)我們介紹了 Spring Security 安全框架中的加解密功能,本節(jié)我們討論 Spring Security 項目單元測試的實現(xiàn)。
在程序開發(fā)過程中,單元測試環(huán)境往往貫穿始終。
本節(jié)我們主要討論如何通過單元測試保障 Spring Security 應(yīng)用的健壯性。
2. 方法安全測試
2.1 場景:構(gòu)造 MessageService
接口,要求認(rèn)證過得用戶才能訪問它。
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "Hello " + authentication;
}
}
認(rèn)證過的用戶調(diào)用 getMessage
方法時,會得到如下返回:
Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
2.1.1 建立 Spring Security 單元測試
在使用 Spring Security 單元測試之前,首先需要做一些初始化,如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class WithMockUserTests {
-
@RunWith
注解用于創(chuàng)建ApplicationContext
對象,這里和其他的 Spring 應(yīng)用單元測試一樣,此處不再贅述。 -
@ContextConfiguration
注解用于上下文相關(guān)的配置選項,此處使用默認(rèn)值即可,同樣,這也是 Spring 單元測試所涉及的內(nèi)容,此處不做贅述。
我們構(gòu)造的 getMessage
方法是需要認(rèn)證用戶才能訪問的,如果是非認(rèn)證用戶訪問,則應(yīng)該拋出相應(yīng)異常。
針對這種場景建立以下單元測試方法:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}
2.1.2 @WithMockUser
接下來我們要創(chuàng)造認(rèn)證用戶,是用的方法是增加 @WithMockUser 注解,該注解會構(gòu)造一個用戶名為「user」,密碼為「password」,角色為「ROLE_USER」的用戶:
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
}
指定用戶的用戶名:
@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
String message = messageService.getMessage();
}
指定用戶的角色:
@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
...
}
不指定用戶的角色,直接定義用戶的權(quán)限:
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
String message = messageService.getMessage();
...
}
冒煙測試用的用戶可以定義在方法前,也可以定義在類聲明前,使整個類范圍內(nèi)都使用相同用戶:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
}
2.1.3 @WithAnonymousUser
如果需要測試匿名用戶,可通過 @WithAnonymousUser
注解實現(xiàn):
@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {
@Test
public void withMockUser1() {
}
@Test
public void withMockUser2() {
}
@Test
@WithAnonymousUser
public void anonymous() throws Exception {
// 使用匿名用戶訪問該方法
}
}
2.1.4 @WithUserDetails
當(dāng) @WithMockUser
不適用時,比如我們需要認(rèn)證主體是一些特殊類型,這時就需要自定義 UserDetails
,假設(shè)我們已經(jīng)有了一個 UserDetailsService 的 bean 聲明:
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
我們也可以指定用戶名,用來在 UserDetailsService 中返回指定用戶:
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
我們還可以指定 UserDetailsService
的實現(xiàn) bean:
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
String message = messageService.getMessage();
...
}
2.1.5 @WithSecurityContext
當(dāng)我們不希望建立 Authentication
對象,而希望直接使用 SecurityContext
時,可用如下方法:
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "rob";
String name() default "Rob Winch";
}
3. Spring MVC 測試集成
這部分需要配合 Spring MVC 的集成測試模塊。
3.1 建立 Spring MVC 單元測試
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfShowcaseTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
...
3.2 測試 CSRF
為請求增加有效的 CSRF Token:
mvc.perform(post("/").with(csrf()))
把 CSRF Token 增加到請求頭中:
mvc.perform(post("/").with(csrf().asHeader()))
增加一個不合法的 CSRF Token:
mvc.perform(post("/").with(csrf().useInvalidToken()))
配置請求攜帶默認(rèn)用戶信息:
mvc.perform(get("/").with(user("user")))
配置請求攜帶自定義用戶信息
mvc.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))
使用自定義的 userDetails
實例:
mvc.perform(get("/").with(user(userDetails)))
使用匿名用戶:
mvc.perform(get("/").with(anonymous()))
使用自定義身份信息:
mvc.perform(get("/").with(authentication(authentication)))
使用自定義安全上下文:
mvc.perform(get("/").with(securityContext(securityContext)))
將用戶信息應(yīng)用到所有請求中:
mvc = MockMvcBuilders
.webAppContextSetup(context)
.defaultRequest(get("/").with(user("user").roles("ADMIN")))
.apply(springSecurity())
.build();
我們還可以使用注解方式配置用戶信息:
@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
.perform(get("/"))
...
}
3.3 測試 HTTP 基礎(chǔ)認(rèn)證
使用 httpBasic
測試基本認(rèn)證:
mvc.perform(get("/").with(httpBasic("user","password")))
這一步相當(dāng)于為請求增加了以下認(rèn)證頭:
Authorization: Basic dXNlcjpwYXNzd29yZA==
4. 小結(jié)
本節(jié)介紹了 Spring Security 項目實現(xiàn)單元測試的方法,主要內(nèi)容有:
- Spring Security 的單元測試是基于 Spring 單元測試擴(kuò)展;
- Spring Security 的單元測試核心思想是模擬出認(rèn)證用戶及相關(guān)用戶信息;
- Spring MVC 環(huán)境下的單元測試可以模擬各種安全頭信息及用戶認(rèn)證信息。
至此,關(guān)于 Spring Security 的內(nèi)容就全部結(jié)束了。