Nacos踩坑记录:配置鉴权后控制台无法登录,返回 Invalid key: javax.crypto.spec.SecretKeySpec

2,202 阅读4分钟

背景

环境:nacos-server-2.2.3jdk1.8.0_131

最近在部署Nacos的时候,按照官方文档的提示按照如下配置开启了鉴权并设置了自定义密钥后,出现了无法登录控制台的情况,登录接口返回 caused: Invalid key: javax.crypto.spec.SecretKeySpec@fa77cae6

nacos.core.auth.system.type=nacos
nacos.core.auth.enabled=true
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=

注:此配置为官方默认配置,请根据实际情况进行调整。

排查过程

因为nacos的日志中未找到该信息,github上也没有找到相关的issue,我决定通过阅读nacos的源码找出相关原因。因为没有错误的堆栈信息,我只能通过全局检索的方式来定位错误发生的地方。搜索之后,我发现错误信息是从NacosSignatureAlgorithm.javagetMacInstance()中返回的:

package com.alibaba.nacos.plugin.auth.impl.jwt;

import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.plugin.auth.exception.AccessException;
import com.alibaba.nacos.plugin.auth.impl.users.NacosUser;

import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public final class NacosSignatureAlgorithm {
    
    private static final String JWT_SEPERATOR = ".";
    
    private static final int HEADER_POSITION = 0;
    
    private static final int PAYLOAD_POSITION = 1;
    
    private static final int SIGNATURE_POSITION = 2;
    
    private static final int JWT_PARTS = 3;
    
    private static final String HS256_JWT_HEADER = "eyJhbGciOiJIUzI1NiJ9";
    
    private static final String HS384_JWT_HEADER = "eyJhbGciOiJIUzM4NCJ9";
    
    private static final String HS512_JWT_HEADER = "eyJhbGciOiJIUzUxMiJ9";
    
    private static final Base64.Encoder URL_BASE64_ENCODER = Base64.getUrlEncoder().withoutPadding();
    
    private static final Base64.Decoder URL_BASE64_DECODER = Base64.getUrlDecoder();
    
    private static final Map<String, NacosSignatureAlgorithm> MAP = new HashMap<>(4);
    
    public static final NacosSignatureAlgorithm HS256 = new NacosSignatureAlgorithm("HS256", "HmacSHA256",
            HS256_JWT_HEADER);
    
    public static final NacosSignatureAlgorithm HS384 = new NacosSignatureAlgorithm("HS384", "HmacSHA384",
            HS384_JWT_HEADER);
    
    public static final NacosSignatureAlgorithm HS512 = new NacosSignatureAlgorithm("HS512", "HmacSHA512",
            HS512_JWT_HEADER);
    
    private final String algorithm;
    
    private final String jcaName;
    
    private final String header;
    
    static {
        MAP.put(HS256_JWT_HEADER, HS256);
        MAP.put(HS384_JWT_HEADER, HS384);
        MAP.put(HS512_JWT_HEADER, HS512);
    }
    
    /**
     * verify jwt.
     *
     * @param jwt complete jwt string
     * @param key for signature
     * @return object for payload
     * @throws AccessException access exception
     */
    public static NacosUser verify(String jwt, Key key) throws AccessException {
        if (StringUtils.isBlank(jwt)) {
            throw new AccessException("user not found!");
        }
        String[] split = jwt.split("\.");
        if (split.length != JWT_PARTS) {
            throw new AccessException("token invalid!");
        }
        String header = split[HEADER_POSITION];
        String payload = split[PAYLOAD_POSITION];
        String signature = split[SIGNATURE_POSITION];
        
        NacosSignatureAlgorithm signatureAlgorithm = MAP.get(header);
        if (signatureAlgorithm == null) {
            throw new AccessException("unsupported signature algorithm");
        }
        NacosUser user = signatureAlgorithm.verify(header, payload, signature, key);
        user.setToken(jwt);
        return user;
    }
    
    /**
     * verify jwt.
     *
     * @param header    header of jwt
     * @param payload   payload of jwt
     * @param signature signature of jwt
     * @param key       for signature
     * @return object for payload
     * @throws AccessException access exception
     */
    public NacosUser verify(String header, String payload, String signature, Key key) throws AccessException {
        Mac macInstance = getMacInstance(key);
        byte[] bytes = macInstance.doFinal((header + JWT_SEPERATOR + payload).getBytes(StandardCharsets.US_ASCII));
        if (!URL_BASE64_ENCODER.encodeToString(bytes).equals(signature)) {
            throw new AccessException("Invalid signature");
        }
        NacosJwtPayload nacosJwtPayload = JacksonUtils.toObj(URL_BASE64_DECODER.decode(payload), NacosJwtPayload.class);
        if (nacosJwtPayload.getExp() >= TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())) {
            return new NacosUser(nacosJwtPayload.getSub());
        }
        
        throw new AccessException("token expired!");
    }
    
    /**
     * get jwt expire time in seconds.
     *
     * @param jwt complete jwt string
     * @param key for signature
     * @return expire time in seconds
     * @throws AccessException access exception
     */
    public static long getExpiredTimeInSeconds(String jwt, Key key) throws AccessException {
        if (StringUtils.isBlank(jwt)) {
            throw new AccessException("user not found!");
        }
        String[] split = jwt.split("\.");
        if (split.length != JWT_PARTS) {
            throw new AccessException("token invalid!");
        }
        String header = split[HEADER_POSITION];
        String payload = split[PAYLOAD_POSITION];
        String signature = split[SIGNATURE_POSITION];
        
        NacosSignatureAlgorithm signatureAlgorithm = MAP.get(header);
        if (signatureAlgorithm == null) {
            throw new AccessException("unsupported signature algorithm");
        }
        return signatureAlgorithm.getExpireTimeInSeconds(header, payload, signature, key);
    }
    
    /**
     * get jwt expire time in seconds.
     *
     * @param header    header of jwt
     * @param payload   payload of jwt
     * @param signature signature of jwt
     * @param key       for signature
     * @return expire time in seconds
     * @throws AccessException access exception
     */
    public long getExpireTimeInSeconds(String header, String payload, String signature, Key key)
            throws AccessException {
        Mac macInstance = getMacInstance(key);
        byte[] bytes = macInstance.doFinal((header + JWT_SEPERATOR + payload).getBytes(StandardCharsets.US_ASCII));
        if (!URL_BASE64_ENCODER.encodeToString(bytes).equals(signature)) {
            throw new AccessException("Invalid signature");
        }
        NacosJwtPayload nacosJwtPayload = JacksonUtils.toObj(URL_BASE64_DECODER.decode(payload), NacosJwtPayload.class);
        return nacosJwtPayload.getExp();
    }
    
    private NacosSignatureAlgorithm(String alg, String jcaName, String header) {
        this.algorithm = alg;
        this.jcaName = jcaName;
        this.header = header;
    }
    
    String sign(NacosJwtPayload nacosJwtPayload, Key key) {
        String jwtWithoutSign = header + JWT_SEPERATOR + URL_BASE64_ENCODER.encodeToString(
                nacosJwtPayload.toString().getBytes(StandardCharsets.UTF_8));
        Mac macInstance = getMacInstance(key);
        byte[] bytes = jwtWithoutSign.getBytes(StandardCharsets.US_ASCII);
        String signature = URL_BASE64_ENCODER.encodeToString(macInstance.doFinal(bytes));
        return jwtWithoutSign + JWT_SEPERATOR + signature;
    }
    
    private Mac getMacInstance(Key key) {
        try {
            Mac instance = Mac.getInstance(jcaName);
            instance.init(key);
            return instance;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new IllegalArgumentException("Invalid key: " + key);
        }
    }
    
    public String getAlgorithm() {
        return algorithm;
    }
    
    public String getJcaName() {
        return jcaName;
    }
    
    public String getHeader() {
        return header;
    }
}

从源码中我们可以看出Nacos支持HS256HS384HS512三种签名算法。 而我设置的nacos.core.auth.plugin.nacos.token.secret.key的值是32个字节(256bit)经过base64转码得到的,是符合HS256的要求的。正在我百思不得其解的时候,我的同事提醒我可能是低版本JDK对加密算法存在限制

解决问题

阅读了相关资料之后,发现低版本JDK受限制的加密强度最大仅支持128bit的key。JDK 8u161以后的版本默认支持 unlimited 的加密策略。 JDK 8u151JDK 8u161的版本需要将crypto.policy 设置为 unlimitedJDK 8u151以前的版本需要下载 local_policy.jar 和 US_export_policy.jar并复制到${JAVA_HOME}/jre/lib/security目录下。

我在更换JDK版本为jdk1.8.0_231后,问题就解决啦。虽然这只是一个小坑,还是决定记录下来。

相关阅读:

JDK 8 : Cryptographic Strength Configuration
jce-enable-unlimited-strength