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, "鉴权失败");}
}

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

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

相关文章

EasyOCR——超强超便捷的OCR开源算法介绍与文本检测模型CRAFT微调方法

背景 最近在实际操作阿拉伯文小语种OCR功能的时候&#xff0c;尝试了诸多开源算法&#xff0c;但效果均不尽如人意。 说实在的&#xff0c;针对阿拉伯文的OCR开源算法&#xff0c;若仅仅是效果没那么优秀&#xff0c;比如识别率能有个70%80%&#xff0c;我还能微调微调&#…

【React系列三】—React学习历程的分享

一、组件实例核心—Refs 通过定义 ref 属性可以给标签添加标识 字符串形式的Refs 这种形式已经不再推荐使用&#xff0c;官方不建议使用 https://zh-hans.legacy.reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs 回调形式的Refs <script type"te…

PostgreSQL中触发器递归的处理 | 翻译

许多初学者在某个时候都会陷入触发器递归的陷阱。通常&#xff0c;解决方案是完全避免递归。但对于某些用例&#xff0c;您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息“超出堆栈深度限制”所困扰&#xff0c;那么这里就是解决方案。…

Pytest参数详解 — 基于命令行模式!

1、--collect-only 查看在给定的配置下哪些测试用例会被执行 2、-k 使用表达式来指定希望运行的测试用例。如果测试名是唯一的或者多个测试名的前缀或者后缀相同&#xff0c;可以使用表达式来快速定位&#xff0c;例如&#xff1a; 命令行-k参数.png 3、-m 标记&#xff08;…

msql事务隔离级别 线上问题

1. 对应代码 解决方式&#xff1a; 在事务隔离级别为可重复读&#xff08;RR&#xff09;时&#xff0c;数据库确实通常会记录当前数据的快照。 在可重复读隔离级别下&#xff0c;事务在执行期间看到的数据是事务开始时的数据快照&#xff0c;即使其他事务对数据进行了修改&am…

Lucas带你手撕机器学习——线性回归

什么是线性回归 线性回归是机器学习中的基础算法之一&#xff0c;用于预测一个连续的输出值。它假设输入特征与输出值之间的关系是线性关系&#xff0c;即目标变量是输入变量的线性组合。我们可以从代码实现的角度来学习线性回归&#xff0c;包括如何使用 Python 进行简单的线…

2024 最新版1200道互联网大厂Java面试题附答案详解

很多 Java 工程师的技术不错&#xff0c;但是一面试就头疼&#xff0c;10 次面试 9 次都是被刷&#xff0c;过的那次还是去了家不知名的小公司。 问题就在于&#xff1a;面试有技巧&#xff0c;而你不会把自己的能力表达给面试官。 应届生&#xff1a;你该如何准备简历&#…

4、CSS3笔记

文章目录 四、CSS3CSS3简介css3概述CSS3私有前缀什么是私有前缀为什么要有私有前缀常见浏览器私有前缀 CSS3基本语法CSS3新增长度单位CSS3新增颜色设置方式CSS3新增选择器CSS3新增盒模型相关属性box-sizing 怪异盒模型resize 调整盒子大小box-shadow 盒子阴影opacity 不透明度 …

【ChatGPT插件漏洞三连发之一】未授权恶意插件安装

漏洞 要了解第一个漏洞&#xff0c;我们必须首先向您展示 OAuth 身份验证的工作原理&#xff1a; 假设您是 Dan&#xff0c;并且您想使用您的 Facebook 帐户连接到 Example.com。当您点击“使用Facebook登录”时会发生什么&#xff1f; 在步骤 2-3 中&#xff1a; 在 Dan 单…

QT枚举类型转字符串和使用QDebug<<重载输出私有枚举类型

一 将QT自带的枚举类型转换为QString 需要的头文件&#xff1a; #include <QMetaObject> #include <QMetaEnum> 测试代码 const QMetaObject *metaObject &QImage::staticMetaObject;QMetaEnum metaEnum metaObject->enumerator(metaObject->indexOf…

