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

かんがるーさんの日記

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

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

概要

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

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出承認画面の作成
      • 承認権限 ( ROLE_APPROVER ) を付与されていないユーザが /lendingapproval にアクセスした場合にはエラーにする処理の実装。

参照したサイト・書籍

  1. 23.Expression-Based Access Control
    https://docs.spring.io/spring-security/site/docs/current/reference/html/el-access.html

    • Spring Security で、Spring EL を使用して権限ベースのアクセス制御を行う方法が記述されているドキュメントです。
    • このドキュメントだと @PreAuthorize("hasRole('USER')") と書かれていますが、これは Spring Security 4 から可能な記述なので ( "ROLE_" が不要になります )、Spring Security 3 を使用している ksbysample-webapp-lending では @PreAuthorize("hasRole('ROLE_USER')") と記述します。

目次

  1. ROLE_USER のみ持つユーザの作成
  2. LendingapprovalController クラスの変更
  3. 動作確認
  4. 次回は。。。

手順

ROLE_USER のみ持つユーザの作成

  1. 動作確認をするのに ROLE_USER のみを持つ使用可能なユーザがないことに気づいたので作成します。

    • username は "ito aoi" にします。
    • password は "aoi" です。パスワードの暗号化文字列は http://localhost:8080/encode?password=aoi で生成します。
    • ROLE は ROLE_USER のみ付与します。
  2. src/test/resources/testdata/base の下の user_info.csv, user_role.csvリンク先の内容 に変更します。

  3. 追加したデータを IntelliJ IDEA の Database tool から手動で追加します。

  4. src/test/java/ksbysample/common/test の下の SecurityMockMvcResource.javaリンク先の内容 に変更します。

  5. 一旦 commit します。

LendingapprovalController クラスの変更

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

