Spring Security源码(三) 授权分析

news2024/12/23 9:26:14

通过前面的分析我们知道经过filterchain的层层赛选后,请求来到了FilterSecurityInterceptor进行权限校验,那么其底层是如何实现的呢,通过本文带你了解其底层实现原理

一 授权流程整体分析

在这里插入图片描述

  • 当客户端向某个资源发起请求,请求到达FilterSecurityInterceptor,然后会调用其父类AbstractSecurityInterceptor
    beforeInvocation方法做授权之前的准备工作

  • 在beforeInvocation法中通过SecurityMetadataSource…getAttributes(object);获得资源所需要的访问权限 ,通过SecurityContextHolder.getContext().getAuthentication()获取当前认证用户的认证信息,即包含了认证信息和权限信息的Authentication对象

  • 然后FilterSecurityInterceptor通过调用AccessDecisionManager.decide(authenticated, object, attributes);进行授权(authenticated中有用户的权限列表,attributes是资源需要的权限),该方法使用投票器投票来决定用户是否有资源访问权限
    AccessDecisionManager接口有三个实现类,他们通过通过AccessDecisionVoter投票器完成投票,三种投票策略如下:
    AffirmativeBased : 只需有一个投票赞成即可通过
    ConsensusBased:需要大多数投票赞成即可通过,平票可以配置
    UnanimousBased:需要所有的投票赞成才能通过

  • 投票通过,访问到对应的站点

二 源码分析

2.1 FilterSecurityInterceptor的创建

通过package org.springframework.security.config.http.HttpConfigurationBuilder中createFilterSecurityInterceptor()创建FilterSecurityInterceptor

void createFilterSecurityInterceptor(BeanReference authManager) {  
    //判断是否配置了use-expressions属性  
  
    //使用 Spring 表达式语言配置访问控制 
    //注意下方备注
    boolean useExpressions = FilterInvocationSecurityMetadataSourceParser.isUseExpressions(httpElt);  
    //根据intercept-url标签列表创建授权需要的元数据信息。httpElt里面有要去的资源路径。根据它们得出securityMds。上面有说明。后面仔细分析  
    BeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser.createSecurityMetadataSource(interceptUrls, httpElt, pc);  
  
    RootBeanDefinition accessDecisionMgr;  
    //创建voter列表,方便使用投票器  
    ManagedList<BeanDefinition> voters =  new ManagedList<BeanDefinition>(2);  
    //根据不同情况使用不同投票器
    //如果是使用了表达式,使用WebExpressionVoter  
    //没使用表达式,就使用RoleVoter、AuthenticatedVoter  
    if (useExpressions) {  
        voters.add(new RootBeanDefinition(WebExpressionVoter.class));  
    } else {  
        voters.add(new RootBeanDefinition(RoleVoter.class));  
        voters.add(new RootBeanDefinition(AuthenticatedVoter.class));  
    }  
    //通过改变BeanDefinition里面的信息,来改变创建对象授权的决策管理类AffirmativeBased的bean
    accessDecisionMgr = new RootBeanDefinition(AffirmativeBased.class);  
    //添加依赖的voter列表  
    accessDecisionMgr.getPropertyValues().addPropertyValue("decisionVoters", voters);  
    accessDecisionMgr.setSource(pc.extractSource(httpElt));  
  
    // Set up the access manager reference for http  
    //access-decision-manager-ref属性,可以使我们手动注入AccessDecisionManager(认证管理器),下面是详细配置
    String accessManagerId = httpElt.getAttribute(""access-decision-manager-ref"");  
    //如果未定义access-decision-manager-ref属性,就使用默认的  
     //AffirmativeBased  
    if (!StringUtils.hasText(accessManagerId)) {  
        accessManagerId = pc.getReaderContext().generateBeanName(accessDecisionMgr);  
        pc.registerBeanComponent(new BeanComponentDefinition(accessDecisionMgr, accessManagerId));  
    }  
    //创建FilterSecurityInterceptor过滤器,通过BeanDefinitionBuilder来手动注入bean,下面有提示
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterSecurityInterceptor.class);  
    //往创建FilterSecurityInterceptor过滤器添加决策管理器  
    //	// 设置属性accessDecisionManager,此属性引用已经定义的String accessManagerId
    builder.addPropertyReference("accessDecisionManager", accessManagerId);  
    //往创建FilterSecurityInterceptor过滤器添加认证管理类  
    //设置属性authenticationManager,此属性引用已经定义的bean authManager
    builder.addPropertyValue("authenticationManager", authManager);  
  
    if ("false".equals(httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) {  
        builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE);  
    }  
    //添加授权需要的安全元数据资源  
    builder.addPropertyValue("securityMetadataSource", securityMds);  
    //得到FilterSecurityInterceptor过滤器的BeanDefinition,通过BeanDefinition创建bean
    BeanDefinition fsiBean = builder.getBeanDefinition();  
    //向ioc容器注册FilterSecurityInterceptor的bean  
    String fsiId = pc.getReaderContext().generateBeanName(fsiBean);  
    pc.registerBeanComponent(new BeanComponentDefinition(fsiBean,fsiId));  
  
    // Create and register a DefaultWebInvocationPrivilegeEvaluator for use with taglibs etc.  
    BeanDefinition wipe = new RootBeanDefinition(DefaultWebInvocationPrivilegeEvaluator.class);  
    wipe.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference(fsiId));  
  
    pc.registerBeanComponent(new BeanComponentDefinition(wipe, pc.getReaderContext().generateBeanName(wipe)));  
  
    this.fsi = new RuntimeBeanReference(fsiId);  
}  
  • BeanDefinition在Spring中是用来描述Bean对象的,其不是一个bean实例,仅仅是包含bean实例的所有信息,比如属性值、构造器参数以及其他信息。Bean对象创建是根据BeanDefinitionc中描述的信息来创建的,BeanDefinitionc存在的作用是为了可以方便的进行修改属性值和其他元信息,比如通过BeanFactoryPostProcessor进行修改一些信息,然后在创建Bean对象的时候就可以结合原始信息和修改后的信息创建对象了。
  • BeanDefinition spring一开始都是使用GenericBeanDefinition类保存Bean的相关信息,在需要时,在将其转换为其他的BeanDefinition类型
  • BeanDefinitionBuilder作用是手动向BeanDefinition注入信息然后通过BeanDefinition手动Spring容器中注入Bean

