NET中使用Identity+CodeFirst+Jwt实现登录、鉴权

目录

前言

一、创建上下文类

1.自定义MyContext上下文类继承IdentityDbContext

2.在Program中添加AddDbContext服务

二、使用Migration数据迁移

1.在控制台中 依次使用add-migration 、updatebase 命令 

2.如何修改表名 

3.如何自定义字段

三、使用Identity实现登录、修改密码

1.在Program中 添加AddIdentityCore服务、AddRoleManager、AddUserManager配置

2.在控制器注入UserManager、RoleManager服务

四、使用JWT实现权限验证

1.在启动类Program.cs中配置Swagger可以输入身份验证方式

2.配置类信息、AddAuthentication服务

3.在登录的接口中返回token

4.在需要鉴权的接口加上 [Authorize]

总结


前言

identity

ASP.NET Core提供了标识(identity)框架,它采用RBAC(role-based access control,基于角色的访问控制)策略,内置了对用户、角色等表的管理及相关的接口,从而简化了系统的开发。

CodeFirst

先创建实体类,再通过实体类反向的创建数据库和表结构

什么是JWT?
JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)

JWT组成
JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)

头信息指定了该JWT使用的签名算法,HS256 表示使用了 HMAC-SHA256 来生成签名。
消息体包含了JWT的意图
未签名的令牌由base64url编码的头信息和消息体拼接而成(使用"."分隔),签名则通过私有的key计算而成。
最后在未签名的令牌尾部拼接上base64url编码的签名(同样使用"."分隔)就是JWT了
典型的JWT的格式:xxxxx.yyyyy.zzzzz


创建上下文类

  安装Microsoft.EntityFrameworkCore

  安装Microsoft.AspNetCore.Identity.EntityFrameworkCore 

1.自定义MyContext上下文类继承IdentityDbContext

示例如下:

    public class MyContext : IdentityDbContext
    {
        public MyContext(DbContextOptions<MyContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);      
        }

    }

2.在Program中添加AddDbContext服务

安装Microsoft.EntityFrameworkCore.SqlServer 

示例如下:


builder.Services.AddDbContext<MyContext>(options =>
{
    var connectionStr = builder.Configuration.GetConnectionString("SqlServer:Connection");
    options.UseSqlServer(connectionStr);
});

在配置文件中appsettings.json配置连接字符串

  "ConnectionStrings": {
    "sqlserver": {
      "Connection": "Server=服务器名称;User Id=账号;Password=密码;Database=数据库;MultipleActiveResultSets=true;Encrypt=True;TrustServerCertificate=True;"
    }
  }

二、使用Migration数据迁移

安装Microsoft.EntityFrameworkCore.Tools 

1.在控制台中 依次使用add-migration 、updatebase 命令 

如图所示

执行成功后 去数据库看数据库已经建立好了

效果如下:

2.如何修改表名 

生成的表都默认是带有AspNet 觉得不喜欢,那怎么修改呢

使用 FluentAPI配置

示例如下:


        public class UserConfig : IEntityTypeConfiguration<IdentityUser>
        {
            public void Configure(EntityTypeBuilder<IdentityUser> builder)
            {
                builder.ToTable("User");
            }
        }

        public class RoleConfig : IEntityTypeConfiguration<IdentityRole>
        {
            public void Configure(EntityTypeBuilder<IdentityRole> builder)
            {
                builder.ToTable("Role");
            }
        }

        public class UserRoleConfig : IEntityTypeConfiguration<IdentityUserRole<string>>
        {
            public void Configure(EntityTypeBuilder<IdentityUserRole<string>> builder)
            {
                builder.ToTable("UserRole");
            }
        }

 在OnModelCreating方法中加入


 // 反射中找项目下所有 继承IEntityTypeConfiguration的配置
 modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);

再次执行add-migration 、updatebata 命令 

再去数据库查看

已经成功修改我们想要的表名了

3.如何自定义字段

比如我想在用户表中添加年龄字段,创建新的用户类去继承IdentityUser类

示例如下:

    public class User: IdentityUser
    {
        /// <summary>
        ///   年龄
        /// </summary>
        public int? Age { get; set; }
        /// <summary>
        /// 备注
        /// </summary>
        public string ReMark { get; set; }
    }

