かんがるーさんの日記

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

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

概要

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

  • 今回の手順で確認できるのは以下の内容です。
    • ログイン機能 ( 基本的な部分 ) の作成

参照したサイト・書籍

  1. Spring Security 3.1

    Spring Security 3.1

    Spring Security 3.1

    • 「3 Custom Authentication」を参照しました。
  2. Spring Boot Security Application
    http://kielczewski.eu/2014/12/spring-boot-security-application/

  3. Spring Security 3.1 リファレンス - テクニカル・サマリ (1)
    http://m12i.hatenablog.com/entry/2013/03/28/004152

    • GrantedAuthority を使う部分を参考にしました。
  4. Thymeleaf + Spring Security integration basics
    http://www.thymeleaf.org/doc/articles/springsecurity.html

目次

  1. はじめに
  2. 1.0.x-make-login-basic ブランチの作成
  3. LoginController クラスの作成
  4. ログイン画面の HTML ファイルから Thymeleaf テンプレートファイルの作成
  5. 動作確認
  6. JRebel 使用のための設定
  7. 暗号化したパスワード文字列生成用 URL の作成
  8. テストデータを user_info テーブルに登録する
  9. UserDetails インターフェース実装クラスの作成
  10. UserInfoDao インターフェースの変更
  11. UserRoleDao インターフェースの変更
  12. UserDetailsService インターフェース実装クラスの作成
  13. テスト用ログイン成功ページの作成
  14. WebSecurityConfig クラスの変更
  15. 動作確認2
  16. commit、Push、Pull Request、マージ

手順

はじめに

今回は基本的な部分として、以下の実装を行います。

  • ユーザ情報を格納するテーブル user_info には username, password, mail_address, enabled が格納されています。
  • ログイン画面の ID には username ではなく mail_address を使用します。
  • UserDetails インターフェースを実装した独自クラスを作成し、username も mail_address も認証後に参照できるようにします。

ユーザ情報を拡張したり、認証処理を変更する場合の手順は以下のようになります。

  1. UserDetails インターフェースを実装したクラスを作成し、独自定義した情報が格納できるようにします。
  2. UserDetailsService インターフェースを実装したクラスを作成し、UserDetails インターフェース実装クラスを返すようにします。
  3. WebSecurityConfigurerAdapter を継承した Java Configuration のクラスの configAuthentication メソッド内で、auth.userDetailsService メソッドに UserDetailsService インターフェース実装クラスを渡します。

1.0.x-make-login-basic ブランチの作成

  1. IntelliJ IDEA で 1.0.x-make-login-basic ブランチを作成します。

LoginController クラスの作成

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

  2. src/main/java/ksbysample/webapp/lending/web の下に LoginController.java を作成します。作成後、リンク先のその1の内容 に変更します。

ログイン画面の HTML ファイルから Thymeleaf テンプレートファイルの作成

  1. src/main/resources/static/html/login.html を src/main/resources/templates の下へコピーします。コピー後、リンク先の内容 に変更します。

動作確認

ログイン画面が表示されるか確認します。

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

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

    f:id:ksby:20150712234634p:plain

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

JRebel を使用するための設定

Tomcat を起動したままソースコードの修正・確認を繰り返したいので、JRebel を使用するために必要な設定をします。

  1. Project View から src/main/java/ksbysample/webapp/lending の下の Application を選択した後、コンテキストメニューを表示して「Run with JRebel」->「Application」を選択します。

    f:id:ksby:20150715052142p:plain

  2. JRebel で Application が起動しようとしますが、spring.profiles.active に値が指定されていないと起動しないようにしているので、この時点では起動しません。

    f:id:ksby:20150715052443p:plain

  3. メインメニューから「Run」-「Edit Configurations...」を選択します。

  4. 「Run/Debug Configurations」ダイアログが表示されます。画面左側のツリーから「Spring Boot」-「Application」を選択します。選択後、画面右側の「VM options」に "-Dspring.profiles.active=develop -Dfile.encoding=UTF-8" を入力して「OK」ボタンをクリックします。

    f:id:ksby:20150715052922p:plain

  5. メイン画面に戻ると「Select Run/Debug Configuration」で「Application」が選択されており、画面右上の JRebel のボタンが押せるようになっています。「Run with JRebel 'Application'」ボタンをクリックして Tomcat を起動します。

    f:id:ksby:20150715053229p:plain

