きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

ログイン機能を追加する(Spring-boot-starter-security)

きり丸アドベントカレンダー2020の10記事目です。

自分の勉強も兼ねてログイン機能を実装してみましたが、ベストプラクティスは分かりませんね…。難しい。


今回の記事でログイン機能を実装します。アプリケーションらしくなりますが、セキュリティをブラックボックス化しているので理解は難しいです。下手に導入すると、動かなくなるかもしれません…。

いろいろ検証して最低限導入できるようにしましたが、本格的に使用するには他の方の記事で学んだほうがいいでしょう。

ゴール

  • ログインできるようにする
  • DBのユーザーで認証する
  • 既存の機能を壊さない
  • ログインしたユーザーでデータを取得する

ゴールしてもできないこと

  • ユーザーの追加

環境

  • Java
    • 15
  • org.springframework.boot:spring-boot-starter-security
    • 2.4.0
  • org.thymeleaf.extras:thymeleaf-extras-springsecurity5
    • 2.4.0

手順

依存関係を追加する


セキュリティとThymeleafを連携するためのライブラリを導入します。

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
}

ちなみに、依存関係を入れるだけで自動でログイン画面が作られます。当然、中身を作りこんでないので、ログインはできません。

f:id:nainaistar:20201128164027p:plain
f:id:nainaistar:20201128164037p:plain

拡張も難しいので、ここからユーザー登録ページに飛ばしたい、等々があれば自分で作りこんでいく必要があります。デフォルトページの生成ロジックはこちらですので、興味があれば確認してください。

https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java

SpringのUserクラスを継承したクラスを作成する

Spring SecurityがUserクラスというのを定義しています。そのままUserクラスを使用してもいいですが、ライブラリの型変更に弱くなってしまうので、継承したUserクラスを使用したほうがよいでしょう。

ファイル名:AuthTargetUser.java

public class AuthTargetUser extends org.springframework.security.core.userdetails.User{
  private User user;

  public AuthTargetUser(User user) {
    super(user.getUsername(), user.getPassword(), user.getAuthorities());
    this.user = user;
  }
}

SpringのUserDetailsServiceクラスを継承したクラスで独自認証を行う


org.springframework.security.core.userdetails.UserDetailsServiceというものが用意されており、こちらを継承すると独自認証を行うことができます。また、アノテーション@Configurationも付与する必要があります。

Userクラスのパスワードの頭に「{noop}」を付与することを忘れないでください。この括弧の中身を読み取って、どんな暗号化で保存されていたのかを判断します。「{noop}」は暗号化していないことを示しています。

リポジトリに関してはアドベントカレンダーの中で紹介しているので、特に紹介しません。

ファイル名:UserDetailsServiceImpl.java

@Configuration
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
  private final UserRepository userRepository;
  @Override
  public UserDetails loadUserByUsername(String username) {
    final UserDto userDto = userRepository.findByUserName(username);
    if (Objects.isNull(userDto)){
      throw new RuntimeException("ユーザ名かパスワードが正しくありません");
    }
    return new AuthTargetUser(new User(username, "{noop}" + userDto.getPassword(), Collections.emptyList()));
  }
}

リポジトリとDBの設定


DBにデータがないと登録できないので、テーブルとリポジトリとデータを登録します。

ファイル名:R__test.sql

DROP TABLE IF EXISTS LOGIN_USER;
CREATE TABLE LOGIN_USER (
  user_id      VARCHAR(120),
  password      VARCHAR(120)
);

-- ログイン用ユーザとパスワード
INSERT INTO LOGIN_USER VALUES('kirimaru', '123456');
INSERT INTO LOGIN_USER VALUES('admin', 'pass');

ファイル名:UserRepository.java

@Mapper
public interface UserRepository {
  @Select("SELECT * FROM LOGIN_USER WHERE user_id=#{username}")
  UserDto findByUserName(String username);
}

いったん、ここまで設定すれば、ログイン機能実装できます。INSERTしているデータとおり、下記を入力すればログインできます。

なお、ログインができるようになったものの、ToDoの追加や削除は壊れてます。

ID: kirimaru
PW: 123456
ID: admin
PW: pass

Thymeleafにログインユーザーを追加する


ログインしているデータは#authentication.principalに登録されています。データ形式としては、上記のUserクラスを拡張したAuthTargetUserクラスが登録されています。UserクラスがgetUsername()を実装しているので、#authentication.principal.usernameで取得できます。

「{ログインユーザー}さんこんにちは」という文言も付けたいので、Thymeleafでは「||」でくくってあげるとデータが取得できます。

   <h1 th:text="|${#authentication.principal.username}さんこんにちは|"></h1>

ThymeleafでCSRFを登録する


デフォルトだとCSRF認証が有効なため、submitするフォームに含めないとエラーとなってしまいます。

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

CSRFとはクロスサイトリクエストフォージェリと言います。CSRFの詳しいことは専門家のページで調べてください。

