Map

Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   
// 返回与 key 之前关联的值, 如果 key 存在,返回与 key 关联的 value
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}

return v;
}


// 返回与 key 关联的当前值(要么是已经存在的,或者是 mappingFunction 函数计算得到的新值), 如果 mappingFunction 计算的新值为 null,该方法也返回 null
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}

return v;
}

Unsplash 图片下载脚本

Unsplash 图片下载脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/bin/bash
#
# description: download pictures from https://unsplash.com/ with curl, jq and axel
# dependency: apt install jq; apt install axel
# author: guo
# date: 2020-05-30

baseDir=/mnt/e/unsplash_pic
paramsStr=""

# kill all axel download processes
# delete all temporary download file: picName.jpg.st, picName.jpg(unfinished)
function clean_func() {
ps -aux | grep 'axel -[n]' | awk '{ print $2 }' | xargs -n 1 kill -9
for name in $(find $baseDir -type f -name "*.jpg.st"); do
picName=${name%.st}
# echo "deleting $name and $picName"
rm $name
rm $picName
done
}

# function to append request query parameters
function add_query_param() {
if [ -z "$paramsStr" ]; then
paramsStr="$1"
else
paramsStr="$paramsStr&$1"
fi
return 0
}

# 2-SIGINT ctrl+c
# 9-SIGKILL forced termination
# 15-SIGTERM default termination behaviour
# 20-SIGTSTP ctrl+z
# trap signal outside, make all download stop and do clean suff
trap 'clean_func' 2 9 15 20

while getopts 'vrq:d:f:n:' OPT; do
case "$OPT" in
v)
showVerbose=1
;;
r)
isRandom=1
;;
f)
add_query_param "featured=$OPTARG"
;;
n)
add_query_param "count=$OPTARG"
;;
d)
add_query_param "orientation=$OPTARG"
;;
q)
add_query_param "query=$OPTARG"
;;
?)
echo "avaliable options: [-vr] [-f feature]" >&2 ## standard error
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"

baseUrl=https://api.unsplash.com/photos
if (($isRandom == 1)); then
url=$baseUrl/random
else
url=$baseUrl
fi

if [ -n "$paramsStr" ]; then
url="$url?$paramsStr"
fi

scriptDir="$baseDir/script"
returnFile="$scriptDir/pic_json.json"
downloadUrlFile="$scriptDir/pic_download_url.txt"

# specify the clientId !!!
# specify the clientId !!!
# specify the clientId !!!
authHeader='Authorization: Client-ID ${clientId}'

echo "url: $url"
curl -H "$authHeader" $url >$returnFile
jq -M .[].links.download_location $returnFile >$downloadUrlFile

# greedy replace mode, replace all " character
# acquire unsplash pi id as a part of file name, format: unsplash_${id}.jpg
# use axel to download