暗号化したパスワード文字列生成用 URL の作成

  1. DB に暗号化したパスワードを保存できるようにするために、暗号化パスワード生成用の URL http://localhost:8080/encode?password=... を作成します。

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

  3. http://localhost:8080/encode?password=... を認証対象外にします。src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.javaリンク先のその1の内容 に変更します。

  4. 今回の変更は build し直しただけでは JRebel でも反映されませんでした。Ctrl+F5 を押して Tomcat を再起動します。

  5. 動作確認します。ブラウザで http://localhost:8080/encode?password=taro にアクセスすると暗号化された文字列が表示されます。

    f:id:ksby:20150715070337p:plain

テストデータを user_info テーブルに登録する

ログインの動作確認に使用するデータを user_info テーブルに登録します。

パスワードを暗号化すると文字列が長くなることを考慮できていませんでした。user_info テーブルを作り直します。また mail_address をログインID に使用したいので mail_address に index を作成します。/sql の下の create_table.sqlリンク先の内容 に変更します。

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

C:\project-springboot\ksbysample-webapp-lending>psql -U ksbylending_user ksbylen
ding                                                                            
ユーザ ksbylending_user のパスワード:                                           
psql (9.4.1)                                                                    
"help" でヘルプを表示します.                                                    
                                                                                
