Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その16 )( ログイン画面の作成7 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その15 )( ログイン画面の作成6 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- ログイン画面・ログアウト機能のテストクラスの作成 ( 前回からの続き )
参照したサイト・書籍
Spring mvc 3.1 integration tests with session support
http://stackoverflow.com/questions/13687055/spring-mvc-3-1-integration-tests-with-session-support- MockMvc に session を引き継ぐ方法を調査した時に参照しました。
spring-projects/spring-test-mvc - Mockito and MockHttpServletRequest #57
https://github.com/spring-projects/spring-test-mvc/issues/57- 「次回から自動的にログインするのテスト」のテストメソッドを実装する際に、remember-me parameter を渡す方法を調査した時に参照しました。記事の中に書かれていた SimpleRequestBuilder をそのまま利用しています。
目次
手順
SecurityMockMvcResource クラス、SecurityMockMvcLoginErrorResource クラスの準備
LoginControllerTest クラスの作成
src/main/java/ksbysample/webapp/lending/web の下の LoginController.java を開いて「Create Test」ダイアログを表示し、テストクラスを作成します。
- 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。
src/test/java/ksbysample/webapp/lending/web の下に LoginControllerTest.java が作成されます。
最初にテストクラスの構造のみ記載します。src/test/java/ksbysample/webapp/lending/web の下の LoginControllerTest.java を リンク先のその1の内容 に変更します。
remember-me 機能のテストを実装するために必要な SimpleRequestBuilder クラスを作成します。src/test/java/ksbysample/common/test の下に SimpleRequestBuilder.java を作成します。作成後、リンク先の内容 に変更します。
※SimpleRequestBuilder クラスは spring-projects/spring-test-mvc - Mockito and MockHttpServletRequest #57 の中に書かれていたものをそのまま使います。
テストを実装します。src/test/java/ksbysample/webapp/lending/web の下の LoginControllerTest.java を リンク先のその2の内容 に変更します。以下に苦労した点を記載します。
- 「次回から自動的にログインするのテスト」で Spring Security の formLogin() を使用しつつ、かつ独自パラメータ ( remember-me ) を渡す方法が本当に分かりませんでした。内容はソースに書いた通りですが、もっと簡単な方法がないのかいまだに疑問です。。。 何度もパラメータを渡す場合にはヘルパークラスを作成した方がよいでしょう。
- ログインした状態を MockMvc に引き継ぐ方法が最初分かりませんでした。結論としては MvcResult result を取得してから
result.getRequest().getSession()
を呼び出して HttpSession session を取得し、次のアクセス時に session メソッドに渡せば引き継ぐことができました。
テストを実行してみます。LoginControllerTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LoginControllerTest' with Coverage」を選択します。
テストが全て成功することが確認できます。
commit、Push、Pull Request、マージ
commit します。「Code Analysis」ダイアログが出ますが、無視して「Commit」ボタンをクリックします。
コマンドラインから以下のコマンドを実行して commit を1つにまとめます。
> git rebase -i HEAD~3
> git commit --amend -m "#19 ログイン画面のテストクラスを作成しました。"GitHub へ Push、1.0.x-make-test-login -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-test-login ブランチを削除、をします。
次回は。。。
画面の共通部分を作成します。
今回テストを作成していて /urllogin で enabled やアカウント/パスワードの有効期限等を見ていないことに気づいたのですが、今はこのままにします。一旦ログイン画面から離れたい。。。
ソースコード
SecurityMockMvcResource.java
package ksbysample.common.test; import org.junit.rules.ExternalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @Component public class SecurityMockMvcResource extends ExternalResource { public final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com"; public final String MAILADDR_SUZUKI_HANAKO = "suzuki.hanako@test.co.jp"; public final String MAILADDR_KIMURA_MASAO = "kimura.masao@test.co.jp"; public final String MAILADDR_ENDO_YOKO = "endo.yoko@sample.com"; public final String MAILADDR_SATO_MASAHIKO = "sato.masahiko@sample.com"; public final String MAILADDR_TAKAHASI_NAOKO = "takahasi.naoko@test.co.jp"; @Autowired private WebApplicationContext context; @Autowired private Filter springSecurityFilterChain; @Autowired private UserDetailsService userDetailsService; public MockMvc authTanakaTaro; public MockMvc authSuzukiHanako; public MockMvc noauth; @Override protected void before() throws Throwable { // 認証ユーザ用MockMvc ( user = tanaka.taro@sample.com ) UserDetails userDetailsTanakaTaro = userDetailsService.loadUserByUsername(MAILADDR_TANAKA_TARO); this.authTanakaTaro = MockMvcBuilders.webAppContextSetup(this.context) .defaultRequest(get("/").with(user(userDetailsTanakaTaro))) .addFilter(springSecurityFilterChain) .build(); // 認証ユーザ用MockMvc ( user = suzuki.hanako@test.co.jp ) UserDetails userDetailsSuzukiHanako = userDetailsService.loadUserByUsername(MAILADDR_SUZUKI_HANAKO); this.authSuzukiHanako = MockMvcBuilders.webAppContextSetup(this.context) .defaultRequest(get("/").with(user(userDetailsSuzukiHanako))) .addFilter(springSecurityFilterChain) .build(); // 非認証ユーザ用MockMvc this.noauth = MockMvcBuilders.webAppContextSetup(this.context) .addFilter(springSecurityFilterChain) .build(); } }
- 認証ユーザ用MockMvc も作っていますが、今回のテストでは非認証ユーザ用MockMvc の方しか使っていません。
- 認証ユーザ用MockMvc では
.defaultRequest(get("/").with(user(...).roles(...)))
の形式ではなく、UserDetails クラスのインスタンスを生成してから.defaultRequest(get("/").with(user(UserDetails クラスのインスタンス)))
の形式で実装しています。
LoginControllerTest.java
■その1
package ksbysample.webapp.lending.web; import ksbysample.webapp.lending.Application; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @RunWith(Enclosed.class) public class LoginControllerTest { @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログイン画面の初期表示のテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログイン成功のテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログインエラーのテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 次回から自動的にログインするのテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログアウトのテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class encodeのテスト { } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class urlloginのテスト { } }
■その2
package ksbysample.webapp.lending.web; import ksbysample.common.test.SecurityMockMvcResource; import ksbysample.common.test.SimpleRequestBuilder; import ksbysample.common.test.TestDataResource; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.config.WebSecurityConfig; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.entity.UserInfo; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.*; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MvcResult; import javax.servlet.http.Cookie; import javax.servlet.http.HttpSession; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.isA; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(Enclosed.class) public class LoginControllerTest { @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログイン画面の初期表示のテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void ログイン画面を表示する() throws Exception { // ログイン画面が表示されることを確認する mvc.noauth.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("login")) .andExpect(model().hasNoErrors()); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログイン成功のテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void 有効なユーザ名とパスワードを入力すればログインに成功する() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") ) .andExpect(status().isFound()) .andExpect(redirectedUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログインエラーのテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Autowired private UserInfoDao userInfoDao; @Test public void 存在しないユーザ名とパスワードを入力すればログインはエラーになる() throws Exception { mvc.noauth.perform(formLogin() .user("id", "user.notexists@sample.com") .password("password", "notexists") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); } @Test public void 存在するユーザ名でもパスワードが正しくなければログインはエラーになる() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "tanaka") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); } @Test public void enabledが0のユーザならばログインはエラーになる() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_KIMURA_MASAO) .password("password", "masao") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(DisabledException.class))); } @Test public void アカウントの有効期限が切れているユーザならばログインはエラーになる() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_ENDO_YOKO) .password("password", "yoko") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(AccountExpiredException.class))); } @Test public void パスワードの有効期限が切れているユーザならばログインはエラーになる() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_SATO_MASAHIKO) .password("password", "masahiko") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(CredentialsExpiredException.class))); } @Test public void ログインを5回失敗すればアカウントはロックされる() throws Exception { // 1回目 mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro1") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); UserInfo userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 1); // 2回目 mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro2") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 2); // 3回目 mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro3") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 3); // 4回目 mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro4") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 4); // 5回目 ( ここまでは BadCredentialsException.class ) mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro5") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(BadCredentialsException.class))); userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 5); // 6回目 ( アカウントがロックされているので LockedException.class に変わる ) mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro6") ) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()) .andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(LockedException.class))); userInfo = userInfoDao.selectByMailAddress(mvc.MAILADDR_TANAKA_TARO); assertThat(userInfo.getCntBadcredentials()).isEqualTo((short) 5); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 次回から自動的にログインするのテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void 次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる() throws Exception { // ログイン前にはログイン後の画面にアクセスできない mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); // 「次回から自動的にログインする」をチェックしてログインし、remember-me Cookie を生成する MockServletContext servletContext = new MockServletContext(); org.springframework.mock.web.MockHttpServletRequest request = formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") .buildRequest(servletContext); request.addParameter("remember-me", "true"); SimpleRequestBuilder simpleRequestBuilder = new SimpleRequestBuilder(request); MvcResult result = mvc.noauth.perform(simpleRequestBuilder) .andExpect(status().isFound()) .andExpect(redirectedUrl("/loginsuccess")) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)) .andReturn(); Cookie[] cookie = result.getResponse().getCookies(); // remember-me Cookie を引き継いでログイン後の画面にアクセスするとアクセスできる mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL).cookie(cookie)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("loginsuccess")) .andExpect(model().hasNoErrors()) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); // ログイン画面にアクセスしても有効な remember-me Cookie があればログイン後の画面にリダイレクトする mvc.noauth.perform(get("/").cookie(cookie)) .andExpect(status().isFound()) .andExpect(redirectedUrl("/loginsuccess")) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class ログアウトのテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void 有効なユーザ名とパスワードを入力すればログインに成功する() throws Exception { // ログイン前にはログイン後の画面にアクセスできない mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); // ログインする MvcResult result = mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") ) .andExpect(status().isFound()) .andExpect(redirectedUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)) .andReturn(); HttpSession session = result.getRequest().getSession(); assertThat(session).isNotNull(); // ログインしたのでログイン後の画面にアクセスできる mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL).session((MockHttpSession) session)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("loginsuccess")) .andExpect(model().hasNoErrors()) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); // ログアウトする mvc.noauth.perform(get("/logout").session((MockHttpSession) session)) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()); // ログアウトしたのでログイン後の画面にアクセスできない mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL).session((MockHttpSession) session)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class urlloginのテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public SecurityMockMvcResource mvc; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class encodeのテスト { @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void encodeで生成したパスワードの暗号化文字列が正しいことを確認する() throws Exception { MvcResult result = mvc.noauth.perform(get("/encode?password=ptest")) .andExpect(status().isOk()) .andExpect(content().contentType("text/plain;charset=UTF-8")) .andReturn(); String crypt = result.getResponse().getContentAsString(); BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); assertThat(passwordEncoder.matches("ptest", crypt)).isTrue(); } } @Test public void 存在するメールアドレスを指定すればログインに成功する() throws Exception { // ログイン前にはログイン後の画面にアクセスできない mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); // 存在するメールアドレスを指定して /urllogin にアクセスすればログインできる MvcResult result = mvc.noauth.perform(get("/urllogin?user=" + mvc.MAILADDR_TANAKA_TARO)) .andExpect(status().isFound()) .andExpect(redirectedUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)) .andReturn(); HttpSession session = result.getRequest().getSession(); assertThat(session).isNotNull(); // ログイン後の画面にアクセスしてもエラーにならない mvc.noauth.perform(get(WebSecurityConfig.DEFAULT_SUCCESS_URL).session((MockHttpSession) session)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("loginsuccess")) .andExpect(model().hasNoErrors()) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); } } }
SimpleRequestBuilder.java
package ksbysample.common.test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.RequestBuilder; import javax.servlet.ServletContext; public class SimpleRequestBuilder implements RequestBuilder { private final MockHttpServletRequest request; public SimpleRequestBuilder(MockHttpServletRequest request) { this.request = request; } public MockHttpServletRequest buildRequest(ServletContext servletContext) { return request; } }
履歴
2015/08/12
初版発行。