Spring Securityを使っているWebアプリのUnitテストについて調べたのでメモしておく。
基本的に次のSpringのドキュメントを動かしてみただけなので詳細はSpringのドキュメントを見てください。
dependency
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
|
※spring-boot-security-testsというものもあったがダウンロードできなかったので上記のdependencyを使えば大丈夫です。
SecurityContext
UnitテストのコードでSecurityContextHolderを使ってUserをセットしても認証できなかったので上記のDependencyを追加して@WithMockUser
などのspring-security-testが用意している手段を使ってテストする必要があります。
test test test
Webアプリをテストする前にspring-security-testを触ってみる。
例えば、以下の様なServiceをテストする。
@Service public class SpringSecurityService {
public boolean isLoggedIn() { return !ObjectUtils.isEmpty(getAuthentication()) && getAuthentication().isAuthenticated(); }
public Optional<UserDetails> getCurrentUserDetails() { if (isLoggedIn()) { Object principal = getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { return Optional.of((UserDetails) principal); } } return Optional.empty(); }
private Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); } }
|
テストコードを書いてみる。
@WithMockUser
をつけるだけでorg.springframework.security.core.userdetails.UserがSecurityContextにセットされる。
username, password, rolesが指定できる。
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = SpringBootSecuritySampleApplication.class) public class SpringSecurityServiceTest {
@Autowired private SpringSecurityService springSecurityService;
@Test @WithMockUser(username="alice",roles={"USER"}) public void testIsLoggedIn() { Assert.assertTrue(springSecurityService.isLoggedIn()); }
@Test public void testIsLoggedInNoLogin() { Assert.assertFalse(springSecurityService.isLoggedIn()); }
@Test @WithMockUser(username="alice",roles={"USER"}) public void testGetCurrentUserDetails() { UserDetails userDetails = springSecurityService.getCurrentUserDetails().orElse(null); Assert.assertNotNull(userDetails); Assert.assertEquals("alice", userDetails.getUsername()); Assert.assertEquals(1, userDetails.getAuthorities().size()); }
|
大半は、CustomUserを使っていると思うので@WithUserDetails
を使って使用するUserDetailsServiceを指定してUserを取得するか、カスタムUserアノテーションを作る必要がある。
今回作ったサンプルがUserEntityを作っていないのでUserDetailsService
も当然作っていない。おそらくDefaultなUserDetailsService
があるはずだけども、取り方がよくわからなかったので@WithSecurityContext
を使って、@WithMockCustomUser
を作ってみる。
@WithMockCustomUser
と指定できるようにアノテーションを作る。
Custom propertyとしてemailを追加してみる。
@Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) public @interface WithMockCustomUser { String username() default "user";
String password() default "password";
String email() default "user@example.com";
String[] roles() default { "USER" }; }
|
WithMockCustomUserSecurityContextFactory
を実装する必要があるので実装する。
※UserEntityを作っていないのでCustomUserDetailsをInnerクラスで定義している。なので、Entity部分は読み替えてください。
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { @Override public SecurityContext createSecurityContext(WithMockCustomUser customUser) { SecurityContext context = SecurityContextHolder.createEmptyContext();
List<GrantedAuthority> authorities = new ArrayList<>(); for (String role : customUser.roles()) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); }
CustomUserDetails principal = new CustomUserDetails(customUser.username(), customUser.password(), authorities, customUser.email()); Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities()); context.setAuthentication(auth); return context; }
@Data public class CustomUserDetails extends User { private String email; public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, String email) { super(username, password, authorities); this.email = email; } } }
|
以下の様にカスタムユーザアノテーションが使えるようになる。
@Test @WithMockCustomUser(username = "admin", email = "admin@ishiis.net") public void testCustomUser() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/home")) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.content().string("Login User Name:admin")) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("admin"))); }
|
まぁ、emailは使ってないのですが・・・。
Webアプリのテスト
前置きが長くなってしまいましたが、Webアプリのテストをします。
まず、SecurityConfigから。
DataSourceは省略しています。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated() .and() .formLogin() .defaultSuccessUrl("/home").failureUrl("/login") .usernameParameter("username").passwordParameter("password") .and() .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/"); } }
|
/
だけ許可して、ログインに失敗したら/login
に遷移させるようにしている。/login
はspring securityが用意しているdefaultの画面を使っている。
Contorollerは次のものを適当につくった。
@Controller public class HomeController {
@Autowired private SpringSecurityService springSecurityService;
@RequestMapping("/") @ResponseBody public String root(){ return "ROOT PAGE"; }
@RequestMapping("/home") @ResponseBody public String home(){ return "Login User Name:" + springSecurityService.getCurrentUsername().orElse("unknown username"); } }
|
テストコードを書いてみる
Webアプリケーションのテストをするために、WebApplicationContext
とMockMvc
が必要になる。
/home
に対するテストコードを書いてみる。
@RunWith(SpringRunner.class) @SpringBootTest(classes = SpringBootSecuritySampleApplication.class) public class HomeControllerTest {
@Autowired private WebApplicationContext context;
private MockMvc mockMvc;
@Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(SecurityMockMvcConfigurers.springSecurity()).build(); }
@Test @WithMockCustomUser(username = "admin", email = "admin@ishiis.net") public void testCustomUser() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/home")) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.content().string("Login User Name:admin")) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("admin"))); }
@Test public void testHomeNotLogIn() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/home")) .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.redirectedUrlPattern("**/login")); }
}
|
MockMvcに対してSpringSecurityを適用するにはMockMvcBuildersの#applyでSecurityMockMvcConfigurers#springSecurityを指定する必要がある。
ちなみに以下の様にMockMvcRequestBuilders
でログインしているユーザをセットする方法もある。
UserDetailsをimplしているオブジェクトが指定できる。
@Test public void testHomeLoggedInUsingWithUser() throws Exception { Collection<GrantedAuthority> authorities =new ArrayList<>() ; authorities.add(new SimpleGrantedAuthority("ADMIN")); mockMvc.perform(MockMvcRequestBuilders.get("/home").with(user(new User("admin", "password", authorities)))) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.content().string("Login User Name:admin")) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("admin"))); }
|
ユーザ認証を使っているアプリであれば頻繁にテストに.with(user(new User()))と書くことになるのでannotationで書けるようにしておいたほうが可読性もあがるのでいいかもしれない。
まとめ
SpringBoot1.4からannotation周りが若干変わってて困ったが、簡単にテストできる様になっているのでいい感じですね。
おわり。
参考
- http://docs.spring.io/spring-security/site/docs/current/reference/html/test-method.html
- http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html