2.2 SecurityMetadataSource的创建

通过org.springframework.security.config.http.FilterInvocationSecurityMetadataSourceParser中的createSecurityMetadataSource用于SecurityMetadataSource的创建

static BeanDefinition createSecurityMetadataSource(List<Element> interceptUrls, Element elt, ParserContext pc) {  
    //创建Url处理类,有两个实现:AntUrlPathMatcher、RegexUrlPathMatcher是spring的路径匹配工具类
    UrlMatcher matcher = HttpSecurityBeanDefinitionParser.createUrlMatcher(elt);  
    boolean useExpressions = isUseExpressions(elt);  
    //解析intercept-url标签,构造所有需要拦截url的map信息  
     //map中的key:RequestKey的bean定义,value:SecurityConfig的bean定义  
    ManagedMap<BeanDefinition, BeanDefinition> requestToAttributesMap = parseInterceptUrlsForFilterInvocationRequestMap(  
            interceptUrls, useExpressions, pc);  
    BeanDefinitionBuilder fidsBuilder;  
  
    if (useExpressions) {  
        //定义表达式处理类的bean  
        Element expressionHandlerElt = DomUtils.getChildElementByTagName(elt, Elements.EXPRESSION_HANDLER);  
        String expressionHandlerRef = expressionHandlerElt == null ? null : expressionHandlerElt.getAttribute("ref");  
  
        if (StringUtils.hasText(expressionHandlerRef)) {  
            logger.info("Using bean '" + expressionHandlerRef + "' as web SecurityExpressionHandler implementation");  
        } else {  
            BeanDefinition expressionHandler = BeanDefinitionBuilder.rootBeanDefinition(DefaultWebSecurityExpressionHandler.class).getBeanDefinition();  
            expressionHandlerRef = pc.getReaderContext().generateBeanName(expressionHandler);  
            pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler, expressionHandlerRef));  
        }  
        //定义表达式类型的FilterInvocationSecurityMetadataSource  
        fidsBuilder = BeanDefinitionBuilder.rootBeanDefinition(ExpressionBasedFilterInvocationSecurityMetadataSource.class);  
        //通过构造函数注入依赖  
        fidsBuilder.addConstructorArgValue(matcher);  
        fidsBuilder.addConstructorArgValue(requestToAttributesMap);  
        fidsBuilder.addConstructorArgReference(expressionHandlerRef);  
    } else {  
        //定义非表达式类型的FilterInvocationSecurityMetadataSource  
        fidsBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultFilterInvocationSecurityMetadataSource.class);  
        //通过构造函数注入依赖  
        fidsBuilder.addConstructorArgValue(matcher);  
        fidsBuilder.addConstructorArgValue(requestToAttributesMap);  
    }  
  
    fidsBuilder.addPropertyValue("stripQueryStringFromUrls", matcher instanceof AntUrlPathMatcher);  
    fidsBuilder.getRawBeanDefinition().setSource(pc.extractSource(elt));  
  
    return fidsBuilder.getBeanDefinition();  
}  

