前言

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
    3
    alter 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 白名单修改

    此选项非 Nacos2.x版本后的变更,但如需在升级过程中修改该配置,需要进行过渡处理

    如需开启服务身份识别功能,禁用原先通过 User-Agent 的判断,可参考官方文档开启服务身份识别功能
    在操作的过程中,建议先开启白名单判断(nacos.core.auth.enable.userAgentAuthWhite值修改为 true),后保证认证头和值均发布到服务上后(nacos.core.auth.server.identity.keynacos.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
  • 重复上述两步骤,直到集群中所有节点均升级到新版本

  • 验证功能是否正常

遇到的问题

  1. 升级后,客户端连接旧服务正常,连接新服务报 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 配置的 secretKey

      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
      /**
      * @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"));
      }
      }