最近遇到一个数据脱敏处理的需求,想要用一种轻量级的技术实现,必须足够简单并且适用于所有场合如前后端加密传输、路由加密、数据脱敏等。抽时间研究了一下Crypto/Jsencrypt加密库的一些API,发现完全符合上述需求,扩展也比较容易。

Crypto/Jsencrypt的简单应用-前后端加密传输

1、crypto-js与Java

1.1、前端加解密

1、安装crypto-js,crypto-js是谷歌开发的一个纯JavaScript的加密算法类库,支持多种加密算法,可以很方便的在前端实现加解密操作。

npm install crypto-js --save-dev

2、加解密实现

const CryptoJS = require('crypto-js')

// 1.秘钥准备(密钥必须是16位十六进制数)
const key = CryptoJS.enc.Utf8.parse('SECRET_KEY_RIGHT')

// 2.偏移量准备(偏移量是可选的,iv称为初始向量,不同的iv加密后的字符串不同,iv也必须是16位十六进制数)
const iv = CryptoJS.enc.Utf8.parse('SECRET_KEY_RIGHT')

const cipherOption = {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
    iv: iv
}

// 3.加密
function encrypt(value) {
    return CryptoJS.AES.encrypt(value, key, cipherOption).toString() // base64编码
}

// 4.解密
function decrypt(value) {
    return CryptoJS.AES.decrypt(value, key, cipherOption).toString(CryptoJS.enc.Utf8);
}

// 5.测试
const value = '19987131172'

console.log(encrypt(value)); // zArydT0+/teKeIwlwuvVUQ==

console.log(decrypt("zArydT0+/teKeIwlwuvVUQ==")) // 19987131172
1.2、后端加解密
/**
 * @description:
 * @date: 2022/8/17 9:29
 */
