かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その20 )( 検索対象図書館登録画面の作成2 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その19 )( 検索対象図書館登録画面の作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 検索対象図書館登録画面 ( 管理者のみ ) の作成

参照したサイト・書籍

  1. Spring MVC and Thymeleaf: how to access data from templates
    http://www.thymeleaf.org/doc/articles/springmvcaccessdata.html

    • Thymeleaf の公式サイトのページです。Thymeleaf から Spring の Bean を呼び出す方法を調査した時に、このページの中の「Spring beans」の記述を参照しました。
  2. Tutorial: Using Thymeleaf - 18 Appendix B: Expression Utility Objects
    http://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf.html#appendix-b-expression-utility-objects

    • Thymeleaf の公式サイトのチュートリアルで、Thymeleaf テンプレート上で使用可能なユーティリティオブジェクトが記載されています。
    • 今回は変数にセットされている文字列が指定された文字列で始まっているかチェックするために ${#strings.startsWith(...)} を使用しました。
  3. Spring Framework Advent Calendar 2011 part.7 - Spring Security で認証成功時に条件によって遷移先を変える
    http://d.hatena.ne.jp/ocs/20111207/1323269801

    • ログインしたユーザが持つ権限に応じて遷移先の画面を変える方法を調査していた時に参照しました。
  4. Spring Boot Security Application
    http://kielczewski.eu/2014/12/spring-boot-security-application/

    • 特定の権限が付与されたユーザしか URL にアクセスできないようにする方法を調査した時に参照しました。
    • Controller クラスに @PreAuthorize アノテーションを付与することでアクセス制御できるようになります。ただしこの記事では権限の文字列を "ADMIN" と書いていますが、今回実装した際には "ROLE_ADMIN" と書かないとアクセス制御できませんでした。どこかのタイミングで Spring Security の仕様変更があったものと思われます。
  5. How do I add a thymeleaf dialect to spring boot?
    http://stackoverflow.com/questions/23531580/how-do-i-add-a-thymeleaf-dialect-to-spring-boot

    • 今回は作成していませんが Thymeleaf で Dialect を作成した時に Spring Boot で使用できるようにするための方法です。
    • AutoConfiguration 対象になっている Dialect へのリンクも貼られており、Dialect を実装する時の参考になりそうでしたのでメモとして残しておきます。

目次

  1. 選択した図書館を保存するテーブルを作成する
  2. 図書館を1つ選択してDBに登録する
  3. 選択中の図書館を画面上部のナビゲーションバーに表示する
  4. 管理権限 ( ROLE_ADMIN ) を持つユーザがログインした場合には最初に検索対象図書館登録画面に遷移させる
  5. 管理権限 ( ROLE_ADMIN ) を持たないユーザが検索対象図書館登録画面にアクセスしようとした場合にはエラーにする
  6. 次回は。。。

手順

選択した図書館を保存するテーブルを作成する

図書館を保存するテーブルを作成していなかったので作成します。

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

コマンドプロンプトから以下のコマンドを実行して library_forsearch テーブルを作成します。

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

ksbylending=> create table library_forsearch (
ksbylending(>       systemid              text primary key
ksbylending(>     , formal                text
ksbylending(> );
CREATE TABLE
ksbylending=> \d library_forsearch
テーブル "public.library_forsearch"
   列     |  型  |  修飾語
----------+------+----------
 systemid | text | not null
 formal   | text |
インデックス:
    "library_forsearch_pkey" PRIMARY KEY, btree (systemid)


ksbylending=> \q

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

最後に Gradle projects view から gen タスクを実行し、Entity, Dao クラスを作り直します。

図書館を1つ選択してDBに登録する

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

  2. src/main/resources/META-INF/ksbysample/webapp/lending/dao/LibraryForsearchDao の下に deleteAll.sql を作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/web/admin/library の下に SetSelectedLibraryForm.java を作成します。作成後、リンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/web/admin/library の下に AdminLibraryService.java を作成します。作成後、リンク先の内容 に変更します。

  5. src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.javaリンク先のその1の内容 に変更します。

  6. src/main/resources/templates/admin/library の下の library.html を リンク先の内容 に変更します。

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

  8. library_forsearch テーブルにデータが登録されていないことを確認します。

    f:id:ksby:20150906040759p:plain

  9. ブラウザを起動し http://localhost:8080/admin/library へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

  10. 検索対象図書館登録画面が表示されたら、"東京都" で検索した後 "著作権情報センター資料室" の「選択」ボタンをクリックします。

    f:id:ksby:20150906041150p:plain

    選択後は検索前の画面に戻ります。

    f:id:ksby:20150906041435p:plain

  11. library_forsearch テーブルを見ると選択した図書館が保存されていることが確認できます。

    f:id:ksby:20150906041609p:plain

  12. 何度か別の図書館を選択して保存されることを確認した後、Ctrl+F2 を押して Tomcat を停止します。

  13. 一旦 commit します。

選択中の図書館を画面上部のナビゲーションバーに表示する

ログイン画面以外の画面でも表示させるので Controller クラスのメソッド内で表示するデータをセットするのではなく、表示するデータを返す Helper クラスを作成して Thymeleaf テンプレートファイルからは Helper クラスのメソッドを呼び出して表示するデータを取得するようにします。

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

  2. src/main/resources/META-INF/ksbysample/webapp/lending/dao/LibraryForsearchDao の下に selectSelectedLibrary.sql を作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/helper の下に library パッケージを作成します。

  4. src/main/java/ksbysample/webapp/lending/helper/library の下に LibraryHelper.java を作成します。作成後、リンク先の内容 に変更します。

  5. src/main/resources/templates/common の下の mainparts.html を リンク先のその1の内容 に変更します。

  6. 動作確認します。library_forsearch テーブルにデータが登録されている場合には削除しておきます。

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

  8. ブラウザを起動し http://localhost:8080/admin/library へアクセスします。図書館が選択されていない時にはナビゲーションバーにオレンジ色の文字で「※図書館が選択されていません」と表示されます。

    f:id:ksby:20150906105935p:plain

  9. "東京都" で検索して表示された図書館一覧から「国立国会図書館東京本館」を選択します。

    f:id:ksby:20150906110527p:plain

    図書館が選択されるとナビゲーションバーに今度は白い文字で選択中の図書館が表示されます。

    f:id:ksby:20150906110743p:plain

  10. 何度か別の図書館を選択して表示が切り替わることを確認した後、Ctrl+F2 を押して Tomcat を停止します。

  11. 一旦 commit します。

管理権限 ( ROLE_ADMIN ) を持つユーザがログインした場合には最初に検索対象図書館登録画面に遷移させる

  1. Spring Security で認証成功時に条件によって遷移先を変える の記事によると SavedRequestAwareAuthenticationSuccessHandler クラスを継承したクラスを作成して onAuthenticationSuccess メソッドを実装すれば実現できるようです。

    • IntelliJ IDEA の Diagram 生成機能で SavedRequestAwareAuthenticationSuccessHandler クラスのある org.springframework.security.web.authentication パッケージのクラス関連の Diagram を作成してみると以下のように表示されました。

      f:id:ksby:20150906150735p:plain

    • SavedRequestAwareAuthenticationSuccessHandler クラスのソースを見てみましたが onAuthenticationSuccess メソッドの中で sendRedirect している部分がありました。確かにこのメソッドでリダイレクトできるようです。

    • onAuthenticationSuccess メソッドを見ていて気づきましたが、非認証時にログイン画面以外の URL ( 例 http://localhost:8080/admin/library ) を指定すると一旦ログイン画面が表示されて、ログインすると指定した URL に遷移します。この機能はこのメソッドで実現されていました。一旦セッションの "SPRING_SECURITY_SAVED_REQUEST" というキーに指定された URL が保存されて ( HttpSessionRequestCache 参照 )、認証が成功して SPRING_SECURITY_SAVED_REQUEST に URL が保存されていたらその URL にリダイレクトされる仕組みになっていました。だからクラス名が "SavedRequestAware~" というネーミングなのですね ( 最初に見た時 SavedRequest って何だろう?と思いました )。

  2. 今回は以下の方針で実装します。

    • 非認証時にログイン画面以外の URL を指定してアクセスされた場合には、管理権限を持つユーザでログインされた場合でも指定された URL へリダイレクトします。
    • 最初にログイン画面にアクセスしてログインした場合に、ログインユーザが管理権限 ( ROLE_ADMIN ) を持つ場合には検索対象図書館登録画面 ( /admin/library ) に遷移させます。管理権限を持たない場合には /loginsuccess へ遷移させます。
  3. src/main/java/ksbysample/webapp/lending/security の下に RoleAwareAuthenticationSuccessHandler.java を作成します。作成後、リンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.javaリンク先の内容 に変更します。

  5. src/test/resources/testdata/base の下の user_info.csvリンク先の内容 に変更します。また DB の user_info テーブルのデータも直接修正しておきます。

  6. 動作確認します。ブラウザを起動している場合には一旦終了させます。

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

  8. ブラウザを起動し http://localhost:8080/ へアクセスします。ログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    tanaka.taro@sample.com は ROLE_ADMIN を付与されているので検索対象図書館登録画面 ( /admin/library ) に遷移します。

    f:id:ksby:20150906161940p:plain

    画面右上の「ログアウト」リンクをクリックしてログアウトします。

  9. ログイン画面に戻りますので、今度は ID に "suzuki.hanako@test.co.jp"、Password に "hanako" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    suzuki.hanako@test.co.jp は ROLE_ADMIN を付与されていないので /loginsuccess に遷移します。

    f:id:ksby:20150906164320p:plain

    ブラウザを一旦終了させます。

  10. 今度はログイン画面以外の URL を指定した場合の動作を確認します。ブラウザを起動し http://localhost:8080/loginsuccess へアクセスします。

    認証していないのでログイン画面が表示されます。ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    f:id:ksby:20150906164549p:plain

    tanaka.taro@sample.com は管理権限 ( ROLE_ADMIN ) を付与されていますが、今回は URL を指定してアクセスされたので /loginsuccess へ遷移しました。

    f:id:ksby:20150906164921p:plain

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

  12. 一旦 commit します。

管理権限 ( ROLE_ADMIN ) を持たないユーザが検索対象図書館登録画面にアクセスしようとした場合にはエラーにする

@PreAuthorize アノテーションを付与して管理権限 ( ROLE_ADMIN ) を付与されていないユーザが /admin/library にアクセスした場合にはエラーになるようにします。

  1. 最初に @PreAuthorize アノテーションを使用可能にするための @EnableGlobalMethodSecurity アノテーションを付与します。src/main/java/ksbysample/webapp/lending の Application.javaリンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.javaリンク先のその2の内容 に変更します。

  3. 動作確認します。ブラウザを起動している場合には一旦終了させます。

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

  5. ブラウザを起動し http://localhost:8080/ へアクセスします。ログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    tanaka.taro@sample.com では検索対象図書館登録画面が表示されて、検索及び選択ができることを確認します。確認後、画面右上の「ログアウト」リンクをクリックしてログアウトします。

  6. ログイン画面に戻りますので、今度は ID に "suzuki.hanako@test.co.jp"、Password に "hanako" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    ログインすると /loginsuccess へ遷移します。ブラウザのアドレスバーの URL を http://localhost:8080/admin/library に変更します。

    suzuki.hanako@test.co.jp は管理権限を付与されていないので、HTTPステータスコードが 403 ( Forbidden ) が返りエラー画面が表示されます。

    f:id:ksby:20150906181131p:plain

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

  8. 一旦 commit します。

次回は。。。

Git for Windows が 2.5.1 にバージョンアップしたので、一旦番外編を入れてインストールする予定です。

その後に「検索対象図書館登録画面の作成3」としてテストを作成した後、マージします。

ソースコード

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);

create table user_role (
    role_id                 bigserial primary key
    , user_id               bigint not null references user_info(user_id) on delete cascade
    , role                  varchar(32) not null
);

create table lending_app (
    lending_app_id          bigserial primary key
    , status                varchar(1) not null
    , lending_user_id       bigint not null references user_info(user_id)
    , approval_user_id      bigint not null references user_info(user_id)
);

create table lending_book (
    lending_book_id         bigserial primary key
    , lending_app_id        bigint not null references lending_app(lending_app_id) on delete cascade
    , isbn                  varchar(17) not null
    , book_name             varchar(128) not null
    , lending_state         varchar(16)
    , lending_app_flg       varchar(1)
    , lending_app_reason    varchar(128)
    , approval_result       varchar(1)
    , approval_reason       varchar(128)
);

create table library_forsearch (
      systemid              text primary key
    , formal                text
);
  • create table library_forsearch ( ... ); を追加します。

LibraryForsearchDao.java

■その1

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LibraryForsearch;
import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LibraryForsearchDao {

    /**
     * @param systemid
     * @return the LibraryForsearch entity
     */
    @Select
    LibraryForsearch selectById(String systemid);

    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LibraryForsearch entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LibraryForsearch entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LibraryForsearch entity);

    @Delete(sqlFile = true)
    int deleteAll();
}

■その2

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LibraryForsearch;
import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LibraryForsearchDao {

    /**
     * @param systemid
     * @return the LibraryForsearch entity
     */
    @Select
    LibraryForsearch selectById(String systemid);

    @Select
    LibraryForsearch selectSelectedLibrary();
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LibraryForsearch entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LibraryForsearch entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LibraryForsearch entity);

    @Delete(sqlFile = true)
    int deleteAll();
}
  • LibraryForsearch selectSelectedLibrary(); を追加します。

deleteAll.sql

delete from
  library_forsearch

SetSelectedLibraryForm.java

package ksbysample.webapp.lending.web.admin.library;

import lombok.Data;

@Data
public class SetSelectedLibraryForm {

    private String systemid;

    private String formal;
    
}

AdminLibraryService.java

package ksbysample.webapp.lending.web.admin.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AdminLibraryService {

    @Autowired
    private LibraryForsearchDao libraryForsearchDao;
    
    public void deleteAndInsertLibraryForSearch(SetSelectedLibraryForm setSelectedLibraryForm) {
        // library_forsearch テーブルのデータを全て削除する
        libraryForsearchDao.deleteAll();

        // 選択された図書館を登録する
        LibraryForsearch libraryForsearch = new LibraryForsearch();
        BeanUtils.copyProperties(setSelectedLibraryForm, libraryForsearch);
        libraryForsearchDao.insert(libraryForsearch);
    }

}

AdminLibraryController.java

■その1

package ksbysample.webapp.lending.web.admin.library;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin/library")
public class AdminLibraryController {

    @Autowired
    private AdminLibraryService adminLibraryService;
    
    @RequestMapping
    public String index() {
        return "admin/library/library";
    }

    @RequestMapping("/addSearchLibrary")
    public String addSearchLibrary(SetSelectedLibraryForm setSelectedLibraryForm) {
        adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm);
        return "redirect:/admin/library";
    }
    
}
  • private AdminLibraryService adminLibraryService; を追加します。
  • addSearchLibrary メソッドの以下の点を変更します。
    • 引数に SetSelectedLibraryForm setSelectedLibraryForm を追加します。
    • 中の処理に adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm); を追加します。
    • return 時に /admin/library にリダイレクトするように変更します。

■その2

package ksbysample.webapp.lending.web.admin.library;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping("/admin/library")
public class AdminLibraryController {

    @Autowired
    private AdminLibraryService adminLibraryService;

    @RequestMapping
    public String index() {
        return "admin/library/library";
    }

    @RequestMapping("/addSearchLibrary")
    public String addSearchLibrary(SetSelectedLibraryForm setSelectedLibraryForm) {
        adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm);
        return "redirect:/admin/library";
    }
    
}
  • クラスに @PreAuthorize("hasAuthority('ROLE_ADMIN')") を付与します。

