• Spring Security中异常上抛机制及对于转型处理的一些感悟


    在使用Spring Security的过程中,我们会发现框架内部按照错误及问题出现的场景,划分出了许许多多的异常,但是在业务调用时一般都会向外抛一个统一的异常出来,为什么要这样做呢,以及对于抛出来的异常,我们又该如何分场景进行差异化的处理呢,今天来跟我一起看看吧。

     一个登陆场景下的外层代码

        @PostMapping("/login")
        public void login(@NotBlank String username,
                            @NotBlank String password, HttpServletRequest request) {
            try {
                request.login(username, password);
                System.out.println("login success");
            } catch (ServletException authenticationFailed) {
                System.out.println("a big exception authenticationFailed");
            }
        }
    

     request.login(username,password)跳入到了HttpServlet3RequestFactory类中,点击去发现login方法只是统一向外抛出了一个ServletException异常。

            public void login(String username, String password) throws ServletException {
                if (this.isAuthenticated()) {
                    throw new ServletException("Cannot perform login for '" + username + "' already authenticated as '" + this.getRemoteUser() + "'");
                } else {
                    AuthenticationManager authManager = HttpServlet3RequestFactory.this.authenticationManager;
                    if (authManager == null) {
                        HttpServlet3RequestFactory.this.logger.debug("authenticationManager is null, so allowing original HttpServletRequest to handle login");
                        super.login(username, password);
                    } else {
                        Authentication authentication;
                        try {
                            authentication = authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
                        } catch (AuthenticationException var6) {
                            SecurityContextHolder.clearContext();
                            throw new ServletException(var6.getMessage(), var6);
                        }
    
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }  

    但是在ProviderManager类中的public Authentication authenticate(Authentication authentication) throws AuthenticationException {}

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            Class<? extends Authentication> toTest = authentication.getClass();
            AuthenticationException lastException = null;
            Authentication result = null;
            boolean debug = logger.isDebugEnabled();
            Iterator var6 = this.getProviders().iterator();
    
            while(var6.hasNext()) {
                AuthenticationProvider provider = (AuthenticationProvider)var6.next();
                if (provider.supports(toTest)) {
                    if (debug) {
                        logger.debug("Authentication attempt using " + provider.getClass().getName());
                    }
    
                    try {
                        result = provider.authenticate(authentication);
                        if (result != null) {
                            this.copyDetails(authentication, result);
                            break;
                        }
                    } catch (AccountStatusException var11) {
                        this.prepareException(var11, authentication);
                        throw var11;
                    } catch (InternalAuthenticationServiceException var12) {
                        this.prepareException(var12, authentication);
                        throw var12;
                    } catch (AuthenticationException var13) {
                        lastException = var13;
                    }
                }
            }
    
            if (result == null && this.parent != null) {
                try {
                    result = this.parent.authenticate(authentication);
                } catch (ProviderNotFoundException var9) {
                    ;
                } catch (AuthenticationException var10) {
                    lastException = var10;
                }
            }
    
            if (result != null) {
                if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                    ((CredentialsContainer)result).eraseCredentials();
                }
    
                this.eventPublisher.publishAuthenticationSuccess(result);
                return result;
            } else {
                if (lastException == null) {
                    lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
                }
    
                this.prepareException((AuthenticationException)lastException, authentication);
                throw lastException;
            }
        }
    

    这里就涉及到了多态的知识点,异常的多态。如子异常AccountStatusException都可以向上转型为统一的验证异常AuthenticationException。

    在设计之初的时候,验证类统一的父级异常是AuthenticationException。然后根据业务需求向下拓展出了很多个场景性质的异常,可能有十个、一百个、一千个。

    但是这些具体的场景异常都是从AuthenticationException延伸出来的。

    在这个验证登陆的方法中,会验证各种场景下登陆是否合法,就有可能出现很多的异常场景,诸如:
    • 密码不正确 BadCredentialsException
    • 账号是否被锁定 LockedException
    • 账号是否被禁用 DisabledException
    • 账号是否在有效期内 AccountExpiredException
    • 密码失效 CredentialsExpiredException
    ...几十个几百个异常,如果每个都需要事无巨细的抛出,那你需要在方法后面写几百个异常。
     
    但是你会发现在验证方法那里统一抛出的是他们的统一父类AuthenticationException,这里用到的就是自动的向上转型。
    到业务层我们拿到AuthenticationException后,需要进行对特定场景下的业务处理,如不同的异常错误返回提示不一样,这个时候就需要用到向下转型。
    Throwable throwable = authenticationFailed.getRootCause();
    if (throwable instanceof BadCredentialsException) {}
    如果父类引用实际指的是凭证错误,则进行密码错误提示,这里又有一个骚操作,ServletException和AuthenticationException是两个框架下的顶级父级别的异常,两个怎么建立联系,直接将两个都统一转为Throwable可抛出的祖先异常,这样向下都可以转成他们自己了,以及各自场景下的所有异常了。

    两个场景下的异常类关系图谱

    ServletException 

    ServletException可以向上转型为Throwable

    BadCredentialsException,密码错误

    BadCredentialsException可以向上转型为Throwable

    账号被禁用,DisabledException

    DisabledException可以向上转型为Throwable 
    怎么转过去的?
    public void login(String username, String password) throws ServletException{
    ...
    catch (AuthenticationException loginFailed) {
    SecurityContextHolder.clearContext();
    throw new ServletException(loginFailed.getMessage(), loginFailed);
    }
    }
    
    // 在捕获到异常之后会构建一个ServletException并将AuthenticationException统一的包装进去,比如说内部报了BadCredentialsException,那么在这里就会向上转型为Throwable
    public ServletException(String message, Throwable rootCause) {
        super(message, rootCause);
    }
    // 在Throwable类中会将最下面冒出来的异常传给cause,getRootCause就能获得异常的具体原因
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
    
    // Throwable向下转型BadCredentialsException
    if (throwable instanceof BadCredentialsException)

    调整后的代码

    在外层根据不同异常而做不同的业务处理的代码就可以改造为如下

        @PostMapping("/login")
        public void login(@NotBlank String username,
                            @NotBlank String password, HttpServletRequest request) {
            try {
                request.login(username, password);
                System.out.println("login success");
            } catch (ServletException authenticationFailed) {
                Throwable throwable = authenticationFailed.getRootCause();
                if (throwable instanceof BadCredentialsException) {
                    System.out.println("user password is wrong");
                }else if (throwable instanceof DisabledException){
                    System.out.println("user is disabled");
                }else {
                    System.out.println("please contact the staff");
                }
            }
        }

  • 相关阅读:
    问题建模---大纲---待补充
    塞库报表封装问题分析--一篇不太成功的问题分析报告
    哲学的根本问题--以人为本
    什么是本体论
    知行合一是做人的最高境界
    什么是问题?--人类才是最大的问题--所有的问题都是在人类认识世界和改造世界中产生的
    还原论与what、how、why
    selinux 开启和关闭
    Macbook上打开多个终端的方法
    PHPStorm 快捷键大全(Win/Linux/Mac)
  • 原文地址:https://www.cnblogs.com/lingyejun/p/10011380.html
Copyright © 2020-2023  润新知