シンプルBBSアプリケーション導入#4


緒論


SimpleBBSアプリを久しぶりに更新しました.前回は簡易ツールリストアプリで使用したspring securityでしたが、今回の主な目的はMVCベースのアプリSimpleBBSに適用することで、難しい点はたくさんありましたが、結局うまく応用できました.

本題


スプリングセキュリティの適用


SimpleTooListはAPIサービスのみを提供するため、ログインおよびログアウトページは表示されません.また、認証・認可もJWTおよびその格納されているユーザ情報を利用しているためSpring Securityのログイン・ログアウト機能を十分に利用することができず、今回のSimple BBSではMVC方式でサーバにログイン・ログアウトページを提供する必要があるため、Spring Securityのログイン・ログアウト機能を適用してみることができる.
ログイン・ログアウト機能だけなら、ちょっと不明ですが、正確にはformLogin設定やログイン・ログアウトURLなどです.Spring Securityが適用されない既存のSimple BBSでは、ログインページに入力されたIDとパスワードに基づき、アカウントサービスが提供するログイン機能を利用してアカウント情報を確認している.
これに基づいて、LoginSessionInfo認証情報を含む独立クラスオブジェクトを作成し、セッション登録後、必要に応じてセッションから情報を抽出して書き込み、またはセッションにLoginSessionInfoオブジェクトが存在するかどうかを検証して認証と認証を適用します.
ただしSpring Securityは上記の機能を提供しているので、これらの機能は必要ありません.さらに、登録過程において、POSTがユーザID(ユーザ名)とパスワード(パスワード)を予め指定された登録URLに渡すと、自動的にログインとセッションが行われ、登録情報がPrincipalのインタフェースオブジェクトとして保持されるので、多くのコードを削減することができる.
また、承認は、以前にもHandler Intercepterを使用して要求セッションのLoginSessionInfoをチェックし、承認が必要なリソースへの承認を確認していたが、Spring Securityを適用した後、自己設定(HttpSecurity.authorizeRequests)を使用して承認を構成することができる.
それ以外にもCSRFトークンを自動的に生成して検証することができ、フレームワークの設定が自由であるため、セキュリティを考慮すればプロジェクト初期から使用することが望ましい.

不要なクラスの削除と統合


まず,今回の更新でSpring Securityとペアを組んだ最大の更新は,多くの不要なクラスを削除したことである.以前は、各リクエスト(会員入力、執筆、文章修正リクエスト、文章修正送信など)がそれぞれのパラメータを含むカスタムオブジェクトクラスを定義していたため、一度に見るのは難しく、重複する点も多かった.
前回、ある企業の課題テストに参加しましたが、与えられたサンプルコードでは、このコマンドオブジェクトに似たクラスが1つのクラス内で静的クラスとして清潔に保たれているのを見ました.これはSimpleToDoListで初めて使用されたもので、まあまあなので、その上でSimpleBBSにも適用されます.
上のコミットログから、個別に存在するコマンドオブジェクトと検証グループインタフェース(アプリケーション@Validated Arnolation)が削除され、以下のような同じクラスの静的クラスとして登録されていることがわかります.
という欠点は、1つのクラスファイルが肥大化することであるが、同じオブジェクト(投稿、コメントなど)を処理する目的(投稿出力、修正など)のためのカスタムオブジェクトは、1つの場所で管理することが望ましい.
たとえば、すべてのフィールドを含むSubmitクラス(投稿の作成、変更に必要な作成者、パスワード、タイトル、コンテンツなどを含む)または投稿の変更権限を決定する場合、必要な投稿の識別子とパスワードフィールドを含むAuthorizeクラスは内部静的クラスとして維持されます.
    @Getter
    @Setter
    @NoArgsConstructor
    public static class Authorize {
        @Positive(message = "Article ID cannot be negative or zero.", groups = {Request.class, Submit.class})
        private long id;

        @NotBlank(message = "Password cannot be blank.", groups = {Submit.class})
        @Length(min = 4, message = "Password should be at least 4 characters.", groups = {Submit.class})
        private String password;

        public interface Request {}
        public interface Submit {}
    }

    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Submit {
        @Positive(message = "Article ID cannot be negative or zero.", groups = {Update.class})
        private long id;

        @NotBlank(message = "Writer cannot be blank.", groups = {Create.class, Update.class})
        @Length(max = 64, message = "Writer cannot exceeds 64 characters.", groups = {Create.class, Update.class})
        private String writer;

        @NotBlank(message = "Password cannot be blank.", groups = {Create.class, Update.class})
        @Length(min = 4, message = "Password should be at least 4 characters.", groups = {Create.class, Update.class})
        private String password;

        @NotBlank(message = "Title cannot be blank.", groups = {Create.class, Update.class})
        @Length(max = 255, message = "Title cannot exceeds 255 characters.", groups = {Create.class, Update.class})
        private String title;

        @NotBlank(message = "Content cannot be blank.", groups = {Create.class, Update.class})
        @Length(max = 65535, message = "Content cannot exceeds 65535 characters.", groups = {Create.class, Update.class})
        private String content;

        private final List<MultipartFile> uploadedFiles = new ArrayList<>();
        private final List<String> delete = new ArrayList<>();

        public interface Create {}
        public interface Update {}

        public void encodePassword(PasswordEncoder encoder) {
            this.password = encoder.encode(password);
        }
        public void encodePassword(PasswordEncoder encoder, String newPassword) {
            this.password = encoder.encode(newPassword);
        }
    }

