今日の役に立たない一言 - Today’s Trifle! -

古い記事ではさまざまなテーマを書いていますが、2007年以降はプログラミング関連の話がほとんどです。

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


RakutenKoboライティングライフ

作成したePubファイルをこちらにアップロードしたら、外部サイトへの誘導があるとかなんとかメッセージが表示されて、修正しろって言われた。

考えてみれば、本文の中で書籍をいくつか紹介していて、Amazonへのリンクを含んでいた。

外部サイトへの誘導があるとダメっぽいので、楽天ブックスへの誘導ではじかれてもめんどいので、内部に含まれているURLをすべて削除してからアップロードしなおした。

それでも外部サイトへの誘導があると怒られた。

書籍の最後に、TwitterFacebookGoogle+ のアカウントへのリンクと、自分のサイト 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/

pom.xmljpaとsecurityを追加。

        <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のセキュリティ設定とか調べても該当箇所はない。

もしかしてWordPressを動かしているロリポップ

ロリポップにログインして管理画面を見ていると、海外アタックガードというところがあった。

海外アタックガードについて・設定・解除方法 / セキュリティ / マニュアル - レンタルサーバーならロリポップ!

ここで、ブログを動作させているドメインに対しては初期設定では「ガード有効」になっているので「無効にする」をクリック。

GAEからの投稿を試してみたら、あっさり成功。

でも海外アタックガードは有効にしておきたい。

ロリポップの海外アタックガードのマニュアルを見ると、特定のいくつかのURLがブロックされていて、その中に xmlrpc.php がある。

xmlrpc.php を別の名前のファイルにコピー。

$ cp xmlrpc.php hogehoge.php

で、ロリポップの設定で海外アタックガードを有効化する。

GAEから投稿するときに、投稿先URLでコピー先のファイルを指定すると、エラーなく投稿できた。