Spring Security 如何进行权限验证
阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!
FilterSecurityInterceptor
FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说,权限验证是一系列业务逻辑处理完成以后,最后需要解决的问题。所以默认情况下 security 会把和权限有关的过滤器,放在 VirtualFilter chains 的最后一位。
FilterSecurityInterceptor.doFilter 方法定义了两个权限验证逻辑。分别是 super.beforeInvocation 方法代表的权限前置验证与 super.afterInvocation 代表的后置权限验证。
public void invoke(FilterInvocation fi) throws IOException, ServletException {if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) {// filter already applied to this request and user wants us to observe// once-per-request handling, so don't re-do security checkingfi.getChain().doFilter(fi.getRequest(), fi.getResponse());} else {// first time this request being called, so perform security checkingif (fi.getRequest() != null && observeOncePerRequest) {fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}
}
和身份验证的设计类似,前置权限验证的业务处理也是参考模板方法设计模式的理念,将核心处理流程定义在父类(super) AbstractSecurityInterceptor 中的。
InterceptorStatusToken token = super.beforeInvocation(fi);
再通过关联 AccessDecisionManager 的方式,把身份验证通过的认证对象委托给 AccessDecisionManager 执行。
Authentication authenticated = authenticateIfRequired();
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);// Attempt authorization
try {this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;
}// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object");}// no further work post-invocationreturn new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object);
}
else {if (debug) {logger.debug("Switching to RunAs Authentication: " + runAs);}SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// need to revert to token.Authenticated post-invocationreturn new InterceptorStatusToken(origCtx, true, attributes, object);
}
在前置权限校验执行完毕后,会使用 this.runAsManager.buildRunAs 与 SecurityContextHolder.getContext().setAuthentication(runAs) 方法将当前上下文中的身份认证成功的对象备份,然后重新拷贝一个新的鉴权成功的对象到上下文的中。
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);
这个处理初看会有点难以理解,但是仔细思考一下,这其实是一个友好且细腻的操作。
原因是 FilterSecurityInterceptor 在大部分情况下是最后一个默认过滤器,如果说还有后续过滤操作的话,那这些肯定是你的自定义的过滤器对象行为产生的。安全起见,security 将权限认证过后的 authenticated 对象拷贝了一份副本,方便用户在后续的自定义过滤器中使用。
即使自定义过滤器中不小心修改了全局上下文中的 authenticate 对象也无妨,因为 super.finallyInvocation 方法
finally {super.finallyInvocation(token);
}
会保证自定义过滤器全部执行完毕以后,再把 copy source 「归还」到上下文中。这样,在后续的业务处理中就不用担心获取到一个不安全地 Authentication 对象了。
PrePostAnnotationSecurityMetadataSource.getAttributes
在 security 中使用的几种鉴权注解一共有以下四种:
- PreFilter
- PreAuthorize
- PostFilter
- PostAuthorize
其中最常用的是 PreAuthorize,它的含义是在执行目标方法前执行鉴权逻辑。前置过滤器执行的第一个逻辑,就是使用 getAttributes 方法获取目标方法上的注解。
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {if (method.getDeclaringClass() == Object.class) {return Collections.emptyList();}logger.trace("Looking for Pre/Post annotations for method '" + method.getName()+ "' on target class '" + targetClass + "'");PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);PreAuthorize preAuthorize = findAnnotation(method, targetClass,PreAuthorize.class);PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);// TODO: Can we check for void methods and throw an exception here?PostAuthorize postAuthorize = findAnnotation(method, targetClass,PostAuthorize.class);if (preFilter == null && preAuthorize == null && postFilter == null&& postAuthorize == null) {// There is no meta-data so returnlogger.trace("No expression annotations found");return Collections.emptyList();}String preFilterAttribute = preFilter == null ? null : preFilter.value();String filterObject = preFilter == null ? null : preFilter.filterTarget();String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();String postFilterAttribute = postFilter == null ? null : postFilter.value();String postAuthorizeAttribute = postAuthorize == null ? null : postAuthorize.value();ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(preFilterAttribute, filterObject, preAuthorizeAttribute);if (pre != null) {attrs.add(pre);}PostInvocationAttribute post = attributeFactory.createPostInvocationAttribute(postFilterAttribute, postAuthorizeAttribute);if (post != null) {attrs.add(post);}attrs.trimToSize();return attrs;}
当通过 getAttributes 获取到对应的鉴权注解后,通过 createPreInvocationAttribute 将注解的 value——一般是 el 表达式,解析成 Express 对象后封装起来等待后续取用。
String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(preFilterAttribute, filterObject, preAuthorizeAttribute);
public PreInvocationAttribute createPreInvocationAttribute(String preFilterAttribute,String filterObject, String preAuthorizeAttribute) {try {// TODO: Optimization of permitAllExpressionParser parser = getParser();Expression preAuthorizeExpression = preAuthorizeAttribute == null ? parser.parseExpression("permitAll") : parser.parseExpression(preAuthorizeAttribute);Expression preFilterExpression = preFilterAttribute == null ? null : parser.parseExpression(preFilterAttribute);return new PreInvocationExpressionAttribute(preFilterExpression,filterObject, preAuthorizeExpression);}catch (ParseException e) {throw new IllegalArgumentException("Failed to parse expression '"+ e.getExpressionString() + "'", e);}}
AccessDecisionManager.decide -> AffirmativeBased.decide
说完了 getAttribute,让我们回到 beforeInvocation 方法的后续处理。你可能已经猜到了,和身份认证一样 filter 是不会实现实际的权限验证逻辑的。权限验证被交给了过滤器关联的 AccessDecisionManage.decide 方法来决定。
AccessDecisionManager 是一个接口,它的实现一共有 3 个类。AbstractAccessDecisionManager 是实现接口的抽象类,提供模板方法的定义。AffirmativeBased、ConsensusBased、UnanimousBased 是三个继承 AbstractAccessDecisionManager 的子类实现,分别代表了三种不同的记分策略。
以 AffirmativeBased 作为代表来说明的话,主要内容就是把 authentication 与封装的鉴权注解 el 表达式传递给关联的成员 decisionVoters 的 vote 方法进行处理。
public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {int deny = 0;for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);if (logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}
PreInvocationAuthorizationAdviceVoter.vote
和上面的架构设计一样, AccessDecisionVoter 的实现 PreInvocationAuthorizationAdviceVoter.vote 看起来像是要开始处理真正意义上的权限问题了,但是其实依然没到最核心的部分。
vote 方法会返回一个 preAdvice.before 的结果,这个结果会被转换成 ACCESS_GRANTED : ACCESS_DENIED 这两个数字。为什么 preAdvice.before 的调用不直接返回 boolean 类型的鉴权结果?想想之前的 AffirmativeBased.decide 方法的设计思路:根据多个不同的 voter 返回的结果,我们可以使用不同的投票策略来对分数进行判定。返回一个数字,而不是 boolean 更便于计算最终得分。
public int vote(Authentication authentication, MethodInvocation method,Collection<ConfigAttribute> attributes) {// Find prefilter and preauth (or combined) attributes// if both null, abstain// else call advice with themPreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);if (preAttr == null) {// No expression based metadata, so abstainreturn ACCESS_ABSTAIN;}boolean allowed = preAdvice.before(authentication, method, preAttr);return allowed ? ACCESS_GRANTED : ACCESS_DENIED;}
ExpressionBasedPreInvocationAdvice.before
这次我向你保证,ExpressionBasedPreInvocationAdvice.before 真的是专门负责判定权限校验是否通过的方法了。由于之前已经封装好了 el 表达式的 Express 对象,所以调用 ExpressionUtils.evaluateAsBoolean 就可以直接根据表达式与执行上下文 ctx -> WebSecurityExpressionRoot extends SecurityExpressionRoot 使用 getValue() 方法获取执行结果——即权限校验的结果。
public boolean before(Authentication authentication, MethodInvocation mi,PreInvocationAttribute attr) {PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,mi);Expression preFilter = preAttr.getFilterExpression();Expression preAuthorize = preAttr.getAuthorizeExpression();if (preFilter != null) {Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);expressionHandler.filter(filterTarget, preFilter, ctx);}if (preAuthorize == null) {return true;}return ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx);}
举一反三一下,既然鉴权逻辑就是一个运行时的 el 表达式解析后的结果,那利用 el 表达式可以调用类方法的特性,是不是自定义一个专门的表达式处理程序就可以随心所欲地返回鉴权结果了呢?
自定义 EL 表达式
/*** sysdomain + edit permission*/public static final String SYSDOMAIN_AND_EDITPERMISSION = SYSDOMAIN + " && " + EDIT_PERMISSION;public static final String EDIT_PERMISSION_MENU = "@authorizeAppService.hasPermission('EDIT_PERMISSION_MENU')";
自定义 EL 表达式处理方法
public class AuthorizeAppService implements AuthorizeAppInterface {@Overridepublic boolean hasDomain(String domain) throws IllegalArgumentException {if (StringUtils.isEmpty(domain)) {throw new IllegalArgumentException("hasDomain accepted a invalid express parameter");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return StringUtils.equals(domain, user.getDomain().getCode());}@Overridepublic boolean hasRole(String... role) throws IllegalArgumentException {if (ArrayUtils.isEmpty(role)) {throw new IllegalArgumentException("hasRole accepted a invalid express parameter\"");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return user.getAuthRoles().stream().map(AuthRole::getCode).collect(Collectors.toList()).containsAll(CollectionUtils.arrayToList(role));}@Overridepublic boolean hasPermission(String... permission) throws IllegalArgumentException {if (ArrayUtils.isEmpty(permission)) {throw new IllegalArgumentException("hasPermission accepted a invalid express parameter\"");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();Set<String> permissions = user.getAllpermissions().stream().map(AuthPermission::getCode).collect(Collectors.toSet());return permissions.containsAll(CollectionUtils.arrayToList(permission));}
}
最后,我们来探讨一下 security 为什么要费尽周折,通过一长串的聚合和依赖把权限校验这个处理流程传递这么长的路径来实现?为什么不在一开始就是 make el Express and return el.value() 搞定所有的事情?
我想这是因为 security 是一个 architect first 的设计产物。架构优先的设计,高扩展性都特别的重要。这一系列的聚合和依赖,都是「用组合解决问题」思想的体现。尽量用组合来解决问题会带来了很多好处,比如通过 GlobalMethodSecurityConfiguration 类进行的高度可配置化。
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware{@Beanpublic MethodSecurityMetadataSource methodSecurityMetadataSource() {List<MethodSecurityMetadataSource> sources = new ArrayList<>();ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(getExpressionHandler());MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();if (customMethodSecurityMetadataSource != null) {sources.add(customMethodSecurityMetadataSource);}boolean hasCustom = customMethodSecurityMetadataSource != null;boolean isPrePostEnabled = prePostEnabled();boolean isSecuredEnabled = securedEnabled();boolean isJsr250Enabled = jsr250Enabled();if (!isPrePostEnabled && !isSecuredEnabled && !isJsr250Enabled && !hasCustom) {throw new IllegalStateException("In the composition of all global method configuration, " +"no annotation support was actually activated");}if (isPrePostEnabled) {sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));}if (isSecuredEnabled) {sources.add(new SecuredAnnotationSecurityMetadataSource());}if (isJsr250Enabled) {GrantedAuthorityDefaults grantedAuthorityDefaults =getSingleBeanOrNull(GrantedAuthorityDefaults.class);Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context.getBean(Jsr250MethodSecurityMetadataSource.class);if (grantedAuthorityDefaults != null) {jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());}sources.add(jsr250MethodSecurityMetadataSource);}return new DelegatingMethodSecurityMetadataSource(sources);}}
异常处理
voter 与 advicer 只做他们的份内事——对鉴权结果投票并返回。不同的得分策略,决定了同样的分数在不同的策略下,可能会有不同的最终结果。
所以 AffirmativeBased.decide 如果得出了鉴权失败的结论的话就会抛出一个 AccessDeniedException 异常。这个异常会一直沿着调用链往上抛,直到抛到 FilterSecurityIntercptor 的调用者,ExceptionTranslationFilter 中。
if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}
ExceptionTranslationFilter 过滤器专门处理和异常相关的事情。它的 doFilter 中定义的 AccessDeniedException 捕获处理会将异常处理交由 accessDeniedHandler.handle 处理。同时得益于组合的设计思想,accessDeniedHandler.handle 也是可配置化的。
回忆之前的自定义身份验证异常处理类 GlobalExceptionHandler。现在只要增加一个新的接口实现并重写 handle 方法,就可以在一个类里面处理身份验证和鉴权异常了。
private void handleSpringSecurityException(HttpServletRequest request,HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {if (exception instanceof AuthenticationException) {logger.debug("Authentication exception occurred; redirecting to authentication entry point",exception);sendStartAuthentication(request, response, chain,(AuthenticationException) exception);}else if (exception instanceof AccessDeniedException) {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {logger.debug("Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",exception);sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication","Full authentication is required to access this resource")));}else {logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler",exception);accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);}}
}
@RestControllerAdvice
public class GlobalExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {log.error("spring security 认证发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, "身份认证失败");}@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {log.error("spring security 鉴权发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_FORBIDDEN, "鉴权失败");}
}
相关文章:
Spring Security 如何进行权限验证
阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大! FilterSecurityInterceptor FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说,权限验证是一系列业务逻辑处理完成以后,最后需要解决的…...