# todo//: follow unsplash's development guide: Triggering a Download(https://help.unsplash.com/en/articles/2511258-guideline-triggering-a-download)
# curl "$baseUrl/:$picId/download" &
while read line; do
downUrl=${line//\"/}
actUrl=$(curl -H "$authHeader" $downUrl | jq .url | tr -d "\"")

picId=$(echo $downUrl | grep --colour=never -o -P [^/]*/download | tr / _ | sed -e 's/_download//')
echo "########## $picId start downloading ##########"
axel -n 10 -o "$baseDir/unsplash_${picId}.jpg" $actUrl &
done <$downloadUrlFile
wait
echo "################ all done ################"

Scripting Redis with Lua

Scripting Redis with Lua

Redis 2.6 版本之前,如果想要一些 Redis 不能提供的功能,要么编写较复杂客户端代码实现,要么编辑 Redis 的 C 语言源码。 即使编辑源码并不困难,但是在商业环境中使用这样的版本,或者试图说服经理使用我们自己的 Redis 服务器版本,都是一件极具挑战的事。

Redis 引入了基于 Lua 语言的服务端脚本的支持,可以使得用户在 Redis 中执行更多的操作,简化代码,提升新能。

Lua

由巴西里约热内卢教皇天主教大学 (The Pontifical Catholic University of Rio de Janeiro in Brazil)的一支团队进行设计,实现和维护,高效,轻量,的嵌入式脚本语言,支持过程式编程,面向对象编程,函数式编程,数据驱动编程和数据描述;动态类型,虚拟机字节码解释执行,自动内存管理。

应用广泛: Adobe Photoshop Lightroom, 重点应用于嵌入式系统和游戏,如魔兽世界,愤怒的小鸟等

如何在 Redis 中使用 Lua 脚本

  1. 加载 Lua 脚本到 Redis 中
1
> SCRIPT LOAD "Lua Script"

Redis 会将字符串的 Lua 脚本存储起来,便于后续执行,并返回脚本的 SHA1 hash 值,当我们想要执行脚本时,通过 EVALSHA 命令,带着这个 hash 值以及 Key 和 其他脚本需要的参数列表

  1. 执行脚本
1
> EVALSHA sha1 numkeys key [key ...] arg [arg ...]
  1. 直接执行脚本
1
> EVAL script numkeys key [key ...] arg [arg ...]

Redis 会自动缓存该脚本,可以直接调用该脚本的 SHA1 hash 值

简单示例

1
2
3
SCRIPT LOAD "return 'hello world!'"

EVALSH sha1 0

注意事项

Lua 脚本返回的数据类型

由于 Lua 允许数据传入和传出的方式的限制,Lua 中提供的某些数据类型不允许返回,或者在返回之前被更改。

Lua value Redis return
true 1
false nil
nil nil
1.5 (浮点数) 1
1e30 平台相关的最小整数值
“string” Unchanged
1 Integer is returned unchanged

由于可能产生歧义性,所以应该尽可能地返回字符串,然后手动执行解析

原子性

和单条命令或者 MULTI/EXEC 命令一样,是原子性地 (单条命令一次运行以个;MULTI/EXEC 实现了简单的事务)。执行 Lua 脚本, EVALEVALSHA 以应该被视作一个命令,只不过较为复杂而已。

当执行 Lua 脚本时, Redis 不允许运行其他的读或写命令。如果编写的脚本永远不返回,就会阻塞其他客户端的执行。 Redis 提供了两种方式来停止脚本的运行。

  • 如果脚本只是执行了一系列的读操作,且执行时间已经超过了 Redis 配置的 lua-time-limit 时间, 那么可以执行 SCRIPT KILL 命令

  • 如果已经执行了写命令,Kill 脚本可能会导致 Redis 处于不一致的状态,想要恢复的化,可以执行 SHUTDOWN NOSAVE 命令, Redis 会丢失上次快照后的所有变更; 或者丢失写到 AOF 文件的操作

记得在部署生产前,一定要严格的测试 Lua 脚本

应用到现实世界

创一个脚本加载器

image-20201103222323879

image-20201103222345132

键值与参数列表

应该尽可能地将要操作地 Key 作为 Keys 列表的一部分,而脚本地其他输入作为参数列表的一部分,这是因为,如果内部实现了多服务器分片,就要对这些 Keys 进行校验,是否存在于同一分片上

转换为使用 Lua 脚本实现之前的功能

image-20201103222503976

image-20201103222524180

应用到项目中例子

1
2
3
4
5
6
local incrres = redis.call('incr', KEYS[1])
if (incrres == 1) then
redis.call('expire', KEYS[1], 300)
return "SUCCESS"
end
return "FALSE"

RSA

RSA

RSA 是一种公钥密码算法(非对称密码算法),由三位开发者的姓氏首字母进行命名。用于公钥密码和数字签名。

RSA 的加密

RSA 中,明文、密文、密钥都是数字,加密过程可用如下公式来表示:

​ $密文 = 明文^E \mod N$ (RSA 加密)

即明文的 E 次方求 N 的余数,这个余数就是密文。 所以直到 E 和 N 就可以进行加密,E 和 N 就是 RSA 加密的密钥,E 和 N 的组合就是 公钥 (E, N).E: Encryption, N: Number. E 和 N 是经过计算得来的,不是随意选取的。

RSA 的解密

RSA 的解密可以用如下公式来表示

​ $明文 = 密文^D \mod N$ (RSA 解密)

即密文的 D 次方求 N 的余数,这个余数就是明文,D 和 N 组合起来就是 RSA 的私钥 (D, N) . D: Decryption N:Number. D 是与 E 有着紧密的额联系的,否则是无法对 E 加密的密文通过 D 进行解密的。

RSA 的加密和解密使用相同的形式,并且可以用公式化进行表示。

生成密钥对

密钥对,即要生成加密使用的 (E, N) 和 解密的 (D, N), 需要生成三个数:E, D, N

(1) 求 N

准备两个很大的质数 p 和 q, p, q 太小使得破译变的容易,太长,会导致计算事件变成,所以需要均衡。假设 p 和 q 都是 521 比特。

如何生成质数?伪随机数算法 + 质数验证算法

如何判断质数?费马测试、米勒拉宾测试

$ N = p\times q$ 

(2) 求 L

L 在加密和解密过程中都不曾出现,只用于密钥对的生成过程。

​ $ L = lcm(p-1, q-1)$ L 是 p-1, q-1 的最小公倍数, lcm - least common multiple

(3) 求 E

​ $ 1 < E < L$ E 是一个比 1 大,比 L 小的数

​ $gcd(E, L) = 1$ E 和 L 的最大公约数必须为 1 (E 和 L 互质), gcd - greatest common divisor

求 E 可使用伪随机数生成器,生成 (1, L) 区间内的候选数,在判断是否满足 $gcd(E, L) = 1$ ,求最大公约数可使用欧几里得辗转相除法

至此,已经求出了 E, N, 生成了密钥对中的公钥

(4) 求 D

D 由 E 计算而得, D, E , L 满足下列关系:

​ $1 < D <L$

​ $E\times D \mod L = 1$

只要数 D 满足上述条件,则通过 E 和 N 进行加密的密文,就可以通过 D 和 N 进行解密。要保证存在满足条件的 D, 就需要保证 E 和 L 的最大公约数为 1 $gcd(E, L) = 1$ , 时钟运算下的倒数,只有某个数和模数(如 12)的最大公约数为1,数学上称为和模数互质,才存在对应的倒数。

RSA 算法利用了求离散对数是困难的(通过密文求得明文进行破解),大整数的质因数分解是困难的(通过分解 N , 还原密钥生成的过程来进行破解)保证了机密性。

时钟运算的乘方,时钟运算的除法,时钟运算的离散对数,大整数的质因数分解

RSA 的攻击

  1. 通过密文求得明文进行破解

    $密文 = ?^E \mod N$ 密文可以通过窃取得到,E, N 都是公开的,求明文, 转化为离散对数问题

  1. 通过分解 N , 还原密钥生成的过程来进行破解

    已知公钥的 E N, 从密钥生成的过程破解,对 N 进行质因数分解,期望得到 p 和 q,从而求出 D, 转化为大整数的质因数分解问题

  1. 中间人攻击 (man-in-the-middle attack)

    针对于所有公钥密码算法,中间人拦截发送方和接收方之间的消息,拦截接收方发送给发送方的公钥,并将自己的公钥伪装成实际接收方的公钥发送给发送方,发送方使用中间人的公钥进行加密发送,被中间人进行拦截,并通过自己的私钥进行解密,截取消息。然后通过获取的接受方的公钥将篡改的消息进行加密发送给接收方。可以使用公钥证书解决该问题。

  1. 选择密文攻击 (Chosen Ciphertext Attack)

    通过将任意数据发送给解密服务,获得提示,利用解密提示,进行破译。解决方法:对密文进行认证,判断密文是否是由直到明文的人通过合法的方式生成的, RSA-OAEP 就是基于这种思路的一种 RSA 改良算法

实践

使用 openssl 生成密钥,进行加密和解密

密钥的生成与查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# 生成私钥
$ openssl genrsa -out rsa_private.pem 2048

# 查看私钥
$ cat rsa_private.pem
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA5kFKwnpv86aLqEoU2VQ1l9aYek/wRvWqE6k4k7kzxBLK9hJp
aOOPyd2bz41fsQqVE0ezw6v9IR1WoEDA1WOqwquPoHaN9zGCBCbhcMSftGlj2E9x
xMb2l0ViR/OHT/5PVFALOwKFB4GlUaruKvkpRWNN7IRd+tjH7WGyZPiGDvbRPlSz
jnyJj9Rjl0TQ1DQ3WJCnOrIjGbThK7re5ShxmHrCO/mlaLCGoe33k5G8XtUyuiFq
aXUtSHLIAOFJCrixQjYYWFcBRvuth8Fo6wW9t6dPxIBWplkeADN662lnEtzxuPK1
Kt/N1cwful7VsZSDN+Frhjkd1DF9kwERZHBDBQIDAQABAoIBAHdBA85cCZWhAZ4k
2E2DTsqYa5qVwnWOEQtjzpi8CDKaJSJzgMLBcZx0uZkyuIgCWhmFPnauoki/YDlZ
sEYU+8EdiCLspDSOIK+zz3lPbQPMvdivtNXc0qSgW/m2CLQGm+GoH1jtwiaUICJ/
LCF3sMduWlKQb/hWxJ2Oxw2CpjQi59QTB8z/gt+iCq+imv26qkpJLNhcvHrTMTpR
V7cmoX2ACAqPO+PWmKlC5RdH4j89F+yCkhE+hCGKzqEiqC1M8XsuyVn1mxL50GK5
/NHRBWsHc5ar5L0/pqiwx6ktSow7y2MXPttAEcX/MztpbiCgBCz4MVcF3AJ5F1qa
uY7+9WECgYEA9a7iGKc1BNVe8Zxg4jlgBxNvPJyxtN5iL2Roub9KLgzyYPEa6gK+
29b/wYUuYXNbJnPpOSe0dVqInp1gOmTMKAI7te+QKI1eiDxD0YgQsBhXIiBiEo0m
TNHBUEizWMO6wdqqdut3eHAVgmigkeQx+7AT2G/drHFp5D/GvqWGkDkCgYEA7+yO
L/T1U3w1EFfQVzIMt1N0+VLsdd4bDmnwOfH3/hTsJGxx+OFpTZLvIO34+HE5mZAH
pbrvJQCLRhYuSdDeFlK30l2P4TxTeN8y7a07jAq4eQP1g2ANnOPIqepoA0VoxLYt
IyzuGWwhv2mOWJpU5Rb8dhBaKwYE6Nu++L+eMS0CgYBqTsAats9kRgeNOINboEQD
C8/IRG1IvCl0JFiEd1db0dJXTDy/IW0Ap2mHjV9iX44Dd4hlaN7XINOY/rQS0Gbe
sQixUvXn9jP/c0RNODkwWXfqlmSZbmYyO1kQqkSgliELnNjCGGVbwfQst6UzO7C9
HZWYw98BrRmK4GXZPLqbUQKBgFLZhjQrS7gbkRtmp4wkvV1lFWSvbPY6z28HyCvK
Uc/Mm8rsI+NEu59NGQRvCOWJ+9D6epmRVtmZOX7nU+6rkLV2tQGQy4mE0nJP6P9I
LeLrJM6tPk+ykKDXy8hZKLfQdSBQpH+cGVBwFatKoRwZly1Q5bdDaE+pEXLzxxuQ
1+i1AoGBAOECFiG5hiv6NxNKndrZ/mbIcLqW/gbdd8Q0Ic/66jH9FmqIgZ68aN/v
J1u7M/NnNcyktgN2vrJhZ6gZyi1lzyZO9/XEjqsPYrSu371Yr8az4jJCwskBttux
whX63VoNZpJFldn1l6A9VI+kNkdZitjPKrOZ3F2dROtbuood5rjj
-----END RSA PRIVATE KEY-----

# 查看 RSA 私钥的组成要素,参考附录中 PKCS1 的 ASN.1 表示
$ openssl rsa -in rsa_private.pem -text -noout
RSA Private-Key: (2048 bit, 2 primes)
modulus:
00:e6:41:4a:c2:7a:6f:f3:a6:8b:a8:4a:14:d9:54:
35:97:d6:98:7a:4f:f0:46:f5:aa:13:a9:38:93:b9:
33:c4:12:ca:f6:12:69:68:e3:8f:c9:dd:9b:cf:8d:
5f:b1:0a:95:13:47:b3:c3:ab:fd:21:1d:56:a0:40:
c0:d5:63:aa:c2:ab:8f:a0:76:8d:f7:31:82:04:26:
e1:70:c4:9f:b4:69:63:d8:4f:71:c4:c6:f6:97:45:
62:47:f3:87:4f:fe:4f:54:50:0b:3b:02:85:07:81:
a5:51:aa:ee:2a:f9:29:45:63:4d:ec:84:5d:fa:d8:
c7:ed:61:b2:64:f8:86:0e:f6:d1:3e:54:b3:8e:7c:
89:8f:d4:63:97:44:d0:d4:34:37:58:90:a7:3a:b2:
23:19:b4:e1:2b:ba:de:e5:28:71:98:7a:c2:3b:f9:
a5:68:b0:86:a1:ed:f7:93:91:bc:5e:d5:32:ba:21:
6a:69:75:2d:48:72:c8:00:e1:49:0a:b8:b1:42:36:
18:58:57:01:46:fb:ad:87:c1:68:eb:05:bd:b7:a7:
4f:c4:80:56:a6:59:1e:00:33:7a:eb:69:67:12:dc:
f1:b8:f2:b5:2a:df:cd:d5:cc:1f:ba:5e:d5:b1:94:
83:37:e1:6b:86:39:1d:d4:31:7d:93:01:11:64:70:
43:05
publicExponent: 65537 (0x10001)
privateExponent:
77:41:03:ce:5c:09:95:a1:01:9e:24:d8:4d:83:4e:
ca:98:6b:9a:95:c2:75:8e:11:0b:63:ce:98:bc:08:
32:9a:25:22:73:80:c2:c1:71:9c:74:b9:99:32:b8:
88:02:5a:19:85:3e:76:ae:a2:48:bf:60:39:59:b0:
46:14:fb:c1:1d:88:22:ec:a4:34:8e:20:af:b3:cf:
79:4f:6d:03:cc:bd:d8:af:b4:d5:dc:d2:a4:a0:5b:
f9:b6:08:b4:06:9b:e1:a8:1f:58:ed:c2:26:94:20:
22:7f:2c:21:77:b0:c7:6e:5a:52:90:6f:f8:56:c4:
9d:8e:c7:0d:82:a6:34:22:e7:d4:13:07:cc:ff:82:
df:a2:0a:af:a2:9a:fd:ba:aa:4a:49:2c:d8:5c:bc:
7a:d3:31:3a:51:57:b7:26:a1:7d:80:08:0a:8f:3b:
e3:d6:98:a9:42:e5:17:47:e2:3f:3d:17:ec:82:92:
11:3e:84:21:8a:ce:a1:22:a8:2d:4c:f1:7b:2e:c9:
59:f5:9b:12:f9:d0:62:b9:fc:d1:d1:05:6b:07:73:
96:ab:e4:bd:3f:a6:a8:b0:c7:a9:2d:4a:8c:3b:cb:
63:17:3e:db:40:11:c5:ff:33:3b:69:6e:20:a0:04:
2c:f8:31:57:05:dc:02:79:17:5a:9a:b9:8e:fe:f5:
61
prime1:
00:f5:ae:e2:18:a7:35:04:d5:5e:f1:9c:60:e2:39:
60:07:13:6f:3c:9c:b1:b4:de:62:2f:64:68:b9:bf:
4a:2e:0c:f2:60:f1:1a:ea:02:be:db:d6:ff:c1:85:
2e:61:73:5b:26:73:e9:39:27:b4:75:5a:88:9e:9d:
60:3a:64:cc:28:02:3b:b5:ef:90:28:8d:5e:88:3c:
43:d1:88:10:b0:18:57:22:20:62:12:8d:26:4c:d1:
c1:50:48:b3:58:c3:ba:c1:da:aa:76:eb:77:78:70:
15:82:68:a0:91:e4:31:fb:b0:13:d8:6f:dd:ac:71:
69:e4:3f:c6:be:a5:86:90:39
prime2:
00:ef:ec:8e:2f:f4:f5:53:7c:35:10:57:d0:57:32:
0c:b7:53:74:f9:52:ec:75:de:1b:0e:69:f0:39:f1:
f7:fe:14:ec:24:6c:71:f8:e1:69:4d:92:ef:20:ed:
f8:f8:71:39:99:90:07:a5:ba:ef:25:00:8b:46:16:
2e:49:d0:de:16:52:b7:d2:5d:8f:e1:3c:53:78:df:
32:ed:ad:3b:8c:0a:b8:79:03:f5:83:60:0d:9c:e3:
c8:a9:ea:68:03:45:68:c4:b6:2d:23:2c:ee:19:6c:
21:bf:69:8e:58:9a:54:e5:16:fc:76:10:5a:2b:06:
04:e8:db:be:f8:bf:9e:31:2d
exponent1:
6a:4e:c0:1a:b6:cf:64:46:07:8d:38:83:5b:a0:44:
03:0b:cf:c8:44:6d:48:bc:29:74:24:58:84:77:57:
5b:d1:d2:57:4c:3c:bf:21:6d:00:a7:69:87:8d:5f:
62:5f:8e:03:77:88:65:68:de:d7:20:d3:98:fe:b4:
12:d0:66:de:b1:08:b1:52:f5:e7:f6:33:ff:73:44:
4d:38:39:30:59:77:ea:96:64:99:6e:66:32:3b:59:
10:aa:44:a0:96:21:0b:9c:d8:c2:18:65:5b:c1:f4:
2c:b7:a5:33:3b:b0:bd:1d:95:98:c3:df:01:ad:19:
8a:e0:65:d9:3c:ba:9b:51
exponent2:
52:d9:86:34:2b:4b:b8:1b:91:1b:66:a7:8c:24:bd:
5d:65:15:64:af:6c:f6:3a:cf:6f:07:c8:2b:ca:51:
cf:cc:9b:ca:ec:23:e3:44:bb:9f:4d:19:04:6f:08:
e5:89:fb:d0:fa:7a:99:91:56:d9:99:39:7e:e7:53:
ee:ab:90:b5:76:b5:01:90:cb:89:84:d2:72:4f:e8:
ff:48:2d:e2:eb:24:ce:ad:3e:4f:b2:90:a0:d7:cb:
c8:59:28:b7:d0:75:20:50:a4:7f:9c:19:50:70:15:
ab:4a:a1:1c:19:97:2d:50:e5:b7:43:68:4f:a9:11:
72:f3:c7:1b:90:d7:e8:b5
coefficient:
00:e1:02:16:21:b9:86:2b:fa:37:13:4a:9d:da:d9:
fe:66:c8:70:ba:96:fe:06:dd:77:c4:34:21:cf:fa:
ea:31:fd:16:6a:88:81:9e:bc:68:df:ef:27:5b:bb:
33:f3:67:35:cc:a4:b6:03:76:be:b2:61:67:a8:19:
ca:2d:65:cf:26:4e:f7:f5:c4:8e:ab:0f:62:b4:ae:
df:bd:58:af:c6:b3:e2:32:42:c2:c9:01:b6:db:b1:
c2:15:fa:dd:5a:0d:66:92:45:95:d9:f5:97:a0:3d:
54:8f:a4:36:47:59:8a:d8:cf:2a:b3:99:dc:5d:9d:
44:eb:5b:ba:8a:1d:e6:b8:e3

# 从私钥中提取公钥,并通过 -RSAPublicKey_out 指定为 PKCS1 格式
$ openssl rsa -in rsa_private.pem -out rsa_public.pem -RSAPublicKey_out

# 查看公钥,公钥只需要包含 D,N 所以长度会相对较小
$ cat rsa_public.pem
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA5kFKwnpv86aLqEoU2VQ1l9aYek/wRvWqE6k4k7kzxBLK9hJpaOOP
yd2bz41fsQqVE0ezw6v9IR1WoEDA1WOqwquPoHaN9zGCBCbhcMSftGlj2E9xxMb2
l0ViR/OHT/5PVFALOwKFB4GlUaruKvkpRWNN7IRd+tjH7WGyZPiGDvbRPlSzjnyJ
j9Rjl0TQ1DQ3WJCnOrIjGbThK7re5ShxmHrCO/mlaLCGoe33k5G8XtUyuiFqaXUt
SHLIAOFJCrixQjYYWFcBRvuth8Fo6wW9t6dPxIBWplkeADN662lnEtzxuPK1Kt/N
1cwful7VsZSDN+Frhjkd1DF9kwERZHBDBQIDAQAB
-----END RSA PUBLIC KEY-----

# 查看 RSA 公钥的组成要素 D,N
$ openssl rsa -in rsa_public.pem -RSAPublicKey_in -text -noout
RSA Public-Key: (2048 bit)
Modulus:
00:e6:41:4a:c2:7a:6f:f3:a6:8b:a8:4a:14:d9:54:
35:97:d6:98:7a:4f:f0:46:f5:aa:13:a9:38:93:b9:
33:c4:12:ca:f6:12:69:68:e3:8f:c9:dd:9b:cf:8d:
5f:b1:0a:95:13:47:b3:c3:ab:fd:21:1d:56:a0:40:
c0:d5:63:aa:c2:ab:8f:a0:76:8d:f7:31:82:04:26:
e1:70:c4:9f:b4:69:63:d8:4f:71:c4:c6:f6:97:45:
62:47:f3:87:4f:fe:4f:54:50:0b:3b:02:85:07:81:
a5:51:aa:ee:2a:f9:29:45:63:4d:ec:84:5d:fa:d8:
c7:ed:61:b2:64:f8:86:0e:f6:d1:3e:54:b3:8e:7c:
89:8f:d4:63:97:44:d0:d4:34:37:58:90:a7:3a:b2:
23:19:b4:e1:2b:ba:de:e5:28:71:98:7a:c2:3b:f9:
a5:68:b0:86:a1:ed:f7:93:91:bc:5e:d5:32:ba:21:
6a:69:75:2d:48:72:c8:00:e1:49:0a:b8:b1:42:36:
18:58:57:01:46:fb:ad:87:c1:68:eb:05:bd:b7:a7:
4f:c4:80:56:a6:59:1e:00:33:7a:eb:69:67:12:dc:
f1:b8:f2:b5:2a:df:cd:d5:cc:1f:ba:5e:d5:b1:94:
83:37:e1:6b:86:39:1d:d4:31:7d:93:01:11:64:70:
43:05
Exponent: 65537 (0x10001)
加密与解密
1
2
3
4
5
6
7
8
9
# 从私钥中提取 pkcs8 格式的公钥
$ openssl rsa -in rsa_private.pem -out rsa_public_pkcs8.pem -pubout

# 加密
$ echo -n 'hello' | openssl rsautl -pubin -inkey rsa_public_pkcs8.pem -keyform PEM -encrypt -out cipher_text

# 解密
$ cat cipher_text | openssl rsautl -inkey rsa_private.pem -keyform PEM -decrypt
hello

使用 JCA 进行解密验证

使用这里的私钥进行解密前,需要将 PKCS1 格式的私钥转换成 PKCS8 格式的私钥:

1
2
3
4
# pkcs8 命令用来处理 PKCS#8 格式的密钥
# -outform DER 指定输出为 ASN.1 的 DER 编码格式
# -nocrypt 不对输出的私钥进行加密(私钥属于私密信息,需要保证安全存储,这里为了在 Java 中使用,不进行加密,加密可使用 KeyStore 存储)
openssl pkcs8 -in rsa_private.pem -outform DER -topk8 -out rsa_private_pkcs8.der -nocrypt

Java 中进行解密验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RSACryptTest {

@SneakyThrows
public static void main(String[] args) {

String pkcs8PrivateKeyPath = "E:\\learning-dir\\shell-learning\\rsa\\rsa_private_pkcs8.der";
String cipherTextPath = "E:\\learning-dir\\shell-learning\\rsa\\cipher_text";

byte[] privateKeyBytes = Files.readAllBytes(Paths.get(pkcs8PrivateKeyPath));
byte[] cipherText = Files.readAllBytes(Paths.get(cipherTextPath));

PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);

byte[] clearText = cipher.doFinal(cipherText);
System.out.println(new String(clearText));
}
}

