ABP vNext微服务架构详细教程——分布式权限框架(上)

2023-11-05 19:38

本文主要是介绍ABP vNext微服务架构详细教程——分布式权限框架(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1

简介

3e5764ada323a9ea07d18cdf184d98f9.png

ABP vNext框架本身提供了一套权限框架,其功能非常丰富,具体可参考官方文档:https://docs.abp.io/en/abp/latest/Authorization

0768681cd19340aa43ba9743ea22e945.gif

但是我们使用时会发现,对于正常的单体应用,ABP vNext框架提供的权限系统没有问题, 但是在微服务架构下,这种权限系统并不是非常的友好。

a58eca97ed79796317a83504dc349bee.gif

我希望我的权限系统可以满足以下要求:

每个聚合服务持有独立的权限集合

每个聚合服务可以独立声明、使用其接口访问所需的权限。

提供统一接口负责管理、存储所有服务权限并实现对角色的授权。

每个接口可以灵活组合使用一个或多个权限码。

权限框架使用尽量简单,减少额外编码量。

8934846192858daf5531d12bde980b57.gif

在ABP vNext框架基础上,重新编写了一套分布式权限框架,大体规则如下:

使用ABP vNext框架中提供的用户、角色模型不做改变,替代重新定义权限模型,重新定义权限的实体及相关服务接口。

在身份管理服务中,实现权限的统一管理、角色授权和权限认证。

在聚合服务中定义其具有的权限信息、权限关系并通过特性声明各接口所需要的权限。

在聚合服务启动时,自动将其权限信息注册到身份管理服务。

客户端访问聚合服务层服务时在聚合服务层中间件中验证当前用户是否具有该接口权限,验证过程需调用身份管理服务对应接口。 

796fc6bddb913ceedc406e955c9db6a3.gif

权限系统具体实现见下文。

2

身份认证服务

2be48d4482e970fa122386e05f90f3fa.gif

在之前的文章中我们已经搭建了身份认证服务的基础框架,这里我们直接在此基础上新增代码。

0063425d49ab05abe57474c708b9a62d.gif

在Demo.Identity.Domain项目中添加Permissions文件夹,并添加Entities子文件夹。在此文件夹下添加实体类SysPermission和RolePermissions如下:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Domain.Entities;namespace Demo.Identity.Permissions.Entities;/// <summary>
/// 权限实体类
/// </summary>
public class SysPermission : Entity<Guid>
{/// <summary>/// 服务名称/// </summary>[MaxLength(64)]public string ServiceName { get; set; }/// <summary>/// 权限编码/// </summary>[MaxLength(128)]public string Code { get; set; }/// <summary>/// 权限名称/// </summary>[MaxLength(64)]public string Name { get; set; }/// <summary>/// 上级权限ID/// </summary>[MaxLength(128)]public string ParentCode { get; set; }/// <summary>/// 判断两个权限是否相同/// </summary>/// <param name="obj"></param>/// <returns></returns>public override bool Equals(object? obj)
{return obj is SysPermission permission&& permission.ServiceName == ServiceName&& permission.Name == Name&& permission.Code == Code&& permission.ParentCode == ParentCode;}/// <summary>/// 设置ID的值/// </summary>/// <param name="id"></param>public void SetId(Guid id)
{Id = id;}
}
using System;
using Volo.Abp.Domain.Entities;namespace Demo.Identity.Permissions.Entities;/// <summary>
/// 角色权限对应关系
/// </summary>
public class RolePermissions : Entity<Guid>
{/// <summary>/// 角色编号/// </summary>public Guid RoleId { get; set; }/// <summary>/// 权限编号/// </summary>public Guid PermissionId { get; set; }
}

02f6bdec93f849b88bd8485abbdf1cd9.gif

将Demo.Identity.Application.Contracts项目中原有Permissions文件夹中所有类删除,并添加子文件夹Dto。在此文件夹下添加SysPermissionDto、PermissionTreeDto、SetRolePermissionsDto

类如下:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Application.Dtos;namespace Demo.Identity.Permissions.Dto;/// <summary>
/// 权限DTO
/// </summary>
public class SysPermissionDto:EntityDto<Guid>
{/// <summary>/// 服务名称/// </summary>[MaxLength(64)]public string ServiceName { get; set; }/// <summary>/// 权限编码/// </summary>[MaxLength(128)]public string Code { get; set; }/// <summary>/// 权限名称/// </summary>[MaxLength(64)]public string Name { get; set; }/// <summary>///     上级权限ID/// </summary>[MaxLength(128)]public string ParentCode { get; set; }
}
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;namespace Demo.Identity.Permissions.Dto;/// <summary>
/// 权限树DTO
/// </summary>
public class PermissionTreeDto : EntityDto<Guid>
{/// <summary>/// 服务名称/// </summary>public string ServiceName { get; set; }/// <summary>/// 权限编码/// </summary>public string Code { get; set; }/// <summary>/// 权限名称/// </summary>public string Name { get; set; }/// <summary>/// 上级权限ID/// </summary>public string ParentCode { get; set; }/// <summary>/// 子权限/// </summary>public List<PermissionTreeDto> Children { get; set; }}
using System;
using System.Collections.Generic;namespace Demo.Identity.Permissions.Dto;/// <summary>
/// 设置角色权限DTO
/// </summary>
public class SetRolePermissionsDto
{/// <summary>/// 角色编号/// </summary>public Guid RoleId { get; set; }/// <summary>/// 权限ID列表/// </summary>public List<Guid> Permissions { get; set; }
}

282a8c294ce4f62ab02dd1a7d3dc66ba.gif

将Demo.Identity.Application.Contracts项目中Permissions文件夹下添加接口IRolePermissionsAppService如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Volo.Abp.Application.Services;namespace Demo.Identity.Permissions;/// <summary>
///     角色管理应用服务接口
/// </summary>
public interface IRolePermissionsAppService: IApplicationService
{/// <summary>/// 获取角色所有权限/// </summary>/// <param name="roleId">角色ID</param>/// <returns></returns>Task<List<PermissionTreeDto>> GetPermission(Guid roleId);/// <summary>/// 设置角色权限/// </summary>/// <param name="dto">角色权限信息</param>/// <returns></returns>Task SetPermission(SetRolePermissionsDto dto);
}

33cb301ffb60cce4ba8bad696d3bd99c.gif

将Demo.Identity.Application.Contracts项目中Permissions文件夹下添加接口ISysPermissionAppService如下:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Volo.Abp.Application.Services;namespace Demo.Identity.Permissions;/// <summary>
/// 权限管理应用服务接口 
/// </summary>
public interface ISysPermissionAppService:IApplicationService
{/// <summary>/// 按服务注册权限/// </summary>/// <param name="serviceName">服务名称</param>/// <param name="permissions">权限列表</param>/// <returns></returns>Task<bool> RegistPermission(string serviceName, List<SysPermissionDto> permissions);/// <summary>/// 按服务获取权限/// </summary>/// <param name="serviceName">服务名称</param>/// <returns>查询结果</returns>Task<List<SysPermissionDto>> GetPermissions(string serviceName);/// <summary>/// 获取完整权限树/// </summary>/// <param name="Permission"></param>/// <returns>查询结果</returns>Task<List<PermissionTreeDto>> GetPermissionTree();/// <summary>/// 获取用户权限码/// </summary>/// <param name="userId">用户编号</param>/// <returns>查询结果</returns>Task<List<string>> GetUserPermissionCode(Guid userId);
}

a842c0641ab27358777cf7627f48ef31.gif

在公共类库文件夹common中创建.Net6类库项目项目Demo.Core,用于存放通用类。

3278852ff639c4fe7f82826c9a3e471c.gif

这里我们在Demo.Core中添加文件夹CommonExtension用于存放通用扩展,添加EnumExtensions和ListExtensions类如下:

namespace Demo.Core.CommonExtension;/// <summary>
/// 枚举扩展类
/// </summary>
public static class EnumExtensions
{/// <summary>/// 获取描述特性/// </summary>/// <param name="enumValue">枚举值</param>/// <returns></returns>public static string GetDescription(this Enum enumValue){string value = enumValue.ToString();FieldInfo field = enumValue.GetType().GetField(value);object[] objs = field.GetCustomAttributes(typeof(DescriptionAttribute), false);  //获取描述属性if (objs == null || objs.Length == 0)  //当描述属性没有时,直接返回名称return value;DescriptionAttribute descriptionAttribute = (DescriptionAttribute)objs[0];return descriptionAttribute.Description;}
}
namespace Demo.Core.CommonExtension;public static class ListExtensions
{/// <summary>/// 集合去重/// </summary>/// <param name="lst">目标集合</param>/// <param name="keySelector">去重关键字</param>/// <typeparam name="T">集合元素类型</typeparam>/// <typeparam name="TKey">去重关键字数据类型</typeparam>/// <returns>去重结果</returns>public static List<T> Distinct<T,TKey>(this List<T> lst,Func<T, TKey> keySelector){List<T> result = new List<T>();HashSet<TKey> set = new HashSet<TKey>();foreach (var item in lst){var key = keySelector(item);if (!set.Contains(key)){set.Add(key);result.Add(item);}}return result;}
}

84a26677b92c9a80074d86c7de486d64.gif

在Demo.Core项目中添加文件夹CommonFunction用于存放通用方法,这里我们添加用于集合比对的ListCompare类如下:

using VI.Core.CommonExtension;namespace VI.Core.CommonFunction;/// <summary>
/// 集合比对
/// </summary>
public class ListCompare
{/** 调用实例:*  MutiCompare<Permission, string>(lst1, lst2, x => x.Code, (obj, isnew) =>*  {*      if (isnew)*      {*          Console.WriteLine($"新增项{obj.Id}");*      }*      else*      {*          Console.WriteLine($"已存在{obj.Id}");*      }*  }, out var lstNeedRemove);*//// <summary>/// 对比源集合和目标集合,处理已有项和新增项,并找出需要删除的项/// </summary>/// <param name="lstSource">源集合</param>/// <param name="lstDestination">目标集合</param>/// <param name="keySelector">集合比对关键字</param>/// <param name="action">新增或已有项处理方法,参数:(数据项, 是否是新增)</param>/// <param name="needRemove">需要删除的数据集</param>/// <typeparam name="TObject">集合对象数据类型</typeparam>/// <typeparam name="TKey">对比关键字数据类型</typeparam>public static void MutiCompare<TObject,TKey>(List<TObject> lstDestination,List<TObject> lstSource,Func<TObject, TKey> keySelector,Action<TObject, bool> action, out Dictionary<TKey, TObject> needRemove){//目标集合去重lstDestination.Distinct(keySelector);//将源集合存入字典,提高查询效率needRemove = new Dictionary<TKey, TObject>();foreach (var item in lstSource){needRemove.Add(keySelector(item),item);}//遍历目标集合,区分新增项及已有项//在字典中排除目标集合中的项,剩余的即为源集合中需删除的项foreach (var item in lstDestination){if (needRemove.ContainsKey(keySelector(item))){action(item, false);needRemove.Remove(keySelector(item));}else{action(item, true);}}}
}

774eda5f91fa5f9f23e62b8ce752335d.gif

在Demo.Identity.Application项目中添加Permissions文件夹。

316da11e90fa1461b08d804effc01b68.gif

在Demo.Identity.Application项目Permissions文件夹中添加PermissionProfileExtensions类用于定义对象映射关系如下:

using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;namespace Demo.Identity.Permissions;public static class PermissionProfileExtensions
{/// <summary>/// 创建权限领域相关实体映射关系/// </summary>/// <param name="profile"></param>public static void CreatePermissionsMap(this IdentityApplicationAutoMapperProfile profile){profile.CreateMap<SysPermission, PermissionTreeDto>();profile.CreateMap<SysPermission,SysPermissionDto>();profile.CreateMap<SysPermissionDto,SysPermission>();}
}

8bc6c8668e75a02b4f5c412b9542eba5.gif

在Demo.Identity.Application项目IdentityApplicationAutoMapperProfile类的IdentityApplicationAutoMapperProfile方法中添加如下代码:

this.CreatePermissionsMap();

75171c85c6a87c6f43a105900b24fdff.gif

在Demo.Identity.Application项目Permissions文件夹中添加PermissionTreeBuilder类,定义构造权限树形结构的通用方法如下:

using System.Collections.Generic;
using System.Linq;
using Demo.Identity.Permissions.Dto;namespace Demo.Identity.Permissions;/// <summary>
/// 权限建树帮助类
/// </summary>
public static class PermissionTreeBuilder
{/// <summary>/// 建立树形结构/// </summary>/// <param name="lst"></param>/// <returns></returns>public static List<PermissionTreeDto> Build(List<PermissionTreeDto> lst){var result = lst.ToList();for (var i = 0; i < result.Count; i++){if (result[i].ParentCode == null){continue;}foreach (var item in lst){item.Children ??= new List<PermissionTreeDto>();if (item.Code != result[i].ParentCode){continue;}item.Children.Add(result[i]);result.RemoveAt(i);i--;break;}}return result;}
}

030c797a2c1931482430a4de28c4d644.gif

之后我们在Demo.Identity.Application项目Permissions文件夹中添加权限管理实现类SysPermissionAppService如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Demo.Core.CommonFunction;
using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
using Demo.Core.CommonExtension;namespace Demo.Identity.Permissions
{/// <summary>/// 权限管理应用服务/// </summary>public class SysPermissionAppService : IdentityAppService, ISysPermissionAppService{#region 初始化private readonly IRepository<RolePermissions> _rolePermissionsRepository;private readonly IRepository<SysPermission> _sysPermissionsRepository;private readonly IRepository<IdentityUserRole> _userRolesRepository;public SysPermissionAppService(IRepository<RolePermissions> rolePermissionsRepository,IRepository<SysPermission> sysPermissionsRepository,IRepository<IdentityUserRole> userRolesRepository
){_rolePermissionsRepository = rolePermissionsRepository;_sysPermissionsRepository = sysPermissionsRepository;_userRolesRepository = userRolesRepository;}#endregion#region 按服务注册权限/// <summary>/// 按服务注册权限/// </summary>/// <param name="serviceName">服务名称</param>/// <param name="permissions">权限列表</param>/// <returns></returns>public async Task<bool> RegistPermission(string serviceName, List<SysPermissionDto> permissions){//根据服务名称查询现有权限var entities = await AsyncExecuter.ToListAsync( (await _sysPermissionsRepository.GetQueryableAsync()).Where(c => c.ServiceName == serviceName));var lst = ObjectMapper.Map<List<SysPermissionDto>, List<SysPermission>>(permissions);ListCompare.MutiCompare(lst, entities, x => x.Code, async (entity, isNew) =>{if (isNew){//新增await _sysPermissionsRepository.InsertAsync(entity);}else{//修改var tmp = lst.FirstOrDefault(x => x.Code == entity.Code);//调用权限判断方法,如果code和name相同就不进行添加if (!entity.Equals(tmp)&&tmp!=null){entity.SetId(tmp.Id);await _sysPermissionsRepository.UpdateAsync(entity);}}}, out var needRemove);foreach (var item in needRemove){//删除多余项await _sysPermissionsRepository.DeleteAsync(item.Value);}return true;}#endregion#region 按服务获取权限/// <summary>///     按服务获取权限/// </summary>/// <param name="serviceName">服务名称</param>/// <returns>查询结果</returns>public async Task<List<SysPermissionDto>> GetPermissions(string serviceName){var query = (await _sysPermissionsRepository.GetQueryableAsync()).Where(x => x.ServiceName == serviceName);//使用AsyncExecuter进行异步查询var lst = await AsyncExecuter.ToListAsync(query);//映射实体类到dtoreturn ObjectMapper.Map<List<SysPermission>, List<SysPermissionDto>>(lst);}#endregion#region 获取完整权限树/// <summary>/// 获取完整权限树/// </summary>/// <returns>查询结果</returns>public async Task<List<PermissionTreeDto>> GetPermissionTree(){var per = await _sysPermissionsRepository.ToListAsync();var lst = ObjectMapper.Map<List<SysPermission>, List<PermissionTreeDto>>(per);return PermissionTreeBuilder.Build(lst);}#endregion#region 获取用户权限码/// <summary>/// 获取用户权限码/// </summary>/// <param name="userId">用户编号</param>/// <returns>查询结果</returns>public async Task<List<string>> GetUserPermissionCode(Guid userId){var query = from user in (await _userRolesRepository.GetQueryableAsync()).Where(c => c.UserId == userId)join rp in (await _rolePermissionsRepository.GetQueryableAsync()) on user.RoleId equals rp.RoleIdjoin pe in (await _sysPermissionsRepository.GetQueryableAsync()) on rp.PermissionId equals pe.Idselect pe.Code;var permission = await AsyncExecuter.ToListAsync(query);return permission.Distinct(x=>x);}#endregion}
}

2e9636e35cd687e69a7bda0f414e2491.gif

添加角色权限关系管理实现类RolePermissionsAppService如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Demo.Identity.Permissions.Dto;
using Demo.Identity.Permissions.Entities;
using Volo.Abp.Domain.Repositories;namespace Demo.Identity.Permissions
{/// <summary>/// 角色管理应用服务/// </summary>public class RolePermissionsAppService : IdentityAppService, IRolePermissionsAppService{#region 初始化private readonly IRepository<RolePermissions> _rolePermissionsRepository;private readonly IRepository<SysPermission> _sysPermissionsRepository;public RolePermissionsAppService(IRepository<RolePermissions> rolePermissionsRepository,IRepository<SysPermission> sysPermissionsRepository
){_rolePermissionsRepository = rolePermissionsRepository;_sysPermissionsRepository = sysPermissionsRepository;}#endregion#region 获取角色所有权限/// <summary>/// 获取角色所有权限/// </summary>/// <param name="roleId">角色ID</param>/// <returns></returns>public async Task<List<PermissionTreeDto>> GetPermission(Guid roleId){var query = from rp in (await _rolePermissionsRepository.GetQueryableAsync()).Where(x => x.RoleId == roleId)join permission in (await _sysPermissionsRepository.GetQueryableAsync())on rp.PermissionId equals permission.Idselect permission;var permissions = await AsyncExecuter.ToListAsync(query);var lst = ObjectMapper.Map<List<SysPermission>, List<PermissionTreeDto>>(permissions);return PermissionTreeBuilder.Build(lst);}#endregion#region 设置角色权限/// <summary>/// 设置角色权限/// </summary>/// <param name="roleId">橘色编号</param>/// <param name="permissions">权限编号</param>/// <returns></returns>public async Task SetPermission(SetRolePermissionsDto dto){await _rolePermissionsRepository.DeleteAsync(x => x.RoleId == dto.RoleId);foreach (var permissionId in dto.Permissions){RolePermissions entity = new RolePermissions(){PermissionId = permissionId,RoleId = dto.RoleId,};await _rolePermissionsRepository.InsertAsync(entity);}}#endregion}
}

