Spring Security之安全异常处理

前言

在我们的安全框架中,不管是什么框架(包括通过过滤器自定义)都需要处理涉及安全相关的异常,例如:登录失败要跳转到登录页,访问权限不足要返回页面亦或是json。接下来,我们就看看Spring Security是怎么处理异常的!

什么是异常处理

在Spring Security中,特指对于安全异常的处理。

我们知道Spring Security主要是基于过滤器来实现的,因此每个安全过滤器都可能发生安全异常,所以处理逻辑会被散落在各个过滤器中。

Spring自然是不能忍受这种设计,于是就有了专门的安全异常处理。

注:下文我们都用异常处理来代指安全异常处理。

异常处理设计

Spring Security将安全异常分为两类。

  • AuthenticationException —— 认证异常
    认证异常

    认证异常触发原因描述
    BadCredentialsException无法识别凭证可能是没有凭证/无法解密/格式不对等
    UsernameNotFoundException没有找到用户用户名没有对应的账号
    SessionAuthenticationException认证过程中与session相关的校验。例如,控制多点登录时,当某用户多点登录超过规定数量就会发生session认证异常
    AuthenticationServiceException认证服务遇到无法处理的情况是触发认证服务异常
    ProviderNotFoundExceptionProviderManager没有配置任何的Provider没有Provider
    PreAuthenticatedCredentialsNotFoundException与第三方认证系统集成时,发现客户端没有传凭证前认证凭证没有找到
    AuthenticationCredentialsNotFoundException这个主要是鉴权的时候发现没有认证,就会抛出没有找到认证凭证。
    RememberMeAuthenticationException-主要与记住我功能,恢复登录态有关
    NonceExpiredException-这个主要与Digest认证方式有关
    AccountStatusException校验账号状态时触发账号状态异常
  • AccessDeniedException —— 访问拒绝异常
    访问拒绝异常

    访问拒绝异常触发原因描述
    AuthorizationServiceException遇到无法处理的鉴权时触发例如配置错误,数据类型错误
    CrsfException防御Crsf时触发这里有两个,分别对应WebFlux和WebMvc

    其实对于鉴权来说,只要发现权限不满足,都是直接抛出AccessDeniedException的。

认证异常和访问拒绝异常的区别

与访问拒绝异常相比,认证异常要复杂不少。这是由认证过程和认证方式的多样性导致的。

  • 认证过程:
    一个完整的用户密码认证过程各组件的调用关系和简化
    一个完整的用户密码认证过程各组件的调用关系和简化组件都有不少,更何况要捋清楚调用关系。上面也只能是给大家看看认证过程中需要干啥,有哪些组件负责。
  • 认证方式:
    这个就不多啰嗦,前面说认证过滤器的时候有说过。

异常处理器

异常类定义异常处理器
认证异常AuthenticationExceptionAuthenticationFailureHandler
访问异常AccessDeniedExceptionAccessDeniedHandler

为什么要搞两个异常,还要搞两个组件来处理呢?

  1. 从安全业务上说,本来就是两种业务,访问跟认证是两个事情。
  2. 从单一职责原则来说,肯定要进行拆分,因为这两个组件处理的是不同的异常。
  3. 一般而言,登录异常我们是需要重定向到登录页面的,而接口访问异常则不然,一般通过返回错误拒绝请求。

实际上,这两个组件定义的可以说一模一样:

public interface AuthenticationFailureHandler {void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throws IOException, ServletException;
}public interface AccessDeniedHandler {void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException;
}

除了方法名,和入参的异常不同,其他的都是一样的。甚至,如果我们进一步看看异常的定义的话,连异常定义也是类似的,都是继承于RuntimeException,没有任何其他多余的字段和逻辑。

AuthenticationFailureHandler的实现

