吉安感知网项目-后端
xiebin
2026-01-06 d207a86cdf1ab52ef8cb7cd83bad8fceab8038cf
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/**
 * BladeX Commercial License Agreement
 * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
 * <p>
 * Use of this software is governed by the Commercial License Agreement
 * obtained after purchasing a license from BladeX.
 * <p>
 * 1. This software is for development use only under a valid license
 * from BladeX.
 * <p>
 * 2. Redistribution of this software's source code to any third party
 * without a commercial license is strictly prohibited.
 * <p>
 * 3. Licensees may copyright their own code but cannot use segments
 * from this software for such purposes. Copyright of this software
 * remains with BladeX.
 * <p>
 * Using this software signifies agreement to this License, and the software
 * must not be used for illegal purposes.
 * <p>
 * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
 * not liable for any claims arising from secondary or illegal development.
 * <p>
 * Author: Chill Zhuang (bladejava@qq.com)
 */
package org.sxkj.gateway.provider;
 
import org.springblade.core.launch.props.BladeProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
 
/**
 * 超级密钥加解密工具类
 *
 * @author BladeX
 */
public class KeyProvider {
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
 
    /**
     * API Key 前缀
     */
    private static final String API_KEY_PREFIX = "ak-";
    /**
     * API Key 配置项
     */
    private static final String BLADE_KEY_ENABLED = "blade.key.enabled";
    /**
     * API Key 配置项
     */
    private static final String BLADE_KEY_CRYPTO_KEY = "blade.key.crypto-key";
    /**
     * 随机字符串因子
     */
    private static final String RANDOM_FACTOR = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    /**
     * 安全随机数生成器
     */
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    /**
     * PKCS7 块大小
     */
    private static final int BLOCK_SIZE = 16;
    /**
     * Hex 编码字符
     */
    private static final byte[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
 
    /**
     * 判断是否为合法 API Key
     *
     * @param auth 认证字符串
     * @return 是否为 API Key
     */
    public static boolean isApiKey(@Nullable String auth, BladeProperties bladeProperties) {
        // 检查功能是否启用
        String enabled = bladeProperties.getEnvironment().getProperty(BLADE_KEY_ENABLED);
        if (!Boolean.parseBoolean(enabled)) {
            return false;
        }
        // 检查前缀并解析
        if (auth != null && auth.startsWith(API_KEY_PREFIX)) {
            String cryptoKey = bladeProperties.getEnvironment().getProperty(BLADE_KEY_CRYPTO_KEY);
            String parsed = parseKey(auth, cryptoKey);
            return parsed != null;
        }
        return false;
    }
 
    /**
     * 生成 API Key
     *
     * @param cryptoKey 加密密钥
     * @return 带前缀的加密 Key
     */
    public static String generateKey(String cryptoKey) {
        return API_KEY_PREFIX + encryptToHex(randomUUID(), cryptoKey);
    }
 
    /**
     * 解析 API Key
     *
     * @param key       带前缀的加密 Key
     * @param cryptoKey 加密密钥
     * @return 解密后的原始值
     */
    @Nullable
    public static String parseKey(@Nullable String key, String cryptoKey) {
        if (isBlank(key) || !key.startsWith(API_KEY_PREFIX)) {
            return null;
        }
        try {
            String encryptedPart = key.substring(API_KEY_PREFIX.length());
            return decryptFormHexToString(encryptedPart, cryptoKey);
        } catch (Exception e) {
            return null;
        }
    }
 
    /**
     * 加密并转换为十六进制字符串
     *
     * @param content    明文内容
     * @param aesTextKey AES 密钥字符串
     * @return 十六进制加密字符串
     */
    public static String encryptToHex(String content, String aesTextKey) {
        return hexEncode(encrypt(content.getBytes(DEFAULT_CHARSET), aesTextKey));
    }
 
    /**
     * 从十六进制字符串解密并返回明文字符串
     *
     * @param content    十六进制加密字符串
     * @param aesTextKey AES 密钥字符串
     * @return 解密后的明文字符串
     */
    @Nullable
    public static String decryptFormHexToString(@Nullable String content, String aesTextKey) {
        byte[] hexBytes = decryptFormHex(content, aesTextKey);
        if (hexBytes == null) {
            return null;
        }
        return new String(hexBytes, DEFAULT_CHARSET);
    }
 
    /**
     * 从十六进制字符串解密并返回字节数组
     *
     * @param content    十六进制加密字符串
     * @param aesTextKey AES 密钥字符串
     * @return 解密后的字节数组
     */
    @Nullable
    public static byte[] decryptFormHex(@Nullable String content, String aesTextKey) {
        if (isBlank(content)) {
            return null;
        }
        return decrypt(hexDecode(content.getBytes(DEFAULT_CHARSET)), aesTextKey);
    }
 
    /**
     * 加密字节数组
     *
     * @param content    明文字节数组
     * @param aesTextKey AES 密钥字符串
     * @return 加密后的字节数组
     */
    public static byte[] encrypt(byte[] content, String aesTextKey) {
        return aes(pkcs7Encode(content), Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET), Cipher.ENCRYPT_MODE);
    }
 
