第四阶段-12关于Spring Security框架,RBAC,密码加密原则
关于csmall-passport项目
此项目主要用于实现“管理员”账号的后台管理功能,主要实现:
- 管理员登录
- 添加管理员
- 删除管理员
- 显示管理员列表
- 启用 / 禁用管理员
关于RBAC
RBAC:Role-Based Access Control,基于角色的访问控制
在涉及权限管理的应用软件设计中,应该至少需要设计以下3张数据表:
- 用户表
- 角色表
- 权限表
并且,还至少需要2张关联表:
- 用户与角色的关联表
- 角色与权限的关联表
关于Spring Security框架
Spring Security主要解决了认证与授权相关的问题。
认证:判断某个账号是否允许访问某个系统,简单来说,就是验证登录
授权:判断是否允许已经通过认证的账号访问某个资源,简单来说,就是判断是否具有权限执行某项操作
添加依赖
在基于Spring Boot的项目中,使用Spring Security需要添加依赖项:
<!-- Spring Boot Security依赖项,用于处理认证与授权相关的问题 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
当在项目中添加以上依赖项后,你的项目会发生以下变化(Spring Boot中的Spring Security的默认行为):
-
所有的请求都是必须要登录才允许访问的,包括错误的URL
-
提供了默认的登录页面,当未登录时,会自动重定向到此登录页面
-
提供了临时的登录账号,用户名是
user
,密码是启动项目时在控制台中的UUID值(每次重启项目都会不同)
- 当登录成功后,将自动重定向到此前尝试访问的URL,如果此前没有尝试访问某个URL,则重定向到根路径
- 可以通过
/logout
路径访问到“退出登录”的页面,以实现登出 - 当登录成功后,
POST
请求都是不允许的,而GET
请求是允许的
关于Spring Security的配置类
在项目的根包下,创建config.SecurityConfiguration
类,继承自WebSecurityConfigurerAdapter
类,在类上添加@Configuration
注解:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
然后,在类中重写void configure(HttpSecurity http)
方法:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {}}
**注意:**在重写的方法中,不要使用super
调用父类的此方法!
由于没有调用父类此方法,再次重启项目后,与此前将有些不同:
- 所有请求都不再要求登录
- 登录、登出的URL不可访问
关于登录表单
在Spring Security配置类的configure(HttpSecurity http)
方法中,根据是否调用了参数对象的formLogin()
方法,决定是否启用登录表单页(/login
)和登出页(/logout
),例如:
@Override
protected void configure(HttpSecurity http) throws Exception {// 调用formLogin()表示启用登录表单页和登出页,如果未调用此方法,则没有登录表单页和登出页http.formLogin();
}
关于URL的访问控制
在Spring Security配置类的configure(HttpSecurity http)
方法中,
// 白名单
// 使用1个星号,表示通配此层级的任意资源,例如:/admin/*,可以匹配 /admin/delete、/admin/add-new
// 但是,不可以匹配多个层级,例如:/admin/*,不可以匹配 /admin/9527/delete
// 使用2个连续的星号,表示通配任何层级的任意资源,例如:/admin/**,可以匹配 /admin/delete、/admin/9527/delete
String[] urls = {"/doc.html","/**/*.js","/**/*.css","/swagger-resources","/v2/api-docs"
};// 配置URL的访问控制
http.authorizeRequests() // 配置URL的访问控制.mvcMatchers(urls) // 匹配某些URL.permitAll() // 直接许可,即:不需要通过认证就可以直接访问.anyRequest() // 任何请求.authenticated(); // 以上配置的请求需要是通过认证的
使用临时的自定义账号实现登录
可以自定义类,实现UserDetailsService
接口,并保证此类是组件类,则Spring Security框架会基于此实现类来处理认证。
在项目的根包下创建security.UserDetailsServiceImpl
类,实现UserDetailsService
接口,并在类上添加@Service
注解,重写接口中定义的抽象方法:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {return null;}
}
当项目中存在UserDetailsService
类型的组件对象时,尝试登录时,Spring Security会自动使用登录表单提交过来的用户名来调用以上loadUserByUsername()
方法,并得到UserDetails
类型的对象,此对象中应该包含用户的相关信息,例如密码、账号状态等,接下来,Spring Security会自动使用登录表单提交过来的密码与UserDetails
中的密码进行对比,且判断账号状态,以决定此账号是否能够通过认证。
所以,重写以上方法:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {// 假设存在可用的账号信息:用户名(root),密码(123456)if ("root".equals(s)) {UserDetails userDetails = User.builder().username("root").password("123456").disabled(false).accountLocked(false).accountExpired(false).credentialsExpired(false).authorities("暂时给个山寨权限,暂时没有作用,只是避免报错而已").build();return userDetails;}return null;
}
**提示:**当项目中存在UserDetailsService
类型的组件对象时,Spring Security框架不再提供临时的账号(用户名为user
密码为启动项目时的UUID值的账号)!
**注意:**Spring Security在处理认证时,要求密码必须经过加密码处理,即使你执意不加密,也必须明确的表示出来!
在SecurityConfiguration
中,通过@Bean
方法配置PasswordEncoder
,并返回NoOpPasswordEncoder
的对象,表示“不对密码进行加密处理”:
@Bean
public PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();
}
完成后,重启项目,通过/login
可以测试访问。
使用数据库中的账号数据实现登录
需要实现“根据用户名查询用户的登录信息”,需要执行的SQL语句大致是:
select id, username, password, enable from ams_admin where username=?
在项目的根包下创建pojo.vo.AdminLoginInfoVO
类:
@Data
public class AdminLoginInfoVO implements Serializable {private Long id;private String username;private String password;private Integer enable;
}
在AdminMapper.java
接口中添加抽象方法:
AdminLoginInfoVO getLoginInfoByUsername(String username);
在AdminMapper.xml
中配置以上抽象方法映射的SQL:
<select ...></select><sql></sql><resultMap></resultMap>
在AdminMapperTests
中编写并执行测试:
接下来,在UserDetailsServiceImpl
中,先自动装配AdminMapper
对象,然后,调整loadUserByUsername()
方法:
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {// 使用参数s作为参数,调用AdminMapper对象的getLoginInfoByUsername()方法执行查询// 判断查询结果是否为null// 是:无此用户名对应的账号信息,返回null// 返回UserDetails对象// username:来自查询结果// password:暂时写死为123456,后续再改成来自查询结果// disable:来自查询结果中的enable,判断enable是否为0// accountExpired等:参考此前的Demo,将各值写死
}
完成后,可以使用数据库中的账号测试登录(暂时不方便测试密码)。
tLoginInfoByUsername()方法执行查询
// 判断查询结果是否为null
// 是:无此用户名对应的账号信息,返回null
// 返回UserDetails对象
// username:来自查询结果
// password:暂时写死为123456,后续再改成来自查询结果
// disable:来自查询结果中的enable,判断enable是否为0
// accountExpired等:参考此前的Demo,将各值写死
}
完成后,可以使用数据库中的账号测试登录(暂时不方便测试密码)。#### 密码为什么需要加密如果未加密,将密码的原文(原始密码)直接存入到数据库中,可以被轻松获取账户的关键信息!以目前主流的网络结构和技术,通常,密码加密主要防范的是内部工作人员(能够接触到服务器的人员)!需要注意:即使密码加密了,也要防范相关的内部工作人员,例如程序员!#### 如何对密码进行加密直接使用现有的某种算法,也就是说,不会自行设计某个算法!#### 使用什么算法对密码进行加密一定**不可以**使用**加密算法**!因为所有加密算法都是可以被逆向运算的,也就是说,可以根据加密得到的结果,进行反向运算,还原出原始密码!通常,加密算法仅用于保障数据在传输过程中的安全!在对密码进行加密处理并存入到数据库中时,应该使用**不可逆**的算法!许多**哈希算法**,或基于哈希算法的**消息摘要算法**都是不可逆的!#### 关于消息摘要算法典型的消息摘要算法有:- SHA(Secure Hash Algorithm)家族算法- SHA-1(160位算法)- SHA-256(256位算法)- SHA-384(384位算法)- SHA-512(512位算法)
- MD(Message Digest)系列算法- MD2(128位算法)- MD4(128位算法)- MD5(128位算法)消息摘要算法原本是用于验证接收方所接收的数据与发送方所发出的数据是否一致。消息摘要算法有几个典型特征:- 如果消息相同,则摘要一定相同
- 如果消息不同,则摘要极大概率会不同- 必然存在n个不同的消息,摘要完全相同
- 使用同一种算法时,无论消息长度是多少,摘要的长度是固定的#### 在项目中使用MD5算法在Spring框架中,提供了`DigestUtils`,可以非常便利的使用MD5算法将消息处理为摘要:```java
public class Md5Tests {@Testvoid encode() {String rawPassword = "123456";String encodedPassword = DigestUtils.md5DigestAsHex(rawPassword.getBytes());System.out.println("原文:" + rawPassword);System.out.println("密文:" + encodedPassword);}}
算法位数对安全性的影响
以MD5算法为例,它是128位的算法,即其运算结果是由128个二进制位组成的,所以,其运算结果的排列组件有2的128次方种,这个数字转换成十进制是:340282366920938463463374607431768211456。
理论上,使用MD5算法时,要想找到2个不同的消息运算出相同的摘要,概率应该是340282366920938463463374607431768211456分之1!或者,也可以认为,你至少需要运算340282366920938463463374607431768211456次,才可以找到2个不同的消息运算出相同的摘要。
相比之下,更高位数的算法,理论上,更难找出不同的消息运算出相同的摘要!
一般情况下,由于MD5的安全系数已经较高,所以,不一定需要使用位数更高的算法!
关于消息摘要算法的破解 – 学术
当2个不同的消息,运算出相同的摘要,从学术上,称之为“碰撞”。
理论上,128位的算法,其碰撞概率应该是2的128次方分之1。
关于消息算法的破解,主要是研究其碰撞概率,是否可以使用更少次数的运算实现碰撞!而不是尝试根据摘要进行逆向运算还原出消息!
目前,SHA-1算法已经被视为不安全的算法,它是160位算法,经过研究,只需要经过2的60几次方的运算就可以发生碰撞,即SHA-1的安全系数与60几位的算法几乎相当。
关于消息摘要算法的“破解” – 根据摘要得到消息
网上有许多平台可以做到“根据密文还原出原文”,这些平台都是记录大量的原文与密文的对应关系,当尝试“破解”时,本质上是在做查询操作,大概是:
select 原文 from 数据表 where 密文=?
例如,某平台明确的说明了:
本站针对md5、sha1、sha256等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,很多复杂密文只有本站才可查询。本站专注于各种公开算法,已稳定运行17年。
如果密码可以使用全部的可打印字符,7位长度的密码的排列组合有约70万亿种,8位长度的密码的排列组件在此基础上需要乘以95,则以上平台不可能记录8位长度的所有明文密文的对应关系!也就是说,只要原始密码的长度达到8位,这些平台就可能无法根据密文查询出原文,原始密码的长度越长,或原始密码的强度越高(由多种元素组成,例如大小写字母、数字、标点符号),被这些平台收录的可能性就越低!
如何进一步保障用户的密码安全 – 加盐
盐值的本质就只是一个外部人员很难预测到的字符串,它将作用于处理加密过程中,例如:
// 以下1行定义了盐值
String salt = "fsd4W87i78oiAsUu43IEF";String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 将原始密码和盐值一起被处理System.out.println("原文:" + rawPassword);
System.out.println("密文:" + encodedPassword);
当然,盐值应该如何使用,也没有明确的规定,你可以:
String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
或者:
String encodedPassword = DigestUtils.md5DigestAsHex((salt + rawPassword).getBytes());
甚至:
String encodedPassword = DigestUtils.md5DigestAsHex((salt + rawPassword + salt + rawPassword + salt + salt).getBytes());
总而言之,使用盐的目的是”使得被MD5运算的原始数据变得更加复杂“。
你甚至可以使用随机的盐值,例如:
String salt = UUID.randomUUID().toString(); // 使用UUID作为盐值
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
使用随机盐时必须注意:你需要将随机的盐值保存下来,否则,后续你将无法验证密码!
至于如何保存,方式有许多,例如在数据表中添加新的字段来保存盐值,或者,把盐值直接作为密码的一部分,例如:
String salt = UUID.randomUUID().toString();
String rawPassword = "123456";
String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes()) + salt;
System.out.println("盐值:" + salt);
System.out.println("原文:" + rawPassword);
System.out.println("密文:" + encodedPassword);
密码加密原则 – 小结
关于密码加密处理:
- 不可以使用加密算法,只能使用消息摘要算法或其它哈希算法
- 不建议使用SHA-1
- 应该要求用户使用更长的、强度更高的密码,避免容易被反查(根据密文查询得到原文)
- 应该进行加盐处理
- 你还可以使用多重加密(使用同一个算法,或不同算法,对数据进行反复运算)
- 可以考虑使用位数更长的算法(在MD5的基础上,改为使用SHA-256 / SHA-384 / SHA-512)
**注意:**无论你综合使用以上哪些做法,最终,可能都无法避免内部人员泄密(算法、加密参数、加密过程、密文都是破解时的已知条件)导致的穷举式的暴力破解,而BCrypt算法是被设计得运算效率极低的算法,可以非常有效的避免被暴力破解。
相关文章:

第四阶段-12关于Spring Security框架,RBAC,密码加密原则
关于csmall-passport项目 此项目主要用于实现“管理员”账号的后台管理功能,主要实现: 管理员登录添加管理员删除管理员显示管理员列表启用 / 禁用管理员 关于RBAC RBAC:Role-Based Access Control,基于角色的访问控制 在涉及…...
JPA——Date拓展之Calendar
Java Calendar 是时间操作类,Calendar 抽象类定义了足够的方法,在某一特定的瞬间或日历上,提供年、月、日、小时之间的转换提供方法 一、获取具体时间信息 1. 当前时间 获取此刻时间的年月日时分秒 Calendar calendar Calendar.getInstance(); int …...

一文吃透 Spring 中的 AOP 编程
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...

Apple主推的智能家居是什么、怎么用?一篇文章带你从零完全入门 HomeKit
如果你对智能家居有所了解,那应该或多或少听人聊起过 HomeKit。由 Apple 开发并主推的的 HomeKit 既因为产品选择少、价格高而难以成为主流,又因其独特的优秀体验和「出身名门」而成为智能家居领域的焦点。HomeKit 究竟是什么?能做什么&#…...

SpringCloud系列知识快速复习 -- part 1(SpringCloud基础知识,Docker,RabbitMQ)
SpringCloud知识快速复习SpringCloud基础知识微服务特点SpringCloud常用组件服务拆分和提供者与消费者概念Eureka注册中心原理Ribbon负载均衡原理负载均衡策略饥饿加载Nacos注册中心服务分级存储模型权重配置环境隔离Nacos与Eureka的区别Nacos配置管理拉取配置流程配置热更新配…...