计算机砖头书的学习建议
纸上得来终觉浅,绝知此事要躬行,技术来源于实践,光看不练意义不大,过阵子全忘记,并且没有实践来深化理论认知。 “砖头书”通常指的是那些厚重、内容详实且权威的书籍,对于计算机科学领域而言,…...
我与C语言二周目邂逅vlog——7.预处理
C语言预处理详解 C语言预处理是编译过程中的重要组成部分,用于对源代码进行文本替换和修改。预处理发生在编译的前期,通过特定的指令来控制代码的编译行为,最终生成可以交给编译器进行进一步处理的代码。预处理的目的是简化代码编写…...
Python无监督学习中的聚类:K均值与层次聚类实现详解
📘 Python无监督学习中的聚类:K均值与层次聚类实现详解 无监督学习是一类强大的算法,能够在没有标签的数据集中发现结构与模式。聚类作为无监督学习的重要组成部分,在各类数据分析任务中广泛应用。本文将深入讲解聚类算法中的两种…...

C++ 中 new 和 delete 详解,以及与 C 中 malloc 和 free 的区别
1. C 中 new 和 delete 的基本用法 在 C 中,new 和 delete 是用来动态分配和释放内存的关键字,它们是面向对象的替代方式,提供了比 C 语言更优雅的内存管理工具。 1.1 new 的使用 new 用于从堆中分配内存,并且自动调用对象的构造…...