最终打印出解密的结果:

1
hello

附录

PKCS#1 公钥 ASN.1 格式

1
2
3
4
RSAPublicKey ::= SEQUENCE {
modulus INTEGER , -- n
publicExponent INTEGER -- e
}

PKCS#1 私钥 ASN.1 格式

1
2
3
4
5
6
7
8
9
10
11
12
RSAPrivateKey ::= SEQUENCE {
version Version ,
modulus INTEGER , -- n
publicExponent INTEGER , -- e
privateExponent INTEGER , -- d
prime1 INTEGER , -- p
prime2 INTEGER , -- q
exponent1 INTEGER , -- d mod (p-1)
exponent2 INTEGER , -- d mod (q-1)
coefficient INTEGER , -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

参考阅读

[1] 图解密码技术

[2] 深入剖析 RSA 密钥原理及实践 - [vivo互联网技术]

[3] how to load the private key from a .der file into java private key object - Stack Overflow

[4] openssl rsa command

[5] openssl pkcs8 command

Quartz Stateful Job

Quartz Stateful Job

如果想在 Quartz 定时任务的每次执行过程中,对 JobDetail 的 JobDataMap 中的属性值进行更新,也就是 stateful 的任务,如不使用 @PersistJobDataAfterExecution 注解,稍后会涉及到,最初肯定是想获取上下文的 JobDetail.JobDataMap 引用,调用 put 方法进行覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ScheduleJob implements Job {

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 在构造 JobDetail, 初始化 JobDataMap 包含属性 `name`,值为 `Jason`
JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();
String name = dataMap.getString("name");

System.out.println("dataMap:" + dataMap.hashCode());
System.out.println("executionContext:" + jobExecutionContext.hashCode());
System.out.println(LocalDateTime.now() + " This is hi from " + name);

// 想要在每次任务执行时进行更新属性 name 的值
dataMap.put("name", name + " la");
System.out.println(dataMap.getString("name"));
}
}

