java spring 向上转型_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);

}

}

}

authenticate()为账号校验的主方法,进入到其中的一个实现类ProviderManager中,会发现方法实际抛出是统一抛出的AuthenticationException异常,方法体内实则会出现很多的场景性的异常,如AccountStatusException、InternalAuthenticationServiceException等等。

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) {

do something...

}

如果父类引用实际指的是凭证错误,则进行密码错误提示。

ServletException和AuthenticationException是两个框架下的特定场景下的顶级异常,两个怎么建立联系呢?

答曰:直接将两个都统一转为Throwable可抛出的祖先异常,这样向下都可以转成他们自己了,以及各自场景下的所有异常了。

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

5a64a5fea9ff96d4b5bae7d9602b0715.png

be53e1e43363535c68b5ff279aa484ba.png

133d583b266582af391e6be82e439140.png

通过上面的图谱我们便知道了,ServletException、BadCredentialsException、DisabledException三个异常都可以向上转型为Throwable。

那是在哪里进行的向上类型转换的呢?

答曰:

public void login(String username, String password) throws ServletException{

something code ...

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;

}

在外层逻辑处理时,针对不同的业务场景我们便可以通过向下类型转化来完成,如使用instanceof来验证对象是否是子类的实例。

// 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");

}

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值