2023上半年北京/上海/广州/深圳NPDP产品经理认证报名
产品经理国际资格认证NPDP是国际公认的唯一的新产品开发专业认证,集理论、方法与实践为一体的全方位的知识体系,为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会(PDMA)成立于1979年…...

面试半年,总结了1000道2023年Java架构师岗面试题
半年前还在迷茫该学什么,怎样才能走出现在的困境,半年后已经成功上岸阿里,感谢在这期间帮助我的每一个人。 面试中总结了1000道经典的Java面试题,里面包含面试要回答的知识重点,并且我根据知识类型进行了分类…...
通过MySQL驱动拦截器实现执行sql耗时计算
文章目录背景具体实现MySQL5MySQL6MySQL8使用方法测试结果背景 公司的一个需求,公司既有的链路追踪日志组件要支持MySQL的sql执行时间打印,要实现链路追踪常用的手段就是实现第三方框架或工具提供的拦截器接口或者是过滤器接口,对于MySQL也不…...

易基因|独家分享:高通量测序后的下游实验验证方法——DNA甲基化篇
大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。此前,我们分享了DNA甲基化研究的测序数据挖掘思路(点击查看详情),进而鉴定出研究的目的基因或目标区域的DNA甲基化。做完测序后,…...
java基础系列(七) 同步和异步理解
一. 问题描述 同步传输和异步传输是web和数据库的重要知识点,会被很多老师强调。那么,它们有什么相同点和不同点?它们对于我们学习编程的意义在哪里? 二. 概念 首先什么是同步和异步? 这里的同步是指&…...
吉林大学 程序设计基础 2022级 OJ期末考试 2.23
本人能力有限,发出只为帮助有需要的人。 以下为实验课的复盘,内容会有大量失真,请多多包涵。 1.双手剑士的最优搭配 每把剑有攻击力和防御力两个属性。双手剑士可以同时拿两把剑,其得到攻击力为两把剑中的攻击力的最大值&#…...
【项目实战】SpringMVC拦截器实战 - 自定义拦截器防止重复提交
一、背景说明 如何能够实现防止重复提交呢?以下是一种可选的方式。 二、代码实战 2.1 注册重复提交拦截器到SpringMVC中 @Configuration @AllArgsConstructor public class ResourcesConfig implements WebMvcConfigurer {private final RepeatSubmitInterceptor repeatSu…...

C++ STL:容器 Container
文章目录1、序列容器1.1、容器共性1.2、vectorvector 结构* vector 扩容原理* vector 迭代器失效1.3、dequedeque 结构deque 迭代器deque 模拟连续空间1.4、listlist 特殊操作list 结构list 迭代器2、关联式容器2.1、容器共性2.2、容器特性3、无序关联式容器3.1、容器共性3.2、…...

urllib之urlopen和urlretrieve的headers传入以及parse、urlparse、urlsplit的使用
urllib库是什么?urllib库python的一个最基本的网络请求库,不需要安装任何依赖库就可以导入使用。它可以模拟浏览器想目标服务器发起请求,并可以保存服务器返回的数据。urllib库的使用:1、request.urlopen(1)只能传入url的方式from http.clie…...

【C++】二叉搜索树的模拟实现
一、概念 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值若它的右子树不为空,则右子树上所有节点的值都大于根节点的值它的左右子树也分别…...

HNU工训中心:元器件及测量基础实验报告
工训中心的牛马实验 1.实验目的 1.熟悉测量验证常用元器件参数、 并采用替代法(测量回路电流)测量其伏安特性的方法。 2.熟悉测量误差及减小测量误差注意事项 2.实验仪器和器材 1.实验仪器. 直流稳压电源型号:IT6302 台式多用表型号:UT805A 2.实验( 箱)器材 电路实验箱…...

博客系统--自动化测试
项目体验地址(账号:123,密码:123)http://120.53.20.213:8080/blog_system/login.html项目后端说明:http://t.csdn.cn/32Nnv项目码云Gitee地址:https://gitee.com/GoodManSS/project/tree/master…...

Day903.自增主键不能保证连续递增 -MySQL实战
自增主键不能保证连续递增 Hi,我是阿昌,今天学习记录的是关于自增主键不能保证连续递增的内容。 MySql保证了主键是自增,但不相对连续;帮助开发人员快速识别每个行的唯一性,并提高查询效率。 自增主键可以让主键索引…...

02-MyBatis查询-
文章目录Mybatis CRUD练习1,配置文件实现CRUD1.1 环境准备Debug01: 别名mybatisx报错1.2 查询所有数据1.2.1 编写接口方法1.2.2 编写SQL语句1.2.3 编写测试方法1.2.4 起别名解决上述问题1.2.5 使用resultMap解决上述问题1.2.6 小结1.3 查询详情1.3.1 编写接口方法1.…...
外盘国际期货招商:2023年3月关注日历,把握重要投资机会
2023年3月大事件日历 关注大事日历,把握重要投资机会 3月1日:马斯克推出特斯拉宏图第三篇章 3月1-2日:G20外长会议 3月4-5日:全国两会召开 3月9日:中国2月CPI、PPI数据 待定(前次进行日期:…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...