library.html

<script th:replace="common/bottom-js"></script>
<script type="text/javascript">
    <!--
    $(document).ready(function() {
        $('#pref').focus();

        $('.js-search-library').click(function(){
            if (!$('#pref').val()) {
                alert("都道府県名を入力してください");
                return false;
            }
            
            $.ajax({
                type: "get",
                url: "/webapi/library/getLibraryList",
                data: { pref: $('#pref').val() },
                async: false,
                dataType: "json",
                success: function(json) {
                    if (json.errcode == 0) {
                        $('#library-list-body tr').remove();
                        for (var i = 0; i < json.content.length; i++) {
                            var library = "<tr>"
                                                + "<th>"
                                                    + "<button class=\"btn btn-sm bg-blue js-select-library\""
                                                        + "data-systemid=\"" + json.content[i].systemid + "\""
                                                        + "data-formal=\"" + json.content[i].formal + "\">選択</button>"
                                                + "</th>"
                                                + "<th class=\"hidden-xs\">" + json.content[i].systemname + "</th>"
                                                + "<th>" + json.content[i].formal + "</th>"
                                                + "<th>" + json.content[i].address + "</th>"
                                            + "</tr>";
                            $('#library-list-body').append(library);
                        }
                    }
                    else {
                        alert(json.errmsg);
                    }
                }
            });

            $('.js-select-library').each(function(){
                $(this).click(function(){
                    $("#setSelectedLibraryForm input[name='systemid']").val($(this).attr('data-systemid'));
                    $("#setSelectedLibraryForm input[name='formal']").val($(this).attr('data-formal'));
                    $('#setSelectedLibraryForm').submit();
                });
            });

            $('#pref').focus().select();
            
            return false;
        });
    });
    -->