在UserConfig类中修改成User

        public class UserConfig : IEntityTypeConfiguration<User>
        {
            public void Configure(EntityTypeBuilder<User> builder)
            {

                builder.Property(x => x.Id).HasColumnOrder(1);//字段排序
                builder.Property(x => x.Age).IsRequired(false); //可以为空
                builder.Property(x => x.ReMark).HasMaxLength(200).IsRequired(false); //指定长度 ,可以为空
                builder.ToTable("User");
            }
        }

注意:上下文MyContext:IdentityDbContext需要修改成MyContext:IdentityDbContext<User>

这时候 再去执行migration命令,再去看数据库,已经加上了

效果如下: 

三、使用Identity实现登录、修改密码

1.在Program中 添加AddIdentityCore服务、AddRoleManager、AddUserManager配置

示例如下: 


builder.Services.AddIdentityCore<User>(options =>
{

    //配置用户名
    options.User = new UserOptions
    {
        RequireUniqueEmail = false, //要求Email唯一
        
        //AllowedUserNameCharacters = "abcdefgABCDEFG123456789" //允许的用户名字符,默认是 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+
    };

    //配置密码
    options.Password = new PasswordOptions
    {
        RequiredLength = 6, //要求密码最小长度,默认是 6 个字符
        RequireDigit = true, //要求有数字
        RequiredUniqueChars = 1, //要求至少要出现的字母数
        RequireLowercase = false, //要求小写字母
        RequireNonAlphanumeric = false, //要求特殊字符
        RequireUppercase = false //要求大写字母
    };

    //锁定账户
    options.Lockout = new LockoutOptions
    {
        AllowedForNewUsers = true, // 新用户锁定账户
        DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1), //锁定时长,默认是 5 分钟
        MaxFailedAccessAttempts = 3 //登录错误最大尝试次数,默认 5 次
    };

     //令牌配置
    //打开此 设置 为 短验证码 不打开为 长验证码
    options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
    options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
 
   
});
var idBuilder = new IdentityBuilder(typeof(User), typeof(IdentityRole), builder.Services);
idBuilder.AddEntityFrameworkStores<MyContext>()
    .AddDefaultTokenProviders()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddUserManager<UserManager<User>>();

2.在控制器注入UserManager、RoleManager服务

示例如下: 

    [ApiController]
    [Route("[controller]/[action]")]
    public class UserController : ControllerBase
    {

        private readonly UserManager<User> _userManager;

        public UserController(UserManager<User> userManager)
        {
            _userManager = userManager;
        }



        /// <summary>
        /// 创建用户
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> CreateUser(LoginRequest loginRequest)
        {    
            User user = await _userManager.FindByNameAsync(loginRequest.UserName);
            if (user == null)
            {
                user = new User
                {
                    UserName = loginRequest.UserName
                };
                var result = await _userManager.CreateAsync(user, loginRequest.Password);
                if (!result.Succeeded)
                {
                    return BadRequest(result.Errors);
                }         
            }
            return Ok();
        }

        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="loginRequest"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> Login(LoginRequest loginRequest)
        {
            string userName = loginRequest.UserName;
            string password = loginRequest.Password;
            var user = await _userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return NotFound($"用户名{userName}不存在!");
            }
            var islocked = await _userManager.IsLockedOutAsync(user);
            if (islocked)
            {
                return BadRequest("用户已锁定!");
            }
            var success = await _userManager.CheckPasswordAsync(user, password);
            if (success)
            {
                return Ok();
            }
            else
            {
                var r = await _userManager.AccessFailedAsync(user);
                if (!r.Succeeded)
                {
                    return BadRequest("访问失败信息写入错误!");
                }
                else
                {
                    return BadRequest("失败!");
                }
            }
        }


        /// <summary>
        /// 修改密码
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> ChangePassword(
                    ChangePasswordRequest req)
        {    
            var user = await _userManager.FindByNameAsync(req.UserName);
            if (user == null)
            {
                return NotFound($"用户名{req.UserName}不存在!");
            }
            var result = await _userManager.ChangePasswordAsync(user,req.oldPassword,req.newPassWord);
            if (!result.Succeeded)
            {
                return BadRequest("修改失败!");
          
            }
            return Ok("Success");
        }

        #region 通过发送邮箱的方式重置密码
        /// <summary>
        /// 重置密码发送Token
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> SendResetPasswordToken(
                    SendResetPasswordTokenRequest req)
        {
            string email = req.Email;
            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"邮箱不存在{email}");
            }
           string token = await _userManager.GeneratePasswordResetTokenAsync(user);
            return Ok($"向邮箱{user.Email}发送Token={token}");
        }

        /// <summary>
        /// 重置密码
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> ResetPasswordToken(
                    ResetPasswordRequest req)
        {
            string userName = req.UserName;
            var user = await _userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return NotFound($"用户名{userName}不存在!");
            }
            var islocked = await _userManager.IsLockedOutAsync(user);
            if (islocked)
            {
                return BadRequest("用户已锁定!");
            }

            var result = await _userManager.ResetPasswordAsync(user, req.token,req.newPassWord);
            if (!result.Succeeded)
            {
                return BadRequest("修改失败!");

            }
            return Ok("Success");
        }
        #endregion

    }



    public record LoginRequest(string UserName, string Password);
    public record ChangePasswordRequest(string UserName, string oldPassword,string newPassWord);

    public record SendResetPasswordTokenRequest(string Email);

    public record ResetPasswordRequest(string UserName, string token,string newPassWord);