AuthenticationFailureHandler描述
AuthenticationEntryPointFailureHandler通过AuthenticationEntryPoint组件处理
SimpleUrlAuthenticationFailureHandler重定向到指定URL,如果没有指定,则退化返回401
ForwardAuthenticationFailureHandler重定向到指定的URL,必须指定URL
ExceptionMappingAuthenticationFailureHandler通过匹配异常寻找对应的处理器,一般由用户自行配置。
DelegatingAuthenticationFailureHandler委托其他的处理器处理

这里有一个特殊的,他使用另一个组件AuthenticationEntryPoint进行处理。

AuthenticationEntryPoint

  • Http403ForbiddenEntryPoint
    他是处理登录异常的通用的可选方案,通常是AbstractPreAuthenticatedProcessingFilter(基于外部认证服务器进行认证)。核心逻辑:总是返回403。
    这个实现是用来兜底的,如果找不到其他的,那就会用他。

  • HttpStatusEntryPoint
    他是一种可选方案,直接返回一个用户指定的http状态,response.setStatus(this.httpStatus.value())。

  • LoginUrlAuthenticationEntryPoint
    如果我们使用的是UsernamePasswordAuthenticationFilter,那么默认使用的就是这个。其核心逻辑也比较简单明了,就是重定向到登录页面。如果我们往上一层对比到SimpleUrlAuthenticationFailureHandler 、ForwardAuthenticationFailureHandler ,他的区别在于如果我们指定了loginPage,那么就会使用他。他会自动识别是绝对地址还是相对地址进行拼接。

  • DigestAuthenticationEntryPoint
    显然,他是为DigestAuthenticationFilter服务的。他会设置一些与Digest相关的请求头,然后调用response.sendError方法处理。

  • BasicAuthenticationEntryPoint
    为BasicAuthenticationFilter服务。核心逻辑与Digest类似,也是设置相关请求头,通过response.sendError方法处理。

  • DelegatingAuthenticationEntryPoint
    委托。没有自己的逻辑,而是交给别的AuthenticationEntryPoint。

AccessDeniedHandler的实现

  • AccessDeniedHandlerImpl
    基础实现,也是默认实现。设置HTTP错误码-403,并转发到错误页面。
  • InvalidSessionAccessDeniedHandler
    显然是为了处理session失效异常的。不过有趣的是,官方在CsrfConfigurer中引入这个。并且是为了处理MissingCsrfTokenException的。并且为了单一职责,还构建了下面的委托处理器。
  • DelegatingAccessDeniedHandler
    委托处理器。他管理着哪些异常对应哪个处理器,并将当前异常的处理交付给对应的处理器处理。故而得名“委托”处理器,算是个代理人吧。当然,他要求必须有个兜底的默认处理器。
  • RequestMatcherDelegatingAccessDeniedHandler
    他也是委托处理器,不同点在于,他是RequestMatcherDelegating,也即基于RequestMatcher进行Request匹配处理器。
  • ObservationMarkingAccessDeniedHandler
    他是用来统计数据的,观察标记。
  • CompositeAccessDeniedHandler
    组合模式的实现,用来管理多个处理器。目前看的话,主要是为了统计服务,因为他会调用每一个处理器,这可能会出现问题。只有统计这个处理器,需要其他的处理器来实现真正的处理,需要配合。

到这里问一句,这里我们看到了几种设计模式?策略模式、委托模式、组合模式。可以看到Spring对于代码的追求,这也是我们阅读源码的目的之一,学习好的设计。而这背后都是设计原则。

异常处理原理

前面我们大概了解了异常处理的来龙去脉,知道了其核心组件。现在我们来深入了解其原理。

认证异常处理原理

要理解这个,就必须回顾一下认证流程(这里以默认的用户密码登录为例):

AbstractAuthenticationProcessingFilter#doFilter
> UsernamePasswordAuthenticationFilter#attemptAuthentication
|-> ProviderManager#authenticate
|-|-> AbstractUserDetailsAuthenticationProvider#authenticate
|-|-|->DaoAuthenticationProvider#retrieveUser
|-|-|-|->JdbcDaoImpl#loadUserByUsername
|-|-|->DaoAuthenticationProvider#additionalAuthenticationChecks
|-|-|->DaoAuthenticationProvider#createSuccessAuthentication
|-|-|->AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication
// 同层级的表示顺序调用,不同层级的:上层方法调用下层方法,是递进关系。层级减少表示方法返回