</script>
</body>
</html>
  • 選択ボタンクリック時のイベントハンドラを割り当てていた $('.js-select-library').each(function(){ ... }); の処理を $('#pref').focus().select(); の前に移動して、一覧更新時に割り当てるように変更します。

selectSelectedLibrary.sql

select
  /*%expand*/*
from
  library_forsearch

LibraryHelper.java

package ksbysample.webapp.lending.helper.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class LibraryHelper {

    @Autowired
    private LibraryForsearchDao libraryForsearchDao;
    
    public String getSelectedLibrary() {
        String result;
        LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary();
        if (libraryForsearch == null) {
            result = "※図書館が選択されていません";
        }
        else {
            result = "選択中:" + libraryForsearch.getFormal();
        }

        return result;
    }

}

mainparts.html

■その1

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>検索対象図書館登録</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
    <!-- Bootstrap 3.3.4 -->
    <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
    <!-- Font Awesome Icons -->
    <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/>
    <!-- Ionicons -->
    <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/>
    <!-- Theme style -->
    <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/>
    <!-- AdminLTE Skins. Choose a skin from the css/skins
         folder instead of downloading all of them to reduce the load. -->
    <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
        <script src="/js/html5shiv.min.js"></script>
        <script src="/js/respond.min.js"></script>
    <![endif]-->

    <style>
        .noselected-library {
            color: #ff8679 !important;
            font-size: 100%;
            font-weight: 700;
        }
        .selected-library {
            color: #ffffff !important;
            font-size: 100%;
            font-weight: 700;
        }
        .content-wrapper {
            background-color: #fffafa;
        }
    </style>
