数据脱敏sensitive(前端或数据库加密,解密)

可以对数据加密,解密,对数据库加密的数据进行解密显示,对数据库没有加密的数据进行加密处理展示前端等待

1:引入数据如下结构

在这里插入图片描述

1-1:SensitiveDecode脱敏解密注解

package com.example.poi.desensitization.annotation;

import java.lang.annotation.*;

/**
 * 脱敏解密注解
 * @Author xu
 * @create 2023/9/4 19
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SensitiveDecode {

    /**
     * 指明需要脱敏的实体类class
     * @return
     */
    Class entity() default Object.class;
}

1-2:SensitiveEncode脱敏加密注解

package com.example.poi.desensitization.annotation;

import java.lang.annotation.*;

/**
 * 脱敏加密注解
 * @Author xu
 * @create 2023/9/4 19
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SensitiveEncode {

    /**
     * 指明需要脱敏的实体类class
     * @return
     */
    Class entity() default Object.class;
}

1-3:SensitiveField字段注解

package com.example.poi.desensitization.annotation;

import com.example.poi.desensitization.enums.SensitiveEnum;

import java.lang.annotation.*;

/**
 * 字段上定义,标识字段存储的信息是敏感的
 * @Author xu
 * @create 2023/9/4 19
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveField {

    /**
     * 不同类型处理不同
     * @return
     */
    SensitiveEnum type() default SensitiveEnum.ENCODE;
}

1-4:SensitiveDataAspect敏感数据切面处理类

package com.example.poi.desensitization.aspect;

import com.example.poi.desensitization.annotation.SensitiveDecode;
import com.example.poi.desensitization.annotation.SensitiveEncode;
import com.example.poi.desensitization.utils.SensitiveInfoUtil;
import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.List;

/**
 * 敏感数据切面处理类
 * @Author xu
 * @create 2023/9/4 20
 */
@Slf4j
@Aspect
@Component
public class SensitiveDataAspect {

    /**
     * 定义切点Pointcut
     */
    @Pointcut("@annotation(com.example.poi.desensitization.annotation.SensitiveEncode) || @annotation(com.example.poi.desensitization.annotation.SensitiveDecode)")
    public void sensitivePointCut() {
    }

    @Around("sensitivePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 处理结果
        Object result = point.proceed();
        if(result == null){
            return result;
        }
        Class resultClass = result.getClass();
        log.debug(" resultClass  = {}" , resultClass);

        if(resultClass.isPrimitive()){
            //是基本类型 直接返回 不需要处理
            return result;
        }
        // 获取方法注解信息:是哪个实体、是加密还是解密
        boolean isEncode = true;
        Class entity = null;
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        SensitiveEncode encode = method.getAnnotation(SensitiveEncode.class);
        if(encode==null){
            SensitiveDecode decode = method.getAnnotation(SensitiveDecode.class);
            if(decode!=null){
                entity = decode.entity();
                isEncode = false;
            }
        }else{
            entity = encode.entity();
        }

        long startTime=System.currentTimeMillis();
        if(resultClass.equals(entity) || entity.equals(Object.class)){
            // 方法返回实体和注解的entity一样,如果注解没有申明entity属性则认为是(方法返回实体和注解的entity一样)
            SensitiveInfoUtil.handlerObject(result, isEncode);
        } else if(result instanceof List){
            // 方法返回List<实体>
            SensitiveInfoUtil.handleList(result, entity, isEncode);
        }else{
            // 方法返回一个对象
            SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
        }
        long endTime=System.currentTimeMillis();
        log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");

        return result;
    }

}

1-5:SensitiveEnum

package com.example.poi.desensitization.enums;

/**
 * 敏感字段信息类型
 * @Author xu
 * @create 2023/9/4 19
 */
public enum SensitiveEnum {


    /**
     * 加密
     */
    ENCODE,

    /**
     * 中文名
     */
    CHINESE_NAME,

    /**
     * 身份证号
     */
    ID_CARD,

    /**
     * 座机号
     */
    FIXED_PHONE,

    /**
     * 手机号
     */
    MOBILE_PHONE,

    /**
     * 地址
     */
    ADDRESS,

    /**
     * 电子邮件
     */
    EMAIL,

    /**
     * 银行卡
     */
    BANK_CARD,

    /**
     * 公司开户银行联号
     */
    CNAPS_CODE;


}


1-6:AesEncryptUtil加密工具类

package com.example.poi.desensitization.utils.encryption;


import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
//import org.apache.shiro.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @Description: AES 加密
 * @Author xu
 * @create 2023/9/4 19
 */
public class AesEncryptUtil {

    /**
     * 使用AES-128-CBC加密模式 key和iv可以相同
     */
    private static String KEY = EncryptedString.key;
    private static String IV = EncryptedString.iv;

    /**
     * 加密方法
     * @param data  要加密的数据
     * @param key 加密key
     * @param iv 加密iv
     * @return 加密的结果
     * @throws Exception
     */
    public static String encrypt(String data, String key, String iv) throws Exception {
        try {

            //"算法/模式/补码方式"NoPadding PkcsPadding
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            int blockSize = cipher.getBlockSize();

            byte[] dataBytes = data.getBytes();
            int plaintextLength = dataBytes.length;
            if (plaintextLength % blockSize != 0) {
                plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
            }

            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);

            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());

            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
            byte[] encrypted = cipher.doFinal(plaintext);

