参考:
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 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 authorities = Arrays.asList(authority1, authority2, authority3);
authorityRepository.save(authorities);
/*角色也一样,没做级联保存,那先保存角色*/
Role role1 = new Role();
role1.setName("管理员");
role1.setAuthorities(new HashSet(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(Arrays.asList(authority1, authority2)));
user1.setRoles(new HashSet(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 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 authorities = Arrays.asList(authority1, authority2, authority3);
authorityRepository.save(authorities);
/*角色也一样,没做级联保存,那先保存角色*/
Role role1 = new Role();
role1.setName("管理员");
role1.setAuthorities(new HashSet(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(Arrays.asList(authority1, authority2)));
user1.setRoles(new HashSet(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