【Spring Security はじめました】#5 ユーザーの実装

Web

今回の目標

前回はログインページのカスタマイズについて説明しました。カスタムしたログインページを使用する場合も設定クラスにその内容を記述していきます。

【Spring Security はじめました】#4 ログインページの作成
Spring Securityにはデフォルトのログインページがあるのですが、それでは何かと味気ないので今回はログインページをカスタマイズする方法を説明します。またフォームのセキュリティの話としてCSRFについても少し説明します。

今回は認証に使用するユーザー情報の実装について説明していきます。

デフォルトのユーザー情報

最初に認証の流れについて簡単におさらいをしておきます。

  1. login formから送信されたusernameでユーザー情報(UserDetails)を検索します。ユーザー情報の検索はUserDetailsServiceで実行されます。
  2. 取得したユーザー情報からパスワードの比較による認証処理を行います。
  3. 認証に成功したらユーザー情報をセッションに登録し、対象ページにリダイレクトします。

※ これはあくまで概略なので実際の処理とは少し異なると思います。

以前の記事で、ユーザー情報を検索するためにはUserDetailsServiceを実装すると説明しました。

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

  if (StringUtils.isEmpty(username)) {
    throw new UsernameNotFoundException("username is empty");
  }

  Account account = repository.findByUsername(username);

  if (account == null) {
    throw new UsernameNotFoundException("Not found username : " + username);
  }

  Collection<GrantedAuthority> authorities = new ArrayList<>();
  authorities.add(new SimpleGrantedAuthority(account.getRole()));

  User user = new User(account.getUsername(), account.getPassword(), authorities);
  return user;
}

このメソッドが返すのはUserDetailsですが、UserDetailsはインターフェースなので実際はこれを実装したクラスのインスタンスを返すことになります。この例ではSpring Securityで実装してあるUserクラスを使用しています。

Userクラスのソースを少しだけ見てみましょう。

public class User implements UserDetails, CredentialsContainer {

  private String password;
  private final String username;
  private final Set<GrantedAuthority> authorities;
  private final boolean accountNonExpired;
  private final boolean accountNonLocked;
  private final boolean credentialsNonExpired;
  private final boolean enabled;

  //略
}

実はこれだけのフィールドしか定義されていません。つまり認証後のユーザー情報として保持できるのはこれだけになります。仮にメールアドレスなどの情報を持たせたい場合はこれでは不十分です。

このような場合には自身でUserDetailsを実装したクラスを作成する必要があります。

ユーザー情報のカスタマイズ

事前準備

簡単な例として次のユーザー情報をDB上で管理しているものとします。

idnameemailpasswordroleenabled
userユーザーuser@example.comUSERtrue
admin管理者admin@example.comADMINtrue

このテーブルに対応するEntityとRepositoryを作成しておきます。

@Entity
@Table(name="users")
public User {
  @Id
  private String id;
  private String name;
  private String email;
  private String password;
  private String role;
  private boolean enabled;

  //Setter/Getter
}
@Repository
public interface UserRepository extends JpaRepository<User, String>{
}

認証後のユーザー情報としても、これらの情報は参照したいものとします。

UserDetailsの実装

まずUserDetailsのソースを確認してみましょう。

public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();
  String getPassword();
  String getUsername();
  boolean isAccountNonExpired();
  boolean isAccountNonLocked();
  boolean isCredentialsNonExpired();
  boolean isEnabled();
}

最低限これだけのメソッドを実装しなければいけません。各メソッドの役割は以下になります。

メソッド役割
getAuthorities()権限情報を持つCollectionを返します。
getPassword()パスワードの値を返します。
getUsername()キーの値を返します。
isAccountNonExpired()アカウントが有効期限内であるかを返します。
isAccountNonLocked()アカウントがロックされていないかを返します。
isCredentialsNonExpired()資格情報が有効期限内であるかを返します。
isEnabled()有効なアカウントであるかを返します。

では実際にUserDetailsの実装クラスを作成してみます。

public class UserDetailsImpl implements UserDetails {

  private User user;
  private Collection<GrantedAuthority> authorities;

  public UserDetailsImpl(User user) {
    this.user = user;
    this.authorities = new ArrayList<>();
    this.authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
  }

  public String getId() {
    return user.getId();
  }

  public String getName() {
    return user.getName();
  }

  public String getEmail() {
    return user.getEmail();
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
  }

  @Override
  public String getPassword() {
    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getId();
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return user.isEnabled();
  }
}

いくつかポイントがあるので確認していきましょう。

フィールドの定義

DB上で管理しているユーザー情報をすべて参照したい場合は、Entityそのものをフィールドとして定義するのが楽だと思います。ただし値の参照のためにはGetterを別途実装する必要があります。

権限の設定

権限は複数所持を考慮した設計になっているため、Collectionとして定義されています。また権限の文字列の前には”ROLE_”を付与します。例えば設定クラスでhasRole(“ADMIN”)などと設定している場合、内部的には”ROLE_ADMIN”で権限の判定が行われます。

getUsername()の実装

getUsername()には認証で使用する項目を指定します。今回はidで認証を行う予定のためidを返すように実装します。

ユーザー制限の実装

isAccountNonExpired()のようにユーザーの利用制限に関する項目は、必要がなければtrueを返すようにします。falseの場合は認証エラーとなってしまいます。

UserDetailsServiceの実装

UserDetailsが実装出来たら、あとは検索結果としてそのインスタンスを返すようにUserDetailsServiceを実装します。

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  //…
  return new UserDetailsImpl(user);
}

認証ユーザーの参照

Thmeleafの参照についてのみ記載しますが、Getterを定義しておけばこれまで同様に追加した項目も参照できます。

<p th:text="${#authentication.principal.email}"></p>

認証エラーの実装

上例では使用しませんでしたが、isAccountNonExpired()などによってユーザーの制限をかけたい場合の実装について少し説明しておきます。

仮にisAccountNotExpired()を使用する場合、このメソッドがfalseを返すときに認証エラーとなります。では認証エラーはどのように処理されるのでしょうか?

isAccountNotExpired()に限らず、認証処理でのエラーはすべて該当するExceptionが投げられます。認証エラーとExceptionの関係を以下に記載します。

BadCredentialsExceptionusername, passwordに該当するユーザー存在しない
AccountExpiredExceptionアカウントの有効期限切れ「isAccountNonExpired()」
AccountLockedExceptionアカウントがロック中「isAccountNonLocked()」
CredentialExpiredException資格の有効期限切れ「isCredentialNonExpired()」
DisabledExceptionアカウントが利用不可「isEnabled()」

ここで投げられたExceptionはセッションに「SPRING_SECURITY_LAST_EXCEPTION」として保存されます。

ログインフォームでエラーメッセージを表示する場合は次のようにします。

<p th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></p>

ただし、表示されるのはデフォルトのメッセージなので変更したい場合は少し手を加える必要があります。少し強引な方法ですが、messages.propertiesを使って変更することが可能です。

<p th:if="${param.error}" th:text="#{${session.SPRING_SECURITY_LAST_EXCEPTION.class}}"></p>
class\u0020org.springframework.security.authentication.BadCredentialsException=IDまたはパスワードが違います

あとがき

今回はユーザー情報のカスタマイズについて説明しました。Spring Securityのデフォルトでは、ユーザー情報として足りないことがほとんどだと思います。そのため今回の実装は必要になってくるので是非覚えておきましょう。

 

- Spring Bootのおすすめ書籍はコチラ -

コメント

タイトルとURLをコピーしました