四、使用JWT实现权限验证


安装Microsoft.AspNetCore.Authentication.JwtBearer

1.在启动类Program.cs中配置Swagger可以输入身份验证方式

示例如下: 

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
        Name = "Authorization",//jwt默认的参数名称
        In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    //添加安全要求
    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
        {
            new OpenApiSecurityScheme{
                Reference =new OpenApiReference{
                    Type = ReferenceType.SecurityScheme,
                    Id ="Bearer"
                }
            },new string[]{ }
        }

    });

});

2.配置类信息、AddAuthentication服务

示例如下:

    public class JWTOptions
    {
        /// <summary>
        /// 颁发者        
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// 接收者       
        /// </summary>
        public string Audience { get; set; } 
        /// <summary>
        /// 密钥
        /// </summary>
        public string SigningKey { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public int ExpireSeconds { get; set; }
    }

在配置文件appsettings.json中加入以下信息

  "JWT": {
    "Issuer": "我是小小鱼",
    "Audience": "我是小小鱼",
    "SigningKey": "fasdfad&9045dafz222#fadpio@0232",
    "ExpireSeconds": "3600"
  }

在添加AddAuthentication服务

builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    x.TokenValidationParameters = new()
    {
        ValidateIssuer = true,//是否验证Issuer
        ValidateAudience = true,//是否验证Audience
        ValidateIssuerSigningKey = true,//是否验证SecurityKey
        ValidIssuer = jwtOpt.Issuer,
        ValidAudience=jwtOpt.Audience,
        IssuerSigningKey = secKey,
        ValidateLifetime = true, //是否验证失效时间
        ClockSkew = TimeSpan.FromSeconds(4)
    };
});

 创建一个Jwt辅助类

  public class JwtHelper
    {
        public static string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
        {
            DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds);
            byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey);
            var secKey = new SymmetricSecurityKey(keyBytes);
            var credentials = new SigningCredentials(secKey,
                SecurityAlgorithms.HmacSha256Signature);
            var tokenDescriptor = new JwtSecurityToken(
                options.Issuer,
                options.Audience,
                expires: expires,
                signingCredentials: credentials, claims: claims);
            return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
        }

    }

3.在登录的接口中返回token

示例如下:

        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="loginRequest"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> Login(LoginRequest loginRequest,[FromServices] IOptions<JWTOptions> jwtOptions)
        {
            string userName = loginRequest.UserName;
            string password = loginRequest.Password;
            var user = await _userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return NotFound($"用户名{userName}不存在!");
            }
            var islocked = await _userManager.IsLockedOutAsync(user);
            if (islocked)
            {
                return BadRequest("用户已锁定!");
            }
            var success = await _userManager.CheckPasswordAsync(user, password);
            if (success)
            {
                var claims = new List<Claim>();
                claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
                claims.Add(new Claim(ClaimTypes.Name, user.UserName));
                var roles = await _userManager.GetRolesAsync(user);
                foreach (string role in roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }
                string Token = JwtHelper.BuildToken(claims, jwtOptions.Value);
                return Ok(Token);
            }
            else
            {
                var r = await _userManager.AccessFailedAsync(user);
                if (!r.Succeeded)
                {
                    return BadRequest("访问失败信息写入错误!");
                }
                else
                {
                    return BadRequest("失败!");
                }
            }



        }

