参考:
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;
-
-
-
- @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.");
- 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"})
-
-
-
- @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;
-
-
-
- @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.");
- 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();
-
- 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"));
- }
-
-
- public void after() {
- userRepository.deleteAllInBatch();
- roleRepository.deleteAllInBatch();
- authorityRepository.deleteAllInBatch();
-
- }
- }
这里主要是构造一个MockMvc,用户信息可在构造MockMvc时加上,也可以在发模拟请求时加上.上面是前者的这种情况,如果你的spring security应用启用了scrf,别忘了带上with(csrf()).我昨天就在这问题上卡了二个多小时,后来调试时,发觉被csrf的filter拦截直接返回304,而没有跳入controller的方法,我刚开始怀疑是源码的bug,是我多虑了!参考还举例了登入和登出.
三.建议结合源码例子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