対策としては、ランダムな値をWebページに埋め込んで、サーバに送付時に埋め込んだ値を検証します。一致していれば正常、一致していない場合はエラーとします。

なお、CSRF対策のトークンの有効期限は設定にもよりますが30分で切れます。基本的にはセッション切れという形で処理するので、再ログインを促しますが、ログインページ自体のCSRFが切れてしまうこともよくあります。

ですので、ログインページに関してはサーバに送信する直前にCSRFトークンを再取得して、再送信するという実装をしているのが一般的ではないでしょうか。

Controllerでログインユーザーを取得する


ログインユーザーをパラメータに設定してもよいですが、せっかくSpring Securityがセッションに登録しているので、それを有効活用しましょう。画面からのパラメータは基本的に信用してはいけません。

HttpSessionクラスをDIさせて、そこからSecurityContextAuthenticationを経由してUserクラスを継承したAuthTargetUserを取得できます。基本的にこれを各メソッドで実装するのは面倒ですので、superクラスを作って、Controllerクラスであればどこでも取得できるようにすると便利です。

private final HttpSession session;

@GetMapping("/")
public String index(Model model) {
    SecurityContext securityContext = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");
    Authentication authentication = securityContext.getAuthentication();
    AuthTargetUser principal = (AuthTargetUser) authentication.getPrincipal();

    List<TodoDto> todos = todoRepository.findList(principal.getUsername());

    model.addAttribute("todos", todos);
    return "index";
};

ソースコード

アドベントカレンダー10日目。
github.com

終わりに

私の理解がかなり怪しい記事です。一度実装した記憶があったとはいえ、検証に6時間かかるとは…。

また、10日目ということで、少しずつ機能を拡張してきている結果、意図していない箇所で機能が壊れていたりします。テストを後回しにしているツケが来てますね。

勉強していきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

敬称略。

めもらば:Spring BootでHTTPセッションをあ使う3つのパターン www.memory-lovers.blog

baeldung:The Registration Process With Spring Security www.baeldung.com

Spring Security と Spring Bootで最小機能のデモアプリケーションを作成する qiita.com

11.4. Spring Securityチュートリアル macchinetta.github.io

類似記事

きり丸アドベントカレンダー2020 adventar.org

きり丸のHerokuページ
https://kirimaru-todoapp.herokuapp.com/

11日目のアドベントカレンダーの記事:ToDo

]きり丸アドベントカレンダー2020の10記事目です。

自分の勉強も兼ねてログイン機能を実装してみましたが、ベストプラクティスは分かりませんね…。難しい。


今回の記事でログイン機能を実装します。アプリケーションらしくなりますが、セキュリティをブラックボックス化しているので理解は難しいです。下手に導入すると、動かなくなるかもしれません…。

いろいろ検証して最低限導入できるようにしましたが、本格的に使用するには他の方の記事で学んだほうがいいでしょう。

ゴール

  • ログインできるようにする
  • DBのユーザーで認証する
  • 既存の機能を壊さない
  • ログインしたユーザーでデータを取得する

ゴールしてもできないこと

  • ユーザーの追加

環境

  • Java
    • 15
  • org.springframework.boot:spring-boot-starter-security
    • 2.4.0
  • org.thymeleaf.extras:thymeleaf-extras-springsecurity5
    • 2.4.0

手順

依存関係を追加する


セキュリティとThymeleafを連携するためのライブラリを導入します。

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
}

ちなみに、依存関係を入れるだけで自動でログイン画面が作られます。当然、中身を作りこんでないので、ログインはできません。

f:id:nainaistar:20201128164027p:plain
f:id:nainaistar:20201128164037p:plain

拡張も難しいので、ここからユーザー登録ページに飛ばしたい、等々があれば自分で作りこんでいく必要があります。デフォルトページの生成ロジックはこちらですので、興味があれば確認してください。

https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java

SpringのUserクラスを継承したクラスを作成する

Spring SecurityがUserクラスというのを定義しています。そのままUserクラスを使用してもいいですが、ライブラリの型変更に弱くなってしまうので、継承したUserクラスを使用したほうがよいでしょう。

ファイル名:AuthTargetUser.java

public class AuthTargetUser extends org.springframework.security.core.userdetails.User{
  private User user;

  public AuthTargetUser(User user) {
    super(user.getUsername(), user.getPassword(), user.getAuthorities());
    this.user = user;
  }
}

SpringのUserDetailsServiceクラスを継承したクラスで独自認証を行う


org.springframework.security.core.userdetails.UserDetailsServiceというものが用意されており、こちらを継承すると独自認証を行うことができます。また、アノテーション@Configurationも付与する必要があります。

Userクラスのパスワードの頭に「{noop}」を付与することを忘れないでください。この括弧中身を読み取って、どんな暗号化で保存されていたのかを判断します。「{noop}」は暗号化していないことを示しています。

