前言
Nacos 确实是一款不错的产品,但奈何文档写的..不清晰
关于 2.x 版本间的升级,看似好像无坑,实际上还是存在一些配置的变动引发的小坑,特此记录
本文是从2.0.3版本升级至2.2.4(实际上为2.2.3)版本,理论上2.x版本间升级均可参考食用
升级流程
如果使用 Nacos
2.0.x版本,先确保是否已经关闭双写开关
有关双写开关相关信息,可参阅https://nacos.io/zh-cn/docs/v2/upgrading/2.0.0-upgrading.html相比 Nacos
2.0.x版本,2.1.x+数据库结构有以下变动,以下为 MySQL 下的语句,需要在升级前先执行以下语句1
2
3alter table config_info add column `encrypted_data_key` text NOT NULL COMMENT '秘钥';
alter table config_info_beta add column `encrypted_data_key` text NOT NULL COMMENT '秘钥';
alter table his_config_info add column `encrypted_data_key` text NOT NULL COMMENT '秘钥';Nacos
2.1.x版本开始支持插件化,且鉴权作为插件化的形式引入,其配置文件值也相应做了更改,需要改动applicaiton.properties对应值1
2
3
4
5
6
7
8
9
10
11# 默认鉴权插件用于生成用户登陆临时accessToken所使用的密钥
# 以下配置为2.1.x之前配置 需替换为 nacos.core.auth.plugin.nacos.token.secret.key
nacos.core.auth.default.token.secret.key=xxxx
# 以下配置为2.1.x之后配置
nacos.core.auth.plugin.nacos.token.secret.key=xxxx
# accessToken的过期时间
# 以下配置为2.1.x之前配置 需替换为 nacos.core.auth.plugin.nacos.token.expire.seconds
nacos.core.auth.default.token.expire.seconds=50
# 以下配置为2.1.x之后配置
nacos.core.auth.plugin.nacos.token.expire.seconds=50其中,
secretKey之前版本存在默认值,存在安全风险,后续版本已去除默认值。
出于安全考虑不建议在生产环境使用默认值,如需修改,可参考官方公告方式进行修改关于 Nacos 默认 token.secret.key 及 server.identity 风险说明及解决方案公告User-Agent 白名单修改
此选项非 Nacos
2.x版本后的变更,但如需在升级过程中修改该配置,需要进行过渡处理如需开启服务身份识别功能,禁用原先通过 User-Agent 的判断,可参考官方文档开启服务身份识别功能
在操作的过程中,建议先开启白名单判断(nacos.core.auth.enable.userAgentAuthWhite值修改为 true),后保证认证头和值均发布到服务上后(nacos.core.auth.server.identity.key及nacos.core.auth.server.identity.value)再将nacos.core.auth.enable.userAgentAuthWhite值修改为 false升级集群中某一节点服务
对于 Docker/K8s 环境部署 Nacos 服务的情况,相当于滚动更新容器;对于虚拟机/物理机部署的情况,相当于停止旧 jar 包,使用新 jar 包启动
此处升级需注意,不仅是 jar 包的替换,新版本的
startup.sh(或docker-startup.sh)亦可能有变化,需使用新版本的 shell 文件查看已经升级节点的日志文件,检查是否升级成功
- 检查 nacos.log
- 检查 core-auth.log
- 检查 naming-*.log
重复上述两步骤,直到集群中所有节点均升级到新版本
验证功能是否正常
遇到的问题
升级后,客户端连接旧服务正常,连接新服务报 Invalid signature (升级至2.2.4以下的版本,2.3.0版本已修复该问题)
包括 Console 下以及 Nacos Client 均可能出现这个情况
- 核心原因
Nacos 2.0.x 版本使用的是 jjwt 插件实现的 jwt 签发和鉴权,2.1.x 版本开始基于插件化实现,2.2.1 版本使用自研的 jwt 插件实现签发和鉴权,两者在对secretKey进行 Base64 解码的处理方式上存在不一致,如果原先的 secretKey 没有进行 Base64 编码,可能会存在此问题
(我之前部署的就不是进行 Base64 编码的,记得之前部署的时候没说要设置 Base64 编码的..)
旧版本对于
secretKey的处理方式为:在com.alibaba.nacos.auth.common.AuthConfigs#getSecretKeyBytes使用io.jsonwebtoken.io.Decoder#decode方法将 secretKey 转换成 byte 数组这个方法对不规范的数据做了处理,会截断后面的数据
新版本对于
secretKey的处理方式为: 在实例化com.alibaba.nacos.plugin.auth.impl.jwt.NacosJwtParser时,使用java.util.Base64.Decoder#decode(java.lang.String)进行 Base64 解码,如果解码失败,直接获取 secretKey 的 byte 数组
出现这个问题,很有可能是走到了 catch 逻辑中,导致 secretKey 的byte数组与之前的不一致,从而导致 JWT Token 签名校验失败,产生 Invalid signature 的错误
解决办法
jjwt 对不规范的数据采用截断方式,于是我想着把代码抽取出来成为独立的类,可以调用convert()对 secretKey 进行转换,入参为老版本 Nacos 配置的 secretKey,出参为新版本 Nacos 配置的 secretKey1
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/**
* @author xYohn
* @date 2023/8/4
*/
public class KeyConvert {
private static final char[] BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
private static final int[] BASE64_IALPHABET = new int[256];
private static final int IALPHABET_MAX_INDEX = BASE64_IALPHABET.length - 1;
private static final int[] IALPHABET = BASE64_IALPHABET;
static {
Arrays.fill(BASE64_IALPHABET, -1);
for (int i = 0, iS = BASE64_ALPHABET.length; i < iS; i++) {
BASE64_IALPHABET[BASE64_ALPHABET[i]] = i;
}
BASE64_IALPHABET['='] = 0;
}
final static byte[] decodeFast(char[] sArr) {
// Check special case
int sLen = sArr != null ? sArr.length : 0;
if (sLen == 0) {
return new byte[0];
}
int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
// Trim illegal chars from start
while (sIx < eIx && IALPHABET[sArr[sIx]] < 0) {
sIx++;
}
// Trim illegal chars from end
while (eIx > 0 && IALPHABET[sArr[eIx]] < 0) {
eIx--;
}
// get the padding count (=) (0, 1 or 2)
int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end.
int cCnt = eIx - sIx + 1; // Content count including possible separators
int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0;
int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes
byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
// Decode all but the last 0 - 2 bytes.
int d = 0;
for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) {
// Assemble three bytes into an int from four "valid" characters.
int i = ctoi(sArr[sIx++]) << 18 | ctoi(sArr[sIx++]) << 12 | ctoi(sArr[sIx++]) << 6 | ctoi(sArr[sIx++]);
// Add the bytes
dArr[d++] = (byte) (i >> 16);
dArr[d++] = (byte) (i >> 8);
dArr[d++] = (byte) i;
// If line separator, jump over it.
if (sepCnt > 0 && ++cc == 19) {
sIx += 2;
cc = 0;
}
}
if (d < len) {
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
int i = 0;
for (int j = 0; sIx <= eIx - pad; j++) {
i |= ctoi(sArr[sIx++]) << (18 - j * 6);
}
for (int r = 16; d < len; r -= 8) {
dArr[d++] = (byte) (i >> r);
}
}
return dArr;
}
private static int ctoi(char c) {
int i = c > IALPHABET_MAX_INDEX ? -1 : IALPHABET[c];
if (i < 0) {
String msg = "Illegal base64 character: '" + c + "'";
throw new RuntimeException(msg);
}
return i;
}
public static String convert(String input){
return java.util.Base64.getEncoder().encodeToString(decodeFast(input.toCharArray()));
}
public static void main(String[] args) {
System.out.println(convert("secretKey"));
}
}