ksbylending=> drop table user_role;                                             
DROP TABLE                                                                      
ksbylending=> drop table user_info;                                             
DROP TABLE                                                                      
ksbylending=> create table user_info (                                          
ksbylending(>     user_id                 bigserial primary key                 
ksbylending(>     , username              varchar(32) not null                  
ksbylending(>     , password              varchar(256) not null                 
ksbylending(>     , mail_address          varchar(256) not null                 
ksbylending(>     , enabled               smallint not null default 1           
ksbylending(> );                                                                
CREATE TABLE                                                                    
ksbylending=> create index user_info_idx_01 on user_info(mail_address);         
CREATE INDEX                                                                    
ksbylending=> create table user_role (                                          
ksbylending(>     role_id                 bigserial primary key                 
ksbylending(>     , user_id               bigint not null references user_info(u
ser_id) on delete cascade                                                       
ksbylending(>     , role                  varchar(32) not null                  
ksbylending(> );                                                                
CREATE TABLE                                                                    
ksbylending=> \d                                                                
                             リレーションの一覧                                 
 スキーマ |               名前               |     型     |      所有者         
----------+----------------------------------+------------+------------------   
 public   | lending_app                      | テーブル   | ksbylending_user    
 public   | lending_app_lending_app_id_seq   | シーケンス | ksbylending_user    
 public   | lending_book                     | テーブル   | ksbylending_user    
 public   | lending_book_lending_book_id_seq | シーケンス | ksbylending_user    
 public   | user_info                        | テーブル   | ksbylending_user    
 public   | user_info_user_id_seq            | シーケンス | ksbylending_user    
 public   | user_role                        | テーブル   | ksbylending_user    
 public   | user_role_role_id_seq            | シーケンス | ksbylending_user    
(8 行)                                                                          
                                                                                
                                                                                
ksbylending=> \q                                                                
                                                                                
C:\project-springboot\ksbysample-webapp-lending>                                
                                                                                

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

f:id:ksby:20150715073342p:plain

user_info, user_role テーブルに以下の画像のデータを登録します。

f:id:ksby:20150715073723p:plain f:id:ksby:20150715075801p:plain

  • password カラムに設定している文字列 $2a$10$LKKepbcPCiT82NxSIdzJr.9ph.786Mxvr.VoXFl4hNcaaAn9u7jjetaro を暗号化したものです。

UserDetails インターフェース実装クラスの作成

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

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

UserInfoDao インターフェースの変更

UserInfoDao インターフェースに user_info テーブルを mail_address カラムで検索するメソッドを追加します。

  1. src/main/java/ksbysample/webapp/lending/dao の下の UserInfoDao インターフェースを リンク先の内容 に変更します。

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

UserRoleDao インターフェースの変更

UserRoleDao インターフェースに user_role テーブルを user_id で検索するメソッドを追加します。

  1. src/main/java/ksbysample/webapp/lending/dao の下の UserRoleDao インターフェースを リンク先の内容 に変更します。

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

UserDetailsService インターフェース実装クラスの作成

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

テスト用ログイン成功ページの作成

ログインが成功した時に表示するテスト用ページを作成します。テスト用ページでは認証されたユーザの情報を表示してみます。

  1. build.gradle を リンク先の内容 に変更します。

  2. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

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

  4. src/main/resources/templates の下に loginsuccess.html を作成します。作成後、リンク先の内容 に変更します。

WebSecurityConfig クラスの変更

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

動作確認2

  1. Ctrl+F5 を押して Tomcat を再起動します。

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

    f:id:ksby:20150718010926p:plain

  3. 認証が成功してテスト用ログイン成功ページが表示され、以下の画像のようにユーザ名、メールアドレス、登録されている Role 一覧が表示されます。

    f:id:ksby:20150718011022p:plain

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

commit、Push、Pull Request、マージ

  1. commit します。「Code Analysis」ダイアログが出ますが、無視して「Commit」ボタンをクリックします。

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

ソースコード

LoginController.java

■その1

package ksbysample.webapp.lending.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class LoginController {

    @RequestMapping
    public String index() {
        return "login";
    }

}

■その2

package ksbysample.webapp.lending.web;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/")
public class LoginController {
    
    @RequestMapping
    public String index() {
        return "login";
    }

    @RequestMapping("/encode")
    @ResponseBody
    public String encode(@RequestParam String password) {
        return new BCryptPasswordEncoder().encode(password);
    }

}

■その3

package ksbysample.webapp.lending.web;

import ksbysample.webapp.lending.security.UserInfoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/")
public class LoginController {
    
    @RequestMapping
    public String index() {
        return "login";
    }

    @RequestMapping("/encode")
    @ResponseBody
    public String encode(@RequestParam String password) {
        return new BCryptPasswordEncoder().encode(password);
    }

    @RequestMapping("/loginsuccess")
    public String loginsuccess() {
        return "loginsuccess";
    }
}

login.html

<!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>
        <!--
        .content-wrapper {
            background-color: #fffafa;
            padding-top: 50px;
        }
        .form-group {
            margin-bottom: 5px;
        }
        -->
    </style>
</head>

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

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12 col-md-push-3 col-md-6">
                        <div class="form-wrap">
                            <div class="text-center"><h1>ksbysample-lending</h1></div>
                            <br/>
                            <form role="form" action="#" th:action="@{/login}" method="post" id="login-form" autocomplete="off">
                                <div class="form-group" th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null">
                                    <p class="form-control-static text-danger" th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}"></p>
                                </div>
                                <div class="form-group">
                                    <label for="id" class="sr-only">ID</label>
                                    <input type="text" name="id" id="id" class="form-control" placeholder="ID を入力して下さい"
                                           th:value="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''"/>
                                </div>
                                <div class="form-group">
                                    <label for="password" class="sr-only">Password</label>
                                    <input type="password" name="password" id="password" class="form-control" placeholder="Password を入力して下さい"/>
                                </div>
                                <br/>
                                <button id="btn-login" class="btn btn-primary btn-block">ログイン</button>
                            </form>
                        </div>
                    </div>
                </div>
            </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>
<script type="text/javascript">
    <!--
    $(document).ready(function() {
        $('#id').focus().select();
    });
    -->
</script>
</body>
</html>
  • 先頭に <!DOCTYPE html> が抜けていたので追加します。

WebSecurityConfig.java

■その1

    @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()
                .anyRequest().authenticated();
  • .antMatchers("/encode").permitAll() を追加します。

■その2

package ksbysample.webapp.lending.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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 {

    @SuppressWarnings("SpringJavaAutowiringInspection")
    @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()
                .anyRequest().authenticated();
        http.formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/loginsuccess")
                .failureUrl("/")
                .usernameParameter("id")
                .passwordParameter("password")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
                .permitAll();
    }

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

}
  • private UserDetailsService userDetailsService; を追加します。
    • 最初 private UserInfoUserDetailsService userInfoUserDetailsService; と書いたのですが、Tomcat 起動時に Caused by: java.lang.IllegalArgumentException: Can not set ksbysample.webapp.lending.security.UserInfoUserDetailsService field ksbysample.webapp.lending.config.WebSecurityConfig.userInfoUserDetailsService to com.sun.proxy.$Proxy64 というエラーが出て起動しませんでした。
    • いろいろ試した結果、実装クラス名ではなくインターフェース名で書いておけば UserInfoUserDetailsService のインスタンスをインジェクションしてくれました。仕組みがよく分かっていないのですが、一旦これで進めます。
    • @SuppressWarnings("SpringJavaAutowiringInspection") は、これがないと赤い波線が表示されるので付けています。
  • .defaultSuccessUrl("/loginsuccess") に変更します。
  • configAuthentication メソッド内に auth.userDetailsService(userDetailsService) を追加し、今回作成したUserDetailsService インターフェース実装クラス UserInfoUserDetailsService の loadUserByUsername メソッドが認証時に呼ばれるようにします。

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
);
create index user_info_idx_01 on user_info(mail_address);
  • password を varchar(32)varchar(256) に変更します。

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.User;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

public class UserInfoUserDetails implements UserDetails {

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

    public UserInfoUserDetails(UserInfo userInfo
            , Set<GrantedAuthority> authorities
            , boolean accountNonExpired
            , boolean accountNonLocked
            , boolean credentialsNonExpired
            , boolean enabled) {
        this.userInfo = userInfo;
        this.authorities = authorities;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        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;
    }
}

UserInfoDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.UserInfo;
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 UserInfoDao {

    /**
     * @param userId
     * @return the UserInfo entity
     */
    @Select
    UserInfo selectById(Long userId);

    @Select
    UserInfo selectByMailAddress(String mailAddress);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(UserInfo entity);

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

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(UserInfo entity);
}
  • UserInfo selectByMailAddress(String mailAddress); を追加します。

selectByUsername.sql

select
  /*%expand*/*
from
  user_info
where
  mail_address = /* mailAddress */'tanaka.taro@sample.com'

UserRoleDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.UserRole;
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;

import java.util.List;

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

    /**
     * @param roleId
     * @return the UserRole entity
     */
    @Select
    UserRole selectById(Long roleId);

    @Select
    List<UserRole> selectByUserId(Long userId);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(UserRole entity);

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

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(UserRole entity);
}
  • List<UserRole> selectByUserId(Long userId); を追加します。

selectByUserId.sql

select
  /*%expand*/*
from
  user_role
where
  user_id = /* userId */1

UserInfoUserDetailsService.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.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
            , boolean accountNonExpired
            , boolean accountNonLocked
            , boolean credentialsNonExpired) {
        this.userInfo = userInfo;
        this.authorities = authorities;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        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;
    }
}

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41"

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.3.1")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.4")
    compile("com.google.guava:guava:18.0")
    testCompile("org.dbunit:dbunit:2.5.1")
    testCompile("com.icegreen:greenmail:1.4.1")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.3.1")
    domaGenRuntime("${jdbcDriver}")
}
  • Thymeleaf テンプレートファイル内で Spring security dialect を使用するので、compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3") を追加します。

loginsuccess.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.w3.org/1999/xhtml">
<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>
        <!--
        .content-wrapper {
            background-color: #fffafa;
            padding-top: 50px;
        }
        .form-group {
            margin-bottom: 5px;
        }
        -->
    </style>
</head>

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

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Main content -->
            <section class="content">
                ログイン成功!<br/>
                ユーザ名: <span sec:authentication="name">Bob</span><br/>
                メールアドレス: <span sec:authentication="principal.mailAddress">test@sample.com</span><br/>
                Roles: <span sec:authentication="principal.authorities">[ROLE_USER, ROLE_ADMIN]</span><br/>
                <div sec:authorize="hasRole('ROLE_ADMIN')">ROLE_ADMIN が付与されています</div>
                <div sec:authorize="hasRole('ROLE_APPROVER')">ROLE_APPROVER が付与されています</div>
                <div sec:authorize="hasRole('ROLE_USER')">ROLE_USER が付与されています</div>
            </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>
<script type="text/javascript">
    <!--
    -->
</script>
</body>
</html>

履歴

2015/07/18
初版発行。