public class SignUtil {
	// 加密
    public static String encrypt(String transformation, String key, String value) {
        try {
            Cipher cipher = Cipher.getInstance(transformation);
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            // Cipher.ENCRYPT_MODE 加密模式
            cipher.init(Cipher.ENCRYPT_MODE, sks);
            // 加密
            byte[] encryptBytes = cipher.doFinal(value.getBytes());
            return Base64Utils.encodeToString(encryptBytes);
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return null;
    }
	
	// 解密
    public static String decrypt(String transformation, String key, String encrypt) {
        try {
            Cipher cipher = Cipher.getInstance(transformation);
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            // Cipher.DECRYPT_MODE 解密模式
            cipher.init(Cipher.DECRYPT_MODE, sks);
            // 解密
            byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(encrypt));
            return new String(decryptBytes);
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return null;
    }
}

测试(我们就拿上述前端加密后的字符串zArydT0+/teKeIwlwuvVUQ==进行测试):

@Test
public void decryptTest() {
    String transformation = "AES/ECB/PKCS5Padding";
    String key = "SECRET_KEY_RIGHT";
    String value = decrypt(transformation, key, "zArydT0+/teKeIwlwuvVUQ==");
    System.out.println(value); // 19987131172
}
1.3、加密后的字符串可以在URL中安全使用问题

详见下图,我们发现+字符传到后端后变成空格了,导致base64解码失败!

在这里插入图片描述
那么如何来处理这个问题呢?

第一种使用encodeURIComponent()处理,如

const tel = 'zArydT0+/teKeIwlwuvVUQ=='
const enTel = encodeURIComponent(tel)
console.log(enTel) // zArydT0%2B%2FteKeIwlwuvVUQ%3D%3D

swagger组件就是使用这种方式进行转换的,如下图

在这里插入图片描述

这种方式处理方式比较简单,但是也有一定的局限性,例如必须有js环境才行。需要注意的是+ / =等在URL中有特殊的含义,对这几个字符进行替换或省略也可以解决问题。

第二种对加密后的字符串再次base64编码

使用第三方js-base64

npm install js-base64 --save-dev

代码实现

const Base64 = require('js-base64').Base64;

console.log(Base64.encode('zArydT0+/teKeIwlwuvVUQ==')); // ekFyeWRUMCsvdGVLZUl3bHd1dlZVUT09

或者自己编写编码函数

var base64 = {};
base64._keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
base64.decode = function (input) {
    var output = "";
    var chr1, chr2, chr3;
    var enc1, enc2, enc3, enc4;
    var i = 0;
    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    while (i < input.length) {
        enc1 = base64._keyStr.indexOf(input.charAt(i++));
        enc2 = base64._keyStr.indexOf(input.charAt(i++));
        enc3 = base64._keyStr.indexOf(input.charAt(i++));
        enc4 = base64._keyStr.indexOf(input.charAt(i++));
        chr1 = (enc1 << 2) | (enc2 >> 4);
        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
        chr3 = ((enc3 & 3) << 6) | enc4;
        output = output + String.fromCharCode(chr1);
        if (enc3 != 64) {
            output = output + String.fromCharCode(chr2);
        }
        if (enc4 != 64) {
            output = output + String.fromCharCode(chr3);
        }
    }
    return output;
}
base64.encode = function (input) {
    var output = "";
    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
    var i = 0;
    input = base64._utf8_encode(input + '');
    while (i < input.length) {
        chr1 = input.charCodeAt(i++);
        chr2 = input.charCodeAt(i++);
        chr3 = input.charCodeAt(i++);
        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;
        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }
        output = output +
            base64._keyStr.charAt(enc1) + base64._keyStr.charAt(enc2) +
            base64._keyStr.charAt(enc3) + base64._keyStr.charAt(enc4);
    }
    return output;
}
base64._utf8_encode = function (string) {
    string = string.replace(/\r\n/g, "\n");
    var utftext = "";
    for (var n = 0; n < string.length; n++) {
        var c = string.charCodeAt(n);
        if (c < 128) {
            utftext += String.fromCharCode(c);
        } else if ((c > 127) && (c < 2048)) {
            utftext += String.fromCharCode((c >> 6) | 192);
            utftext += String.fromCharCode((c & 63) | 128);
        } else {
            utftext += String.fromCharCode((c >> 12) | 224);
            utftext += String.fromCharCode(((c >> 6) & 63) | 128);
            utftext += String.fromCharCode((c & 63) | 128);
        }

    }
    return utftext;
}
base64._utf8_decode = function (utftext) {
    var string = "";
    var i = 0, c1, c2, c3;
    var c = c1 = c2 = 0;
    while (i < utftext.length) {
        c = utftext.charCodeAt(i);
        if (c < 128) {
            string += String.fromCharCode(c);
            i++;
        } else if ((c > 191) && (c < 224)) {
            c2 = utftext.charCodeAt(i + 1);
            string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
            i += 2;
        } else {
            c2 = utftext.charCodeAt(i + 1);
            c3 = utftext.charCodeAt(i + 2);
            string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
            i += 3;
        }
    }
    return string;
}

console.log(base64.encode('zArydT0+/teKeIwlwuvVUQ==')); // ekFyeWRUMCsvdGVLZUl3bHd1dlZVUT09

后端还原即可,例如

在这里插入图片描述

最后再来看效果

在这里插入图片描述

2、jsencrypt与Java

2.1、前端加解密

jsencrypt是使用RSA加密算法,是一种非对称的加密算法。

安装

npm install jsencrypt --save-dev

RSA密钥对生成

window = {};
const JSEncrypt = require('jsencrypt');

// 生成密钥对
// 密钥实际上是一组数字,通过 default_key_size 指定大小,通常为1024或者2048
// default_key_size 越大密钥越长,加密强度越高,解密所需时间也越长
const encrypt = new JSEncrypt({ default_key_size: 2048 });

// 公钥
const publicKey = encrypt.getPublicKey();
console.log(publicKey);
// 私钥
const privateKey = encrypt.getPrivateKey();
console.log(privateKey);

// 或者通过 crypto 来生成
/*
const { generateKeyPairSync } = require('crypto');

// 生成 RSA 密钥对
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
  modulusLength: 2048, // 密钥长度
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem'
  }
});

console.log(publicKey);
console.log(privateKey);
*/

详见下图

加解密实现

window = {};
const JSEncrypt = require('jsencrypt');