结果是,每次执行时,获取到的 name 属性值还是 Jason,为此,对获取到的 dataMap 进行了 hashCode 值的打印,结果每次输出都是一样的,自认为每次 Job 执行时获取到的 JobDataMap 应用都指向同一个对象,为什么每次 put 不成功?

这个问题的答案在于,获取到的 JobDataMap 不是同一个对象,但返回的 hashCode 一样,是因为 JobDataMap 内部封装了一个 HashMap,HashMap hashCode 方法是对其中包含的每一个 Node(Entry) 中的 <K,V> 计算 HashCode 的值的加和,所以如果不同 HashMap 对象,存储相同的 K,V, 这两个 HashMap 的 hashCode 是相等,但实际上是两个不同的对象。

1
2
3
4
5
6
7
8
// java.util.AbstractMap#hashCode
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

再回到如何更新 JobDetail 中 JobDataMap 中的值,通过 debug, Job 每次执行的流程是,由 QuartzSchedulerThread 线程创建 JobRunShell 时,由于未使用持久化,使用默认的 RAMJobStore,每次执行时,从 JobStore 中获取执行时的上下文数据,并将其封装在 TriggerFiredBundle 对象中,并且再初始化该对象时,返回 JobDetail 对象的一个克隆对象,对应的 JobDataMap 也是原始 JobDataMap 的一个克隆对象(浅拷贝 shallow copy ),最后将这些数据封装在 JobExecutionContext 中, JobShell(Runnanle 对象) 由工作线程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 // org.quartz.simpl.RAMJobStore#triggersFired
public List<TriggerFiredResult> triggersFired(List<OperableTrigger> firedTriggers) {
//... 前面代码省略
TriggerFiredBundle bndle = new TriggerFiredBundle(retrieveJob(
tw.jobKey), trigger, cal,
false, new Date(), trigger.getPreviousFireTime(), prevFireTime,
trigger.getNextFireTime());
//... 后面代码省略
}