YOLOv11来了 | 自定义目标检测
概述 YOLO11 在 2024 年 9 月 27 日的 YOLO Vision 2024 活动中宣布:https://www.youtube.com/watch?vrfI5vOo3-_A。 YOLO11 是 Ultralytics YOLO 系列的最新版本,结合了尖端的准确性、速度和效率,用于目标检测、分割、分类、定向边界框和…...

Vue3 集成Monaco Editor编辑器
Vue3 集成Monaco Editor编辑器 1. 安装依赖2. 使用3. 效果 Monaco Editor (官方链接 https://microsoft.github.io/monaco-editor/)是一个由微软开发的功能强大的在线代码编辑器,被广泛应用于各种 Web 开发场景中。以下是对 Monaco Editor 的…...
一文详解Mysql索引
背景 索引是存储引擎用于快速找到一条记录的数据结构。索引对良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。接下来,就来详细探索一下索引。 索引是什么 索引(Index)是帮助数据库高效获取数据的…...

基于JAVA+SpringBoot+Vue的旅游管理系统
基于JAVASpringBootVue的旅游管理系统 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末附源码下载链接🍅 哈喽兄…...

STM32_实验3_控制RGB灯
HAL_Delay 是 STM32 HAL 库中的一个函数,用于在程序中产生一个指定时间的延迟。这个函数是基于系统滴答定时器(SysTick)来实现的,因此可以实现毫秒级的延迟。 void HAL_Delay(uint32_t Delay); 配置引脚: 点击 1 到 IO…...

RISC-V笔记——Pipeline依赖
1. 前言 RISC-V的RVWMO模型主要包含了preserved program order、load value axiom、atomicity axiom、progress axiom和I/O Ordering。今天主要记录下preserved program order(保留程序顺序)中的Pipeline Dependencies(Pipeline依赖)。 2. Pipeline依赖 Pipeline依赖指的是&a…...

构建后端为etcd的CoreDNS的容器集群(六)、编写自动维护域名记录的代码脚本
本文为系列测试文章,拟基于自签名证书认证的etcd容器来构建coredns域名解析系统。 一、前置文章 构建后端为etcd的CoreDNS的容器集群(一)、生成自签名证书 构建后端为etcd的CoreDNS的容器集群(二)、下载最新的etcd容…...

Leetcode 剑指 Offer II 098.不同路径
题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 一个机器人位于一个 m x n 网格的左上角 (起始点在下…...

LabVIEW智能螺杆空压机测试系统
基于LabVIEW软件开发的螺杆空压机测试系统利用虚拟仪器技术进行空压机的性能测试和监控。系统能够实现对螺杆空压机关键性能参数如压力、温度、流量、转速及功率的实时采集与分析,有效提高测试效率与准确性,同时减少人工操作,提升安全性。 项…...
在 Ubuntu 22.04 上安装 PHP 8.2
在 Ubuntu 22.04 上安装 PHP 8.2,可以按照以下步骤进行: 更新系统软件包: 首先,确保你的系统软件包是最新的。 sudo apt update sudo apt upgrade 安装 PHP PPA(Personal Package Archive): U…...

Java生死簿管理小系统(简单实现)
学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……) 2、学会Oracle数据库入门到入土用法(创作中……) 3、手把手教你开发炫酷的vbs脚本制作(完善中……) 4、牛逼哄哄的 IDEA编程利器技巧(编写中……) 5、面经吐血整理的 面试技…...

【VoceChat】一个即时聊天(IM)软件,又是一个可以嵌入任何网页聊天系统
为什么要搭建私人聊天软件 在当今数字化时代,聊天软件已经成为人们日常沟通和协作的重要工具。市面上的公共聊天平台虽然方便,但也伴随着诸多隐私、安全、广告和功能限制的问题。对于那些注重数据安全、追求高效沟通的个人或团队来说,搭建一…...

【LeetCode】动态规划—96. 不同的二叉搜索树(附完整Python/C++代码)
动态规划—96. 不同的二叉搜索树 题目描述前言基本思路1. 问题定义2. 理解问题和递推关系二叉搜索树的性质:核心思路:状态定义:状态转移方程:边界条件: 3. 解决方法动态规划方法:伪代码: 4. 进一…...

Nginx UI 一个可以管理Nginx的图形化界面工具
Nginx UI 是一个基于 Web 的图形界面管理工具,支持对 Nginx 的各项配置和状态进行直观的操作和监控。 Nginx UI 的功能非常丰富: 在线查看服务器 CPU、内存、系统负载、磁盘使用率等指标 在线 ChatGPT 助理 一键申请和自动续签 Let’s encrypt 证书 在…...

Vue向上滚动加载数据时防止内容闪动
目前的需求:当前组件向上滚动加载数据,dom加载完后,页面的元素位置不能发生变化 遇到的问题:加载完数据后,又把滚轮滚到之前记录的位置时,内容发生闪动 现在的方案: 加载数据之前记录整体滚动条…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...

协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...