@Validated


今回、いくつかのクラスで@Validated Arnolationと検証グループインタフェースを使用しました.これは、投稿の作成、変更、変更、削除など、複数の内部静的クラスで同じフィールドを繰り返したくないためです.
投稿の作成と変更では、異なるのは投稿の識別子だけです.したがって,コード重複の観点から,作成に必要なフィールドを含むクラスと修正に必要なフィールドを含むクラスを区別することは望ましくない.
@PostMapping("/write")
public String submitBoardArticle(
        Model model,
        @ModelAttribute("command") 
        @Validated(Submit.Create.class) Submit command,
        BindingResult bindingResult,
        @CurrentSecurityContext SecurityContext context) {
        ...
@PostMapping("/edit/submit")
public String submitEditArticle(
	Model model,
	@ModelAttribute("article") 
	@Validated(Submit.Update.class) Submit command,
	BindingResult bindingResult,
	Principal principal) {
	...
逆に、コントローラまたは他の場所で検証するときに状況に応じたフィールドのみを検証するためには、検証グループインタフェースと@Validated Arnotationを積極的に使用する必要があります.上のコードから分かるように、Submitクラスの更新検証グループが完全なフィールドが必要な修正に適用され、識別子以外のフィールドが必要な作成にCreate検証グループが適用されている.
では、上のSubmitクラスとAuthorizeクラスも分ける必要はありませんか?もちろんですが、すべてのフィールドを極端に同じクラスに処理するべきではないと思います.前述したように、目的によって少し分けたほうが簡単だと思ったので、分けました.
もちろん、これらの原則を最初から制定してプロジェクトを行ったわけではないので、他のクラスでは異なる応用がある可能性があります.これにより、今後のコードでさらにチェックと修正が行われます.

APIとMVC環境のフィードバック


単純BBS(MVC)アプリケーションを修正する際に確かに感じる違いは、単純APIアプリケーションとは異なり、異常や問題が発生した場合、異常のみを投げ出し、ExceptionHandlerによってキャプチャして処理する方式が適切ではないことである.
投稿の作成中に最大長を超えるテキスト長などの検証エラーが発生し、例外ページに直接ジャンプすると、作成したすべてのテキストが失われます.これはユーザーエクスペリエンス(UX?)です.横から見るとあまりよくないです.
そこで,BindingResultと@ModelAttributeを積極的に利用し,検証に失敗したり,他の問題があったりしても,以前に作成した内容を含め,正確にはサーバ側に提出しようとした内容をページをレンダリングすることで実現した.
もちろん、パスワードを省略したり、再記入する必要のないフィールドを省略したりすることができます.

n/a.結論


スプリングセキュリティメカニズムを適用すると、多くのコードを削除し、不要なクラス、例外をクリアし、統合しながら、より多くのコードを削除できます.スプリングの安全を早く勉強しなければなりません.
特に、セッションに認証情報を保存するためのLoginSessionInfoのようなクラスが構成されていなくても、Spring Securityはそれをサポートします.すべては本当の必要から生まれたと思います.