2.3 FilterSecurityInterceptor鉴权

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  
        throws IOException, ServletException {  
    //封装request, response, chain,方便参数传递、增加代码阅读性  
    FilterInvocation fi = new FilterInvocation(request, response, chain);  
    invoke(fi);  
}  
  
public void invoke(FilterInvocation fi) throws IOException, ServletException {  
    if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)  
            && observeOncePerRequest) {  
        if (fi.getRequest() != null) {  
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);  
        }  
        //执行父类beforeInvocation,类似于aop中的before  
        InterceptorStatusToken token = super.beforeInvocation(fi);  
  
        try {  
            //filter传递  
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  
        } finally {  
            //执行父类的afterInvocation,类似于aop中的after  
            super.afterInvocation(token, null);  
        }  
    }  
}  

在FilterSecurityInterceptor中,会调用父类的beforeInvocation(filterInvocation)方法进行处理,最终返回一个InterceptorStatusToken对象,它就是spring security处理鉴权的入口。

	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		// 1. 判断object是不是FilterInvocation
		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
			throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
					+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
					+ getSecureObjectClass());
		}
		// 2. 获取配置的访问控制规则 any request =》authenticated ,没有配置,return null
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
		if (CollectionUtils.isEmpty(attributes)) {
			Assert.isTrue(!this.rejectPublicInvocations,
					() -> "Secure object invocation " + object
							+ " was denied as public invocations are not allowed via this interceptor. "
							+ "This indicates a configuration error because the "
							+ "rejectPublicInvocations property is set to 'true'");
			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Authorized public object %s", object));
			}
			publishEvent(new PublicInvocationEvent(object));
			return null; // no further work post-invocation
		}
		// 3. 判断认证对象Authentication是否为null
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"), object, attributes);
		}
		// 4. 获取Authentication对象
		Authentication authenticated = authenticateIfRequired();
		if (this.logger.isTraceEnabled()) {
			this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
		}
		// Attempt authorization
		// 5. 进行授权判断
		attemptAuthorization(object, attributes, authenticated);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
		}
		// 6. 发布授权成功
		if (this.publishAuthorizationSuccess) {
			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
		}

		// Attempt to run as a different user
		// 7. 对Authentication进行再处理,这里没有处理,直接返回null
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
		if (runAs != null) {
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
			}
			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
		this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
		// no further work post-invocation
		// 8. 返回InterceptorStatusToken
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

	}

在beforeInvocation方法中的核心方法为attemptAuthorization,它会调用授权管理器进行决策,当失败发生异常时,会爆出异常。

	/**
	 * 授权判断
	 *
	 * @param object        filter invocation [GET /test]
	 * @param attributes 配置的URL放行、需要验证路径等配置
	 * @param authenticated 认证对象
	 */
	private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
			Authentication authenticated) {
		try {
			// 1. 调用授权管理器进行决策
			this.accessDecisionManager.decide(authenticated, object, attributes);
		} catch (AccessDeniedException ex) {
			// 2. 访问被拒绝。抛出AccessDeniedException异常
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
						attributes, this.accessDecisionManager));
			} else if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
			}
			// 3. 发布授权失败事件
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
			throw ex;
		}
	}

决策者进行投票

调用授权管理器进行决策,会进入默认的决策器AffirmativeBased,上面说过它的投票机制,这里获取到的选民只有一个。
在这里插入图片描述
在这里插入图片描述

开始投票

进入WebExpressionVoter的vote方法开始投票。

	// 投票
	@Override
	public int vote(Authentication authentication, FilterInvocation filterInvocation,
			Collection<ConfigAttribute> attributes) {
		// 1. 校验参数
		Assert.notNull(authentication, "authentication must not be null");
		Assert.notNull(filterInvocation, "filterInvocation must not be null");
		Assert.notNull(attributes, "attributes must not be null");
		// 2. 获取http配置项
		WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);
		// 3. 没有配置规则,弃权
		if (webExpressionConfigAttribute == null) {
			this.logger
					.trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute");
			return ACCESS_ABSTAIN;
		}
		// 4. 对EL表达式进行处理
		EvaluationContext ctx = webExpressionConfigAttribute.postProcess(
				this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
		boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);
		if (granted) {
			// 5. 符合条件,赞成票
			return ACCESS_GRANTED;
		}
		this.logger.trace("Voted to deny authorization");
		// 6. 最后都没有则反对票
		return ACCESS_DENIED;
	}