</head>

<!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. -->
<body class="skin-blue layout-top-nav">
<div class="wrapper">

    <!-- Main Header -->
    <header class="main-header" th:fragment="main-header">
        <nav class="navbar navbar-static-top">
            <div class="container">
                <div class="navbar-header">
                    <a href="#" class="navbar-brand"><b>ksbysample-lending</b></a>
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
                        <i class="fa fa-bars"></i>
                    </button>
                </div>

                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse pull-left" id="navbar-collapse">
                    <ul class="nav navbar-nav">
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">メニュー <span class="caret"></span></a>
                            <ul class="dropdown-menu" role="menu">
                                <li><a href="/booklist">貸出希望書籍登録</a></li>
                                <li><a href="/lendingapp">貸出申請</a></li>
                                <li><a href="/confirmresult">貸出申請結果確認</a></li>
                                <li class="divider"></li>
                                <li><a href="/lendingapproval">貸出承認</a></li>
                                <li class="divider"></li>
                                <li><a href="/admin/library">検索対象図書館登録</a></li>
                            </ul>
                        </li>
                    </ul>
                </div>
                <!-- /.navbar-collapse -->

                <!-- Navbar Right Menu -->
                <div class="navbar-custom-menu">
                    <p class="navbar-text" th:classappend="${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')} ? 'noselected-library' : 'selected-library'" th:text="${@libraryHelper.getSelectedLibrary()}">※図書館が選択されていません</p>
                    <ul class="nav navbar-nav">
                        <li><a href="/logout">ログアウト</a></li>
                    </ul>
                </div>
                <!-- /.navbar-custom-menu -->
            </div>
            <!-- /.container-fluid -->
        </nav>
    </header>

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Content Header (Page header) -->
            <section class="content-header">
                <h1>画面名</h1>
            </section>

            <!-- Main content -->
            <section class="content">
                ここに画面本体を入れる
            </section>
            <!-- /.content -->
        </div>
        <!-- /.container -->
    </div>