動作確認

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

  2. 最初に ROLE_APPROVER 権限を付与されているユーザで貸出承認画面が表示されることを確認します。

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

    貸出承認画面が表示されることが確認できます。

    f:id:ksby:20160119004326p:plain

  3. 次に ROLE_APPROVER 権限を付与されていないユーザで貸出承認画面が表示されないことを確認します。

    画面右上の「ログアウト」リンクをクリックしてログアウトした後、http://localhost:8080/lendingapproval?lendingAppId=105 へアクセスします。ログイン画面が表示されますので ID に "ito.aoi@test.co.jp"、Password に "aoi" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    貸出承認画面は表示されず、共通エラー画面が表示されることが確認できます。画面上には "Access is denied" のメッセージが表示されます。

    f:id:ksby:20160119004739p:plain

    共通エラー画面が表示された時の HTTPステータスコードもアクセス禁止を示す 403 ( Forbidden ) が返ってきています。Fiddler ( http://www.telerik.com/fiddler ) というツールで確認しています。

    f:id:ksby:20160119005104p:plain

    ※共通エラー画面表示時に 403 が返るのは src/main/java/ksbysample/webapp/lending/web/ExceptionHandlerAdvice.javaif (e instanceof org.springframework.security.access.AccessDeniedException) { response.setStatus(HttpStatus.FORBIDDEN.value()); } の実装によるものです。

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

  5. 一旦 commit します。

次回は。。。

テストを作成します。

ソースコード

user_info.csv, user_role.csv

■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,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
2,"suzuki hanako",$2a$10$.fiPEZ155Rl41/e.mdM3A.mG0iEQNPmhjFL/aIiV8dZnXsCd.oqji,suzuki.hanako@test.co.jp,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
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,"9999-12-31 23:59:00.000000","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"
8,"ito aoi",$2a$10$wD9VL.2cfiL0UY05N4PAs.l0Zd/X9CC0LouzGaHqp2W6PKLjQXJNq,ito.aoi@test.co.jp,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
  • 8,"ito aoi",... のデータを追加します。

■user_role.csv

role_id,user_id,role
1,1,ROLE_USER
2,1,ROLE_ADMIN
3,1,ROLE_APPROVER
4,2,ROLE_USER
5,2,ROLE_APPROVER
6,3,ROLE_USER
7,4,ROLE_USER
8,5,ROLE_USER
9,8,ROLE_USER
  • 9,8,ROLE_USER のデータを追加します。

SecurityMockMvcResource.java

package ksbysample.common.test;

import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.Filter;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@Component
public class SecurityMockMvcResource extends ExternalResource {

    public final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";
    public final String MAILADDR_SUZUKI_HANAKO = "suzuki.hanako@test.co.jp";
    public final String MAILADDR_KIMURA_MASAO = "kimura.masao@test.co.jp";
    public final String MAILADDR_ENDO_YOKO = "endo.yoko@sample.com";
    public final String MAILADDR_SATO_MASAHIKO = "sato.masahiko@sample.com";
    public final String MAILADDR_TAKAHASI_NAOKO = "takahasi.naoko@test.co.jp";
    public final String MAILADDR_ITO_AOI = "ito.aoi@test.co.jp";

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private Filter springSecurityFilterChain;

    @Autowired
    private UserDetailsService userDetailsService;

    public MockMvc authTanakaTaro;
    public MockMvc authSuzukiHanako;
    public MockMvc authItoAoi;
    public MockMvc noauth;

    @Override
    protected void before() throws Throwable {
        // 認証ユーザ用MockMvc ( user = tanaka.taro@sample.com )
        UserDetails userDetailsTanakaTaro = userDetailsService.loadUserByUsername(MAILADDR_TANAKA_TARO);
        this.authTanakaTaro = MockMvcBuilders.webAppContextSetup(this.context)
                .defaultRequest(get("/").with(user(userDetailsTanakaTaro)))
                .addFilter(springSecurityFilterChain)
                .build();

        // 認証ユーザ用MockMvc ( user = suzuki.hanako@test.co.jp )
        UserDetails userDetailsSuzukiHanako = userDetailsService.loadUserByUsername(MAILADDR_SUZUKI_HANAKO);
        this.authSuzukiHanako = MockMvcBuilders.webAppContextSetup(this.context)
                .defaultRequest(get("/").with(user(userDetailsSuzukiHanako)))
                .addFilter(springSecurityFilterChain)
                .build();

        // 認証ユーザ用MockMvc ( user = ito.aoi@test.co.jp )
        UserDetails userDetailsItoAoi = userDetailsService.loadUserByUsername(MAILADDR_ITO_AOI);
        this.authItoAoi = MockMvcBuilders.webAppContextSetup(this.context)
                .defaultRequest(get("/").with(user(userDetailsItoAoi)))
                .addFilter(springSecurityFilterChain)
                .build();

        // 非認証ユーザ用MockMvc
        this.noauth = MockMvcBuilders.webAppContextSetup(this.context)
                .addFilter(springSecurityFilterChain)
                .build();
    }

}
  • public final String MAILADDR_ITO_AOI = "ito.aoi@test.co.jp"; を追加します。
  • public MockMvc authItoAoi; を追加します。
  • before メソッド内に // 認証ユーザ用MockMvc ( user = ito.aoi@test.co.jp ) の処理を追加します。

LendingapprovalController.java

package ksbysample.webapp.lending.web.lendingapproval;

import ksbysample.webapp.lending.exception.WebApplicationRuntimeException;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
import ksbysample.webapp.lending.helper.thymeleaf.SuccessMessagesHelper;
import org.seasar.doma.jdbc.OptimisticLockException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.mail.MessagingException;

@Controller
@PreAuthorize("hasRole('ROLE_APPROVER')")
@RequestMapping("/lendingapproval")
public class LendingapprovalController {

    @Autowired
    private LendingapprovalService lendingapprovalService;

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    @Autowired
    private LendingapprovalFormValidator lendingapprovalFormValidator;

    @InitBinder(value = "lendingapprovalForm")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(lendingapprovalFormValidator);
    }

    @RequestMapping
    public String index(@Validated LendingapprovalParamForm lendingapprovalParamForm
            , BindingResult bindingResult
            , LendingapprovalForm lendingapprovalForm
            , BindingResult bindingResultOfLendingapprovalForm) {
        if (bindingResult.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("LendingapprovalParamForm.lendingAppId.emptyerr", null));
        }

        // 画面に表示するデータを取得する
        lendingapprovalService.setDispData(lendingapprovalParamForm.getLendingAppId(), lendingapprovalForm);

        // 指定された貸出申請IDで申請中、承認済のデータがない場合には、貸出承認画面上にエラーメッセージを表示する
        if (lendingapprovalForm.getLendingApp() == null) {
            bindingResultOfLendingapprovalForm.reject("LendingapprovalForm.lendingApp.nodataerr");
        }
        
        return "lendingapproval/lendingapproval";
    }

    @RequestMapping(value = "/complete", method = RequestMethod.POST)
    public String complete(@Validated LendingapprovalForm lendingapprovalForm
            , BindingResult bindingResult
            , Model model) throws MessagingException {
        if (bindingResult.hasErrors()) {
            return "lendingapproval/lendingapproval";
        }

        try {
            // データを更新し、承認完了メールを送信する
            lendingapprovalService.complete(lendingapprovalForm);
    
            // 画面に表示するデータを取得する
            lendingapprovalService.setDispData(lendingapprovalForm.getLendingApp().getLendingAppId(), lendingapprovalForm);
    
            // 画面に表示する通常メッセージをセットする
            SuccessMessagesHelper successMessagesHelper = new SuccessMessagesHelper("確定しました");
            successMessagesHelper.setToModel(model);
        } catch (OptimisticLockException e) {
            bindingResult.reject("Global.optimisticLockException");
        }        

        return "lendingapproval/lendingapproval";
    }

}

履歴

2016/01/19
初版発行。