リポジトリに関してはアドベントカレンダーの中で紹介しているので、特に紹介しません。

ファイル名:UserDetailsServiceImpl.java

@Configuration
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
  private final UserRepository userRepository;
  @Override
  public UserDetails loadUserByUsername(String username) {
    final UserDto userDto = userRepository.findByUserName(username);
    if (Objects.isNull(userDto)){
      throw new RuntimeException("ユーザ名かパスワードが正しくありません");
    }
    return new AuthTargetUser(new User(username, "{noop}" + userDto.getPassword(), Collections.emptyList()));
  }
}

リポジトリとDBの設定


DBにデータがないと登録できないので、テーブルとリポジトリとデータを登録します。

ファイル名:R__test.sql

DROP TABLE IF EXISTS LOGIN_USER;
CREATE TABLE LOGIN_USER (
  user_id      VARCHAR(120),
  password      VARCHAR(120)
);

-- ログイン用ユーザとパスワード
INSERT INTO LOGIN_USER VALUES('kirimaru', '123456');
INSERT INTO LOGIN_USER VALUES('admin', 'pass');

ファイル名:UserRepository.java

@Mapper
public interface UserRepository {
  @Select("SELECT * FROM LOGIN_USER WHERE user_id=#{username}")
  UserDto findByUserName(String username);
}

いったん、ここまで設定すれば、ログイン機能実装できます。INSERTしているデータとおり、下記を入力すればログインできます。

なお、ログインができるようになったものの、ToDoの追加や削除は壊れてます。

ID: kirimaru
PW: 123456
ID: admin
PW: pass

Thymeleafにログインユーザーを追加する


ログインしているデータは#authentication.principalに登録されています。データ形式としては、上記のUserクラスを拡張したAuthTargetUserクラスが登録されています。UserクラスがgetUsername()を実装しているので、#authentication.principal.usernameで取得できます。

「{ログインユーザー}さんこんにちは」という文言も付けたいので、Thymeleafでは「||」でくくってあげるとデータが取得できます。

   <h1 th:text="|${#authentication.principal.username}さんこんにちは|"></h1>

ThymeleafでCSRFを登録する


デフォルトだとCSRF認証が有効なため、submitするフォームに含めないとエラーとなってしまいます。

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

CSRFとはクロスサイトリクエストフォージェリと言います。CSRFの詳しいことは専門家のページで調べてください。

対策としては、ランダムな値をWebページに埋め込んで、サーバに送付時に埋め込んだ値を検証します。一致していれば正常、一致していない場合はエラーとします。

なお、CSRF対策のトークンの有効期限は設定にもよりますが30分で切れます。基本的にはセッション切れという形で処理するので、再ログインを促しますが、ログインページ自体のCSRFが切れてしまうこともよくあります。

ですので、ログインページに関してはサーバに送信する直前にCSRFトークンを再取得して、再送信するという実装をしているのが一般的ではないでしょうか。

Controllerでログインユーザーを取得する


ログインユーザーをパラメータに設定してもよいですが、せっかくSpring Securityがセッションに登録しているので、それを有効活用しましょう。画面からのパラメータは基本的に信用してはいけません。

HttpSessionクラスをDIさせて、そこからSecurityContextAuthenticationを経由してUserクラスを継承したAuthTargetUserを取得できます。基本的にこれを各メソッドで実装するのは面倒ですので、superクラスを作って、Controllerクラスであればどこでも取得できるようにすると便利です。

private final HttpSession session;

@GetMapping("/")
public String index(Model model) {
    SecurityContext securityContext = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");
    Authentication authentication = securityContext.getAuthentication();
    AuthTargetUser principal = (AuthTargetUser) authentication.getPrincipal();

    List<TodoDto> todos = todoRepository.findList(principal.getUsername());

    model.addAttribute("todos", todos);
    return "index";
};

ソースコード

アドベントカレンダー10日目。
github.com

終わりに

私の理解がかなり怪しい記事です。一度実装した記憶があったとはいえ、検証に6時間かかるとは…。

また、10日目ということで、少しずつ機能を拡張してきている結果、意図していない箇所で機能が壊れていたりします。テストを後回しにしているツケが来てますね。

勉強していきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

敬称略。

めもらば:Spring BootでHTTPセッションをあ使う3つのパターン www.memory-lovers.blog

baeldung:The Registration Process With Spring Security www.baeldung.com

Spring Security と Spring Bootで最小機能のデモアプリケーションを作成する qiita.com

11.4. Spring Securityチュートリアル macchinetta.github.io

類似記事

きり丸アドベントカレンダー2020 adventar.org

きり丸のHerokuページ
https://kirimaru-todoapp.herokuapp.com/

11日目のアドベントカレンダーの記事 https://nainaistar.hatenablog.com/entry/2020/12/11/083000nainaistar.hatenablog.com