🙌

【チームプロジェクトReview5】例外クラスでError Handling/ Spring Interceptorを通して認証強化

2024/02/16に公開

はじめに

今日は、例外クラスを作成して例外処理をした方法をお話したいと思います。
Serviceから例外を発生させて、Controllerから処理した経験はありますが、直接例外クラスを作成して例外を処理することが初めてでしたので、とても良い勉強になりました。
最初ITスクールでなぜ例外クラスを作成するのかが分かりませんでしたが、少しは理解ができました。

Error Handling過程

Error Handling

例外クラスを作成し、SpringBootのBindingResultと連携しつつ、コードの量を減らし、例外を処理しました。

会員登録ロジックの場合、ID(PK)、携帯番号(UK)、メールアドレス(UK) 、3つのバリデーションチェックが必要な状況でした。
そのため、ServiceオブジェクトからControllerに例外を三回投げる(例外を発生させる)ことになりましたが、その場合、以下のような問題点がありました。

1.IllegalStateExceptionで処理してみる。

MemberService.java
 public void join(MemberAddDTO memberAddDTO){
        Member member = MemberAddDTO.MemberAddDTOToMember(memberAddDTO);
        memberRepository.findById(member.getMemberId())
                .ifPresent(m -> {throw new IllegalStateException("存在しているIDです。");});
        memberRepository.findByEmail(member.getMemberEmail())
                .ifPresent(m -> {throw new IllegalStateException("存在しているメールです。");});
        memberRepository.findByHp(member.getMemberHp())
                .ifPresent(m -> {throw new IllegalStateException("存在している携帯番号です。");});
        memberRepository.save(member);
    }


この場合、ControllerはIllegalStateExceptionが3つもあるので、**どのようにメッセージを具別し、メッセージを出力できるか、**ピントこなかったです。
そのため、例外クラスを直接、作成してみることにしました。


2.3つの例外クラスで処理してみる。

このように、3つの例外クラスを作成し、Serviceからthrowする例外クラスも変更しました。

DuplicatedEmailException.java
public class DuplicatedEmailException extends RuntimeException{
    public DuplicatedEmailException() {
    }

    public DuplicatedEmailException(String message) {
        super(message);
    }
}
MemberService.java

// IllegalStateException -> 3つの例外クラス
 public void join(MemberAddDTO memberAddDTO){
        Member member = MemberAddDTO.MemberAddDTOToMember(memberAddDTO);
        memberRepository.findById(member.getMemberId())
                .ifPresent(m -> {throw new DuplicatedIdException("存在しているIDです。");});
        memberRepository.findByEmail(member.getMemberEmail())
                .ifPresent(m -> {throw new DuplicatedEmailException("存在しているメールです。");});
        memberRepository.findByHp(member.getMemberHp())
                .ifPresent(m -> {throw new DuplicatedHpException("存在している携帯番号です。");});
        memberRepository.save(member);
    }

しかし、この場合、例外クラスも3つになるので、ControllerからもCatch文を3つ作成しなければいけなくなり、コードが汚くなる問題点がありました。

MemberController.java
memberService.join(memberAddDTO);
            return "redirect:/members/add-success";
        }
        catch (DuplicatedIdException e){
            bindingResult.rejectValue("memberId", null, e.getMessage());
            return "/members/add-member";
        }
        catch (DuplicatedEmailException e){
            bindingResult.rejectValue("memberEmail", null, e.getMessage());
            return "/members/add-member";
        }
        catch (DuplicatedHpException e){
            bindingResult.rejectValue("memberHp", null, e.getMessage());
            return "/members/add-member";
        }

    }


3.例外クラスを一つに減らす。

DupulicatedFieldException.java
@Getter
public class DuplicatedFieldException extends RuntimeException{

    private final String fieldName;

    public DuplicatedFieldException(String fieldName,String message) {
        super(message);
        this.fieldName = fieldName;
    }
}

MemberService
        memberRepository.findById(member.getMemberId())
                .ifPresent(m -> {throw new DuplicatedFieldException("memberId","存在しているIDです。");});
        memberRepository.findByEmail(member.getMemberEmail())
                .ifPresent(m -> {throw new DuplicatedFieldException("memberEmail",""存在しているメールです。");});
        memberRepository.findByHp(member.getMemberHp())
                .ifPresent(m -> {throw new DuplicatedFieldException("memberHp","存在している携帯番号です。");});
        memberRepository.save(member);
    }

これにより、例外が生じる場合、同じクラスのインスタンスですが、フィールドの値によってメッセージがことなくなるロジックを組みました。


MemberController.java

try{
            memberService.join(memberAddDTO);
            return "redirect:/members/add-success";
        }

        catch (DuplicatedFieldException e){
            bindingResult.rejectValue(e.getFieldName(),null,e.getMessage());
            return "members/add-member";

これにより、繰り返されるcatchを一つにまとめることができました。
例外処理が成功的にできた場合の画像です。

認証強化

以前、APIを設計する際に、URIを同じ場所に作成することをルールにしたと説明したのですが、InterCeptorの機能を活用したいと思ったからです。

以前の記事はこちらになります。


現在ログインした会員の会員番号は4番になっております。 
/members/4/show-info のURLがあり、2番目の「/」あとのみURIが出るようにAPIを設計したからです。

会員情報更新に入っても同じパターンでAPIが作成されたことが分かります。
現在のURLは /members/4/update になっております。


@Slf4j
public class MemberCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        HttpSession session = request.getSession(false);
        String requestedMemberNo = null;

        
        Member member = (Member) session.getAttribute("loginMember");
       
        if (member != null) {

            Long currentNoLong = member.getMemberNo();
            String currentNo = Long.toString(currentNoLong);

            String[] path = requestURI.split("/");

            if(path.length > 2 && path[1].equals("members")){
                requestedMemberNo = path[2];
            }

このようにクライアントRequestするURLから他の会員情報を見れる可能性があるので、それを防ぐため、Javaのsplitメソッドを活用し、URIを判別するコードを作成しました。
例えば、/members/4/show-infoの場合、「/」にsplit("/") に分けますと、
["","members","4","show-info"」のような配列になります。

"4"のようなURIは番インデックスに常にあるようにAPIを設計したので、一気にまとめることができました。

万が一、異なる会員情報にアクセスする場合は、404エラーを発生させ、エラーページをリターンし、サーバーにWARN levelのログを残すようにコードを作成しました。

          
    if (!currentNo.equals(requestedMemberNo)) {
        log.warn("{}番会員から {}番会員の情報に未認証アクセス", currentNo, requestedMemberNo);
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return false;
    }    

例えば、さきほどの4番会員が3番会員の情報にアクセスする場合は以下のように404エラーページが出ます。

Discussion