授权成功处理

没有抛出异常,则认为授权通过,FilterSecurityInterceptor会进入finallyInvocation方法。这个方法主要是判断需不需要重新设置 SecurityContext内容,这里没有配置,直接跳过。

    protected void finallyInvocation(InterceptorStatusToken token) {
        if (token != null && token.isContextHolderRefreshRequired()) {
            SecurityContextHolder.setContext(token.getSecurityContext());
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.of(() -> {
                    return "Reverted to original authentication " + token.getSecurityContext().getAuthentication();
                }));
            }
        }
    }

接下来进入后置处理afterInvocation方法,再次调用了finallyInvocation方法,然后查询是否还有决策后置处理器,如果有,再次进行决策。最后的最后,才代表授权成功,就交由Spring MVC ,访问到我们的controller方法了。

    protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
        if (token == null) {
            return returnedObject;
        } else {
            this.finallyInvocation(token);
            if (this.afterInvocationManager != null) {
                try {
                    returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(), token.getSecureObject(), token.getAttributes(), returnedObject);
                } catch (AccessDeniedException var4) {
                    this.publishEvent(new AuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(), token.getSecurityContext().getAuthentication(), var4));
                    throw var4;
                }
            }
            return returnedObject;
        }
    }

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

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

相关文章

PYTHON链家租房数据分析:岭回归、LASSO、随机森林、XGBOOST、KERAS神经网络、KMEANS聚类、地理可视化...

全文下载链接:http://tecdat.cn/?p29480作者&#xff1a;Xingsheng Yang1 利用 python 爬取链家网公开的租房数据&#xff1b;2 对租房信息进行分析&#xff0c;主要对房租相关特征进行分析&#xff0c;并搭建模型用于预测房租&#xff08;点击文末“阅读原文”获取完整代码数…

Docker基础组件、安装启动和Docker生命周期

Docker安装部署 Docker引擎 运行镜像生成容器。应用程序跑在容器中 Docker Daemon 安装使用Docker&#xff0c;得先运行Docker Daemon进程&#xff0c;用于管理Docker&#xff0c;如&#xff1a; 镜像 images容器 containers网络 network数据卷 Data Volumes Rest接口 提…

【数据结构】图的实现

文章目录图1.图的基本概念2.图的存储结构3.邻接矩阵3.1邻接矩阵的优缺点3.2邻接矩阵的实现4.邻接表4.1邻接表的实现5.图的遍历5.1广度优先遍历5.2深度优先遍历5.3如何遍历不连通的图&#xff1f;图 1.图的基本概念 图是由顶点集合及顶点间的关系组成的一种数据结构&#xff1…

第二章 使用Maven:IDEA环境

前一章是在命令行环境下用Maven&#xff0c;也是为我们之后的操作打一个基础&#xff0c;但我想了一下&#xff0c;以后应该用得不多&#xff0c;所以这里就跳过了&#xff0c;直接学在IDEA中Maven的使用 我这里就直接简化笔记了&#xff0c;只把知识大致的整体架构写出来 一.创…

yolov5量化注意事项(二)

一、引言 前面的博文&#xff0c;是PTQ的注意事项。本篇文章是记录QAT部分需要修改的一些要点。 注&#xff1a;本文仅供自己的笔记作用&#xff0c;防止未来自己忘记一些坑的处理方式 QAT的大致流程&#xff1a;&#xff08;1&#xff09;训练生成基础模型&#xff0c;通常是…

Linux系统中驱动格式基本实现

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;编写Linux驱动格式与方法。 目录 第一&#xff1a;基本格式实验 1、编写外设结构体 2、定义IO复用寄存器组的基地址 3、定义访问指针 第二&#xff1a;实验程序编写 第一&#xff1a;基本格式实验 可以利用模仿C语言中结构…

第二十三章 原型链

一、原型链 1、含义&#xff1a;就是对象的访问机制。从任何一个数据类型触发&#xff0c;由_proto_串联起来的链状结构&#xff0c;就是原型链 2、原型对象 &#xff1a;是函数天生自带的一个属性 , prototype 是一个对象数据类型 3、对象原型&#xff1a; 是对象天生自带的…

数字化门店| 瑜伽馆管理系统小程序| 小程序开发教程