</div>
<!-- ./wrapper -->

<!-- jQuery 2.1.4 -->
<script src="/js/jQuery-2.1.4.min.js" type="text/javascript"></script>
<!-- Bootstrap 3.3.2 JS -->
<script src="/js/bootstrap.min.js" type="text/javascript"></script>
<!-- AdminLTE App -->
<script src="/js/app.min.js" type="text/javascript"></script>
</body>
</html>
  • <p class="navbar-text noselected-library">※図書館が選択されていません</p><p class="navbar-text" th:classappend="${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')} ? 'noselected-library' : 'selected-library'" th:text="${@libraryHelper.getSelectedLibrary()}">※図書館が選択されていません</p> へ変更します。

RoleAwareAuthenticationSuccessHandler.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.config.WebSecurityConfig;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class RoleAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
        // ログイン画面以外のURLを指定してアクセスされていた場合には、処理を SavedRequestAwareAuthenticationSuccessHandler へ委譲する
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }

        // 特定の権限を持っている場合には対応するURLへリダイレクトする
        String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL;
        GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
        if (authentication.getAuthorities().contains(roleAdmin)) {
            // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる
            targetUrl = "/admin/library";
        }

        clearAuthenticationAttributes(request);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

}

WebSecurityConfig.java

package ksbysample.webapp.lending.config;

