1. 程式人生 > >Spring Security中異常上拋機制及對於轉型處理的一些感悟

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)