效果如下

4.在需要鉴权的接口加上 [Authorize]

示例如下:

        /// <summary>
        /// 获取用户信息
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Authorize]
        public async Task<IActionResult> GetUser() {

            var claimsPrincipal = this.HttpContext.User;
            var name = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
            var user = await _userManager.FindByNameAsync(name);
            if (user == null)
            {
                return BadRequest("token有误");
            }
            return Ok($"获取用户名:{user.UserName},邮箱:{user.Email}");
        }

运行效果


总结

以上简单用Identity框架在通过migration命令建库建表,再使用 FluentAPI配置表名、字段,用dentity框架封装的UserManager实现登录、修改密码,以及通过token实现鉴权

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【网络奇遇记】揭秘计算机网络的性能指标:速率|带宽|吞吐量|时延

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 速率1.1 数据量1.2 速率 二. 带宽三. 吞吐量四. 时延4.1 发送时延4.2 传播时延…

编解码异常分析

前言 最近在做的项目&#xff0c;有H264解码的需求。部分H264文件解码播放后&#xff0c;显示为绿屏或者花屏。 分析 如何确认是否是高通硬解码的问题 adb 指令 adb root adb remount adb shell setenforce 0 adb shell setprop vendor.gralloc.disable_ubwc 1 adb shell c…

基于JAVA的校园电商物流云平台 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 商品数据模块2.3 快递公司模块2.4 物流订单模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 商品表3.2.2 快递公司表3.2.3 物流订单表 四、系统展示五、核心代码5.1 查询商品5.2 查询快递公司5.3 查…

模型稳定性评估 简介

OOT OOT&#xff08;Out of Time&#xff09;是在建模过程中用于描述模型在时间维度上的性能变化的术语。当模型在训练时使用的数据集与实际应用场景中的数据分布发生差异&#xff0c;特别是在时间上存在间隔时&#xff0c;就可能出现OOT问题。 OOT通常指的是模型在训练时使用…

nestjs swagger文档调用需要鉴权的接口

目标 nestjs经常需要设置一些鉴权&#xff08;登录后&#xff09;才能访问的接口&#xff0c;但是生成的swagger文档可以发起接口请求&#xff0c;文档发起的请求默认是不携带登录token的&#xff0c;所以需要移除swagger文档发起请求的守卫拦截。 nestjs守卫拦截设置见另一篇…

C++力扣题目20--有效的括号

给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括…

【FPGA】Verilog 实践:优先级编码器 | Priority encoder

0x00 优先级编码器&#xff08;Priority encoder&#xff09; "能将多个二进制输入压缩成更少数目输出的电路或算法的编码器" 优先级编码器是一种编码器&#xff0c;它考虑了两个或更多输入位同时变为 1 但没有收到输入的情况。当输入进来时&#xff0c;优先级编码…

直线中点算法

中点算法是基于隐函数方程设计的&#xff0c;使用像素网格中点来判断如何选取距离理想直线最近的像素点&#xff0c;直线的中点算法不仅与 Bresenham 算法产生同样的像素点集&#xff0c;二期还可以推广到圆和椭圆。 原理 直线的隐函数表示 F ( x , y ) y − k x − b 0 F(…

ClickHouse 入门与实战教程

目录 1. ClickHouse 简介 什么是 ClickHouse&#xff1f; ClickHouse 的优势和特点 适用场景 2. 安装 ClickHouse 3. ClickHouse 的基本概念 4. ClickHouse 的基本操作 创建数据库和表、插入和查询数据 使用 MergeTree 引擎处理时序数据 管理分区 创建带有分区的 Mer…

电路设计(6)——彩灯控制器的multism仿真