    /**
     * 解密字节数组
     *
     * @param content    加密的字节数组
     * @param aesTextKey AES 密钥字符串
     * @return 解密后的字节数组
     */
    public static byte[] decrypt(byte[] content, String aesTextKey) {
        return pkcs7Decode(aes(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET), Cipher.DECRYPT_MODE));
    }
 
    /**
     * AES 加解密核心方法
     */
    private static byte[] aes(byte[] data, byte[] aesKey, int mode) {
        Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32");
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(mode, keySpec, iv);
            return cipher.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 
    // ==================== 内联工具方法 ====================
 
    /**
     * 生成指定长度的随机字符串
     */
    public static String randomKey(int count) {
        char[] buffer = new char[count];
        for (int i = 0; i < count; i++) {
            buffer[i] = RANDOM_FACTOR.charAt(SECURE_RANDOM.nextInt(RANDOM_FACTOR.length()));
        }
        return new String(buffer);
    }
 
    /**
     * 生成无横线的 UUID
     */
    private static String randomUUID() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return new UUID(random.nextLong(), random.nextLong()).toString().replace("-", "");
    }
 
    /**
     * 检查字符串是否为空或空白
     */
    private static boolean isBlank(@Nullable String str) {
        if (str == null || str.isEmpty()) {
            return true;
        }
        for (int i = 0; i < str.length(); i++) {
            if (!Character.isWhitespace(str.charAt(i))) {
                return false;
            }
        }
        return true;
    }
 
    /**
     * 字节数组转十六进制字符串
     */
    private static String hexEncode(byte[] data) {
        int len = data.length;
        byte[] out = new byte[len << 1];
        for (int i = 0, j = 0; i < len; i++) {
            out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
            out[j++] = HEX_DIGITS[0x0F & data[i]];
        }
        return new String(out, DEFAULT_CHARSET);
    }
 
    /**
     * 十六进制字节数组解码为原始字节数组
     */
    private static byte[] hexDecode(byte[] data) {
        int len = data.length;
        if ((len & 0x01) != 0) {
            throw new IllegalArgumentException("hexBinary needs to be even-length: " + len);
        }
        byte[] out = new byte[len >> 1];
        for (int i = 0, j = 0; j < len; i++) {
            int f = Character.digit(data[j++], 16) << 4;
            f |= Character.digit(data[j++], 16);
            out[i] = (byte) (f & 0xFF);
        }
        return out;
    }
 
    /**
     * PKCS7 编码(填充)
     */
    private static byte[] pkcs7Encode(byte[] src) {
        int count = src.length;
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        byte pad = (byte) (amountToPad & 0xFF);
        byte[] dest = new byte[count + amountToPad];
        System.arraycopy(src, 0, dest, 0, count);
        Arrays.fill(dest, count, dest.length, pad);
        return dest;
    }
 
    /**
     * PKCS7 解码(去填充)
     */
    private static byte[] pkcs7Decode(byte[] decrypted) {
        int pad = decrypted[decrypted.length - 1];
        if (pad < 1 || pad > BLOCK_SIZE) {
            pad = 0;
        }
        if (pad > 0) {
            return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
        }
        return decrypted;
    }
}