533db24a7e27c1df73b6eb3a7547613b.gif

 在Demo.Identity.EntityFrameworkCore项目IdentityDbContext类中加入以下属性:

public DbSet<SysPermission> SysPermissions { get; set; }
public DbSet<RolePermissions> RolePermissions { get; set; }

541fee81fe0e6e01440cada0ca593859.gif

在Demo.Identity.EntityFrameworkCore项目目录下启动命令提示符,执行以下命令分别创建和执行数据迁移:

dotnet-ef migrations add AddPermissions
dotnet-ef database update

7b7b9ffcbbf1363fbedb091b3b45329d.gif

在Demo.Identity.EntityFrameworkCore项目IdentityEntityFrameworkCoreModule类ConfigureServices方法中找到 options.AddDefaultRepositories(includeAllEntities: true); ,在其后面加入以下代码:

options.AddDefaultRepository<IdentityUserRole>();

51cc46674ce16b0cac6aa979d588dcf6.png

完成后运行身份管理服务,可正常运行和访问各接口,则基础服务层修改完成。后续操作请看下一篇

f8e71b4940c5412ab101fd7014ee2fb3.png

end

781c6fa3e12d116dbb90390fd14e7a12.png

cc34818bd82fcfd1bb2808e7b6dbe677.png

5b994a5a44dd2ca3f1d7d13201560b2a.png