            return Base64.encode(encrypted);
            //return Base64.encodeToString(encrypted);

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 解密方法
     * @param data 要解密的数据
     * @param key  解密key
     * @param iv 解密iv
     * @return 解密的结果
     * @throws Exception
     */
    public static String desEncrypt(String data, String key, String iv) throws Exception {
        //update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
        byte[] encrypted1 = Base64.decode(data);

        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());

        cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);

        byte[] original = cipher.doFinal(encrypted1);
        String originalString = new String(original);
        //加密解码后的字符串会出现\u0000
        return originalString.replaceAll("\\u0000", "");
        //update-end-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
    }

    /**
     * 使用默认的key和iv加密
     * @param data
     * @return
     * @throws Exception
     */
    public static String encrypt(String data) throws Exception {
        return encrypt(data, KEY, IV);
    }

    /**
     * 使用默认的key和iv解密
     * @param data
     * @return
     * @throws Exception
     */
    public static String desEncrypt(String data) throws Exception {
        return desEncrypt(data, KEY, IV);
    }


    /**
     * 测试
     */
    public static void main(String args[]) throws Exception {
        String test1 = "sa";
        String test =new String(test1.getBytes(),"UTF-8");
        String data = "4I80+jJsZ/aR+n+MsRd7qw==";
        String key =  KEY;
        String iv = IV;
        String jiemi =desEncrypt(data, key, iv).trim();
        System.out.println("解密:"+jiemi);
        String aa="1234567897891";
        String encrypt = SecureUtil.aes(key.getBytes()).encryptBase64(aa);
        String s = SecureUtil.aes(key.getBytes()).decryptStr(encrypt);
        System.out.println("");
    }
}

1-7:EncryptedString

package com.example.poi.desensitization.utils.encryption;


import lombok.Data;

/**
 * 秘钥和偏移量
 * @Author xu
 * @create 2023/9/4 19
 */
@Data
public class EncryptedString {

    /**
     * 长度为16个字符
     */
    public static  String key = "1234567890adbcde";

    /**
     * 长度为16个字符
     */
    public static  String iv  = "1234567890hjlkew";
}

1-8:CommonConstant通用常量

package com.example.poi.desensitization.utils;

/**
 * @Description: 通用常量
 * @Author xu
 * @create 2023/9/4 19
 */
public interface CommonConstant {

    /**
     * 未知的
     */
    String UNKNOWN = "unknown";
    

    /**
     * String 类型的空值
     */
    String STRING_NULL = "null";
    

}

1-9:oConvertUtils

package com.example.poi.desensitization.utils;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.sql.Date;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


@Slf4j
public class oConvertUtils {
	public static boolean isEmpty(Object object) {
		if (object == null) {
			return (true);
		}
		if ("".equals(object)) {
			return (true);
		}
		if (CommonConstant.STRING_NULL.equals(object)) {
			return (true);
		}
		return (false);
	}
	
	public static boolean isNotEmpty(Object object) {
		if (object != null && !"".equals(object) && !object.equals(CommonConstant.STRING_NULL)) {
			return (true);
		}
		return (false);
	}

	public static String decode(String strIn, String sourceCode, String targetCode) {
		String temp = code2code(strIn, sourceCode, targetCode);
		return temp;
	}

