参考:
1.http://spring.io/blog/2014/05/07/preview-spring-security-test-method-security
2.http://spring.io/blog/2014/05/23/preview-spring-security-test-web-security
3.http://spring.io/blog/2014/05/23/preview-spring-security-test-htmlunit
应用一旦嵌入用户权限功能,测试就会变得稍为复杂,不止是spring security,其它框架应该亦如此.spring security提供了比较完善的测试案例.ROB WINCH写了三篇博客,从标题可大概看出主要讲什么,我对第1,2篇感兴趣,第3篇也不难,但不想花时间去实践.本文主要举例对第1,第2篇的部分关键点讲述与理解.
一.先来看看测试spring security的方法,个人可理解为:嵌入了权限方法(即使用了@PreAuthorize等注解的方法)的测试.在http://blog.csdn.net/xiejx618/article/details/42739707基础上进行修改,将权限声明都放在service方法的接口上.
org.exam.service.UserService
public interface UserService { @PreAuthorize("hasAuthority('USER_QUERY')") Page<User> findAll(Pageable pageable); @PreAuthorize("hasAuthority('USER_SAVE')") User save(User user); @PreAuthorize("hasAuthority('USER_QUERY')") User findOne(Long id); @PreAuthorize("hasAuthority('USER_DELETE')") void delete(Long id); }要测试这些方法,单元测试可像如下写:
package org.exam.service; import org.exam.config.AppConfig; import org.exam.config.SecurityConfig; import org.exam.domain.Authority; import org.exam.domain.Role; import org.exam.domain.User; import org.exam.repository.AuthorityRepository; import org.exam.repository.RoleRepository; import org.exam.repository.UserRepository; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.context.web.ServletTestExecutionListener; import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.HashSet; import java.util.List; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * Created by xin on 15.9.28. */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {AppConfig.class, SecurityConfig.class}) @Transactional(transactionManager = "transactionManager") @TestExecutionListeners(listeners = { ServletTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, WithSecurityContextTestExecutionListener.class}) public class UserServiceTest { @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private AuthorityRepository authorityRepository; public static final String USERNAME = "admin"; @Autowired private UserService userService; @Before public void before(){ /*没做级联保存,那先保存这几个权限*/ Authority authority1 = new Authority(); authority1.setName("查看用户"); authority1.setAuthority("USER_QUERY"); Authority authority2 = new Authority(); authority2.setName("保存用户"); authority2.setAuthority("USER_SAVE"); Authority authority3 = new Authority(); authority3.setName("删除用户"); authority3.setAuthority("USER_DELETE"); List<Authority> authorities = Arrays.asList(authority1, authority2, authority3); authorityRepository.save(authorities); /*角色也一样,没做级联保存,那先保存角色*/ Role role1 = new Role(); role1.setName("管理员"); role1.setAuthorities(new HashSet<Authority>(Arrays.asList(authority2, authority3))); roleRepository.save(role1); /*最后保存用户*/ User user1 = new User(); user1.setUsername(USERNAME); user1.setPassword("$2a$04$fCqcakHV2O.4AJgp3CIAGO9l5ZBq61Gt6YNzjcyC8M.js0ucpyun.");//admin user1.setCredentialsNonExpired(true); user1.setAccountNonLocked(true); user1.setEnabled(true); user1.setAccountNonExpired(true); user1.setAuthorities(new HashSet<Authority>(Arrays.asList(authority1, authority2))); user1.setRoles(new HashSet<Role>(Arrays.asList(role1))); user1 = userRepository.save(user1); assertNotNull(user1); } /*虚拟一个用户*/ @WithMockUser(username = "admin",password = "admin",authorities = {"USER_QUERY","USER_SAVE","USER_DELETE"}) /*通过UserDetailsService.loadUserByUsername根据用户名加载一个用户,与WithMockUser一样会先实例化用户, 比执行@Before方法还要早,即如果下面的testFindAll()先保存一个用户,再通过@WithUserDetails是不能找到这个用户的*/ //@WithUserDetails(UserRepositoryTest.USERNAME) @Test public void testFindAll() { Pageable pageable=new PageRequest(0,10); Page<User> page=userService.findAll(pageable); assertTrue("org.exam.service.UserService.findAll:failed",page.getContent().size()>0); } @After public void after(){ userRepository.deleteAllInBatch(); roleRepository.deleteAllInBatch(); authorityRepository.deleteAllInBatch(); } }参考中有说明@RunWith和@ContextConfiguration注解与其它的spring单元测试没有什么不同.我加入@Transactional是为了事务默认回滚.@TestExecutionListeners指明spring测试模块添加默认的一些监听器使用WithSecurityContextTestExcecutionListener来确保我们的测试使用正确的用户来运行.通过在运行我们的测试之前,放入SecurityContextHolder,测试完成后,清除SecurityContextHolder来实现这样的功能,这就是原理所在,很关键的理解.
1.使用@WithMockUser来虚拟一个用户,实际上,这个用户可以不存在的.上面的testFindAll就是一个例子.
2.使用@WithUserDetails,通过UserDetailsService.loadUserByUsername根据用户名加载一个用户.从上面的原理可知,这个用户必须在运行单元测试之前就存在,这对有些测试带来不便,我也想不出什么方法来改善.
3.通过@WithSecurityContext实现自定义的@WithMockUser和@WithUserDetails,一般都用不上吧.
二.再看看测试web层,也就是和Controller打交道.下面是单元测试方法的举例.
package org.exam.web; import org.exam.config.AppConfig; import org.exam.config.SecurityConfig; import org.exam.config.WebMvcConfig; import org.exam.domain.Authority; import org.exam.domain.Role; import org.exam.domain.User; import org.exam.repository.AuthorityRepository; import org.exam.repository.RoleRepository; import org.exam.repository.UserRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; import java.util.Arrays; import java.util.HashSet; import java.util.List; import static org.junit.Assert.assertNotNull; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; /** * Created by xin on 15.9.28. */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {AppConfig.class, SecurityConfig.class, WebMvcConfig.class}) @Transactional(transactionManager = "transactionManager") @WebAppConfiguration public class UserControllerTest { private static final String USERNAME = "admin"; @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private AuthorityRepository authorityRepository; @Autowired private Filter springSecurityFilterChain; @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; private void initData() { /*没做级联保存,那先保存这几个权限*/ Authority authority1 = new Authority(); authority1.setName("查看用户"); authority1.setAuthority("USER_QUERY"); Authority authority2 = new Authority(); authority2.setName("保存用户"); authority2.setAuthority("USER_SAVE"); Authority authority3 = new Authority(); authority3.setName("删除用户"); authority3.setAuthority("USER_DELETE"); List<Authority> authorities = Arrays.asList(authority1, authority2, authority3); authorityRepository.save(authorities); /*角色也一样,没做级联保存,那先保存角色*/ Role role1 = new Role(); role1.setName("管理员"); role1.setAuthorities(new HashSet<Authority>(Arrays.asList(authority2, authority3))); roleRepository.save(role1); /*最后保存用户*/ User user1 = new User(); user1.setUsername(USERNAME); user1.setPassword("$2a$04$fCqcakHV2O.4AJgp3CIAGO9l5ZBq61Gt6YNzjcyC8M.js0ucpyun.");//admin user1.setCredentialsNonExpired(true); user1.setAccountNonLocked(true); user1.setEnabled(true); user1.setAccountNonExpired(true); user1.setAuthorities(new HashSet<Authority>(Arrays.asList(authority1, authority2))); user1.setRoles(new HashSet<Role>(Arrays.asList(role1))); user1 = userRepository.save(user1); assertNotNull(user1); } @Before public void before() throws Exception { initData(); //如果启用了csrf,别忘了带上with(csrf()) mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .defaultRequest(get("/").with(csrf()).with(user(USERNAME).password(USERNAME) .authorities(new SimpleGrantedAuthority("USER_QUERY"), new SimpleGrantedAuthority("USER_SAVE"), new SimpleGrantedAuthority("USER_DELETE")))) .addFilters(springSecurityFilterChain) .build(); } @Test public void testLoginAndLogout() throws Exception { mockMvc.perform(formLogin("/login").user(USERNAME).password(USERNAME)).andExpect(authenticated().withUsername(USERNAME)); mockMvc.perform(logout("/logout")); } @Test public void testList() throws Exception { mockMvc.perform(get("/user/list").param("page", "0").param("size", "10")).andExpect(status().isOk()); } @Test public void testSave() throws Exception { User user=new User("xiejx618"); user.setPassword("123456"); mockMvc.perform(post("/user/save").param("passNonUpdate", "true").param("username", user.getUsername()).param("password", user.getPassword()) ).andExpect(status().is3xxRedirection()); } @Test public void testDelete() throws Exception { User user=userRepository.findByUsername(USERNAME); mockMvc.perform(get("/user/delete").param("id",user.getId().toString())) .andExpect(view().name("redirect:list")); } //@After public void after() { userRepository.deleteAllInBatch(); roleRepository.deleteAllInBatch(); authorityRepository.deleteAllInBatch(); //注销用户 } }
三.建议结合源码例子https://github.com/rwinch/spring-security-test-blog/blob/master/src/test/java/sample/htmlunit/MockMvcHtmlUnitCreateMessageTest.java ,我理解要先启动web应用,通过htmlunit来模拟用户的一些操作行为来测试,哈哈不感兴趣!
温馨提示:如果使用的IDE是idea,对于一个类自动生成相应单元测试类的方法是:右键编辑区-->Go To-->Test.(windows的快捷键是ctrl+shift+t)
源码:http://download.csdn.net/detail/xiejx618/9145825