当前位置: 首页 > news >正文

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可抛出的祖先异常,这样向下都可以转成他们自己了,以及各自场景下的所有异常了。

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

ServletException-Servlet异常

BadCredentialsException-密码错误

DisabledException-账号被禁用

通过上面的图谱我们便知道了,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");
        }
    }
}

相关文章:

  • python学习记录-打印九九乘法表
  • Oracle数据库查看表空间是否为自增的
  • 将excel项目管理融入生活
  • Python正则表达式的7个使用典范
  • 线程组之间的JMeter传递变量
  • PIP总结
  • gulp压缩合并js与css
  • testng.xml 配置大全
  • 解决Mysql数据库提示innodb表不存在的问题!
  • pychar安装第三方库MySQL/mysqlclient报错:error: Microsoft V
  • 折叠机何时有?LG新商标给提示
  • vue项目搭建
  • keras 实现 GAN
  • Django下orm学习 一对多
  • 微信小程序开发:canvas 多行文字换行
  • 【comparator, comparable】小总结
  • Android单元测试 - 几个重要问题
  • Go 语言编译器的 //go: 详解
  • Golang-长连接-状态推送
  • JavaScript异步流程控制的前世今生
  • Spark in action on Kubernetes - Playground搭建与架构浅析
  • SpringBoot几种定时任务的实现方式
  • vue2.0一起在懵逼的海洋里越陷越深(四)
  • 第十八天-企业应用架构模式-基本模式
  • 翻译 | 老司机带你秒懂内存管理 - 第一部(共三部)
  • 后端_MYSQL
  • 计算机在识别图像时“看到”了什么?
  • 记一次删除Git记录中的大文件的过程
  • 近期前端发展计划
  • 实现菜单下拉伸展折叠效果demo
  • 收藏好这篇,别再只说“数据劫持”了
  • C# - 为值类型重定义相等性
  • ###C语言程序设计-----C语言学习(3)#
  • #define与typedef区别
  • $(function(){})与(function($){....})(jQuery)的区别
  • ${factoryList }后面有空格不影响
  • (Note)C++中的继承方式
  • (Python第六天)文件处理
  • (博弈 sg入门)kiki's game -- hdu -- 2147
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (转载)Google Chrome调试JS
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • .a文件和.so文件
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .L0CK3D来袭:如何保护您的数据免受致命攻击
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET 简介:跨平台、开源、高性能的开发平台
  • .NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)
  • .NET 中让 Task 支持带超时的异步等待
  • .net开发引用程序集提示没有强名称的解决办法
  • .NET开源项目介绍及资源推荐:数据持久层