import ksbysample.webapp.lending.security.RoleAwareAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public static final String DEFAULT_SUCCESS_URL = "/loginsuccess";
    public static final String REMEMBERME_KEY = "ksbysample-webapp-lending";
    
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します
                // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します
                // .antMatchers("/country/**").permitAll()
                .antMatchers("/fonts/**").permitAll()
                .antMatchers("/html/**").permitAll()
                .antMatchers("/encode").permitAll()
                .antMatchers("/urllogin").permitAll()
                .antMatchers("/webapi/**").permitAll()
                .anyRequest().authenticated();
        http.formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL)
                .failureUrl("/")
                .usernameParameter("id")
                .passwordParameter("password")
                .successHandler(new RoleAwareAuthenticationSuccessHandler())
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .deleteCookies("remember-me")
                .invalidateHttpSession(true)
                .permitAll()
                .and()
                .rememberMe()
                .key(REMEMBERME_KEY)
                .tokenValiditySeconds(60 * 60 * 24 * 30);
    }

    @Bean
    public AuthenticationProvider daoAuhthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        return daoAuthenticationProvider;
    }

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuhthenticationProvider())
                .userDetailsService(userDetailsService);
    }

}
  • configure メソッド内で login の処理を定義している部分に .successHandler(new RoleAwareAuthenticationSuccessHandler()) を追加します。