// 生成密钥对(这里密钥对太长了,为了方便演示就重新生成了,实际开发时生成后保存下来即可)
const crypt = new JSEncrypt({ default_key_size: 1024 });
const publicKey = crypt.getPublicKey();
const privateKey = crypt.getPrivateKey();

// 加密实例
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);

// 解密实例
const decrypt = new JSEncrypt();
decrypt.setPrivateKey(privateKey);

const value = '19987131178';

// 加密
const en = encrypt.encrypt(value);
console.log('密文 --> ', en);
// 密文 -->  aTavrF4UXow829LfnNO0HPRiLS65xc8g8D32qfFVc1iZwYxEI34Dg1LXFKN5ydu1WDjgKPYdQ8uL8FW8RbZepQ0HLEBUHf9K1HlPVNlydNXlJEGP8WpKVVEujx+cIBfS4X62E3U43q/KEc5XQZ/3DBxNcIRaNl/QdT5JgQBjjZc=

// 加密
const de = decrypt.decrypt(en);
console.log('明文 --> ', de);
// 明文 -->  19987131178
2.2、后端加解密

引入Bouncy Castle(Java自带的工具不支持解密RSA,所以我们需要其他库来解密)

<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

后端工具类实现

/**
 * TODO
 *
 * @Description
 * @Author laizhenghua
 * @Date 2025/1/16 11:55
 **/
public class RSACryptUtil {
    /**
     * 加密算法
     */
    private static final String ALGORITHM = "RSA";
    /**
     * 公钥
     */
    private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRimVglDtJDAO6cFkYLG9Y2r9ujR3Id1D/vp46uePo6GkB8/ihM8l2ZGAbMgQDrWEt9MR+YMCk/Xlz65qPIVFmsY5ixcuyDS5hZ5hVkQuNamlvRnhgxObULrUXMeavJet3DyHAAcSXH4jkbsGy8kLgpXxDm39BKAAHQ0ztV1W6RwIDAQAB";
    /**
     * 秘钥
     */
    private static final String PRIVATE_KEY = "MIICWwIBAAKBgQCRimVglDtJDAO6cFkYLG9Y2r9ujR3Id1D/vp46uePo6GkB8/ihM8l2ZGAbMgQDrWEt9MR+YMCk/Xlz65qPIVFmsY5ixcuyDS5hZ5hVkQuNamlvRnhgxObULrUXMeavJet3DyHAAcSXH4jkbsGy8kLgpXxDm39BKAAHQ0ztV1W6RwIDAQABAoGAB+h2M7Y6NnDhrvq1zJt1fWV9a1tdl+vrycmovVYmbRxbwFBqXQ/8TWOM2U1xbGW6Vw3qs1c8gHqJY+QUZNyRmKaRTI/1ztDtinUIYZSE2O1IKQGp50om7cnOcCCsjAAmml1UkJa5HiYHMDWoSrZhfoQxAeZQXQHYkrjgl9NCujkCQQDnP5Nhm+lzb2YZx5WjZAIUCQdfnjmGu4cJ9rTSqHA+djsZe44YjoicDGP8wpDWrOY9LplkTrhW4ArlVK7Qr+8dAkEAoR5Y04R8ao+BsmarA/tDlPyHsJTtOUPR8XhL3MlN4QQvq9LFxJGCiJD5KNk4fTKP0TmZ35vXzxH/kdn//gRdswJAI/oMH991znPCWrhmW2kvuZY+A25GXOPH+pDbSPrTm6QhRbGnRcLHFiAHXkeW6Q81MseRLb3hiAKLL2qhV+5HMQJAcGZTYXxTr6Ndv6+QLr7jbtSddLrwo7qEhAiAJA7rncbl2uC3x2Ibxloc+DpSBkV3v2aHyk9WRscvm/iRdgxlsQJANTU1m9w0y3D8rhe+uLuOX2t5shQfMFe3GG9g6Qocxc0dOK5VD7Q8Wab62muuNC3roJOWqtwGpSl0Yde3+Oa3UA==";

    public static String encrypt(String value) {
        if (StrUtil.isEmpty(value)) {
            return null;
        }
        byte[] publicBytes = Base64.getDecoder().decode(PUBLIC_KEY.getBytes(StandardCharsets.UTF_8));
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicBytes);

