提交 1714fbbd 编写于 作者: S star

代码整洁之道

上级 8b2c4afe
......@@ -220,6 +220,32 @@ Spring,SpringBoot 与 SpringCloud:
SpringBoot 自定义 Starter:
需要用到的注解:
1. @Configuration:指定配置类
2. @ConditionalOnXxx:在指定条件成立的情况下自动配置类生效
3. @AutoConfigureAfter:指定自动配置类的顺序
4. @Bean:给容器中添加组件
5. @ConfigurationPropertie:结合相关 xxxProperties 类来绑定相关的配置
6. @EnableConfigurationProperties:让 xxxProperties 生效加入到容器中
此外,还要将需要启动就加载的自动配置类,配置在 META‐INF/spring.factories 中。
启动器设计规约:
1. 通常来说,SpringBoot Starter 启动器应该只用来做依赖导入,它是一空的 jar 包,用来提供辅助性依赖管理,不应该存在任何的 java 代码。
2. 命名:
1. 官方启动器:spring-boot-starter-xxx,如:spring-boot-starter-web
2. 自定义启动器:xxx-spring-boot-starter,如:druid-spring-boot-starter
---
### 4.SpringBoot AutoConfiguration
......@@ -1610,19 +1636,141 @@ public FilterRegistrationBean<StatViewFilter> statViewFilter(){
**结合 MyBatis 配置多数据源**
1. 搭建项目:创建 SpringBoot 项目,引入需要的项目依赖(不同数据库时需要引入不同的数据库 Driver)。
2. 编写项目配置:
~~~properties
# 服务端口
server.port=8080
# MySQL 数据源连接相关信息
spring.datasource.mysql.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.mysql.url=jdbc:mysql://192.168.253.128:3306/common?serverTimezone=UTC
spring.datasource.mysql.username=root
spring.datasource.mysql.password=TinyStar0920
# Oracle 数据源连接相关信息
spring.datasource.oracle.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.oracle.url=jdbc:oracle:thin:@192.168.253.128:1521:ORCLCDB
spring.datasource.oracle.username=C##STAR
spring.datasource.oracle.password=123456
# Druid 相关配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size=5
# 配置 Druid 监控
spring.datasource.druid.filter.commons-log.connection-logger-name=stat,wall,log4j
spring.datasource.druid.stat-view-servlet.enabled=true
# 日志相关配置
logging.level.com.star.md=debug
~~~
3. 添加作用在 mapper 上的注解用来使用不同的数据源:
~~~java
@Documented
@Repository
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MysqlRepository {
@AliasFor(annotation = Repository.class)
String value() default "";
}
~~~
同理,添加 OracleRepository 注解。
4. 使用 MysqlProperties 封装 Mysql 配置属性:
~~~java
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public class MysqlProperties {
private String url;
private String username;
private String password;
private String driverClassName;
}
~~~
同理,使用 OracleProperties 封装 Oracle 配置属性。
5. 配置 DataSource,SessionFactory,SessionTemplate 组件:
~~~java
@Data
@Configuration
@MapperScan(basePackages = "com.star.md.dao", annotationClass = MysqlRepository.class,
sqlSessionFactoryRef = "mysqlSessionFactory", sqlSessionTemplateRef = "mysqlSessionTemplate")
@MapperScan(basePackages = "com.star.md.dao", annotationClass = OracleRepository.class,
sqlSessionFactoryRef = "oracleSessionFactory", sqlSessionTemplateRef = "oracleSessionTemplate")
public class DataSourceConfig {
@Bean
@Primary
public DataSource mysqlDataSource(MysqlProperties properties) {
DruidDataSource mysqlDataSource = DruidDataSourceBuilder.create().build();
mysqlDataSource.setDriverClassName(properties.getDriverClassName());
mysqlDataSource.setUrl(properties.getUrl());
mysqlDataSource.setUsername(properties.getUsername());
mysqlDataSource.setPassword(properties.getPassword());
return mysqlDataSource;
}
@Bean
public SqlSessionFactory mysqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean mysqlSessionFactoryBean = new SqlSessionFactoryBean();
//配置数据源
mysqlSessionFactoryBean.setDataSource(dataSource);
//配置 mysql mapper 文件位置
mysqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/mysql/*Mapper.xml"));
SqlSessionFactory sessionFactory = mysqlSessionFactoryBean.getObject();
assert sessionFactory != null;
sessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
return sessionFactory;
}
@Bean
public SqlSessionTemplate mysqlSessionTemplate(@Qualifier("mysqlSessionFactory") SqlSessionFactory sessionFactory) {
return new SqlSessionTemplate(sessionFactory);
}
@Bean
public DataSource oracleDataSource(OracleProperties properties) {
DruidDataSource oracleDataSource = DruidDataSourceBuilder.create().build();
oracleDataSource.setDriverClassName(properties.getDriverClassName());
oracleDataSource.setUrl(properties.getUrl());
oracleDataSource.setUsername(properties.getUsername());
oracleDataSource.setPassword(properties.getPassword());
return oracleDataSource;
}
@Bean
public SqlSessionFactory oracleSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean oracleSessionFactoryBean = new SqlSessionFactoryBean();
//配置数据源
oracleSessionFactoryBean.setDataSource(dataSource);
//配置 oracle mapper 位置
oracleSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/oracle/*Mapper.xml"));
SqlSessionFactory sessionFactory = oracleSessionFactoryBean.getObject();
assert sessionFactory != null;
org.apache.ibatis.session.Configuration sessionConfiguration = sessionFactory.getConfiguration();
sessionConfiguration.setMapUnderscoreToCamelCase(true);
return sessionFactory;
}
@Bean
public SqlSessionTemplate oracleSessionTemplate(@Qualifier("oracleSessionFactory") SqlSessionFactory sessionFactory) {
return new SqlSessionTemplate(sessionFactory);
}
}
~~~
1. 向容器中添加了 2 个数据源, 2 个 SqlSessionFactory,2 个 SqlSessionTemplate。
2. 使用 **@MapperScan** 标注了要扫描的 mapper 组件,由于容器中存在多个 SqlSessionFactory 和多个 SqlSessionTemplate 组件,所以还要额外为扫描到的 mapper 指定要使用的 SqlSessionFactory 和 SqlSessionTemplate。
......
> :key: 密码学:研究编制密码和破译密码的科学。
#### 1.密码学的发展
密码学:密码学有数千年的历史,从最开始的替换法到如今的非对称加密算法,经历了古典密码学,近代密码学和现代密码学三个阶段。密码学不仅仅是数学家们的智慧,更是如今网络空间安全的重要基础。
**古典密码学**
古典密码学的核心原理为替换法和移位法,通常用来在战争中保护重要的通信资料。
- 替换法:替换法就是用固定的信息将原文替换成无法直接阅读的密文信息。例如将 `b` 替换成 `w``e` 替换成 `p` ,这样 `bee` 单词就变换成了 `wpp`,不知道替换规则的人就无法阅读出原文的含义。
替换法又分为单表替换和多表替换:
- 单表替换:原文和密文使用同一张替换表,相对比较简单。
- 多表替换:多表替换有多张原文密文对照表单,按照约定顺序进行加密解密。
> :books: 多表替换实现:例如制定好 3 张替换表单(1:abcde-swtrp、2:abcde-chfhk、3:abcde-jftou),同时约定第 1 个字母用第 3 张表单,第 2 个字母用第 1 张表单,第 3 个字母用第 2 张表单;此时 `bee` 单词就变成了 `fpk`(312),破解难度更高,其中 312 又叫做密钥,密钥可以事先约定好,也可以在传输过程中标记出来。
- 移位法:移位法就是将原文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后得出密文,例如约定好向后移动 2 位 `abcde` -> `cdefg`,这样 `bee` 单词就变换成了 `dgg`;与替换法一样,移位法也分为单表移位和多表移位,典型的单表移位法有 `恺撒加密`;而典型的多表移位案例有 `维尼吉亚密码`(维热纳尔密码)。
**频率分析法**
频率分析法是古典密码的一种破译方式,基于概率论分析研究字母或者字母组合在文本中出现的频率,以此破解古典密码。
> 频率分析法破译原理:
>
> 在英文单词中字母出现的频率是不同的,e 以 12.702% 的百分比占比最高,而 z 只占到了 0.074%,如果密文的数量足够大,仅仅采用频率分析法就可以破解单表的替换法或移位法;而多表的替换法或移位法虽然难度较高,但如果数据量足够大的话,也是可以破解的。
**近代密码学**
近代密码学的核心原理仍然是替换法和移位法,只不过因为密码表种类极多,同时加密解密机器化,所以破解难度较高;
[恩尼格玛机](https://baike.baidu.com/item/%E6%81%A9%E5%B0%BC%E6%A0%BC%E7%8E%9B%E5%AF%86%E7%A0%81%E6%9C%BA/5691350):恩尼格玛机是二战时期纳粹德国使用的加密机器,它是一系列相似的转子机械加解密机器的统称,包括了许多不同的型号。
> :bulb: 恩尼格玛机的密码虽然复杂,后来仍被英国破译,参与破译的人员中包括后来的被称作计算机科学之父、人工智能之父的 [图灵](https://baike.baidu.com/item/%E8%89%BE%E4%BC%A6%C2%B7%E9%BA%A6%E5%B8%AD%E6%A3%AE%C2%B7%E5%9B%BE%E7%81%B5/3940576)。
**现代密码学**
现代密码学:在现代密码学中,主要分为了 3 中加密(解密)方式
- 散列函数:如 MD5,SHA-1,SHA-256 等,多应用在文件校验,数字签名中。
- 对称加密(解密):对称加密分为流加密和块加密两种,常见的如 DES 加密解密,AES 加密解密。
- 非对称加密:非对称加密有两支密钥(公钥和私钥),加密和解密运算使用不同的密钥,常见的算法如 RSA,ECC 等。
我们如何设置密码才安全?
1. 密码不能太简单常见,如 123456 这种。
2. 不同的应用软件尽量设置不同的密码,防止一个应用的密码泄露而造成全部的应用密码崩塌。
3. 在设置密码时可以增加注册时间、注册地点、应用特性等特殊标记,比如 jdxxxx1234(京东)密码。
---
#### 2.密码学基础
ASCII 编码:American Standard Code for Information Interchange,美国信息交换标准代码,它是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言,它是现今最通用的单字节编码系统,并等同于国际标准 ISO/IEC 646。
> :anger: ASCII 码中,字母 A 对应的十进制数字为 65,B-Z 65 后依次增加;字母 a 对应的十进制数字为 97,b-z 97 后依次增加。
Java 中,我们可以基于 ASCII 编码实现凯撒加密:
~~~java
/**
* 加密方法
* @param input 原文
* @param key 秘钥(位移位数)
* @return 密文
*/
private static String encrypt(String input, int key) {
StringBuilder secret = new StringBuilder();
for (char c : input.toCharArray()) {
// 位移 key 个字符
secret.append((char) (c + key));
}
return secret.toString();
}
~~~
加密时,将字母移动指定位数;在解密时,只需要将字母向反方向位移指定位数即可。~注:此处仅为实现原理,并未严格按照字母进行位移。~
:question: ​Byte 和 Bit
Byte:字节,数据存储的基本单位,简称 B;
> 常见的数据存储单位及大小:
>
> B ~x1024~ KB ~x1024~ MB ~x1024~ GB ~x1024~ TB ~x1024~ PB ~x1024~ EB ...
Bit:比特,又叫位,是计算机运算(传输)的基本单位,1 个 bit 就是 1 位二进制数。
Byte 和 Bit 的联系:8 个 Bit 组成一个 Byte,将 8 个 0 或者 1 组合在一起,就可以说它们是 8 个比特或者 1 个字节。
> :eyes: 宽带带宽与下载速度:
>
> 1M 的带宽指的是 1M(1024*1024)的 Bit,而下载速度一般是以 Byte 计算的,所以一般来说下载速度约为带宽的 1/8,并且不会超过此值。
英文与字节:
- 在英文中,字母(符号)对应的 Byte 就是其对应的 ASCII 编码。
- 在中文中,根据编码的格式不一样,一个汉字对应的 Byte 个数也不一样;UTF-8 编码为 3 个字节,而 GBK 为 2 个字节。
~~~java
public static void main(String[] args) throws UnsupportedEncodingException {
String en = "A";
System.out.println(en + " 的 UTF-8 编码:" + Arrays.toString(en.getBytes()));
System.out.println(en + " 的 GBK 编码:" + Arrays.toString(en.getBytes("GBK")));
String zh = "中";
System.out.println(zh + " 的 UTF-8 编码:" + Arrays.toString(zh.getBytes()));
System.out.println(zh + " 的 GBK 编码:" + Arrays.toString(zh.getBytes("GBK")));
}
-----------------------------------------------------------------------------------
出:
A 的 UTF-8 编码:[65]
A 的 GBK 编码:[65]
的 UTF-8 编码:[-28, -72, -83]
的 GBK 编码:[-42, -48]
~~~
此可见:英文字母的 Byte 数组就是其对应那一个的 ASCII 编码,而中文则根据不同的编码集由多个 Byte 组成。
---
#### 3.对称加密
对称加密的加密和解密用的都是同一个秘钥。
流加密和块加密:
- 流加密:使用秘钥生成一个伪随机的加密数据流如 `1001010110101...`,再使用原文数据的数据流按顺序进行加密(一般为异或)操作得到密文。
- 块加密:又称分组加密,将信息分割成大小相等的数据块(如果最后一个块的大小不够,则会按照特定规则进行补全),然后对每一个数据块分别进行加密运算,块加密的允许使用同一个密钥对多于一块的数据进行加密,所以一般来说比流加密更安全一些。
> 流密码与块密码,各自有各自的优缺点,出于安全考量,在现代密码学中一般都优先考虑分组加密算法。
对称加密的特点:
1. 加密速度快, 可以加密大文件。
2. 密文可逆, 一旦密钥文件泄漏, 就会导致数据暴露。
3. 加密后如果在编码表找不到对应字符, 则会出现乱码。
4. 一般需要结合 **Base64** 进行使用。
常见的加密算法:
- **DES***Data Encryption Standard*,数据加密标准。是一种使用密钥加密的块算法,1977 年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。
- **AES***Advanced Encryption Standard*,高级加密标准。在密码学中又称 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的 DES,已经被多方分析且广为全世界所使用。
==Java 中的加密解密类:**javax.crypto.Cipher**,该类提供加密和解密的功能,时 Java 加密扩展框架(JCE)的核心。==
使用 Java 实现 DES 加密:
~~~java
public class FileDemo {
public static void main(String[] args) throws Exception {
// 原文
String input = "今天是个好日子";
// 秘钥(DES 加密的秘钥必须为 8 个字节)
String key = "12354678";
// 创建加密对象,传入加密算法
Cipher cipher = Cipher.getInstance("DES");
// 创建加密规则:参数1(秘钥的字节数组),参数二(加密类型)
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES");
// 初始化加密对象:参数1(模式:加密/解密),参数二(加密规则)
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
// 进行加密
byte[] encrypt = cipher.doFinal(input.getBytes());
System.out.println(new String(encrypt));
}
}
---------------------------------------------------
输出:�^i��Pۺic���قL
~~~
由于在加密后,byte 数组可能会出现 ASCII 编码表之外的值,此时会出现乱码,此时一般使用 Base64 进行转码。
使用 `com.sun.org.apache.xerces.internal.impl.dv.util.Base64` 类调用 `encode` 方法即可进行 Base64 编码。
~~~java
// 进行加密
byte[] encrypt = cipher.doFinal(input.getBytes());
String encode = Base64.encode(encrypt);
System.out.println(encode);
---------------------------------------------------
此时输出lF6LaY67UNu6uQjCCGmtY6qN2e8I2YJM
~~~
使用 Java 实现 DES 解密:
~~~java
private static String decrypt(String secret, String key) throws Exception {
// 创建解密对象,传入加密算法
Cipher cipher = Cipher.getInstance("DES");
// 创建解密规则:参数1(秘钥的字节数组),参数二(解密类型)
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES");
// 初始化加密对象:参数1(模式:加密/解密),参数二(加密规则)
cipher.init(Cipher.DECRYPT_MODE, keySpec);
// 将密文进行 Base64 解码
byte[] decode = Base64.decode(secret);
// 进行解密
byte[] bytes = cipher.doFinal(decode);
// 返回原文
return new String(bytes);
}
~~~
注:由于前面的密文经过了 Base64 编码,所以拿到密文后第一步就应该进行 Base64 解码。
> :aerial_tramway: AES 加密解密与 DES 加密解密的 Java 代码一致(参数传入 AES),唯一不同的是 AES 的秘钥必须为 16 个字符。
Base64 是一种基于 64 个可打印字符来表示二进制数据的方法,是网络上最常见的用于传输 8Bit(Byte) 字节码的编码方式之一。
Base64 不是加密算法而是一种可读性算法,它由 64 个字符组成,分别是 `A-Z、a-z、0-9、+、/`
Base64 原理:它将每 3 个字节分为一组,每个字节是 8 位共 24 位,然后将它们转为 4 组,每组 6 位,在每组的高位补 2 个 0 构成 4 个字节,这样将每个字节的数字控制在 0-63 之内,然后按照 Base64 编码表进行字符转换(就是上面的 64 个字符顺序)。
在编码过程中,有时候原文的字节数组不一定是 3 的倍数,这时 Base64 会进行补等号:
- 当最后一组仅有 1 个字节时:将字节分为 8 个 Bit,前 6 个 Bit 正常编码,后 2 个 Bit 追加 4 个 0 组成 6 位然后进行正常编码得到 2 个字符的 Base64 编码,然后在字符后方补 2 个 `=` 组成 4 个字符。
- 当最后一组有 2 个字节时:将 2 个字节分为 16 个 Bit,前 12 个 Bit 正常编码,后 4 个 Bit 追加 2 个 0 组成 6 位然后进行正常编码得到 3 个字符的 Base64 编码,然后在字符后方补 1 个 `=` 组成 4 个字符。
---
#### 4.加密模式和填充模式
在分组加密中,存在着多种加密模式(算法),常见的加密模式有:ECB、CBC 等。
- **ECB**:*Electronic Code Book*,电子密码本;将需要加密的消息按照块密码的块大小被分为数个块,每个块使用同一个 key 进行独立加密。
- 优点:可以并行处理数据。
- 缺点:安全性较低。
- **CBC**:*Cipher-block chaining*, 密码块链接;通过初始化向量(VI)和 key 生成第一个密文块,后续的每个明文块先与前一个密文块进行异或后,再进行加密。
- 优点:同样的原文生成的密文不一样,安全性较高。
- 缺点:只能够串行的处理数据。
在分组加密中,当数据长度不符合块处理需求时, 则会按照一定的方法填充块长的规则。
- **NoPadding**:不填充,此时 DES 要求原文长度是 8Byte 的整数倍,AES 要求原文长度是 16Byte 的整数倍。
- **PKCS5Padding**:数据块的大小为 8Byte,不够就进行补足,补足的填充值算法:value = k - (d % k) ~k:块大小,d:数据长度~。
指定加密模式和填充模式使用 Cipher:
~~~java
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
~~~
>:deer: 默认情况下,javax.crypto.Cipher 中的加密模式和填充模式分别为 ECB PKCS5Padding。
果使用 CBC 加密模式,需要初始化一个 IV 向量:
~~~java
private static String cbcDecrypt(String plain, String key) throws Exception {
// 创建解密对象,传入加密算法 DES、加密模式 CBC、填充模式 PKCS5Padding
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
// 创建 CBC 加密的初始化向量
IvParameterSpec iv = new IvParameterSpec(key.getBytes());
// 创建解密规则:参数1(秘钥的字节数组),参数二(解密类型)
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES");
// 初始化加密对象:参数1(模式:加密/解密),参数二(加密规则),参数三(初始化向量)
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 进行解密
byte[] encrypt = cipher.doFinal(plain.getBytes());
// 返回原文
return Base64.encode(encrypt);
}
~~~
理,解密 CBC 加密模式的密文时,也需要初始化一个 IV 向量:
~~~java
// 创建 CBC 解密的初始化向量
IvParameterSpec iv = new IvParameterSpec(key.getBytes());
// 创建解密规则:参数1(秘钥的字节数组),参数二(解密类型)
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES");
// 初始化加密对象:参数1(模式:加密/解密),参数二(加密规则)
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
~~~
---
#### 5.消息摘要
消息摘要:*Message Digest*,又称数字摘要,它可以将任意长度的消息变成固定长度的短消息。
- 它由一个单向的 Hash 加密函数对消息进行作用而产生,所以消息摘要是单向、不可逆的。
- 使用消息摘要生成的值是不可以篡改的,所以它被广泛用来验证文件的安全性。
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。
- 只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同,但相同的输入必定会产生相同的输出(改变文件名不会影响消息摘要)。
> :zap: 在很多文件下载网站都会给出文件的 **SHA512**、**MD5** 等消息摘要值,我们可以使用下载下来的文件生成消息摘要进行比对以确保文件没有被篡改。
常见的消息摘要算法有:**MD5****SHA1****SHA256****SHA512**
在 Java 中,消息摘要算法由 **java.security.MessageDigest** 提供。
使用 Java 获取字符串的消息摘要:
~~~java
// 创建消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 原文
String plain = "今天是个好日子";
// 获取消息摘要数组
byte[] digest = md5.digest(plain.getBytes());
~~~
得到消息摘要字节数组后,还可能会出现乱码,此时一般的做法不是采用 Base64 转码,而是将字节数组的每一个 Byte 转为 16 进制的字符串(如果转换为16进制的字符串的长度为 1 则在前面补 0):
~~~java
// 转换消息摘要
StringBuilder builder = new StringBuilder();
for (byte b : digest) {
String s = Integer.toHexString(b & 0xff);
builder.append(s.length() == 1 ? "0" : "").append(s);
}
System.out.println(builder.toString());
~~~
> 为什么要使用 Byte & 0xff :question:
>
> 1. 0xff 是 16 进制的表达方式,f 指 15,0xff 的十进制是 255,二进制是 1111 1111,int 为 0000 0000 0000 0000 0000 0000 1111 1111。
> 2. 在计算机中,负数以其正值的补码(二进制取反 + 1)形式进行存储,正数的补码就是其原码。
> 3. 数据由 Byte 转换为 int 就是在最高位补符号数(正数补 0,负数补 1)。
>
> 例如:
>
> 1. Byte -5 在计算机中存储为 0000 0101 -> 1111 1010 + 1 -> 1111 1011,转换为 int 则为 1111 1111 1111 1111 1111 1111 1111 1011。
> 2. 我们直接对 -5 转 16 进制的结果为 fffffffb(多了很多的符号位)。
> 3. 在转换消息摘要这里我们不关心数值的具体大小,而只需要字节的低 8 位转换而成的十六进制字符串(两位长度),所以使用 0xff 与 -5 相与得 0000 0000 0000 0000 0000 0000 1111 1011,其低 8 位 的补码(1111 1011)与 Byte 的 8 位 补码一致,转换后的十六进制也只有 2 位。
==如果需要对文件求 MD5 值,则先需要将文件转为 Byte数组。==
---
#### 6.非对称加密
非对称加密算法是现代加密算法最流行的加密方式,它是计算机通信安全的基石,保证了加密数据不会被破解。
- 与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。
- 如果用 publickey 对数据进行加密,只有用对应的 privatekey 才能解密。
- 如果用 privatekey 对数据进行加密,只有用对应的 publickey 才能解密。
- 非对称加密处理数据的速度较慢, 但是安全级别高。
目前市面上最常见的非对称加密算法有:**RSA** 和 **ECC**。
在 Java 中,我们使用 **java.security.KeyPairGenerator** 生成公钥和私钥。
使用 Java 生成 RSA 公钥和私钥:
~~~java
public class RSADemo {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 获取 key 生成器
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
// 获取密钥对
KeyPair keyPair = rsa.generateKeyPair();
// 获取公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥
PrivateKey privateKey = keyPair.getPrivate();
//获取秘钥对应字节数组
byte[] publicKeyEncoded = publicKey.getEncoded();
byte[] privateKeyEncoded = privateKey.getEncoded();
//打印秘钥
System.out.println(Base64.encode(publicKeyEncoded));
System.out.println(Base64.encode(privateKeyEncoded));
}
}
~~~
使用 Java 实现 RSA 加密解密:
~~~java
public class RSADemo {
public static void main(String[] args) throws Exception {
String algorithm = "RSA";
String plain = "今天是个好日子";
// 获取 key 生成器
KeyPairGenerator rsa = KeyPairGenerator.getInstance(algorithm);
// 获取密钥对
KeyPair keyPair = rsa.generateKeyPair();
// 生成 RSA 加密对象
Cipher cipher = Cipher.getInstance(algorithm);
// 使用私钥初始化加密对象(key 为 PublicKey 或 PrivateKey)
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
// 进行加密
byte[] bytes = cipher.doFinal(plain.getBytes());
String secret = Base64.encode(bytes);
System.out.println(secret);
// 使用公钥初始化加密对象(key 为 PublicKey 或 PrivateKey)
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPublic());
// 进行解密
byte[] plainBytes = cipher.doFinal(Base64.decode(secret));
System.out.println(new String(plainBytes));
}
}
~~~
在实际使用过程中,我们一般讲公钥和私钥保存至文件中,然后在每次加密解密时直接读取字符串进行使用。
保存秘钥字符串:
~~~java
private static void saveKey2File() throws Exception {
// 获取 key 生成器
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
// 获取密钥对
KeyPair keyPair = rsa.generateKeyPair();
// 指定秘钥保存文件(当前项目目录下)
File publicKeyFile = new File("public.txt");
File privateKeyFile = new File("private.txt");
try (FileWriter publicWriter = new FileWriter(publicKeyFile);
FileWriter privateWriter = new FileWriter(privateKeyFile)){
// 保存 Base64 编码过后的字符串到文件中
publicWriter.write(Base64.encode(keyPair.getPublic().getEncoded()));
privateWriter.write(Base64.encode(keyPair.getPrivate().getEncoded()));
}
}
~~~
使用秘钥字符串生成私钥:
~~~java
private static PrivateKey getPrivateKey(String keyEncode) throws Exception {
// 获取 RSA 秘钥生成工厂
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 获取满足私钥 key 规范的 KeySpec
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(keyEncode));
// 生成私钥并返回
return keyFactory.generatePrivate(keySpec);
}
~~~
使用秘钥字符串生成公钥:
~~~java
private static PublicKey getPublicKey(String keyEncode) throws Exception {
// 获取 RSA 秘钥生成工厂
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 获取满足公钥 key 规范的 KeySpec
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(keyEncode));
// 生成公钥并返回
return keyFactory.generatePublic(keySpec);
}
~~~
公钥或私钥生成后,就可以使用其进行加密或解密。
> :smiling_imp: 在非对称加密中,公钥一般是公钥是公开的,所有人都可以认领;而私钥绝对保密,一般不参与数据传输。
---
#### 7.数字签名和数字证书
数字签名:指公钥数字签名,只有信息的发送者才能产生别人无法伪造的一段数字串。它的含义是:在网络中传输数据时候,给数据添加一个数字签名,表示是谁发的数据,而且还能证明数据没有被篡改。
数字签名原理:
1. A 发送一段信息给 B,在写完信息后,A 对信息进行消息摘要,然后对消息摘要使用私钥加密得到数字签名。
2. A 将数字签名附着在信息下方然后发出(如果需要防止信息被窃听,还可以对信息本身使用私钥加密)。
3. B 收到信息后取下数字签名,对数字签名使用公钥解密,如果解密成功,则证明信息确实是由 A 发出的。
4. B 对信息本身(解密后)进行散列,如若散列结果与信息签名一致,则信息未被修改。
使用 Java 生成数字签名和校验数字签名:
~~~java
// 原文
String plain = "今天是个好日子";
// 获取 key 生成器
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
// 获取密钥对
KeyPair keyPair = rsa.generateKeyPair();
// 获取签名生成器
Signature signature = Signature.getInstance("SHA256withRSA");
// 使用私钥初始化签名生成器
signature.initSign(keyPair.getPrivate());
// 传入原文
signature.update(plain.getBytes());
// 生成数字签名
String signature = Base64.encode(signature.sign());
System.out.println(signature);
// 使用公钥初始化签名校验器
verify.initVerify(keyPair.getPublic());
// 传入原文
verify.update(plain.getBytes());
// 校验数字签名
boolean valid = verify.verify(Base64.decode(signature));
System.out.println(valid);
~~~
> :artificial_satellite: 数字签名存在的问题:如果 B 保存的公钥被人篡改,然后用私钥给 B 发送信息,此时虽然 B 接收的消息是冒充的但是却无法察觉。
数字证书:在我们对签名进行验证时,需要用到公钥。如果公钥是伪造的或者丢失了公钥,那我们就无法验证数字签名,也就不可能从数字签名确定对方的合法性,这时候就需要用到数字证书(公钥 + 数字签名打包)。
> :tiger: 数字证书被广泛用于 **HTTPS** 协议。
数字证书原理:
1. A 去找 CA 证书中心做公钥认证,证书中心用自己的私钥,对 A 的公钥和一些相关信息一起加密,生成数字证书。
2. A 向 B 发送信息时,除了数字签名,还要附加上这张数字证书。
3. B 收到信息后使用 CA 的公钥解密这份数字证书得到 A 的公钥,然后使用 A 的公钥进行验证数字签名流程。
> :joystick: 既然公钥可以被伪造,那么数字证书也有可能是伪造的,如何保证数字证书的可靠性呢?
>
> B 从 CA 证书中心获取一份根证书,里面存储 CA 公钥来验证所有 CA 分中心颁发的数字证书,只要 A 的公钥在 CA 任意一个分支中心做了认证,那么 B 的根证书就可以验证 A 的数字证书的合法性。
>
> 根证书是自验证证书,CA 机构是获得社会绝对认可和有绝对权威的第三方机构,这一点保证了根证书的绝对可靠,如果根证书都有问题那么整个加密体系毫无意义。
>
> 在我们的 Windows 系统中,预留了很多 CA 根证书,使得我们访问的大多数资源时都能够得到有效认证。
HTTPS 原理:
1. 客户端向服务器发出加密请求(reques)。
2. 服务器用自己的私钥加密网页数据后,连同本身的数字证书,一起发送给客户端。
3. 客户端(浏览器)内置了一个证书管理器,里面包含了「受信任的根证书颁发机构」的列表,客户端(浏览器)会根据这张列表,查看数字证书的发送机构是否在根证书颁发机构列表之内。
4. 如果数字证书是可靠的,客户端就可以使用 CA 中心的公钥,对证书进行解密得到服务器的公钥,然后与服务器交换信息。
5. 如果证书存在问题,浏览器则会发出警告「此网站的安全证书有问题」。
---
#### 8.keytool
**keytool** 是 JDK 自带的一个用来管理公钥、私钥和数字证书的工具,其中包含一系列的命令,能够帮助我们快速的生成证书、导出证书等;此工具位于 jre 下的 bin 目录中。
keytool 用法:
~~~bash
P:\Java\jre1.8.0_251\bin>keytool -help
密钥和证书管理工具
命令:
-certreq 生成证书请求
-changealias 更改条目的别名
-delete 删除条目
-exportcert 导出证书
-genkeypair 生成密钥对
-genseckey 生成密钥
-gencert 根据证书请求生成证书
-importcert 导入证书或证书链
-importpass 导入口令
-importkeystore 从其他密钥库导入一个或所有条目
-keypasswd 更改条目的密钥口令
-list 列出密钥库中的条目
-printcert 打印证书内容
-printcertreq 打印证书请求的内容
-printcrl 打印 CRL 文件的内容
-storepasswd 更改密钥库的存储口令
~~~
使用 genseckey 命令生成用户的密钥(生成的文件默认在家目录下的 .keystore 中):
~~~bash
P:\Java\jre1.8.0_251\bin>keytool -genkey -keypass 951112 -keyalg RSA -storepass 951112
您的名字与姓氏是什么?
[Unknown]: 何鑫
您的组织单位名称是什么?
[Unknown]: 小星星集团
您的组织名称是什么?
[Unknown]: star
您所在的城市或区域名称是什么?
[Unknown]: 成都市
您所在的省/市/自治区名称是什么?
[Unknown]: 四川省
该单位的双字母国家/地区代码是什么?
[Unknown]: 86
CN=何鑫, OU=小星星集团, O=star, L=成都市, ST=四川省, C=86是否正确?
[否]: y
Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore C:\Users\star\.keystore -destkeystore C:\Users\star\.keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。
~~~
查看 keystore 详细信息:
~~~bash
P:\Java\jre1.8.0_251\bin>keytool -list -v
输入密钥库口令:
密钥库类型: jks
密钥库提供方: SUN
您的密钥库包含 1 个条目
别名: mykey
创建日期: 2021-4-19
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=何鑫, OU=小星星集团, O=star, L=成都市, ST=四川省, C=86
发布者: CN=何鑫, OU=小星星集团, O=star, L=成都市, ST=四川省, C=86
序列号: 5a5cd516
有效期为 Mon Apr 19 00:22:16 CST 2021 至 Sun Jul 18 00:22:16 CST 2021
证书指纹:
MD5: 87:8A:12:A3:8D:D3:CE:47:24:F0:70:2A:AD:1E:85:A5
SHA1: 3F:3B:BE:5D:96:79:BB:67:FD:DB:8B:87:BC:F0:D4:45:AA:1C:3C:72
SHA256: 6A:D9:3B:16:C0:6D:A4:87:DA:A9:7F:99:41:F6:B9:BB:B7:3D:ED:1C:E2:D4:49:06:CE:AB:44:8C:67:3D:C0:4C
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3
扩展:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 81 1D F4 5F 63 B0 9D 20 45 F7 63 01 D9 CE 1A 8C ..._c.. E.c.....
0010: 64 3B 91 84 d;..
]
]
*******************************************
*******************************************
~~~
将证书导出为 crt 文件:
~~~bash
P:\Java\jre1.8.0_251\bin>keytool -export -file C:\Users\star\Desktop\star.crt
输入密钥库口令:
存储在文件 <C:\Users\star\Desktop\star.crt> 中的证书
~~~
删除证书:
~~~bash
P:\Java\jre1.8.0_251\bin>keytool -delete -keystore C:\Users\star\.keystore -storepass 951112
~~~
:bulb: ***Later equals never.***
#### 1.有意义的命名
软件中随处可见命名,我们给变量、函数、类和包命名,下面是起一个好名字应该遵从的规则。
==名副其实==
选个好名字要花时间,但是省下来的时间比花掉的多,注意命名,而且一旦发现有更好的命名就换掉旧的,这样做,读代码的人都会更开心。
1. 指明计量单位和计量单位的名称,如:int elapsedTimeInDays、int fileAgeInDays 等。
2. 选择体现业务本意的名称,而不是字母(如:d、e),类型(如:theList、list1)等名称。
3. 利用类代替复杂的数据结构。
例如:应用中使用 int\[]\[] 数组表示坐标,如果在代码中大量使用这个二维数组,代码会变得极其繁杂,我们可以定义一个类
~~~java
public class Position {
private int value;
private int x;
private int y;
}
~~~
4. 避免使用魔术变量(未经定义的常量),例如:
~~~java
if(age == 5){
// do something
}
~~~
此时,我们应当将 5 这个魔术数放到文件前面并定义一个数字,如:`private static final int MIN_AGE = 5;`
==避免误导==
程序员必须避免留下掩藏代码本意的错误线索,避免使用与本意相悖的词。
1. 避免使用某些专有名称作为变量名,例如:aix、sco、bat 等,虽然看起来像是不错的缩写,但是极易引起误导。
2. 不要使用 accountList 来表示一组账号,除非它真的是 List 类型,否则会引起错误的判断,此时使用 accountGroup 或 accounts 都是更好的选择。
3. 不要使用外形相似度较高的名称,例如 XYZControllerForEfficientHandlingOfStrings 和 XYZControllerForEfficientStorageOfStrings,更有甚者使用小写的字母 l 和大写的字母 O,他们看起来完全像常量「壹」和「零」。
==做有意义的区分==
在代码中,同一作用范围的两样不同的东西不能重名,如果我们只是为满足编译器或解释器的需要而修改代码,那么将会带来无尽麻烦。
1. 只是在命名后添加数字远远不够,每个不同的命名都应该能表达出不同的意义,如 a1、a2、a3、a4... 等名称纯属误导,完全没有提供正确的信息,例:
~~~java
public void copyChars(char[] c1, char[] c2) {
for (i = 0; i< c1.length; i++) {
c2[i] = c1[i];
}
}
~~~
此时,两个参数名 c1 和 c2 完全没有任何意义,如果将其替换为 source 和 destination,函数就会好看许多。
2. 避免使用废话来区分命名,假设有一个 Product 类,还有一个 ProductData 和 ProductInfo 类,那么它们虽然名称不同,但是都是意义含混的废话;当我们面对 getActiveAccount、getActiveAccounts 和 getActiveAccountInfo 这 3 个函数时,也无法快速的分辨到底该调用哪一个函数。
3. 避免冗余的命名,例如 variable 永远不要出现在变量名中,table 永远不要出现在数据库表名中,nameString 也不会比 name 更好。
==使用读得出来的名称==
人类擅长于记忆和使用单词,如果名称读不出来,在讨论的时候就会变得十分尴尬。
1. 不要使用字母组合命名,例如程序里面有一个 genymdhms 函数(生成年月日时分秒),这样的函数名完全无法读出来也无法一眼看出它的含义,如果换成 genTimestamp 这样的命名就会使函数读起来更像人话。
2. 不要使用不恰当的缩写,例如:DtaRcrd、Cstm、Acct 并不会比 DataRecord、Customer、Account 这些完整的单词更好更简洁。
==使用可搜索的名称==
对于单字母名称和数字常量,有一个问题就是很难再一大段代码中搜索出来。
1. 搜索 MAX_CLASSES_PER_STUDENT 很容易,但是查找数字 7 就比较麻烦了,同时字母 e、s 等也不是一个便于搜索的好名称,我们应该尽量避免使用单字母名称和数字常量。
2. 名称长短应该与其作用域大小相对应,如果变量或常量可能在代码中多处使用,则应该赋予它们便于搜索的名称。
==避免使用标记==
现代编程语言有丰富的类型系统,并且强制使用类型,所以把类型和作用域编进名称里面,只会突然增加负担。
1. 不必使用 m_ 来标记成员变量,我们要学会无视前缀或后缀,只看到名称中有意义的部分。
2. 在创建接口时,不要使用前导字母 I,比如 IShapeFactory 对于 ShapeFactory 来说,前面的 I 字母根本就是一句废话。
==类名和方法名==
1. 类名和对象名应该是名称和名词短语,例如 Customer、Account 等,此外还要避免使用 Data、Info、Manager 这样语义广泛的名称。
2. 方法名应当是动词或动词短语,如 postPayment、deletePage 等。
==一词一义==
1. 每个概念值对应一个词,假如一堆代码中的控制器有 controller、manager 和 driver,name就会让人产生困惑,我们的命名应当一以贯之。
2. 避免将同一单词用于不同目的(双关),例如:使用 add 用于向集合中添加元素,而在另外的地方用来拼接字符串,这种做法是强烈禁止的。
==使用解决方案领域名称和问题领域名称==
1. 只有程序员才会读你的代码,所以我们可以尽情的使用那些计算机科学术语,算法名,模式名,没有程序员不知道 AccoutVisitor 的含义,同样,大多数程序员也都了解 JobQueue 的含义。
2. 如果不能使用程序员熟悉的术语进行命名,那么可以考虑从所涉及的问题的领域获取命名,与软件的实际功能更为贴近。
==为代码添加语境==
很少有名称是能够自我说明的,如果不能自我说明,我们还剩最后一招,给名称添加前缀语境。
1. 当我们看见一堆单词如 state、city、street、name、houseNumber 时,我们很容易推断这是一个地址的描述,但是如果我们只看见一个孤零零的 name 变量呢,我们无法进行推断,但是我们可以添加前缀进行说明:addrName,这样我们就可以明白这个变量是更大结构的一部分。
2. 当需要使用到一堆相关联的变量时,可以使用一个类封装它们,类也是语境的一部分。
3. 不要添加无意义的语境,假如有一个名为 Gas Station Deluxe(加油站豪华版)的应用,如果给其中的每一个类都加上 GSD 前缀就不是什么好点子了;只要名称足够清楚,短名称就比长名称好。
---
#### 2.函数
在编程的早期岁月,系统由程序和子程序组成,后来到了 Fortran 和 PL/1 的年代,系统由程序、子程序和函数组成,如今只有函数保留了下来,函数是所有程序中的第一组代码。
==短小==
函数的第一条规则是短小,第二条规则还是要短小。
1. 函数尽量不要写得过长,**20** 行封顶最佳。
> :gear: 个人不倾向于这这么短的函数,频繁的函数调用不仅不会降低代码编写难度,还会提高难度。一般情况下,**50-60** 行左右封顶为最佳。
2. if、else、while 等语句,其中的代码应该只占一行,该行一般为一个函数调用语句,这样能够很好的保持函数大小。
3. 函数中的代码嵌套层级(if、while 等嵌套)不宜过多,否则不易于阅读和理解,最好是一层或两层,不可超过三层。
==只做一件事==
函数应该只做一件事,做好这件事,只做一件事。
如何判断函数只做了一件事,还是做了几件事?例:
~~~java
public void buy() {
// 1.走进商店
// 2.选好商品
// 3.掏出钱包
// 4.付钱
// 5.放回钱包
// 6.走出商店
}
~~~
在这个函数中,模拟了一个买东西的情节,其中 1、2、6 步骤都是买东西过程下的抽象层中的一件事,但是 3、4、5 却是在买东西这个动作下的更细分的步骤,很明显不属于函数名下的抽象层,此时 3、4、5 就合一被拆出一个新函数「结账」。
> :yellow_heart: 向下原则:让每个函数后面跟着位于下一抽象层及的函数,在阅读时就能遵循抽象层级向下阅读。
==使用具有描述性的名称==
函数起一个具有描述性的名称,函数的功能越集中,就越便于起一个好名字。
要害怕长名称,长名称要比短而费解的短名称好。
同一个模块中,函数的命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。
==函数参数==
理想的函数参数是 0,其次是 1,再次是 2,应当尽量避免 3 参数的函数(除非万不得已)。
1. 一般来说,信息通过参数输入函数,然后通过返回值进行输出,所以尽量不要在参数中输出函数结果。
2. 不要使用标识参数:向函数输入 boolean 值是一种不优雅的做法,相当于大声宣布本函数不只做一件事,在任何情况下我们都不要这样做;此时我们应该将这种情况拆分为 2 个函数 doXxxForXxx 和 doXxxForNonXxx。
3. 双参数函数:
1. 两个参数最好是某个值的有序组成部分,如:`new Point(0, 0);`。
2. 使用两个同类型的参数时,应额外注意其先后顺序,如:`assertEquals(expected, actual);`中一般约定期望值在前,实际值在后。
3. 尽量的转化双参数函数为单参数函数,可以使用诸如添加成员变量、新建一个类等方法。
4. 三参数函数:如果一个函数看起来需要 2 个、3 个及 3 个以上的参数,说明一些参数应该封装成类了,例:
~~~java
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
~~~
5. 可变参数:在 Java 中,可以使用 ... 向函数传入可变长度的参数。
6. 函数和参数应当形成一种非常良好的动词/名词的对应关系,例如: `writeField(name);`。
==分割指令和询问==
1. 函数要么做什么事,要么回答什么事,两者不可得兼,例:
~~~java
boolean isSuccess = setAttribute("username", "unclebob");
---------------------------------------------------------
if(attributeExists("username")) {
setAttribute("username", "unclebob");
}
~~~
此两种代码明显后者更优。
2. 使用异常替代返回错误码,将询问以异常的形式抛出,能够很好的分割指令和询问。
1. 将 try/catch 代码块单独抽离乘一个函数,使之从主代码块中分离出来,代码就会变得更简洁。
2. 错误处理就是一件事,处理错误的函数不应该做其他的事,如果在抛出错误的时候需要做另外的一件事,那么应该好好考虑此处是否应该抛出错误。
3. 尽量使用异常代替错误码,返回错误码一般是一个枚举类,其他许多类都要依赖并使用它,使得在修改这个枚举类时造成了很大的负面压力,所以,尽量使用异常派生类。
---
#### 3.注释
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册