user_info.csv

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password
1,"tanaka taro",$2a$10$LKKepbcPCiT82NxSIdzJr.9ph.786Mxvr.VoXFl4hNcaaAn9u7jje,tanaka.taro@sample.com,1,0,"2015-10-17 12:46:14.790000","2016-08-18 12:46:30.354000"
2,"suzuki hanako",$2a$10$.fiPEZ155Rl41/e.mdM3A.mG0iEQNPmhjFL/aIiV8dZnXsCd.oqji,suzuki.hanako@test.co.jp,1,0,"2015-09-30 22:19:02.783000","2015-08-31 22:19:22.176000"
3,"kimura masao",$2a$10$yP1dLPIq9j7WQVH6ruSwkepf8jIkPxTtncbSnYM0/jAGQ4HCQO8R.,kimura.masao@test.co.jp,0,0,"2015-12-31 22:30:54.425000","2015-10-15 22:31:03.316000"
4,"endo yoko",$2a$10$PVFe8Lh1Pkjc54DWS9mJL.q407x51ZK8MSXhwuTF9zxCnnt80LKwy,endo.yoko@sample.com,1,0,"2015-01-10 22:31:55.454000","2015-12-31 22:32:11.886000"
5,"sato masahiko",$2a$10$qIU0kM/p1pa7KSIjF6YA4eORd2wL1Eo6TlvH./DmPs7D.xXQPEq7a,sato.masahiko@sample.com,1,0,"2015-12-31 22:34:14.827000","2014-08-05 22:34:22.818000"
6,"takahasi naoko",$2a$10$iXp/d4wXmfaLKTjQKBvik.kETgx4nQ.FL1NjYt4ALJOGSyVOSchW6,takahasi.naoko@test.co.jp,1,0,"2015-12-01 22:39:48.475000","2015-11-10 22:39:55.422000"
7,"kato hiroshi",$2a$10$g5dtFTtNBdJO30aHg50rluGNa2pEAzArcwYkYyCG91ElBZPs9sDi2,kato.hiroshi@sample.com,0,5,"2014-01-01 15:58:53.295000","2013-12-31 15:59:07.668000"
  • No.2 の suzuki hanako の「expired_password」を "2015-08-31 22:19:22.176000" → "2016-08-31 22:19:22.176000" へ変更します。

Application.java

package ksbysample.webapp.lending;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

import java.io.Serializable;
import java.text.MessageFormat;

@ImportResource("classpath:applicationContext-${spring.profiles.active}.xml")
@SpringBootApplication
@ComponentScan("ksbysample")
@EnableRedisHttpSession
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {

    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!StringUtils.equals(springProfilesActive, "product")
                && !StringUtils.equals(springProfilesActive, "develop")
                && !StringUtils.equals(springProfilesActive, "unittest")) {
            throw new UnsupportedOperationException(MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。", springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • @EnableGlobalMethodSecurity(prePostEnabled = true) を追加します。

履歴

2015/09/06
初版発行。