1.功能设计 使用两个运算放大器、两个计数器芯片&#xff0c;实现了彩灯的循环移位控制。 整体原理图如下所示&#xff1a; 运行效果截图如下&#xff1a; 小灯分为两组&#xff0c;一组十个&#xff0c;在脉冲的驱动下&#xff0c;轮流发光&#xff01; 2.设计思路 两个运放…

视觉学习(4) —— 添加地址传递数据

Modbus Slave 选择一个地址右键&#xff0c;选择发送的数据类型 视觉软件 一、添加地址 当地址为100时&#xff0c;先将首地址改为100&#xff0c;第0个地址为100&#xff0c;第1个地址为101&#xff0c;往后累加 若想使用100—150的地址&#xff0c;即首地址为100&#xff…

基于Java SSM框架实现电子药品商城系统项目【项目源码+论文说明】

基于java的SSM框架实现电子药品商城系统演示 摘要 随着社会的发展&#xff0c;计算机的优势和普及使得电子商城系统的开发成为必需。电子商城系统主要是借助计算机&#xff0c;通过对信息进行管理。减少管理员的工作&#xff0c;同时也方便广大用户对个人所需信息的及时查询以…

程序设计语言与语言处理程序基础

内容概要 编译与解释 正规式&#xff08;重点&#xff09; 例题 答案&#xff1a;D&#xff0c;C 有限自动机 例题 答案&#xff1a;C 表达式&#xff08;重点&#xff09; 把表达式构造成一颗树&#xff0c;然后再根据树的前序&#xff0c;中序&#xff0c;后序遍历来得出答…

递归如何书写?

目录 第一步&#xff1a;首先你分析问题&#xff0c;要有递归的思路&#xff0c;知道要递归什么来解决问题。 第二步&#xff1a;先按照思路&#xff08;第一层&#xff09;写出函数的定义与函数体 第三步&#xff1a;根据函数的定义与函数体进一步确定需要的参数 第四步&a…

golang基于window下实现文件遍历(效率高于filepath.Wlak)

golang基于window下实现文件遍历(效率高于filepath.Wlak) package mainimport ("fmt""os""path""path/filepath""syscall""time""unsafe" )const MAX_PATH 260type FILETIME struct {dwLowDateTime …

html之为什么使用表单,常用表单元素使用?

文章目录 一、为什么使用表单呢&#xff1f;二、常用表单元素使用三、总结 一、为什么使用表单呢&#xff1f; 为什么使用表单呢&#xff0c;使用表单是为了更好的收集用户数据&#xff0c;并且安全 二、常用表单元素使用 1、password密码框 密码框&#xff1a;会隐藏数据&a…

06.微服务组件 Gateway

1、Gateway 简介 在SpringCloud中网关的实现包括两种&#xff1a; Zuul是基于Servlet的实现&#xff0c;属于阻塞式编程。SpringCloudGateway是基于Spring5中提供的WebFlux心属于响应式编程的实现&#xff0c;具备更好的性能。 2、搭建网关服务 步骤一&#xff1a;创建gatewa…

TwIST算法MALTLAB主程序详解

TwIST算法MALTLAB主程序详解 关于TwIST算法的具体原理可以参考&#xff1a; 链接: https://ieeexplore.ieee.org/abstract/document/4358846 链接: https://blog.csdn.net/jbb0523/article/details/52193209 该算法的MATLAB源代码&#xff1a; 链接: http://www.lx.it.pt/~bi…

Unity查安卓Native Crash的方法,定位SO报错函数

需要用到两个工具Il2CppDumper和IDA_Pro&#xff0c;网上可以下到对应的软件 可以看到报错的位置是libil2cpp.so 0000000000AFF820 接下来要做的事情就是找到0000000000AFF820对应的函数是哪个 解包 Il2CppDumper解析so文件和符号表&#xff0c;查看对应的函数表 把apk后缀…

Cloudstack多个管理服务器节点

https://docs.cloudstack.apache.org/en/4.18.0.0/adminguide/reliability.html 参考翻译&#xff1a; 代理上支持多个管理服务器 在具有多个管理服务器的Cloudstack环境中&#xff0c;可以根据算法配置代理&#xff0c;将其连接到哪个管理服务器。这对于内部负载均衡器或高可…