【ubuntu18.04】ubuntu18.04升级cmake-3.29.8及还原系统自带cmake操作说明

参考链接 cmake升级、更新&#xff08;ubuntu18.04&#xff09;-CSDN博客 升级cmake操作说明 下载链接 Download CMake 下载版本 下载软件包 cmake-3.30.3-linux-x86_64.tar.gz 拷贝软件包到虚拟机 cp /var/run/vmblock-fuse/blockdir/jrY8KS/cmake-3.29.8-linux-x86_64…

详解mac系统通过brew安装mongodb与使用

本文目录 一、通过brew安装MongoDB二、mongodb使用示例1、启动数据库2、创建/删除数据库3、创建/删除集合 三、MongoDB基本概念1&#xff09;数据库 (database)2&#xff09;集合 &#xff08;collection&#xff09;3) 文档&#xff08;document&#xff09;4&#xff09;mong…

什么是感知与计算融合?

感知与计算融合&#xff08;Perception-Computing Fusion&#xff09;是指将感知技术&#xff08;如传感器、摄像头等&#xff09;与计算技术&#xff08;如数据处理、人工智能等&#xff09;有机结合&#xff0c;以实现对环境的更深层次理解和智能反应的过程。该技术广泛应用于…

基于ISO13400实现的并行刷写策略

一 背景及挑战 随着车辆智能化的逐渐普及&#xff0c;整车控制器数量的急剧增加&#xff0c;加之软件版本的迭代愈发频繁&#xff0c;使整车控制器刷写的数据量变得越来越大。面对如此多的控制器刷写&#xff0c;通过传统的控制器顺序刷写则易出现刷写时间过长的情况&#xff…

将本地文件上传到GIT上

上传文件时&#xff0c;先新建一个空文件&#xff0c;进行本地库初始化&#xff0c;再进行远程库克隆&#xff0c;将要上传的文件放到克隆下来的文件夹里边&#xff0c;再进行后续操作 1.在本地创建文件夹&#xff0c;将要上传的文件放在该文件下 2.在该文件页面中打开Git Bas…

免登录H5快手商城系统/抖音小店商城全开源运营版本

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 最近因为直播需要然后在互站花500买了一套仿抖音的商城系统&#xff0c;感觉确实还可以&#xff0c;反正都买了所以就分享给有需要的人 以下是互站那边的网站介绍可以了看一下&#…

【路径规划】基于蚁群算法的飞行冲突解脱

摘要 飞行冲突解脱是空中交通管理中的重要问题&#xff0c;确保飞机之间安全的距离避免冲突尤为重要。本文提出了一种基于蚁群算法的飞行冲突解脱方法&#xff0c;通过优化飞行器的路径&#xff0c;实现冲突的有效解脱。蚁群算法是一种模拟蚂蚁觅食行为的启发式算法&#xff0…

大厂为什么要禁止使用数据库自增主键

大表为何不能用自增主键&#xff1f; 数据库自增主键&#xff0c;以mysql为例&#xff0c;设置表的ID列为自动递增&#xff0c;便可以在插入数据时&#xff0c;ID字段值自动从1开始自动增长&#xff0c;不需要人为干预。 在小公司&#xff0c;或者自己做项目时&#xff0c;设置…

爬虫基础--requests模块

1、requests模块的认识 requests模块的认识请跳转到 requests请求库使用_使用requests库-CSDN博客 2、爬取数据 这里我们以b站动漫追番人数为例。 首先进去b站官网 鼠标右键点击检查或者键盘的F12&#xff0c;进入开发者模式。&#xff08;这里我使用的是谷歌浏览器为例&#…

二分查找_ x 的平方根搜索插入位置山脉数组的峰顶索引

x 的平方根 在0~X中肯定有数的平方大于X&#xff0c;这是肯定的。我们需要从中找出一个数的平方最接近X且不大于X。0~X递增&#xff0c;它们的平方也是递增的&#xff0c;这样我们就可以用二分查找。 我们找出的数的平方是<或者恰好X&#xff0c;所以把0~X的平方分为<X …