负责处理认证请求的AbstractAuthenticationProcessingFilter#doFilter方法中会捕获异常,并交给unsuccessfulAuthentication方法处理。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBeanimplements ApplicationEventPublisherAware, MessageSourceAware {protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {// 1. 清空安全上下文this.securityContextHolderStrategy.clearContext();// 2. 记住我功能,清空cookie,认证失败的处理this.rememberMeServices.loginFail(request, response);// 3. 通过认证失败处理处理this.failureHandler.onAuthenticationFailure(request, response, failed);}
}

默认情况下,会使用SimpleUrlAuthenticationFailureHandler重定向到登录页面。

什么?怎么知道是这个处理器?行吧,我们来看看FormLoginConfigurer的源码吧。

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extendsAbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {public FormLoginConfigurer() {// 调用父类构造器super(new UsernamePasswordAuthenticationFilter(), null);usernameParameter("username");passwordParameter("password");}/*** 在配置过目标过滤器之前,会先调用这个方法进行Configurer的初始化*/@Overridepublic void init(H http) throws Exception {// 初始化父类super.init(http);// 初始化默认的登录页面initDefaultLoginFilter(http);}
}public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>extends AbstractHttpConfigurer<T, B> {protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {// 调用另一个构造器this();this.authFilter = authenticationFilter;if (defaultLoginProcessingUrl != null) {// 由于FormLoginConfigurer的构造器中传的是null,因此不会走到这// 当然,由于这个方法是public,因此也可以在配置时被我们调用// 他无非就是指定什么地址是认证请求罢了loginProcessingUrl(defaultLoginProcessingUrl);}}protected AbstractAuthenticationFilterConfigurer() {// 构造器中设置登录页面urisetLoginPage("/login");}private void setLoginPage(String loginPage) {this.loginPage = loginPage;// 指定AuthenticationEntryPoint,后面异常处理过滤器用的是这个来处理没有登录的异常。this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);}@Overridepublic void init(B http) throws Exception {// 更新/初始化认证相关的默认组件updateAuthenticationDefaults();// 更新访问权限-认证页面、认证请求、认证失败updateAccessDefaults(http);// 注册默认的AuthenticationEntryPointregisterDefaultAuthenticationEntryPoint(http);}protected final void updateAuthenticationDefaults() {if (this.loginProcessingUrl == null) {loginProcessingUrl(this.loginPage);}if (this.failureHandler == null) {// 指定默认的异常跳转页面failureUrl(this.loginPage + "?error");}LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");}}public final T failureUrl(String authenticationFailureUrl) {// 就是这个啦T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));this.failureUrl = authenticationFailureUrl;return result;}protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {// 所谓注册就是注册到异常处理过滤器上// 思考个问题:下面这种处理方式不就耦合ExceptionHandlingConfigurer了吗?// 为什么不是像其他的sharedObject那样,直接放到HttpSecurityd#sharedObjects中,在ExceptionHandlingConfigurer再自行获取设置。// 答:如果这样的话,会导致BUG。init方法是在执行了用户配置方法之后在HttpSecurity构建过滤器链的时候调用的。有可能将用户配置的覆盖了。ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);if (exceptionHandling == null) {return;}exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),getAuthenticationEntryPointMatcher(http));}
}

从FormLoginConfigurer出发,我们知道UsernamePasswordAuthenticationFilter使用的是SimpleUrlAuthenticationFailureHandler,同时ExceptionTranslationFilter使用的是LoginUrlAuthenticationEntryPoint。但这个设计我没有很理解,个人觉得应该在顶层都使用AuthenticationFailureHandler才合理。不知道是不是为了区分场景。

  • 场景一:登录处理时发生的异常,直接被捕获处理了。
  • 场景二:是鉴权时发现没有任何凭证,由异常处理过滤器处理。

但SimpleUrlAuthenticationFailureHandler、LoginUrlAuthenticationEntryPoint,内部处理没有太大区别,都是为了跳转到登录页面。

访问拒绝异常处理原理

鉴权相关的,之前我们聊过,忘记的同学可以通过下面的链接再回忆复习一下。可能文章的题目可能说的权限配置,但同时也从原理上给大家分析了如何鉴权的。也正是因为有两种方式,所以没有单独写鉴权过滤器。因为基于HttpRequest的配置方式的鉴权原理是通过AuthorizationFilter,也就是过滤器实现的。而另一种权限配置方式-基于方法配置权限-则是通过AOP实现的。

  • Spring Security之基于方法配置权限
  • Spring Security之基于HttpRequest配置权限

ExceptionTranslationFilter

他主要负责处理的是鉴权过程中发生的异常。这里就包括用户权限不足的AccessDeniedException,以及鉴权时发现用户还没有登录而抛出的认证异常。

public class AuthorizationFilter extends GenericFilterBean {private Authentication getAuthentication() {Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();if (authentication == null) {throw new AuthenticationCredentialsNotFoundException("An Authentication object was not found in the SecurityContext");}return authentication;}
}
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {try {chain.doFilter(request, response);}catch (IOException ex) {throw ex;}catch (Exception ex) {// 尝试从异常堆栈中找到安全异常Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);if (securityException == null) {securityException = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}if (securityException == null) {// 不是安全异常,直接重新抛出rethrow(ex);}if (response.isCommitted()) {// 如果response已经提交,则抛出servlet异常throw new ServletException("Unable to handle the Spring Security Exception "+ "because the response is already committed.", ex);}// 处理安全异常handleSpringSecurityException(request, response, chain, securityException);}}private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, RuntimeException exception) throws IOException, ServletException {if (exception instanceof AuthenticationException) {// 处理认证异常handleAuthenticationException(request, response, chain, (AuthenticationException) exception);}else if (exception instanceof AccessDeniedException) {// 处理访问异常handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);}}private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, AuthenticationException exception) throws ServletException, IOException {// 这里是转换方法名,是一种代码追求、也是一种代码的自解释:对于上层方法的作用是处理认证异常,而处理认证异常的手段是发送开始认证(其实就是跳转到登录页面开始登录流程),因为走到这的都是鉴权时不存在凭证导致的认证异常。sendStartAuthentication(request, response, chain, exception);}private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {// 对于匿名用户或者不是记住我用户,直接跳转登录页开始登录流程sendStartAuthentication(request, response, chain,new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication","Full authentication is required to access this resource")));}else {// 正常用户则通过AccessDeniedHandler处理this.accessDeniedHandler.handle(request, response, exception);}}protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,AuthenticationException reason) throws ServletException, IOException {// SEC-112: Clear the SecurityContextHolder's Authentication, as the// existing Authentication is no longer considered validSecurityContext context = this.securityContextHolderStrategy.createEmptyContext();// 清空安全上下文		this.securityContextHolderStrategy.setContext(context);// 记录当前权限不足的请求,登录成功后可能需要自动跳转this.requestCache.saveRequest(request, response);// 通过AuthenticationEntryPoint处理,这里是跳转到登录页面this.authenticationEntryPoint.commence(request, response, reason);}
}

总结

  1. 异常处理体系包括
    异常定义分两类 —— 认证异常、访问拒绝异常(鉴权异常)
    异常处理器 —— AuthenticationFailureHandler、AccessDeniedHandler分别对应异常分类
    异常的处理 —— 认证过滤器和异常处理器
  2. 认证异常的处理一般是跳转到登录页面。
    访问异常的处理默认则是AccessDeniedHandlerImpl处理,发送403错误码或者跳转到错误页。

后记

前阵子搬家,一直在适应新屋子的生活节奏,拖了不少时间,对不住了各位。后面应该会回复正常。
至此,咱们聊了认证、鉴权、session、异常处理,接下来咱们聊聊认证过程中一些小的功能点,例如:登录后跳转到之前异常的请求、RemenberMe、多处登录控制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/378790.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

llama-index,uncharted and llama2:7b run locally to generate Index

题意&#xff1a;本地运行 llama-index、uncharted 以及 llama2:7b 来生成索引 问题背景&#xff1a; I wanted to use llama-index locally with ollama and llama3:8b to index utf-8 json file. I dont have a gpu. I use uncharted to convert docs into json. Now If it …

抖音短视频seo矩阵系统源码(搭建技术开发分享)

#抖音矩阵系统源码开发 #短视频矩阵系统源码开发 #短视频seo源码开发 一、 抖音短视频seo矩阵系统源码开发&#xff0c;需要掌握以下技术&#xff1a; 网络编程&#xff1a;能够使用Python、Java或其他编程语言进行网络编程&#xff0c;比如使用爬虫技术从抖音平台获取数据。…

大数据基础:Doris重点架构原理

文章目录 Doris重点架构原理 一、Apache Doris介绍 二、Apache Doris使用场景 三、Apache Doris架构原理 四、Apache Doris 特点 Doris重点架构原理 一、Apache Doris介绍 基于 MPP 架构的高性能、实时的分析型数据库&#xff0c;以极速易用的特点被人们所熟知&#xff…

Hadoop-34 HBase 安装部署 单节点配置 hbase-env hbase-site 超详细图文 附带配置文件

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; HadoopHDFSMapReduceHiveFlumeSqoopZookeeperHBase 正在 章节内容 上节我们完成了&#xff1a; HBase的由…

三、GPIO口

我们在刚接触C语言时&#xff0c;写的第一个程序必定是hello world&#xff0c;其他的编程语言也是这样类似的代码是告诉我们进入了编程的世界&#xff0c;在单片机中也不例外&#xff0c;不过我们的传统就是点亮第一个LED灯&#xff0c;点亮电阻&#xff0c;电容的兄弟&#x…

【PPT笔记】1-3节 | 默认设置/快捷键/合并形状

文章目录 说明笔记1 默认设置1.1 OFFICE版本选择1.1.1 Office某某数字专属系列1.1.2 Office3651.1.3 产品信息怎么看 1.2 默认设置1.2.1 暗夜模式1.2.2 无限撤回1.2.3 自动保存&#xff08;Office2013版本及以上&#xff09;1.2.4 图片压缩1.2.5 字体嵌入1.2.6 多格式导出1.2.7…

Python 在Word表格中插入、删除行或列

Word文档中的表格可以用于组织和展示数据。在实际应用过程中&#xff0c;有时为了调整表格的结构或适应不同的数据展示需求&#xff0c;我们可能会需要插入、删除行或列。以下提供了几种使用Python在Word表格中插入或删除行、列的方法供参考&#xff1a; 文章目录 Python 在Wo…

内容安全(深度行为检测技术、IPS、AV、入侵检测方法)

1、深度行为检测技术 深度行为检测技术&#xff1a;是一种基于深度学习和机器学习的技术&#xff0c;它通过分析用户在网络中的行为模式&#xff0c;识别异常或潜在威胁行为&#xff0c;从而保护网络安全和内容安全 分类&#xff1a; 深度包检测技术&#xff08;Deep Packet…

k8s核心操作_存储抽象_K8S中使用Secret功能来存储密码_使用免密拉取镜像_k8s核心实战总结---分布式云原生部署架构搭建033

注意在看的时候一定要把 dxxxx中的xxxx换成--o----c----k----e----r 然后我们再来看一个k8s中的secret的功能,这个功能 用来存储密码的,configMap是用来存配置的 比如我们有个pod,他的镜像,如果是需要密码的,那么 我们现在是从公共仓库拉取的,如果我们从私有仓库拉取,有密码…

无需业务改造,一套数据库满足 OLTP 和 OLAP,GaiaDB 发布并行查询能力

在企业中通常存在两类数据处理场景&#xff0c;一类是在线事务处理场景&#xff08;OLTP&#xff09;&#xff0c;例如交易系统&#xff0c;另一类是在线分析处理场景&#xff08;OLAP&#xff09;&#xff0c;例如业务报表。 OLTP 数据库擅长处理数据的增、删、改&#xff0c…

【Arduino IDE】安装及开发环境、ESP32库

一、Arduino IDE下载 二、Arduino IDE安装 三、ESP32库 四、Arduino-ESP32库配置 五、新建ESP32-S3N15R8工程文件 乐鑫官网 Arduino官方下载地址 Arduino官方社区 Arduino中文社区 一、Arduino IDE下载 ESP-IDF、MicroPython和Arduino是三种不同的开发框架&#xff0c;各自适…

防火墙之双机热备篇

为什么要在防火墙上配置双机热备技术呢&#xff1f; 相信大家都知道&#xff0c;为了提高可靠性&#xff0c;避免单点故障 肯定有聪明的小伙伴会想到那为什么不直接多配置两台防火墙&#xff0c;然后再将他们进行线路冗余&#xff0c;不就完成备份了吗&#xff1f; 答案是不…

使用Django框架实现音频上传功能

数据库设计&#xff08;models.py&#xff09; class Music(models.Model):""" 音乐 """name models.CharField(verbose_name"音乐名字", max_length32)singer models.CharField(verbose_name"歌手", max_length32)# 本质…

R语言实现神经网络ANN

# 常用激活函数 # 自定义Sigmoid函数 sigmod <- function(x){return(1/(1exp(-x))) } # 绘制Sigmoid曲线 x <- seq(-10,10,length.out 100) plot(x,sigmod(x),type l,col blue,lwd 2,xlab NA,ylab NA,main Sigmoid函数曲线)# 自定义Tanh函数 tanh <- function(…

QT-RTSP相机监控视频流

QT-RTSP相机监控视频流 一、演示效果二、关键程序三、下载链接 一、演示效果 二、关键程序 #include "mainwindow.h"#include <QDebug>MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), m_settings("outSmart", "LiveWatcher&…

Web开发:卡片翻转效果(HTML、CSS)

目录 一、实现效果 二、完整代码 三、实现过程 1、页面结构 2、初始样式 3、翻转效果 4、图片大小问题 一、实现效果 如下图所示&#xff0c;当鼠标移入某个盒子&#xff0c;就反转这个盒子&#xff0c;并显示其背面的内容——卡片翻转效果&#xff1b; 卡片翻转效果 二…

Java二十三种设计模式-工厂方法模式(2/23)

工厂方法模式&#xff1a;设计模式中的瑞士军刀 引言 在软件开发中&#xff0c;工厂方法模式是一种常用的创建型设计模式&#xff0c;它用于处理对象的创建&#xff0c;将对象的实例化推迟到子类中进行。这种模式不仅简化了对象的创建过程&#xff0c;还提高了代码的可维护性…

基于Vue CLI 3构建Vue3项目(Vue2也可参考)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

Android Framework学习笔记(4)----Zygote进程

Zygote的启动流程 Init进程启动后&#xff0c;会加载并执行init.rc文件。该.rc文件中&#xff0c;就包含启动Zygote进程的Action。详见“RC文件解析”章节。 根据Zygote对应的RC文件&#xff0c;可知Zygote进程是由/system/bin/app_process程序来创建的。 app_process大致处…

【Java--数据结构】二叉树oj题(上)

前言 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 判断是否是相同的树 oj链接 要判断树是否一样&#xff0c;要满足3个条件 根的 结构 和 值 一样左子树的结构和值一样右子树的结构和值一样 所以就可以总结以下思路…