// org.quartz.simpl.RAMJobStore#retrieveJob
public JobDetail retrieveJob(JobKey jobKey) {
synchronized(lock) {
JobWrapper jw = jobsByKey.get(jobKey);
return (jw != null) ? (JobDetail)jw.jobDetail.clone() : null;
}
}


// org.quartz.impl.JobDetailImpl#clone
@Override
public Object clone() {
JobDetailImpl copy;
try {
copy = (JobDetailImpl) super.clone();
if (jobDataMap != null) {
copy.jobDataMap = (JobDataMap) jobDataMap.clone();
}
} catch (CloneNotSupportedException ex) {
throw new IncompatibleClassChangeError("Not Cloneable.");
}

return copy;
}

上面的代码解释了为什么每次在 Job 执行时,对 JobDetail.JobDataMap 的字符串属性进行更新时,不起效,因为每次获取都是不同的 JobDataMap 对象,对克隆对象做的更新不能反映到原始的对象中,但是对引用类型的属性进行更新,可以生效,因为原始的克隆是浅拷贝(比如,在 JobDataMap 中使用一个 ArrayList, 并在每次任务执行时添加元素到这个 List 中)。

那么要如何实现对 JobDetail 中的 JobDataMap 进行更新,使得每次都可以更新一个状态,保存在里面呢?答案是使用 @PersistJobDataAfterExecution 注解,在 Job 执行结束时,会触发 JobStore 的 triggeredJobComplete 方法,做一些收尾工作, 其中会判断如果 JobClass 类上使用了该注解,会更新 JobStore 中保存的 JobDetial 和其 JobDataMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//org.quartz.simpl.RAMJobStore#triggeredJobComplete
public void triggeredJobComplete(OperableTrigger trigger,
JobDetail jobDetail, CompletedExecutionInstruction triggerInstCode) {
//...
if (jd.isPersistJobDataAfterExecution()) {
JobDataMap newData = jobDetail.getJobDataMap();
if (newData != null) {
newData = (JobDataMap)newData.clone();
newData.clearDirtyFlag();
}
jd = jd.getJobBuilder().setJobData(newData).build();
jw.jobDetail = jd;
}
//...
}