	@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
    public static String StrToUTF(String strIn, String sourceCode, String targetCode) {
		strIn = "";
		try {
			strIn = new String(strIn.getBytes("ISO-8859-1"), "GBK");
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return strIn;

	}

	private static String code2code(String strIn, String sourceCode, String targetCode) {
		String strOut = null;
		if (strIn == null || "".equals(strIn.trim())) {
			return strIn;
		}
		try {
			byte[] b = strIn.getBytes(sourceCode);
			for (int i = 0; i < b.length; i++) {
				System.out.print(b[i] + "  ");
			}
			strOut = new String(b, targetCode);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		return strOut;
	}

	public static int getInt(String s, int defval) {
		if (s == null || s == "") {
			return (defval);
		}
		try {
			return (Integer.parseInt(s));
		} catch (NumberFormatException e) {
			return (defval);
		}
	}

	public static int getInt(String s) {
		if (s == null || s == "") {
			return 0;
		}
		try {
			return (Integer.parseInt(s));
		} catch (NumberFormatException e) {
			return 0;
		}
	}

	public static int getInt(String s, Integer df) {
		if (s == null || s == "") {
			return df;
		}
		try {
			return (Integer.parseInt(s));
		} catch (NumberFormatException e) {
			return 0;
		}
	}

	public static Integer[] getInts(String[] s) {
		if (s == null) {
			return null;
		}
		Integer[] integer = new Integer[s.length];
		for (int i = 0; i < s.length; i++) {
			integer[i] = Integer.parseInt(s[i]);
		}
		return integer;

	}

	public static double getDouble(String s, double defval) {
		if (s == null || s == "") {
			return (defval);
		}
		try {
			return (Double.parseDouble(s));
		} catch (NumberFormatException e) {
			return (defval);
		}
	}

	public static double getDou(Double s, double defval) {
		if (s == null) {
			return (defval);
		}
		return s;
	}

	/*public static Short getShort(String s) {
		if (StringUtil.isNotEmpty(s)) {
			return (Short.parseShort(s));
		} else {
			return null;
		}
	}*/

	public static int getInt(Object object, int defval) {
		if (isEmpty(object)) {
			return (defval);
		}
		try {
			return (Integer.parseInt(object.toString()));
		} catch (NumberFormatException e) {
			return (defval);
		}
	}
	
	public static Integer getInt(Object object) {
		if (isEmpty(object)) {
			return null;
		}
		try {
			return (Integer.parseInt(object.toString()));
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public static int getInt(BigDecimal s, int defval) {
		if (s == null) {
			return (defval);
		}
		return s.intValue();
	}

	public static Integer[] getIntegerArry(String[] object) {
		int len = object.length;
		Integer[] result = new Integer[len];
		try {
			for (int i = 0; i < len; i++) {
				result[i] = new Integer(object[i].trim());
			}
			return result;
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public static String getString(String s) {
		return (getString(s, ""));
	}

	/**
	 * 转义成Unicode编码
	 * @param s
	 * @return
	 */
	/*public static String escapeJava(Object s) {
		return StringEscapeUtils.escapeJava(getString(s));
	}*/
	
	public static String getString(Object object) {
		if (isEmpty(object)) {
			return "";
		}
		return (object.toString().trim());
	}

	public static String getString(int i) {
		return (String.valueOf(i));
	}

	public static String getString(float i) {
		return (String.valueOf(i));
	}

	public static String getString(String s, String defval) {
		if (isEmpty(s)) {
			return (defval);
		}
		return (s.trim());
	}

	public static String getString(Object s, String defval) {
		if (isEmpty(s)) {
			return (defval);
		}
		return (s.toString().trim());
	}

	public static long stringToLong(String str) {
		Long test = new Long(0);
		try {
			test = Long.valueOf(str);
		} catch (Exception e) {
		}
		return test.longValue();
	}

	/**
	 * 获取本机IP
	 */
	public static String getIp() {
		String ip = null;
		try {
			InetAddress address = InetAddress.getLocalHost();
			ip = address.getHostAddress();

		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return ip;
	}

	/**
	 * 判断一个类是否为基本数据类型。
	 * 
	 * @param clazz
	 *            要判断的类。
	 * @return true 表示为基本数据类型。
	 */
	private static boolean isBaseDataType(Class clazz) throws Exception {
		return (clazz.equals(String.class) || clazz.equals(Integer.class) || clazz.equals(Byte.class) || clazz.equals(Long.class) || clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Character.class) || clazz.equals(Short.class) || clazz.equals(BigDecimal.class) || clazz.equals(BigInteger.class) || clazz.equals(Boolean.class) || clazz.equals(Date.class) || clazz.isPrimitive());
	}

	/**
	 * @param request
	 *            IP
	 * @return IP Address
	 */
	public static String getIpAddrByRequest(HttpServletRequest request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || CommonConstant.UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || CommonConstant.UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || CommonConstant.UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}

	/**
	 * @return 本机IP
	 * @throws SocketException
	 */
	public static String getRealIp() throws SocketException {
        // 本地IP,如果没有配置外网IP则返回它
		String localip = null;
        // 外网IP
		String netip = null;

		Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
		InetAddress ip = null;
        // 是否找到外网IP
		boolean finded = false;
		while (netInterfaces.hasMoreElements() && !finded) {
			NetworkInterface ni = netInterfaces.nextElement();
			Enumeration<InetAddress> address = ni.getInetAddresses();
			while (address.hasMoreElements()) {
				ip = address.nextElement();
                // 外网IP
				if (!ip.isSiteLocalAddress() && !ip.isLoopbackAddress() && ip.getHostAddress().indexOf(":") == -1) {
					netip = ip.getHostAddress();
					finded = true;
					break;
				} else if (ip.isSiteLocalAddress() && !ip.isLoopbackAddress() && ip.getHostAddress().indexOf(":") == -1) {
                    // 内网IP
				    localip = ip.getHostAddress();
				}
			}
		}

		if (netip != null && !"".equals(netip)) {
			return netip;
		} else {
			return localip;
		}
	}

	/**
	 * java去除字符串中的空格、回车、换行符、制表符
	 * 
	 * @param str
	 * @return
	 */
	public static String replaceBlank(String str) {
		String dest = "";
		if (str != null) {
		    String reg = "\\s*|\t|\r|\n";
			Pattern p = Pattern.compile(reg);
			Matcher m = p.matcher(str);
			dest = m.replaceAll("");
		}
		return dest;

	}

	/**
	 * 判断元素是否在数组内
	 * 
	 * @param substring
	 * @param source
	 * @return
	 */
	public static boolean isIn(String substring, String[] source) {
		if (source == null || source.length == 0) {
			return false;
		}
		for (int i = 0; i < source.length; i++) {
			String aSource = source[i];
			if (aSource.equals(substring)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 获取Map对象
	 */
	public static Map<Object, Object> getHashMap() {
		return new HashMap<>(5);
	}

	/**
	 * SET转换MAP
	 * 
	 * @param str
	 * @return
	 */
	public static Map<Object, Object> setToMap(Set<Object> setobj) {
		Map<Object, Object> map = getHashMap();
		for (Iterator iterator = setobj.iterator(); iterator.hasNext();) {
			Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) iterator.next();
			map.put(entry.getKey().toString(), entry.getValue() == null ? "" : entry.getValue().toString().trim());
		}
		return map;

	}

	public static boolean isInnerIp(String ipAddress) {
		boolean isInnerIp = false;
		long ipNum = getIpNum(ipAddress);
		/**
		 * 私有IP:A类 10.0.0.0-10.255.255.255 B类 172.16.0.0-172.31.255.255 C类 192.168.0.0-192.168.255.255 当然,还有127这个网段是环回地址
		 **/
		long aBegin = getIpNum("10.0.0.0");
		long aEnd = getIpNum("10.255.255.255");
		long bBegin = getIpNum("172.16.0.0");
		long bEnd = getIpNum("172.31.255.255");
		long cBegin = getIpNum("192.168.0.0");
		long cEnd = getIpNum("192.168.255.255");
		String localIp = "127.0.0.1";
		isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || localIp.equals(ipAddress);
		return isInnerIp;
	}

	private static long getIpNum(String ipAddress) {
		String[] ip = ipAddress.split("\\.");
		long a = Integer.parseInt(ip[0]);
		long b = Integer.parseInt(ip[1]);
		long c = Integer.parseInt(ip[2]);
		long d = Integer.parseInt(ip[3]);

		long ipNum = a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
		return ipNum;
	}

	private static boolean isInner(long userIp, long begin, long end) {
		return (userIp >= begin) && (userIp <= end);
	}
	
	/**
	 * 将下划线大写方式命名的字符串转换为驼峰式。
	 * 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
	 * 例如:hello_world->helloWorld
	 * 
	 * @param name
	 *            转换前的下划线大写方式命名的字符串
	 * @return 转换后的驼峰式命名的字符串
	 */
	public static String camelName(String name) {
		StringBuilder result = new StringBuilder();
		// 快速检查
		if (name == null || name.isEmpty()) {
			// 没必要转换
			return "";
		} else if (!name.contains(SymbolConstant.UNDERLINE)) {
			// 不含下划线,仅将首字母小写
			//update-begin--Author:zhoujf  Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
			//update-begin--Author:zhoujf  Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
			return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
			//update-end--Author:zhoujf  Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
		}
		// 用下划线将原始字符串分割
		String[] camels = name.split("_");
		for (String camel : camels) {
			// 跳过原始字符串中开头、结尾的下换线或双重下划线
			if (camel.isEmpty()) {
				continue;
			}
			// 处理真正的驼峰片段
			if (result.length() == 0) {
				// 第一个驼峰片段,全部字母都小写
				result.append(camel.toLowerCase());
			} else {
				// 其他的驼峰片段,首字母大写
				result.append(camel.substring(0, 1).toUpperCase());
				result.append(camel.substring(1).toLowerCase());
			}
		}
		return result.toString();
	}
	
	/**
	 * 将下划线大写方式命名的字符串转换为驼峰式。
	 * 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
	 * 例如:hello_world,test_id->helloWorld,testId
	 * 
	 * @param names
	 *            转换前的下划线大写方式命名的字符串
	 * @return 转换后的驼峰式命名的字符串
	 */
	public static String camelNames(String names) {
		if(names==null||"".equals(names)){
			return null;
		}
		StringBuffer sf = new StringBuffer();
		String[] fs = names.split(",");
		for (String field : fs) {
			field = camelName(field);
			sf.append(field + ",");
		}
		String result = sf.toString();
		return result.substring(0, result.length() - 1);
	}
	
	//update-begin--Author:zhoujf  Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
	/**
	 * 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
	 * 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
	 * 例如:hello_world->HelloWorld
	 * 
	 * @param name
	 *            转换前的下划线大写方式命名的字符串
	 * @return 转换后的驼峰式命名的字符串
	 */
	public static String camelNameCapFirst(String name) {
		StringBuilder result = new StringBuilder();
		// 快速检查
		if (name == null || name.isEmpty()) {
			// 没必要转换
			return "";
		} else if (!name.contains(SymbolConstant.UNDERLINE)) {
			// 不含下划线,仅将首字母小写
			return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
		}
		// 用下划线将原始字符串分割
		String[] camels = name.split("_");
		for (String camel : camels) {
			// 跳过原始字符串中开头、结尾的下换线或双重下划线
			if (camel.isEmpty()) {
				continue;
			}
			// 其他的驼峰片段,首字母大写
			result.append(camel.substring(0, 1).toUpperCase());
			result.append(camel.substring(1).toLowerCase());
		}
		return result.toString();
	}
	//update-end--Author:zhoujf  Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
	
	/**
	 * 将驼峰命名转化成下划线
	 * @param para
	 * @return
	 */
	public static String camelToUnderline(String para){
	    int length = 3;
        if(para.length()<length){
        	return para.toLowerCase(); 
        }
        StringBuilder sb=new StringBuilder(para);
        //定位
        int temp=0;
        //从第三个字符开始 避免命名不规范 
        for(int i=2;i<para.length();i++){
            if(Character.isUpperCase(para.charAt(i))){
                sb.insert(i+temp, "_");
                temp+=1;
            }
        }
        return sb.toString().toLowerCase(); 
	}

	/**
	 * 随机数
	 * @param place 定义随机数的位数
	 */
	public static String randomGen(int place) {
		String base = "qwertyuioplkjhgfdsazxcvbnmQAZWSXEDCRFVTGBYHNUJMIKLOP0123456789";
		StringBuffer sb = new StringBuffer();
		Random rd = new Random();
		for(int i=0;i<place;i++) {
			sb.append(base.charAt(rd.nextInt(base.length())));
		}
		return sb.toString();
	}
	
	/**
	 * 获取类的所有属性,包括父类
	 * 
	 * @param object
	 * @return
	 */
	public static Field[] getAllFields(Object object) {
		Class<?> clazz = object.getClass();
		List<Field> fieldList = new ArrayList<>();
		while (clazz != null) {
			fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
			clazz = clazz.getSuperclass();
		}
		Field[] fields = new Field[fieldList.size()];
		fieldList.toArray(fields);
		return fields;
	}
	
	/**
	  * 将map的key全部转成小写
	 * @param list
	 * @return
	 */
	public static List<Map<String, Object>> toLowerCasePageList(List<Map<String, Object>> list){
		List<Map<String, Object>> select = new ArrayList<>();
		for (Map<String, Object> row : list) {
			 Map<String, Object> resultMap = new HashMap<>(5);
			 Set<String> keySet = row.keySet(); 
			 for (String key : keySet) { 
				 String newKey = key.toLowerCase(); 
				 resultMap.put(newKey, row.get(key)); 
			 }
			 select.add(resultMap);
		}
		return select;
	}

	/**
	 * 将entityList转换成modelList
	 * @param fromList
	 * @param tClass
	 * @param <F>
	 * @param <T>
	 * @return
	 */
	public static<F,T> List<T> entityListToModelList(List<F> fromList, Class<T> tClass){
		if(fromList == null || fromList.isEmpty()){
			return null;
		}
		List<T> tList = new ArrayList<>();
		for(F f : fromList){
			T t = entityToModel(f, tClass);
			tList.add(t);
		}
		return tList;
	}

	public static<F,T> T entityToModel(F entity, Class<T> modelClass) {
		log.debug("entityToModel : Entity属性的值赋值到Model");
		Object model = null;
		if (entity == null || modelClass ==null) {
			return null;
		}

		try {
			model = modelClass.newInstance();
		} catch (InstantiationException e) {
			log.error("entityToModel : 实例化异常", e);
		} catch (IllegalAccessException e) {
			log.error("entityToModel : 安全权限异常", e);
		}
		BeanUtils.copyProperties(entity, model);
		return (T)model;
	}

	/**
	 * 判断 list 是否为空
	 *
	 * @param list
	 * @return true or false
	 * list == null		: true
	 * list.size() == 0	: true
	 */
	public static boolean listIsEmpty(Collection list) {
		return (list == null || list.size() == 0);
	}

	/**
	 * 判断 list 是否不为空
	 *
	 * @param list
	 * @return true or false
	 * list == null		: false
	 * list.size() == 0	: false
	 */
	public static boolean listIsNotEmpty(Collection list) {
		return !listIsEmpty(list);
	}

	/**
	 * 读取静态文本内容
	 * @param url
	 * @return
	 */
	public static String readStatic(String url) {
		String json = "";
		try {
			//换个写法,解决springboot读取jar包中文件的问题
			InputStream stream = oConvertUtils.class.getClassLoader().getResourceAsStream(url.replace("classpath:", ""));
			json = IOUtils.toString(stream,"UTF-8");
		} catch (IOException e) {
			log.error(e.getMessage(),e);
		}
		return json;
	}
}

1-10:SensitiveInfoUtil

package com.example.poi.desensitization.utils;

import com.example.poi.desensitization.enums.SensitiveEnum;
import com.example.poi.desensitization.annotation.SensitiveField;
import com.example.poi.desensitization.utils.encryption.AesEncryptUtil;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.List;

/**
 * @Author xu
 * @create 2023/9/4 20
 */
@Slf4j
public class SensitiveInfoUtil {

    /**
     * 处理嵌套对象
     * @param obj 方法返回值
     * @param entity 实体class
     * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
     * @throws IllegalAccessException
     */
    public static void handleNestedObject(Object obj, Class entity, boolean isEncode) throws IllegalAccessException {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if(field.getType().isPrimitive()){
                continue;
            }
            if(field.getType().equals(entity)){
                // 对象里面是实体
                field.setAccessible(true);
                Object nestedObject = field.get(obj);
                handlerObject(nestedObject, isEncode);
                break;
            }else{
                // 对象里面是List<实体>
                if(field.getGenericType() instanceof ParameterizedType){
                    ParameterizedType pt = (ParameterizedType)field.getGenericType();
                    if(pt.getRawType().equals(List.class)){
                        if(pt.getActualTypeArguments()[0].equals(entity)){
                            field.setAccessible(true);
                            Object nestedObject = field.get(obj);
                            handleList(nestedObject, entity, isEncode);
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * 处理Object
     * @param obj 方法返回值
     * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
     * @return
     * @throws IllegalAccessException
     */
    public static Object handlerObject(Object obj, boolean isEncode) throws IllegalAccessException {
        log.debug(" obj --> "+ obj.toString());
        long startTime=System.currentTimeMillis();
        if (oConvertUtils.isEmpty(obj)) {
            return obj;
        }
        // 判断是不是一个对象
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            boolean isSensitiveField = field.isAnnotationPresent(SensitiveField.class);
            if(isSensitiveField){
                // 必须有SensitiveField注解 才作处理
                if(field.getType().isAssignableFrom(String.class)){
                    //必须是字符串类型 才作处理
                    field.setAccessible(true);
                    String realValue = (String) field.get(obj);
                    if(realValue==null || "".equals(realValue)){
                        continue;
                    }
                    SensitiveField sf = field.getAnnotation(SensitiveField.class);
                    if(isEncode==true){
                        //加密
                        String value = SensitiveInfoUtil.getEncodeData(realValue,  sf.type());
                        field.set(obj, value);
                    }else{
                        //解密只处理 encode类型的
                        if(sf.type().equals(SensitiveEnum.ENCODE)){
                            String value = SensitiveInfoUtil.getDecodeData(realValue);
                            field.set(obj, value);
                        }
                    }
                }
            }
        }
        //long endTime=System.currentTimeMillis();
        //log.info((isEncode ? "加密操作," : "解密操作,") + "当前程序耗时:" + (endTime - startTime) + "ms");

        return obj;
    }

    /**
     * 处理 List<实体>
     * @param obj
     * @param entity
     * @param isEncode(true: 加密操作 / false:解密操作)
     */
    public static void handleList(Object obj, Class entity, boolean isEncode){
        List list = (List)obj;
        if(list.size()>0){
            Object first = list.get(0);
            if(first.getClass().equals(entity)){
                for(int i=0; i<list.size(); i++){
                    Object temp = list.get(i);
                    try {
                        handlerObject(temp, isEncode);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }


    /**
     * 处理数据 获取解密后的数据
     * @param data
     * @return
     */
    public static String getDecodeData(String data){
        String result = null;
        try {
            result = AesEncryptUtil.desEncrypt(data);
        } catch (Exception exception) {
            log.debug("数据解密错误,原数据:"+data);
        }
        //解决debug模式下,加解密失效导致中文被解密变成空的问题
        if(oConvertUtils.isEmpty(result) && oConvertUtils.isNotEmpty(data)){
            result = data;
        }
        return result;
    }

    /**
     * 处理数据 获取加密后的数据 或是格式化后的数据
     * @param data 字符串
     * @param sensitiveEnum 类型
     * @return 处理后的字符串
     */
    public static String getEncodeData(String data, SensitiveEnum sensitiveEnum){
        String result;
        switch (sensitiveEnum){
            case ENCODE:
                try {
                    result = AesEncryptUtil.encrypt(data);
                } catch (Exception exception) {
                    log.error("数据加密错误", exception.getMessage());
                    result = data;
                }
                break;
            case CHINESE_NAME:
                result = chineseName(data);
                break;
            case ID_CARD:
                result = idCardNum(data);
                break;
            case FIXED_PHONE:
                result = fixedPhone(data);
                break;
            case MOBILE_PHONE:
                result = mobilePhone(data);
                break;
            case ADDRESS:
                result = address(data, 3);
                break;
            case EMAIL:
                result = email(data);
                break;
            case BANK_CARD:
                result = bankCard(data);
                break;
            case CNAPS_CODE:
                result = cnapsCode(data);
                break;
            default:
                result = data;
        }
        return result;
    }


    /**
     * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
     * @param fullName 全名
     * @return <例子:李**>
     */
    private static String chineseName(String fullName) {
        if (oConvertUtils.isEmpty(fullName)) {
            return "";
        }
        return formatRight(fullName, 1);
    }

    /**
     * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
     * @param familyName 姓
     * @param firstName 名
     * @return <例子:李**>
     */
    private static String chineseName(String familyName, String firstName) {
        if (oConvertUtils.isEmpty(familyName) || oConvertUtils.isEmpty(firstName)) {
            return "";
        }
        return chineseName(familyName + firstName);
    }

    /**
     * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。
     * @param id 身份证号
     * @return <例子:*************5762>
     */
    private static String idCardNum(String id) {
        if (oConvertUtils.isEmpty(id)) {
            return "";
        }
        return formatLeft(id, 4);

    }

    /**
     * [固定电话] 后四位,其他隐藏
     * @param num 固定电话
     * @return <例子:****1234>
     */
    private static String fixedPhone(String num) {
        if (oConvertUtils.isEmpty(num)) {
            return "";
        }
        return formatLeft(num, 4);
    }

    /**
     * [手机号码] 前三位,后四位,其他隐藏
     * @param num 手机号码
     * @return <例子:138******1234>
     */
    private static String mobilePhone(String num) {
        if (oConvertUtils.isEmpty(num)) {
            return "";
        }
        int len = num.length();
        if(len<11){
            return num;
        }
        return formatBetween(num, 3, 4);
    }

    /**
     * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护
     * @param address 地址
     * @param sensitiveSize 敏感信息长度
     * @return <例子:北京市海淀区****>
     */
    private static String address(String address, int sensitiveSize) {
        if (oConvertUtils.isEmpty(address)) {
            return "";
        }
        int len = address.length();
        if(len<sensitiveSize){
            return address;
        }
        return formatRight(address, sensitiveSize);
    }

    /**
     * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示
     * @param email 电子邮箱
     * @return <例子:g**@163.com>
     */
    private static String email(String email) {
        if (oConvertUtils.isEmpty(email)) {
            return "";
        }
        int index = email.indexOf("@");
        if (index <= 1){
            return email;
        }
        String begin = email.substring(0, 1);
        String end = email.substring(index);
        String stars = "**";
        return begin + stars + end;
    }

    /**
     * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号
     * @param cardNum 银行卡号
     * @return <例子:6222600**********1234>
     */
    private static String bankCard(String cardNum) {
        if (oConvertUtils.isEmpty(cardNum)) {
            return "";
        }
        return formatBetween(cardNum, 6, 4);
    }

    /**
     * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号
     * @param code 公司开户银行联号
     * @return <例子:12********>
     */
    private static String cnapsCode(String code) {
        if (oConvertUtils.isEmpty(code)) {
            return "";
        }
        return formatRight(code, 2);
    }


    /**
     * 将右边的格式化成*
     * @param str 字符串
     * @param reservedLength 保留长度
     * @return 格式化后的字符串
     */
    private static String formatRight(String str, int reservedLength){
        String name = str.substring(0, reservedLength);
        String stars = String.join("", Collections.nCopies(str.length()-reservedLength, "*"));
        return name + stars;
    }

    /**
     * 将左边的格式化成*
     * @param str 字符串
     * @param reservedLength 保留长度
     * @return 格式化后的字符串
     */
    private static String formatLeft(String str, int reservedLength){
        int len = str.length();
        String show = str.substring(len-reservedLength);
        String stars = String.join("", Collections.nCopies(len-reservedLength, "*"));
        return stars + show;
    }

    /**
     * 将中间的格式化成*
     * @param str 字符串
     * @param beginLen 开始保留长度
     * @param endLen 结尾保留长度
     * @return 格式化后的字符串
     */
    private static String formatBetween(String str, int beginLen, int endLen){
        int len = str.length();
        String begin = str.substring(0, beginLen);
        String end = str.substring(len-endLen);
        String stars = String.join("", Collections.nCopies(len-beginLen-endLen, "*"));
        return begin + stars + end;
    }

}

1-11:SymbolConstant

package com.example.poi.desensitization.utils;

/**
 * @Description: 符号和特殊符号常用类
 * @Author xu
 * @create 2023/9/4 19
 */
public class SymbolConstant {

    /**
     * 符号:点
     */
    public static final String SPOT = ".";

    /**
     * 符号:双斜杠
     */
    public static final String DOUBLE_BACKSLASH = "\\";

    /**
     * 符号:冒号
     */
    public static final String COLON = ":";

    /**
     * 符号:逗号
     */
    public static final String COMMA = ",";

    /**
     * 符号:左花括号 }
     */
    public static final String LEFT_CURLY_BRACKET = "{";

    /**
     * 符号:右花括号 }
     */
    public static final String RIGHT_CURLY_BRACKET = "}";

    /**
     * 符号:井号 #
     */
    public static final String WELL_NUMBER = "#";

    /**
     * 符号:单斜杠
     */
    public static final String SINGLE_SLASH = "/";

    /**
     * 符号:双斜杠
     */
    public static final String DOUBLE_SLASH = "//";

    /**
     * 符号:感叹号
     */
    public static final String EXCLAMATORY_MARK = "!";

    /**
     * 符号:下划线
     */
    public static final String UNDERLINE = "_";

    /**
     * 符号:单引号
     */
    public static final String SINGLE_QUOTATION_MARK = "'";

    /**
     * 符号:星号
     */
    public static final String ASTERISK = "*";

    /**
     * 符号:百分号
     */
    public static final String PERCENT_SIGN = "%";

    /**
     * 符号:美元 $
     */
    public static final String DOLLAR = "$";

    /**
     * 符号:和 &
     */
    public static final String AND = "&";

    /**
     * 符号:../
     */
    public static final String SPOT_SINGLE_SLASH = "../";

    /**
     * 符号:..\\
     */
    public static final String SPOT_DOUBLE_BACKSLASH = "..\\";

    /**
     * 系统变量前缀 #{
     */
    public static final String SYS_VAR_PREFIX = "#{";

    /**
     * 符号 {{
     */
    public static final String DOUBLE_LEFT_CURLY_BRACKET = "{{";

    /**
     * 符号:[
     */
    public static final String SQUARE_BRACKETS_LEFT = "[";
    /**
     * 符号:]
     */
    public static final String SQUARE_BRACKETS_RIGHT = "]";

}

2:必须导入的POM的依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.10</version>
</dependency>

3:使用操作

将SensitiveField放到对象的的属性上

将SensitiveEncode或SensitiveDecode放到方法的请求上,表示对方法返回的实体对象进行脱敏加密处理和是脱敏解密处理

注意对基本类型不能进行脱敏处理,不然需要改以上代码

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Below is a list of Skein files included on the NIST submission CD, along with a very brief description of each file. In both the reference and optimized directories, all C files should be compiled to generate a SHA3 NIST API "library" for Skein. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following files are identical and common between the reference and optimized versions of the code: File Name Description -------------------------------------------------------------------------------- brg_endian.h Brian Gladman's header file to auto-detect CPU endianness (with a few extensions for handling various platforms/compilers) brg_types.h Brian Gladman's header file to auto-detect integer types (with a few extensions for handling various platforms/compilers) SHA3api_ref.h API definitions for SHA3 API, implemented in SHA3api_ref.c SHA3api_ref.c "Wrapper" code that implements the NIST SHA3 API on top of the Skein API. skein_debug.h Header for with routines used internally by Skein routines for generating debug i/o (e.g., round-by-round intermediate values) If SKEIN_DEBUG is not defined at compile time, these interface declarations instead become "dummy" macros so that there is no performance impact. skein_debug.c Debug i/o routines called by Skein functions. skein.h Function prototypes, data structures, and constant definitions for Skein. The Skein API is more general than the NIST API (e.g., MAC functions). ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following files are different for the reference and optimized versions of the code. Note that the source files in Optimized_32bit and Optimized_64bit directories are identical. File Name Description -------------------------------------------------------------------------------- skein_port.h Definitions that might need to be changed to port Skein to a different CPU platform (e.g., big-endian). The Skein code should run on most CPU platforms, but the macros/functions here may be helpful in making the code run more efficiently skein.c The main Skein interface functions: Init, Update, and Final, for all three Skein block sizes. Additionally, the InitExt() function allows for MAC and other extended functionality. skein_block.c The Skein block processing function, based on the Threefish block cipher. This module contains the most performance-sensitive code and can be replaced by the assembly modules for slight speedups on some platforms. The functions here are only for internal use inside "skein.c" and are not intended for external APIs. skein_iv.h Initial values for various Skein hash functions. Note that these values are NOT "magic constants", as they are computed using the initial Skein "configuration" block. These values are used only by the optimized code, in order to speed up the hash computations. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following files are included in the Additional_Implementations directory: File Name Description -------------------------------------------------------------------------------- skein_test.c The Skein test module, used to measure performance and generate KAT vectors for testing. This module should be compiled together with the Skein source files (i.e., from the Reference or the Optimized directories) to generate an executable, skein_test.exe. This program is used internally to test/validate/compare different implementations (e.g., Reference, Optimized, Assembly). skein_block_x64.asm This is the 64-bit assembly language version of skein_block.c. It may be used to replace that file in the Optimized_64bit directory to improve performance on 64-bit Intel/AMD systems. It should be assembled with ml64.exe. skein_block_x86.asm This is the 32-bit assembly language version of skein_block.c. It may be used to replace that file in the Optimized_32bit directory to improve performance on 32-bit Intel/AMD systems. It should be assembled with ml.exe. skein_rot_search.c This is the program that searches for the Threefish rotation constants. It has many different command-line switches, but by default it generates the constants used in the Skein paper. This file is a stand-alone C file. To run it, simply re-direct the output to a test file: "skein_rot_search > srs_log.txt". Note that it takes nearly 3 DAYS on a Core 2 Duo to complete program execution in this case. Alternately, to generate individual files, run the following command lines: skein_rot_search -b256 > srs_256.txt skein_rot_search -b512 > srs_512.txt skein_rot_search -b1024 > srs_1024.txt srs_256.txt These three files contain the results of running skein_rot_search.exe srs_512.txt for the three different Skein block sizes. They are rather large. srs_1024.txt At the end of each file, the "finalists" are re-graded with different number of random samples. Atmel_AVR.c This file was used to compile on the Atmel AVR 8-bit CPU. It includes the optimized versions of skein.c and skein_block.c with compile-time settings to only implement one at time. This was compiled with the free AVR tool set from Atmel and simulated to give the 8-bit C performance numbers. skein_8bit_estimates.xls This file is a spreadsheet used to generate the estimates for code size and speed of assembly versions of Skein on the Atmel 8-bit CPU family. Note that this is MUCH faster than the C versions, since it uses static variables, with optimized loading and rotations. No attempt is made here to minimize code size by sharing code using calls, although the code size could be shrunk significantly using calls, at some cost in performance. skein_perf_core2.txt This file contains code size and performance data running on an Intel Core 2 Duo CPU under Windows Vista 64-bit, using the Microsoft and other compilers and assemblers. It includes results for both 32-bit and 64-bit code. skein_MSC_v9_perf.txt This file contains a subset of the skein_perf_core2.txt file, including only results from the MSVC 2008 compiler, with message sizes that are powers of 10. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following files are included in the KAT_MCT directory, in addition to the KAT/MCT files required by NIST: genKAT.c NIST-supplied source file for generating KAT_MCT vectors. This module should be compiled together with the Skein source files (i.e., from the Reference or the Optimized directories) to generate an executable genKAT.exe, which can generate the KAT_MCT vectors. [FWIW, compiling this source file under gcc gives several nasty compiler warnings!] skein_golden_kat.txt The "golden" KAT file generated using "skein_test.exe -k". This file tries to cover various block sizes, message sizes, and output sizes, as well as MAC modes. It is used for testing compliance of a Skein implementation, using skein_test.c skein_golden_kat_internals.txt The KAT file generated using "skein_test.exe -k -dc". It covers the same test as "skein_golden_kat.txt" , but also prints out intermediate (round-by-round) values. The file is very large, but it is quite useful in debugging when porting Skein to a new CPU platform and/or programming language. skein_golden_kat_short.txt This is a shorter version (subset) of skein_golden_kat.txt skein_golden_kat_short_internals.txt This is a shorter version (subset) of skein_golden_kat_internals.txt

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值