随着悦己消费及对自身形象的高需求增加&#xff0c;越来越多的女性加入了瑜伽队列&#xff0c;对需求者而言&#xff0c;在同城找到合适的场馆或专业的老师不太容易&#xff0c;毕竟瑜伽教练证很好考&#xff0c;门槛低&#xff0c;不少场馆的教练老师略显不专业。对商家来说&a…

前端部署iis后axios跨域请求问题

一. 安装 ARR 3.0&#xff08;Application Request Routing&#xff09; 访问 IIS 官方网站&#xff1a;https://www.iis.net/downloads/microsoft/application-request-routing &#xff0c;有两种安装方式。 一&#xff0c;通过 Web 平台安装程序&#xff08;Web Platform …

2023年天津/辽宁/安徽/北京DAMA-CDGA/CDGP数据治理认证报名

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

光源基础(3)——光的折射反射偏振原理

费马原理 费马原理:光线沿光程为平稳值的路径传播。 光场中从P点到Q点&#xff0c;一条实际光线满足其路径积分泛函的变分为0&#xff0c;即如下条件: 路径积分的平稳值是相对于临近路径积分值相对而言的&#xff0c;有三种基本含义: 极小值:这是最常见的情形(光的直线传播、…

Allegro如何制作routekeepin操作指导

Allegro如何制作routekeepin操作指导 Allegro上可以快捷的制作Route keepin ,让Cline以及铜皮都在routekeepin里面,以下图板框为例 具体操作如下 选择Edit-Z-copy命令 Options选择画在Route keepin-All层 Size选择Contract Offset输入19.8 相对于板框内缩0.5mm Find选择…

hive补全连续或非连续空值数据sql

目录 一、背景 二、测试数据准备 三、实现 一、背景 爬虫或业务场景运行中经常会出现丢数据的情况&#xff0c;可能随机丢一分钟&#xff0c;或者丢几十分钟&#xff0c;完全没有规律&#xff0c;如果想用上一个有效值来补全的话单纯用lag函数无法实现 二、测试数据准备 c…

骑行适合戴什么耳机,几款适合在骑行过程佩戴的耳机推荐

耳机的诞生解决了在封闭场合下需要接收声音的问题&#xff0c;因此更多的人也是对耳机产生了依赖&#xff0c;不管在什么场景下都需要耳机的陪伴&#xff0c;随着关于耳道病疾也愈发严重。而新型的骨传导耳机因为自身不入耳的特性&#xff0c;也是逐步受到更多的人喜爱&#xf…

Linux环境下MySQL的数据目录

MySQL的数据目录 数据库文件的存放路径 MySQL数据库文件的存放路径&#xff1a; /var/lib/mysqlmysql> show variables like datadir; -------------------------------- | Variable_name | Value | -------------------------------- | datadir | /var/…

Python 采集109个中国风风格PPT

PPT下载链接&#xff1a;https://pan.baidu.com/s/1PJ671YEj6M9khtdhh6TSxA?pwdg37t 提取码&#xff1a;g37t 源码下载链接&#xff1a;ppt.rar - 蓝奏云 采集的参数 page_count 1 # 每个栏目开始业务content"text/html; charsetgb2312"base_url "https:…

RabbitMQ中的集群架构介绍

文章目录前言一、普通集群(副本集群)1.架构图二、镜像集群1.架构图前言 在之前我们是以单节点的形式来运行mq。在真正的生产实践中&#xff0c;mq主要用来完成两个应用系统间的通信&#xff0c;如果在某一时刻mq宕机了&#xff0c;会导致系统瘫痪&#xff0c;就是无法进行通信…

美中嘉和在港交所招股书失效:去年亏损约5亿元,杨建宇为实控人

12月1日&#xff0c;贝多财经从港交所披露易了解到&#xff0c;美中嘉和医学技术发展集团股份有限公司&#xff08;下称“美中嘉和”&#xff09;的上市申请材料失效&#xff0c;目前已无法正常查看或下载。据贝多财经了解&#xff0c;美中嘉和于2022年5月31日在港交所递表。 公…

C++STL——vector类与模拟实现

vector类vector常用接口介绍初始化reserve与resizeassign缩容接口算法库中的findvector的底层小部分框架模拟实现vectot模拟vector的整体代码迭代器失效问题深层深浅拷贝问题vector vector是表示可变大小数组的序列容器,就像数组一样&#xff0c;采用连续存储空间来存储元素&a…

Three.js初识:渲染立方体、3d字体、修改渲染背景颜色

用场景对three.js进行渲染&#xff1a;场景、相机、渲染器 const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );const renderer new THREE.WebGLRenderer(); renderer.setSize( window.i…