読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その10 )( ログイン画面の作成4 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その9 )( ログイン画面の作成3 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • ID の有効期限を過ぎている場合にはログインできないようにする機能の作成
    • パスワードの有効期限を過ぎている場合にはログインできないようにする機能の作成

参照したサイト・書籍

  1. PostgreSQL 9.4.0文書 - 第 9章関数と演算子 - 9.9. 日付/時刻関数と演算子
    https://www.postgresql.jp/document/9.4/html/functions-datetime.html

  2. Java 最強リファレンス

    Java 最強リファレンス

    Java 最強リファレンス

    • 「12-07 2つの日時を比較する」を参照しました。
  3. 倖せの迷う森 - Spring Framework Advent Calendar 2011 part.7 - Spring Security で認証成功時に条件によって遷移先を変える
    http://d.hatena.ne.jp/ocs/20111207/1323269801

    • 今回実装していませんが、ログイン時にパスワードの有効期限を過ぎていたらパスワード変更画面にリダイレクトさせる記事を見つけました。今後実装する時のために備忘録としてメモしておきます。

目次

  1. はじめに
  2. 1.0.x-expired-idpass ブランチの作成
  3. user_info テーブルの変更
  4. UserInfoUserDetails クラスの変更
  5. UserInfoUserDetailsService クラスの変更
  6. 動作確認
  7. commit、Push、Pull Request、マージ

手順

はじめに

ID とパスワードに有効期限を設けて、有効期限を過ぎている場合にはエラーメッセージを表示してログインできないようにします。

以下の内容で実装を進めます。

  • user_info テーブルに ID、パスワードそれぞれの有効期限を設定するためのカラムを追加します。
  • UserInfoUserDetails クラスで現在日時と有効期限を比較して、現在日時を超えている場合には accountNonExpired, credentialsNonExpired を適宜 false に設定します。

1.0.x-expired-idpass ブランチの作成

  1. IntelliJ IDEA で 1.0.x-expired-idpass ブランチを作成します。

user_info テーブルの変更

/sql の下の create_table.sqlリンク先の内容 に変更します。

コマンドプロンプトから以下のコマンドを実行して user_info テーブルに expired_account, expired_password カラムを追加します。

C:\project-springboot\ksbysample-webapp-lending>psql -U ksbylending_user ksbylending
ユーザ ksbylending_user のパスワード:
psql (9.4.1)
"help" でヘルプを表示します.

ksbylending=> alter table user_info add column expired_account timestamp not null default now() + interval '90 day';
ALTER TABLE
ksbylending=> alter table user_info add column expired_password timestamp not null default now() + interval '30 day';
ALTER TABLE
ksbylending=> \d user_info
                                          テーブル "public.user_info"
         列         |             型              |                           修
飾語
--------------------+-----------------------------+-------------------------------------------------------------
 user_id            | bigint                      | not null default nextval('user_info_user_id_seq'::regclass)
 username           | character varying(32)       | not null
 password           | character varying(256)      | not null
 mail_address       | character varying(256)      | not null
 enabled            | smallint                    | not null default 1
 cnt_badcredentials | smallint                    | not null default 0
 expired_account    | timestamp without time zone | not null default (now() + '90 days'::interval)
 expired_password   | timestamp without time zone | not null default (now() + '30 days'::interval)
インデックス:
    "user_info_pkey" PRIMARY KEY, btree (user_id)
    "user_info_idx_01" btree (mail_address)
参照元:
    TABLE "user_role" CONSTRAINT "user_role_user_id_fkey" FOREIGN KEY (user_id) REFERENCES user_info(user_id) ON DELETE CASCADE


ksbylending=> \q

C:\project-springboot\ksbysample-webapp-lending>

IntelliJ IDEA の Database Tools の「Shynchronize」ボタンをクリックして変更内容を反映します。

f:id:ksby:20150719124854p:plain

登録済の username = 'tanaka taro' のデータを見ると expired_account, expired_password のカラムにはカラムを追加した日時の 90日後、30日後の日時がセットされていました。

f:id:ksby:20150719125048p:plain

Gradle projects View から gen タスクを実行して user_info テーブルの Entity クラスを作成し直します。

UserInfoUserDetails クラスの変更

  1. src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetails.javaリンク先の内容 に変更します。

UserInfoUserDetailsService クラスの変更

  1. src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetailsService.javaリンク先の内容 に変更します。

動作確認

  1. Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  2. まずは正常にログインできることを確認します。ブラウザを起動して http://localhost:8080/ にアクセスし、ログイン画面を表示します。

  3. ID に "tanaka.taro@sample.com"、Password に "taro" を入力後「ログイン」ボタンをクリックして、「ログイン成功!」の画面が表示されることを確認します。

  4. 次に ID の有効期限を過ぎている場合にログインできないことを確認します。user_info テーブルの username = 'tanaka taro' のデータの expired_account の値を昨日の日付に変更します。

    f:id:ksby:20150719135120p:plain

  5. ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。

    ログインはできず「入力された ID の有効期限が切れています」のメッセージが表示されました。

    f:id:ksby:20150719134704p:plain

  6. user_info テーブルの username = 'tanaka taro' のデータの expired_account の値を元に戻します。

  7. 最後にパスワードの有効期限を過ぎている場合にログインできないことを確認します。user_info テーブルの username = 'tanaka taro' のデータの expired_password の値を昨日の日付に変更します。

    f:id:ksby:20150719135437p:plain

  8. ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。

    ログインはできず「入力された ID のパスワードの有効期限が切れています」のメッセージが表示されました。

    f:id:ksby:20150719135645p:plain

  9. user_info テーブルの username = 'tanaka taro' のデータの expired_password の値を元に戻します。

  10. Ctrl+F2 を押して Tomcat を停止します。

commit、Push、Pull Request、マージ

  1. commit します。

  2. GitHub へ Push、1.0.x-expired-idpass -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-expired-idpass ブランチを削除、をします。

ソースコード

create_table.sql

create table user_info (
    user_id                 bigserial primary key
    , username              varchar(32) not null
    , password              varchar(256) not null
    , mail_address          varchar(256) not null
    , enabled               smallint not null default 1
    , cnt_badcredentials    smallint not null default 0
    , expired_account       timestamp not null default now() + interval '90 day'
    , expired_password      timestamp not null default now() + interval '30 day'
);
create index user_info_idx_01 on user_info(mail_address);
  • expired_account, expired_password の2つのカラムを追加します。
  • expired_account には登録日時から 90日後、expired_password には 30日後の日時がデフォルトで設定されるようにします。

UserInfoUserDetails.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.entity.UserInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Set;

public class UserInfoUserDetails implements UserDetails {

    private UserInfo userInfo;
    private final Set<? extends GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;

    public UserInfoUserDetails(UserInfo userInfo
            , Set<? extends GrantedAuthority> authorities) {
        LocalDateTime now = LocalDateTime.now(); 
        this.userInfo = userInfo;
        this.authorities = authorities;
        this.accountNonExpired = !userInfo.getExpiredAccount().isBefore(now);
        this.accountNonLocked = (userInfo.getCntBadcredentials() < 5);
        this.credentialsNonExpired = !userInfo.getExpiredPassword().isBefore(now);
        this.enabled = (userInfo.getEnabled() == 1);
    }

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

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

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

    public String getMailAddress() {
        return userInfo.getMailAddress();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
  • コンストラクタの以下の点を変更します。
    • 引数から boolean accountNonExpired、boolean credentialsNonExpired を削除します。
    • this.accountNonExpired にセットする値を !userInfo.getExpiredAccount().isBefore(now) へ変更します。
    • this.credentialsNonExpired にセットする値を !userInfo.getExpiredPassword().isBefore(now) に変更します。

UserInfoUserDetailsService.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.dao.UserInfoDao;
import ksbysample.webapp.lending.dao.UserRoleDao;
import ksbysample.webapp.lending.entity.UserInfo;
import ksbysample.webapp.lending.entity.UserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Component
public class UserInfoUserDetailsService implements UserDetailsService {

    @Autowired
    private UserInfoDao userInfoDao;

    @Autowired
    private UserRoleDao userRoleDao;

    @Autowired
    private MessageSource messageSource;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = userInfoDao.selectByMailAddress(username);
        if (userInfo == null) {
            throw new UsernameNotFoundException(
                    messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound"
                            , new Object[]{}, LocaleContextHolder.getLocale()));
        }

        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId());
        if (userRoleList != null) {
            authorities.addAll(
                    userRoleList.stream().map(userRole -> new SimpleGrantedAuthority(userRole.getRole()))
                            .collect(Collectors.toList()));
        }

        return new UserInfoUserDetails(userInfo, authorities);
    }

}
  • loadUserByUsername メソッドの最後の return 文から最後の , true, true を削除します。

履歴

2015/07/19
初版発行。