        try {
            KeyFactory rsaInstance = KeyFactory.getInstance(ALGORITHM);
            PublicKey publicKey = rsaInstance.generatePublic(x509EncodedKeySpec);

            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            byte[] bytes = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(bytes);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(String encrypt) {
        if (StrUtil.isEmpty(encrypt)) {
            return null;
        }
        // java自带的工具不支持解密RSA,我们需要新增一个使用Bouncy Castle库来解密
        java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        byte[] privateBytes = Base64.getDecoder().decode(PRIVATE_KEY.getBytes(StandardCharsets.UTF_8));

        PKCS8EncodedKeySpec pKCS8EncodedKeySpec = new PKCS8EncodedKeySpec(privateBytes);

        try {
            KeyFactory rsaInstance = KeyFactory.getInstance(ALGORITHM);
            PrivateKey privateKey = rsaInstance.generatePrivate(pKCS8EncodedKeySpec);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);

            byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encrypt));
            return new String(bytes);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}

测试与验证

@Test
public void test1() {
	// 解密
    String encrypt = "F/XM7kbW/n5C7MkqP/Jo58GCI7iJPs5ZdBa87Th22QCq/QHJGBCemiRrhhFE5u2YhCsoaJlOVXUVNkNAyeRjQ/1wnJ6DLnjK9wCs8AgDgHdQvU4BP5OtoZU8VxSuhhQpMVMsqNV2JyeclP/zNAaVJeJ4y7LmV44tTKfqKV7aZQY=";
    // String encrypt = "IHJ4ykzcQgn12ZZi8j4ek8XtxkjcMFBy4Uu+DezKsLDpntZ39RAg0ur6zL2Nv6bEe4HnlVEV0BgDG5TNsqAF/xg+SWODcfohgYB2MRR18aLpOTsYXq6OQeBBHvKjQLYVNYVrcnlWbcROcCjudWnMUV5u6l0RvsYmopM8e2FGK+I=";
    String decrypt = RSACryptUtil.decrypt(encrypt);
    System.out.println(decrypt);
}

@Test
public void test2() {
	// 加密
    String encrypt = RSACryptUtil.encrypt("19987131178");
    System.out.println(encrypt);
}

3、扩展(AOP统一解密)

在后端我们希望有一个统一的入口去解密参数(即传到Controller层是明文参数),而不是在每个Controller的方法里加上与业务无关的参数解密代码。

此时我们就可以使用AOP和自定义注解去实现这一入口,通过自定义注解标记需要解密的参数然后在切面类上完成参数的解密,如

自定义注解

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DecryptParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    String transformation() default "AES/ECB/PKCS5Padding";

    String key() default "SECRET_KEY_RIGHT";
}

标记需要解密的参数

@GetMapping("/4")
@ApiOperation(value = "测试4(测试aes加密数据+字符被替换成空格问题)")
public R test4(
        @DecryptParam
        @ApiParam(value = "加密后的手机号")
        @RequestParam("tel") String tel) {
    return R.success(tel);
}

在切面类上完成参数的解密

@Aspect
@Component
public class DecryptAspect {
    // 拦截controller包下的所有public方法
    @Pointcut("execution(public * com.laizhenghua.arthas.controller.*Controller.*(..))")
    public void pointcut() {

    }
    @Around("pointcut()")
    public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Parameter[] parameters = method.getParameters();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            DecryptParam decryptParam = parameter.getAnnotation(DecryptParam.class);
            if (ObjectUtil.isNull(decryptParam)) {
                continue;
            }
            String key = decryptParam.key();
            String transformation = decryptParam.transformation();
            // String name = decryptParam.name();
            Object pv = args[i];
            if (ObjectUtil.isNull(pv)) {
                continue;
            }
            byte[] decode = Base64.getDecoder().decode(pv.toString());
            String input = new String(decode);
            String decrypt = CryptoUtil.decrypt(transformation, key, input);
            args[i] = decrypt;
        }
        return joinPoint.proceed(args);
    }
}

效果

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