Springの学習にCRUD機能を作りました。機能自体はうまくできたのですが、そこにSpringSecurityのデフォルトのログイン認証機能を取り付けたところ、@PutMapping
と@DeleteMapping
が急に反応しなくなりました。ひとまず即席の対処法を考えたので残しておきます。また、他にもっと良い方法があればぜひともご意見いただけるとありがたいです。
user名とパスワードはひとまず設定ファイルのほうに用意しています。
@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("john").password("{noop}test123").roles("EMPLOYEE");
auth.inMemoryAuthentication().withUser("mary").password("{noop}test123").roles("MANAGER");
auth.inMemoryAuthentication().withUser("susan").password("{noop}test123").roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
.and()
.logout().permitAll();
}
}
これでログインせずにアクセスするとshowMyLoginPageに遷移するようになります。ログインの機能もきちんと動きました。
コントローラは以下のようになっています。
public class DBtestController {
@Autowired
InstructorService service;
//INDEX
@GetMapping
public ModelAndView test(
//@ModelAttributeでInstructorFormは自動的にインスタンス化される
//htmlに反映させるにはmav.addObjectが必須
@ModelAttribute("InstructorForm") InstructorForm instructorForm,
ModelAndView mav) {
instructorForm.setNewInstructor(true);
mav.addObject("instructorForm", instructorForm);
List<Instructor> list = service.findAll();
mav.addObject("list", list);
mav.addObject("title", "メンバー一覧");
mav.setViewName("test");
return mav;
}
//Before UPDATE
@GetMapping(value = "/{id}")//編集ページ
public ModelAndView showUpdate(
@ModelAttribute("InstructorForm") InstructorForm instructorForm,
@PathVariable Integer id,
ModelAndView mav) {
Optional<InstructorForm> form = service.getInstructorForm(id);
if(!form.isPresent()) {
mav.setViewName("redirect:/test");
return mav;
//return new ModelAndView("redirect:/test")
}
mav.addObject("instructorId", id);
mav.addObject("instructorForm", form.get());
List<Instructor> list = service.findAll();
mav.addObject("list", list);
mav.addObject("title", "更新フォーム");
mav.setViewName("test");
return mav;
}
//INSERT
@PostMapping
@Transactional(readOnly = false)
public ModelAndView insert2(
@Validated InstructorForm instructorForm,//ヴァリデーションはフォームクラスに対して行う
BindingResult result,
ModelAndView mav) {
Instructor instructor = makeInstructor(instructorForm);
//redirect、失敗したらそのままHTML表示
if(!result.hasErrors()) {
service.save(instructor);
mav.setViewName("redirect:/test");
}else {
instructorForm.setNewInstructor(true);
mav.addObject("instructorForm", instructorForm);
List<Instructor> list = service.findAll();
mav.addObject("list", list);
mav.addObject("title", "メンバー一覧2");
mav.setViewName("test");
}
return mav;
}
//UPDATE
@PutMapping("/{id}")
@Transactional(readOnly = false)
public ModelAndView update(
@PathVariable Integer id,
@ModelAttribute("InstructorForm") InstructorForm instructorForm,
ModelAndView mav) {
Instructor instructor = makeInstructor(id, instructorForm);
System.out.println(instructor);
service.save(instructor);
mav.setViewName("redirect:/test/" + id);
return mav;
}
//DELETE
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional(readOnly = false)//削除の場合必須
public ModelAndView delete(
@PathVariable Integer id,
ModelAndView mav) {
service.deleteById(id);
mav.setViewName("redirect:/test");
return mav;
}
private Instructor makeInstructor(InstructorForm iForm) {
InstructorDetail iD = new InstructorDetail(iForm.getYoutubeChannel(), iForm.getHobby());
Instructor i = new Instructor(iForm.getFirstName(), iForm.getLastName(), iForm.getEmail());
i.setInstructorDetail(iD);
return i;
}
private Instructor makeInstructor(int id,InstructorForm iForm) {
InstructorDetail iD = new InstructorDetail(iForm.getYoutubeChannel(), iForm.getHobby());
Instructor i = new Instructor(id, iForm.getFirstName(), iForm.getLastName(), iForm.getEmail());
i.setInstructorDetail(iD);
return i;
}
}
HTMLは以下のようになります。新規登録用と更新用のフォームがどちらも用意されています。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1 th:text="${title}">ようこそ</h1>
<ul>
<li th:each="error : ${#fields.detailedErrors()}"
th:text="${error.message}" />
</ul>
<form method="post" action="#" th:action="@{/test}" th:if="${instructorForm.isNewInstructor}" th:object="${instructorForm}">
<label>FirstName</label>
<input type="text" name="firstName" th:value="*{firstName}" />
<label>LastName</label>
<input type="text" name="lastName" th:value="*{lastName}" />
<label>Email</label>
<div th:if="${#fields.hasErrors('email')}"
th:errors="*{email}"></div>
<div th:if="${#fields.hasErrors('validEmail')}"
th:errors="*{validEmail}"></div>
<input type="text" name="email" th:value="*{email}" />
<div th:if="${#fields.hasErrors('youtubeChannel')}"
th:errors="*{youtubeChannel}"></div>
<label>YoutubeChannel</label>
<input type="text" name="youtubeChannel" th:value="*{youtubeChannel}" />
<label>Hobby</label>
<input type="text" name="hobby" th:value="*{hobby}" />
<input type="submit" value="送信">
</form>
<form method="POST" th:action="@{/test/{id}(id=${instructorId})}" th:unless="${instructorForm.isNewInstructor}" th:object="${instructorForm}">
<label>FirstName</label>
<input type="text" name="firstName" th:value="*{firstName}" />
<label>LastName</label>
<input type="text" name="lastName" th:value="*{lastName}" />
<label>Email</label>
<input type="text" name="email" th:value="*{email}" />
<label>YoutubeChannel</label>
<input type="text" name="youtubeChannel" th:value="*{youtubeChannel}" />
<label>Hobby</label>
<input type="text" name="hobby" th:value="*{hobby}" />
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="変更">
</form>
<table>
<tr th:each="obj : ${list}">
<td th:text=${obj.id}></td>
<td th:text=${obj.firstName}></td>
<td th:text=${obj.email}></td>
<td th:text=${obj.instructorDetail.hobby}></td>
<td><a type="button" th:href="@{/test/{id}(id=${obj.id})}">編集</a></td>
<td><form method="POST" th:action="@{/test/{id}(id=${obj.id})}">
<input type="hidden" name="_method" value="DELETE">
<input type="submit" value="削除">
</form>
</tr>
</table>
<form action="#" th:action="@{/logout}" method="POST">
<input type="submit" value="Logout" />
</form>
</body>
</html>
本来<input type="hidden" name="_method" value="PUT">
のhiddenタグを用意しておけばPOSTメソッドで送信して@PutMapping
のアノテーションがついたメソッドに処理が飛ぶはずです。
しかし、Sprigngecurity取り付け以降は更新処理をしようとすると以下のようなエラーをはくようになりました。
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Sep 26 09:18:55 JST 2018
There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'POST' not supported
確認してみたところ処理自体@PutMapping
のついたメソッドに渡されておらずマッピングができておりません。
いろいろ調べたところによると「CORS」が関係しているようですが根本的な解決策はみつかりませんでした。
ひとまず即席でやった対処法を記しておきます。正しい方法が見つかり次第更新したいと思います。
アノテーションを以下のように変更します。
//UPDATE
@PostMapping("/update/{id}")
@Transactional(readOnly = false)
public ModelAndView update(
@PathVariable Integer id,
@ModelAttribute("InstructorForm") InstructorForm instructorForm,
ModelAndView mav) {
Instructor instructor = makeInstructor(id, instructorForm);
System.out.println(instructor);
service.save(instructor);
mav.setViewName("redirect:/test/" + id);
return mav;
}
//DELETE
@PostMapping("/delete/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional(readOnly = false)//削除の場合必須
public ModelAndView delete(
@PathVariable Integer id,
ModelAndView mav) {
service.deleteById(id);
mav.setViewName("redirect:/test");
return mav;
}
単純にすべて@PostMapping
に変更し、URLにupdateとdeleteをつけ足しました。当然、HTMLのアクセス先も変更しておきます。これで正常に動作するようになりました。用意してあるhiddenタグは使用されていないことになります。
解決法としてなんだか納得がいかないのですが、そもそもSpringSecurityでログイン認証を取り付けたことによってPutMappingが使えなくなったのは何故なのでしょうか。ドキュメントを確認すると、
@PutMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod.PUT).
とありますが、hiddenタグによって疑似的にPUTにする方法は望ましくないのでしょうか。どなたかわかる方コメントいただけると大変助かります。