SpringSecurityでカスタムログインページを作ってみる

デフォルトのログインページから独自のログインページを作ってみたいなっと思うことってあるじゃないですか。なので、やってみようかなと思いまして。

公式リファレンスではこちらの箇所にカスタムのログインページの作り方が書いてあります。

https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#servlet-authentication-form

SpringBootで自動構成が有効になっている状態では勝手にログインページが生成されますが、そのログインページをカスタムのログインページにするためには以下の2つのステップが必要なようです。

  1. Configurationでログインページのパスを示すこと。
  2. カスタムのログインページのHTMLを作成すること。

どちらも上記のリファレンスに参考例があるので参考にするといいと思います。

Configurationでログインページのパスを示す

以下のように configure メソッドでフォームログインを有効化すると同時に、ログインページのパスを指定します。SpringBootの自動構成でも /login がログインページのパスだった気がしますが、このようにして明示しないとログインページの自動構成が無効にならないのでしょう。

作ってみたコードはこんな感じになりました。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin(form -> form
            .loginPage("/login").permitAll()
            .defaultSuccessUrl("/home", true)
    );
                                                              
    http.authorizeRequests()
            .mvcMatchers("/home/**").authenticated()
            .mvcMatchers("/manage/**").hasRole("ADMIN")
            .anyRequest().permitAll();
}

あと /login パスに対応するコントローラーも必要になります。自動構成では勝手に生成されたHTMLを返してもらえていましたが、独自に作成したログインページを返すことになりますね。

@Controller
public class LoginController {

    @GetMapping("login")
    public String login() {
        return "login";
    }

}

カスタムのログインページのHTMLを作成する

ログインページを作る上で知っておく必要があるのは以下の内容かなと。

  • ログイン自体は /login にPOSTリクエストを行う
  • CSRFトークンが必要ならリクエストに含めること(Thymeleafを使っている場合自動的に送信してくれます。便利!)
  • ユーザー名は username で送信
  • パスワードは password で送信
  • もしHTTPパラメーターに error があればユーザー名/パスワードが間違っている
  • もしHTTPパラメーターに logout があればログアウトが行われた

これらの構成はデフォルトの話なので、Configurationによって変更することはできます。ですがとりあえずデフォルトのまま使っていても問題ないかなと。

作ってみたらこんな感じになりました。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <div class="row mt-5">
        <div class="col">
          <form class="w-50 mx-auto p-3 bg-light" method="post" th:action="@{/login}">
            <h3 class="mb-4 text-center">ログイン</h3>
            <div class="alert alert-success" role="alert" th:if="${param.logout}">ログアウトしました。</div>
            <div class="alert alert-danger" role="alert" th:if="${param.error}">ユーザー名またはパスワードが違います。</div>
            <div class="form-group">
              <label for="username">ユーザー名</label>
              <input class="form-control" id="username" name="username" type="text"/>
            </div>
            <div class="form-group">
              <label for="password">パスワード</label>
              <input class="form-control" id="password" name="password" type="password"/>
            </div>
            <div class="d-flex flex-row-reverse">
              <button class="btn btn-primary" type="submit">ログイン</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </body>
</html>

Thymeleafで param 変数にHTTPパラメーターが詰められていることを知りました。この仕組を使うことでログアウト時とエラー時の挙動を実装できます。

実際にレンダリングされるHTMLはこんな感じになって、CSRFトークンがhiddenパラメーターになっていることがわかります。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <div class="row mt-5">
        <div class="col">
          <form class="w-50 mx-auto p-3 bg-light" method="post" action="/login"><input type="hidden" name="_csrf" value="7958d02e-069e-4ecc-8202-1fe79733270b"/>
            <h3 class="mb-4 text-center">ログイン</h3>
            
            
            <div class="form-group">
              <label for="username">ユーザー名</label>
              <input class="form-control" id="username" name="username" type="text"/>
            </div>
            <div class="form-group">
              <label for="password">パスワード</label>
              <input class="form-control" id="password" name="password" type="password"/>
            </div>
            <div class="d-flex flex-row-reverse">
              <button class="btn btn-primary" type="submit">ログイン</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </body>
</html>

とまあ、カスタムログインページの話でした。