Spring BootでPOSTしたときに403エラーとなる問題の解決方法
Spring Bootでフォームを作成してPOSTすると、403エラーとなる場合がある。
<form method="post" action="/hoge/" th:object="${formModel}"> <label for="title">名前</label>: <input type="text" name="name" th:value="*{name}" /> <br/> <label for="description">説明</label>: <input type="text" name="description" th:text="*{description}"> <br/> <input type="submit" /> </form>
formタグのaction属性を、Thymeleafのタグに変更すると正常にPOSTできるようになる。
<form method="post" th:action="@{/hoge/}" th:object="${formModel}"> <label for="title">名前</label>: <input type="text" name="name" th:value="*{name}" /> <br/> <label for="description">説明</label>: <input type="text" name="description" th:text="*{description}"> <br/> <input type="submit" /> </form>
青空ePub3で作ったepubファイルのエラー修正方法
以前、「こどものトリセツ」という本をKindleで出版した。
これをいまさらながら、楽天Koboにも登録しようと思った。
で、楽天Koboに登録してアップロードしたら、いろいろとエラーが発生した。エラーが発生している箇所のファイル名が表示されてるので、epubファイルの中身はいろんなファイルが圧縮されているらしい。
修正しないと出版できないみたいなので、修正方法を調べてみた。
まず、epubファイルの拡張子をzipに変更する。
すると、epubファイルを普通にzipで解凍できる。
あとは、表示されているエラーの箇所をエディターで修正していく。
エラーの多くは「alt属性は使用できません」みたいなものだったので、xhtmlファイルからalt属性の部分を削除するだけでエラーが解消された。
残ったのが「mimetypeファイルでZIPフォーマットの拡張フィールド属性を使うことは許可されていません」というエラー。
ぐぐってみたら、zip で圧縮する時にXオプションを付ければいいらしい。
圧縮後に拡張子をepubに戻してアップロードしたら、エラー表示が出なくなった。
$ zip -r0X book.zip mimetype META-INF OPS $ mv book.zip book.epub
作成したePubファイルをこちらにアップロードしたら、外部サイトへの誘導があるとかなんとかメッセージが表示されて、修正しろって言われた。
考えてみれば、本文の中で書籍をいくつか紹介していて、Amazonへのリンクを含んでいた。
外部サイトへの誘導があるとダメっぽいので、楽天ブックスへの誘導ではじかれてもめんどいので、内部に含まれているURLをすべて削除してからアップロードしなおした。
それでも外部サイトへの誘導があると怒られた。
書籍の最後に、Twitter・Facebook・Google+ のアカウントへのリンクと、自分のサイト http://www.satoshis.jp/ へのリンクを掲載しているのが気に入らないらしい。
これも削除してアップロードしなおしてみた。
いまのところ審査中。
追記(10/26)
本の中に、引用目的で別の本のページを撮影した写真を貼り付けてあるんだけど、それが原因で審査NGになりました。
KindleはOKなのに。貼り替えるのもめんどいのでKoboの審査を通すのは諦めました。
GAE/Javaでローカルデータストアのデータが消えてしまう問題
過去に作ったGAEアプリを触ってたら appengine-web.xml ファイルで警告が表示されるようになってる。
appengine-web.xml ファイルのほぼ先頭に以下のような記述の部分。
<application>project-name</application>
現在は次の警告が表示される。
Project ID should be specified at deploy time
この行を削除すると警告は消えるんだけど、dev server を再起動すると、ローカルのデータストアの内容が消滅してしまう。
この部分に書かれているプロジェクト名がデータストアの内容と関連付けられているみたいで、この行を復活させて dev server を再起動するとデータが復活してくれた。
過去にもデータストアの内容が消えて困ったことがあったけど、今になって思うと、プロジェクト名を書き換えたことが原因だったかもしれないなーと。
今回は、http://localhost:8080/_ah/admin/ にアクセスした時に「no_app_id Development Console」と表示されてて気付いた。
Spring Boot でJDBCを使ってユーザー登録・ユーザー認証する方法
まずは公式に従う。
≫ https://spring.io/guides/gs/securing-web/
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
DBは、HSQLDBを使用。
appliaction.properties は以下の通り。
spring.datasource.url=jdbc:hsqldb:hsql://localhost/auth spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect spring.jpa.hibernate.ddl-auto=update
トップページを(index.html)を作成。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>トップページ</title> </head> <body> <h1>トップページ</h1> <a href="/mypage">マイページ</a> </body> </html>
認証が必要なページ(mypage.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>login</title> </head> <body> <h1>マイページ</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="ログアウト" /> </form> </body> </html>
ログイン画面(login.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>login</title> </head> <body> <div th:if="${param.error}"> エラー: ユーザ名・パスワードが違います。 </div> <form th:action="@{/login}" method="post"> <div><label>ユーザ名: <input type="text" name="username"/> </label></div> <div><label>パスワード: <input type="password" name="password"/> </label></div> <div><input type="submit" value="ログイン"/></div> </form> <div><a href="/newuser">新規ユーザー登録</a></div> <a href="/">戻る</a> </body> </html>
新規ユーザー登録画面(newuser.html)を作成。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>新規ユーザー登録</title> </head> <body> <h1>新規ユーザー登録</h1> <form th:action="@{/newuser}" method="post"> <div><label>ユーザ名: <input type="text" name="username"/> </label></div> <div><label>パスワード: <input type="password" name="password"/> </label></div> <div><label>パスワード再入力: <input type="password" name="password2"/> </label></div> <div><input type="submit" value="登録"/></div> </form> <a href="/">戻る</a> </body> </html>
コントローラを作成。
package hoge; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping(value = "/") public String index() { return "index"; } @RequestMapping(value = "/login") public String login() { return "login"; } @RequestMapping(value = "/newuser") public String newuser() { return "newuser"; } @RequestMapping(value = "/mypage") public String top() { return "mypage"; } }
この状態でSpring Bootアプリを起動すると、認証の制限がかかってないので、各ページを自由に行き来できる。
/ と /newuser は誰でもアクセス可能。
/mypage は、ログインしないとアクセスできないようにする。
package hoge; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/newuser").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); }
上記のWebSecurityConfig を作ってから再起動すると、/ と /newuser にはアクセス可能。/mypage にアクセスしようとすると、/login に飛ばされる。
JDBCで認証できるようにする。
公式にはちらっとJDBC認証の方法がある。
≫ https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { UserBuilder users = User.withDefaultPasswordEncoder(); auth .jdbcAuthentication() .dataSource(dataSource) .withDefaultSchema() .withUser(users.username("user").password("password").roles("USER")); }
しかし、このコードだと一発目は動作するけど、二回目からは例外が発生してSpring Boot アプリが起動しなくなる。
withDefaultSchema()とwithUser()あたりが例外の原因だった。
以下の1行だけにすれば二回目からも問題なく動作する。
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(dataSource); }
この先の実装方法がわからなくていろいろぐぐってみた。
が、公式のドキュメントが少ないし、ぐぐってみても割と複雑なコードで実装してる人が多くて困った。
なんとなく、もっと簡単なコードで実装できそうな気がすると思ったので適当にコード書いたら動いちゃった。
新規ユーザー登録できるようにする。
とりあえずJdbcUserDetailsManager をコントローラにDIしてみる。
@Autowired private JdbcUserDetailsManager userManager;
/newuser の POST でユーザー登録する処理を書く。
JdbcUserDetailsManagerにcreateUser()というメソッドがあったので、これ使えばできるんじゃないかなと。
上記の公式の説明でInMemoryUserDetailsManagerでもそうしてたし。
スーパークラスが同じだからたぶんいけるはず!
@RequestMapping(value = "/newuser", method = RequestMethod.GET) public String newuser() { return "newuser"; } @RequestMapping(value = "/newuser", method = RequestMethod.POST) public String register(@RequestParam("username") String username, @RequestParam("password") String password) { UserBuilder users = User.withDefaultPasswordEncoder(); userManager.createUser(users.username(username).password(password).roles("USER").build()); return "login"; }
User.withDefaultPasswordEncoder()でdeprecatedの警告が出るので、気になる人は適当なPasswordEncoderを設定してね。
これで起動しようとしたら、JdbcUserDetailsManagerをDIしたいけど、定義がないからできないよって怒られる。
JdbcUserDetailsManager と DataSource をWebSecurityConfigに用意する。
よくわからんけど、インスタンス生成してDataSourceだけ設定しとけば動くんじゃね?的な。
@Autowired private DataSource dataSource; @Bean public JdbcUserDetailsManager jdbcUserDetailsManager() throws Exception { JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(); jdbcUserDetailsManager.setDataSource(dataSource); return jdbcUserDetailsManager; }
問題なく起動できた。
でも、DBにユーザーが存在しないのでログインできない。
/newuser から新規ユーザー登録する。
新規ユーザー登録後は、USERSテーブルに登録されているのが確認できる。パスワードも暗号化されてる。
というわけで、ユーザー登録は成功。
登録したユーザーでログインすると、/mypage に行けるようになった。
ユーザー認証も成功。
登録済みのユーザー名で再び登録しようとすると例外が発生する。
そのエラーハンドリングと、パスワード再入力のチェックを追加する。
@RequestMapping(value = "/newuser", method = RequestMethod.POST) public ModelAndView register( ModelAndView mav, @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("password2") String password2) { if (!password.equals(password2)) { mav.setViewName("newuser"); mav.addObject("error", "パスワードが一致していません。"); return mav; } UserBuilder users = User.withDefaultPasswordEncoder(); try { userManager.createUser(users.username(username).password(password).roles("USER").build()); mav.setViewName("login"); } catch (Exception e) { mav.setViewName("newuser"); mav.addObject("error", "ユーザー名は使用できません。:" + username); } return mav; }
これでJDBCを使用したユーザー登録とユーザー認証の動作ができるようになった。
WebSecurityConfig の全体。
package hoge; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.provisioning.JdbcUserDetailsManager; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private DataSource dataSource; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/newuser").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource); } @Bean public JdbcUserDetailsManager jdbcUserDetailsManager() throws Exception { JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(); jdbcUserDetailsManager.setDataSource(dataSource); return jdbcUserDetailsManager; } }
コントローラの全体。
package hoge; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User.UserBuilder; import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @Controller public class MyController { @Autowired private JdbcUserDetailsManager userManager; @RequestMapping(value = "/") public String index() { return "index"; } @RequestMapping(value = "/login") public String login() { return "login"; } @RequestMapping(value = "/newuser", method = RequestMethod.GET) public String newuser() { return "newuser"; } @RequestMapping(value = "/newuser", method = RequestMethod.POST) public ModelAndView register( ModelAndView mav, @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("password2") String password2) { if (!password.equals(password2)) { mav.setViewName("newuser"); mav.addObject("error", "パスワードが一致していません。"); return mav; } UserBuilder users = User.withDefaultPasswordEncoder(); try { userManager.createUser(users.username(username).password(password).roles("USER").build()); mav.setViewName("login"); } catch (Exception e) { mav.setViewName("newuser"); mav.addObject("error", "ユーザー名は使用できません。:" + username); } return mav; } @RequestMapping(value = "/mypage") public String mypage() { return "mypage"; } }
WordPressにXMLRPCで投稿しようとしたら「405 METHOD NOT ALLOWED」エラーになる
XMRPCでWordPressに投稿するプログラム。
ローカルで動かしているGAEの開発サーバーからだと正常に投稿できるのに、GAEにデプロイしたシステムから投稿しようとすると、「405 METHOD NOT ALLOWED」エラーになる。
どこでエラーになっているのかと、WordPressのソースで405を出しているところを全部チェックしたけど、どこも該当しない。
もしかして投稿元のIPアドレス?
WordPressのセキュリティ設定とか調べても該当箇所はない。
ロリポップにログインして管理画面を見ていると、海外アタックガードというところがあった。
≫ 海外アタックガードについて・設定・解除方法 / セキュリティ / マニュアル - レンタルサーバーならロリポップ!
ここで、ブログを動作させているドメインに対しては初期設定では「ガード有効」になっているので「無効にする」をクリック。
GAEからの投稿を試してみたら、あっさり成功。
でも海外アタックガードは有効にしておきたい。
ロリポップの海外アタックガードのマニュアルを見ると、特定のいくつかのURLがブロックされていて、その中に xmlrpc.php がある。
xmlrpc.php を別の名前のファイルにコピー。
$ cp xmlrpc.php hogehoge.php
で、ロリポップの設定で海外アタックガードを有効化する。
GAEから投稿するときに、投稿先URLでコピー先のファイルを指定すると、エラーなく投稿できた。