更多精彩

关注我获得

4d2710f8e7134bc22784242e2f96ca64.png

这篇关于ABP vNext微服务架构详细教程——分布式权限框架(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/352041

相关文章

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

Window Server创建2台服务器的故障转移群集的图文教程

《WindowServer创建2台服务器的故障转移群集的图文教程》本文主要介绍了在WindowsServer系统上创建一个包含两台成员服务器的故障转移群集,文中通过图文示例介绍的非常详细,对大家的... 目录一、 准备条件二、在ServerB安装故障转移群集三、在ServerC安装故障转移群集,操作与Ser

windos server2022的配置故障转移服务的图文教程

《windosserver2022的配置故障转移服务的图文教程》本文主要介绍了windosserver2022的配置故障转移服务的图文教程,以确保服务和应用程序的连续性和可用性,文中通过图文介绍的非... 目录准备环境:步骤故障转移群集是 Windows Server 2022 中提供的一种功能,用于在多个

解决systemctl reload nginx重启Nginx服务报错:Job for nginx.service invalid问题

《解决systemctlreloadnginx重启Nginx服务报错:Jobfornginx.serviceinvalid问题》文章描述了通过`systemctlstatusnginx.se... 目录systemctl reload nginx重启Nginx服务报错:Job for nginx.javas

龙蜥操作系统Anolis OS-23.x安装配置图解教程(保姆级)

《龙蜥操作系统AnolisOS-23.x安装配置图解教程(保姆级)》:本文主要介绍了安装和配置AnolisOS23.2系统,包括分区、软件选择、设置root密码、网络配置、主机名设置和禁用SELinux的步骤,详细内容请阅读本文,希望能对你有所帮助... ‌AnolisOS‌是由阿里云推出的开源操作系统,旨

PyTorch使用教程之Tensor包详解

《PyTorch使用教程之Tensor包详解》这篇文章介绍了PyTorch中的张量(Tensor)数据结构,包括张量的数据类型、初始化、常用操作、属性等,张量是PyTorch框架中的核心数据结构,支持... 目录1、张量Tensor2、数据类型3、初始化(构造张量)4、常用操作5、常用属性5.1 存储(st

Java操作PDF文件实现签订电子合同详细教程

《Java操作PDF文件实现签订电子合同详细教程》:本文主要介绍如何在PDF中加入电子签章与电子签名的过程,包括编写Word文件、生成PDF、为PDF格式做表单、为表单赋值、生成文档以及上传到OB... 目录前言:先看效果:1.编写word文件1.2然后生成PDF格式进行保存1.3我这里是将文件保存到本地后

windows系统下shutdown重启关机命令超详细教程

《windows系统下shutdown重启关机命令超详细教程》shutdown命令是一个强大的工具,允许你通过命令行快速完成关机、重启或注销操作,本文将为你详细解析shutdown命令的使用方法,并提... 目录一、shutdown 命令简介二、shutdown 命令的基本用法三、远程关机与重启四、实际应用

python库fire使用教程

《python库fire使用教程》本文主要介绍了python库fire使用教程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1.简介2. fire安装3. fire使用示例1.简介目前python命令行解析库用过的有:ar

LinuxMint怎么安装? Linux Mint22下载安装图文教程

《LinuxMint怎么安装?LinuxMint22下载安装图文教程》LinuxMint22发布以后,有很多新功能,很多朋友想要下载并安装,该怎么操作呢?下面我们就来看看详细安装指南... linux Mint 是一款基于 Ubuntu 的流行发行版,凭借其现代、精致、易于使用的特性,深受小伙伴们所喜爱。对