XML文档加签验签

本文代码参考了别人的一些 代码,还有加上自己的理解及一些改进。有问题欢迎大家讨论。

大致思路:
1. 生成私钥公钥,分别经base64 encode后存放本地文件夹。
2. 加签: 对xml文档经私钥加签名。签名元素整个再经base64 encode,创建新的 标签塞入encode后的值且替换原来整个的签名。将加签名后的文档保存到本地文件。
3. 验签: 取加签后的文档, 把的值经base64 decode还原为原签名元素,再把替换为转后真正的签名元素,然后验证签名文档。
4. 文件demo

好了,上干货
KryptoUtil: 用来生成公钥私钥对,并保存到本地文件。同时提供根据文件路径拿到公私钥对象的接口。
SignatureUtil: 提供一些公共接口,比如把Node/Document 转换成string 或者把string反转换成 Node/Element/Document,或者把本地文件转换成document及把document转换成本地文件存放
XmlDigitalSignatureGenerator:生成加签后的xml文件
XmlDigitalSignatureVerifier:验证签名后的xml文件

/**
 * use to generate public key and priviate key
 * 
 * @author as
 * @version $Id: KryptoUtil.java, v 0.1 29 Jun 2016 16:31:54 as Exp $
 */
public class KryptoUtil {

    /**
     * Name of the algorithm
     */
    private static final String ALGORITHM = "RSA";

    /**
     * This method is used to generate key pair based upon the provided
     * algorithm
     *
     * @return KeyPair
     */
    private KeyPair generateKeyPairs() {
        KeyPair keyPair = null;
        KeyPairGenerator keyGen;
        try {
            keyGen = KeyPairGenerator.getInstance(ALGORITHM);
            keyGen.initialize(2048);
            keyPair = keyGen.genKeyPair();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return keyPair;
    }

    /**
     * Method used to store Private and Public Keys inside a directory
     *
     * @param dirPath to store the keys
     */
    public void storeKeyPairs(String dirPath) {
        KeyPair keyPair = generateKeyPairs();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
        storeKeys(dirPath + File.separator + "publickey.pem", publicKey);
        storeKeys(dirPath + File.separator + "privatekey.pem", privateKey);
    }

    /**
     * Method used to store the key(Public/Private)
     *
     * @param filePath , name of the file
     * @param key
     */
    private void storeKeys(String filePath, Key key) {
        byte[] keyBytes = key.getEncoded();
        File file = new File(filePath);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        OutputStream outStream = null;
        try {
            outStream = new FileOutputStream(filePath);
            //base64 encode before store it
//            System.out.println("keyBytes -> string: " + new String(keyBytes));
//            System.out.println("base64 encode keyBytes -> string: " + Base64.encodeBase64String(keyBytes));
            outStream.write(Base64.encodeBase64String(keyBytes).getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outStream != null) {
                try {
                    outStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Method used to retrieve the keys in the form byte array
     *
     * @param filePath of the key
     * @return byte array
     */
    private byte[] getKeyData(String filePath) {
        File file = new File(filePath);
        byte[] buffer = new byte[(int) file.length()];
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            fis.read(buffer);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return buffer;
    }

    /**
     * Method used to get the generated Private Key
     *
     * @param filePath of the PrivateKey file
     * @return PrivateKey
     */
    public PrivateKey getStoredPrivateKey(String filePath) {
        PrivateKey privateKey = null;
//        System.out.println("private key string " + new String(getKeyData(filePath)));
//        System.out.println("private key string " + new String(getKeyData(filePath)));
        byte[] keydata = Base64.decodeBase64(new String(getKeyData(filePath)));
        PKCS8EncodedKeySpec encodedPrivateKey = new PKCS8EncodedKeySpec(keydata);
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        try {
            privateKey = keyFactory.generatePrivate(encodedPrivateKey);
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return privateKey;
    }

    /**
     * Method used to get the generated Public Key
     *
     * @param filePath of the PublicKey file
     * @return PublicKey
     */
    public PublicKey getStoredPublicKey(String filePath) {
        PublicKey publicKey = null;
//        System.out.println("private key string " + new String(getKeyData(filePath)));
//        System.out.println("private key string " + new String(getKeyData(filePath)));
        byte[] keydata = Base64.decodeBase64(new String(getKeyData(filePath)));
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        X509EncodedKeySpec encodedPublicKey = new X509EncodedKeySpec(keydata);
        try {
            publicKey = keyFactory.generatePublic(encodedPublicKey);
        } catch (NullPointerException npe) {
            npe.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return publicKey;
    }
}

public class SignatureUtil {

    /**
     * Method used to get the XML document by parsing
     *
     * @param xmlFilePath , file path of the XML document
     * @return Document
     */
    public static Document getXmlDocument(String xmlFilePath) {
        Document doc = null;
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        //if doesn't setNamespaceAware(true); 
        //getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
        dbf.setNamespaceAware(true);
        try {
            doc = dbf.newDocumentBuilder().parse(new FileInputStream(xmlFilePath));
        } catch (ParserConfigurationException ex) {
            ex.printStackTrace();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (SAXException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return doc;
    }

    /**
     * Convert node to string
     * 
     * @param node
     * @return String
     */
    public static String convertNodeToString(Node node) {
        StringWriter sw = new StringWriter();
        try {
          Transformer t = TransformerFactory.newInstance().newTransformer();
//          t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
//          t.setOutputProperty(OutputKeys.INDENT, "yes");
          t.transform(new DOMSource(node), new StreamResult(sw));
        } catch (TransformerException te) {
          System.out.println("nodeToString Transformer Exception");
        }
        return sw.getBuffer().toString();
    }

    /**
     * convert string to Element
     * 
     * @param xmlStr
     * @return Element
     */
    public static Element convertStringToElement(String xmlStr) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
        //if doesn't setNamespaceAware(true); 
        //getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
        factory.setNamespaceAware(true);
        DocumentBuilder builder;  
        try {  
            builder = factory.newDocumentBuilder();  
            Document doc = builder.parse( new InputSource( new StringReader( xmlStr ) ) );
            return doc.getDocumentElement();
        } catch (Exception e) {  
            e.printStackTrace();  
        } 
        return null;
    }

    /**
     * convert String to Document
     * 
     * @param xmlStr
     * @return Document
     */
    public static Document convertStringToDocument(String xmlStr) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
        //if doesn't setNamespaceAware(true); 
        //getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
        factory.setNamespaceAware(true);
        DocumentBuilder builder;  
        try {  
            builder = factory.newDocumentBuilder();  
            Document doc = builder.parse( new InputSource( new StringReader( xmlStr ) ) );
            return doc;
        } catch (Exception e) {  
            e.printStackTrace();  
        } 
        return null;
    }

    /**
     * convert Document to String
     * 
     * @param doc
     * @return String
     */
    public static String convertDocumentToString(Document doc) {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            transformer = tf.newTransformer();
            // below code to remove XML declaration
            // transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            StringWriter writer = new StringWriter();
            transformer.transform(new DOMSource(doc), new StreamResult(writer));
            String output = writer.getBuffer().toString();
            return output;
        } catch (TransformerException e) {
            e.printStackTrace();
        }

        return null;
    }

    /*
     * Method used to store the signed XMl document
     */
    public static void storeSignedDoc(Document doc, String destnSignedXmlFilePath) {
        TransformerFactory transFactory = TransformerFactory.newInstance();
        Transformer trans = null;
        File file = new File(destnSignedXmlFilePath);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            trans = transFactory.newTransformer();
        } catch (TransformerConfigurationException ex) {
            ex.printStackTrace();
        }
        try {
            StreamResult streamRes = new StreamResult(file);
            trans.transform(new DOMSource(doc), streamRes);
        } catch (TransformerException ex) {
            ex.printStackTrace();
        }
        System.out.println("XML file with attached digital signature generated successfully ...");
    }
}

package com.tech.diudiu.signature.demo3;
/**
 * 
 * @author as
 * @version $Id: XmlDigitalSignatureGenerator.java, v 0.1 2016年7月1日 下午2:25:48 as Exp $
 */
@SuppressWarnings("restriction")
public class XmlDigitalSignatureGenerator {

    /**
     * Method used to get the KeyInfo
     *
     * @param xmlSigFactory
     * @param publicKeyPath
     * @return KeyInfo
     */
    private static KeyInfo getKeyInfo(XMLSignatureFactory xmlSigFactory, String publicKeyPath) {
        KeyInfo keyInfo = null;
        KeyValue keyValue = null;
        PublicKey publicKey = new KryptoUtil().getStoredPublicKey(publicKeyPath);
        KeyInfoFactory keyInfoFact = xmlSigFactory.getKeyInfoFactory();
        try {
            keyValue = keyInfoFact.newKeyValue(publicKey);
        } catch (KeyException ex) {
            ex.printStackTrace();
        }
        keyInfo = keyInfoFact.newKeyInfo(Collections.singletonList(keyValue));
        return keyInfo;
    }

    /**
     * 
     * 
     * @param originalXmlFilePath
     * @param destnSignedXmlFilePath
     * @param privateKeyFilePath
     * @param publicKeyFilePath
     * @param isBase64
     */
    public static void generateXMLDigitalSignature(String originalXmlFilePath,
                                            String destnSignedXmlFilePath,
                                            String privateKeyFilePath, String publicKeyFilePath, boolean isBase64) {
        //Get the XML Document object
        Document doc = SignatureUtil.getXmlDocument(originalXmlFilePath);
        //Create XML Signature Factory
        XMLSignatureFactory xmlSigFactory = XMLSignatureFactory.getInstance("DOM");
        PrivateKey privateKey = new KryptoUtil().getStoredPrivateKey(privateKeyFilePath);
        DOMSignContext domSignCtx = new DOMSignContext(privateKey, doc.getDocumentElement());
        Reference ref = null;
        SignedInfo signedInfo = null;
        try {
            ref = xmlSigFactory.newReference("", xmlSigFactory.newDigestMethod(DigestMethod.SHA256,
                null), Collections.singletonList(xmlSigFactory.newTransform(Transform.ENVELOPED,
                (TransformParameterSpec) null)), null, null);
            signedInfo = xmlSigFactory
                .newSignedInfo(xmlSigFactory.newCanonicalizationMethod(
                    CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
                    xmlSigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null), Collections
                        .singletonList(ref));
        } catch (NoSuchAlgorithmException ex) {
            ex.printStackTrace();
        } catch (InvalidAlgorithmParameterException ex) {
            ex.printStackTrace();
        }
        //Pass the Public Key File Path 
        KeyInfo keyInfo = getKeyInfo(xmlSigFactory, publicKeyFilePath);
        //Create a new XML Signature
        XMLSignature xmlSignature = xmlSigFactory.newXMLSignature(signedInfo, keyInfo);
        try {
            //Sign the document
            xmlSignature.sign(domSignCtx);
        } catch (MarshalException ex) {
            ex.printStackTrace();
        } catch (XMLSignatureException ex) {
            ex.printStackTrace();
        }
        if (isBase64) {
            //encode the signature element before store it
            try {
                Element root = doc.getDocumentElement();
                NodeList signatures = root.getElementsByTagName("Signature");
                if (signatures.getLength() != 1) {
                    throw new Exception("Signature error");
                }else {
                    Node signature = signatures.item(0);
                    Element sign2 = doc.createElement("signature");
                    String sigContent = SignatureUtil.convertNodeToString(signature);
                    sign2.setTextContent(Base64.encodeBase64String(sigContent.getBytes("utf-8")));
                    root.replaceChild(sign2, signature);
                }

            } catch (Exception e) {
                // TODO: handle exception
            }

        }
        //Store the digitally signed document inta a location
        SignatureUtil.storeSignedDoc(doc, destnSignedXmlFilePath);
    }
}

/**
 * 
 * @author as
 * @version $Id: XmlDigitalSignatureVerifier.java, v 0.1 2016年7月1日 下午2:26:57 as Exp $
 */
@SuppressWarnings("restriction")
public class XmlDigitalSignatureVerifier {

    /**
     * 
     * 
     * @param signedXmlFilePath
     * @param pubicKeyFilePath
     * @param isBase64
     * @return boolean
     * @throws Exception
     */
    public static boolean isXmlDigitalSignatureValid(String signedXmlFilePath,
                                                     String pubicKeyFilePath, boolean isBase64) throws Exception {
        Document doc = SignatureUtil.getXmlDocument(signedXmlFilePath);

        if (isBase64) {
            //encode the signature element before store it
            try {
                Element root = doc.getDocumentElement();
                NodeList signatures = root.getElementsByTagName("signature");
                if (signatures.getLength() != 1) {
                    throw new Exception("Signature error");
                }else {
                    Node signature = signatures.item(0);
                    String signatureContent = new String(Base64.decodeBase64(signature.getTextContent()));
                    Node newChild = doc.importNode(SignatureUtil.convertStringToElement(signatureContent), true);
                    root.replaceChild(newChild, signature);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if (nl.getLength() == 0) {
            throw new Exception("No XML Digital Signature Found, document is discarded");
        }
        PublicKey publicKey = new KryptoUtil().getStoredPublicKey(pubicKeyFilePath);
        DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(0));
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
        XMLSignature signature = fac.unmarshalXMLSignature(valContext);
        return signature.validate(valContext);
    }
}

**

文件demo

**

私钥:privatekey.pem
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdHF222UUcghltzBX4y6JJ/FAvmNG+GzcEMGkec2pKmLekJ4IhgYZVuhx8g1B3saZYKFn/uD0dghNA/QGmKoBHxyRWeBBagUaxlRswFY7tu3TH0pr7odKyxQf3P+8qd1iuu+lqASsWwoiNQPoy4G7+IX5/mpDcFOaVdGB6vbuiuLkNL6Gobt/Rih9jsIQoUKdqSXJBmptZro+EqtsX9djikaMUDuxVIejM15EnQbIsskmdAP1Li9EF9KEypWZdg6AUAzxxe6+2leO6myq4MnXIJQwYcTlw0FaFyTfLC0iDyD39apdSC96ePDFOsLtmjErTI1rOH0UuQ6lVnMl64OlxAgMBAAECggEAQsMQC6guFxwtxBuKZqvZrcjbJi6xSjB0Y4JS2Q7AZyyYNNhZEvXgaMNqYXrHkeVaw3F6NRFMXyKsHdyle7/pvRahmWDxyhPPP50V7YvrrZgP2u3iz/UG/NJ/PRDdB9WpSK8vU81lSwmlTykCaOY4JCOFhqHe1rbG+VF3whCoEE/1yOHb8Z+LgN589gP0ujARfV7Bb70wEKiq82Mu30Ypzgkh/f12xvxxW36inGcvWlc3bc1rINwUtckkGjXSooLPkEBLr7ZxuzwDH4QZaWV1gm32zVH/0tg6DroqFEOkxwjHROMLvYBYZnFbZc9+l45XEYOP1iSgVWNbp/LCucUnlQKBgQDZiMcPvupau0yGNQ6OeCDngYDY89QqrP4ctWAivB9Exn2zjbQJSMEmjObc/qM+bvsPRGWu5yt+QeJ+TI367XIbi2HlZF/OHt0v5LHnOpwZaqEngsJrihkJJxzWbDUR2nKI+md7mDLE/9afX8kcfX1sW8YljSyGItxU6n3EKs/LewKBgQC45F8c0Icz8QVCcBCs++WyyaWFtc8xcPWUQq7n8/RHUHT4ftmNMjPqOmzDiYlgSPnPkRFm6jmuqfKD8Kd+ez4JNR5IvQ9dwf2bDEXu5bO2jaIbNnN5m6smll5GKCwD6btaq/BEanPF10Nr8HpIQxbhCp78X6foA9wG+oTuWF5lAwKBgGg9yA4+kXULHf+N+zzAShxGDaVpKnmMznSZYckjVuSRLXlWZTixuYUsrhCmoTlw2zQUtZlbw5bMIaoeK0dcc3rJjw7qy5ItbhbeQ3YnutffJfyWB26bDaY6LPJ/herZQFttixtJ8sI+Su2ya+AQLoM3cztZbEWW/PhgKgZP8aVzAoGAMxyGoouO5K/7OTuLkJZuEDeoN0dkSJGmgd7RRTPuUoyOWWePsnWvXk/aY0+xF1n1HQlIWz/ixEMc1JaBZvig1KDHh2okRlHrTqJc7sa3IH8U0hsCXxrGfHtTAmf2ivphHZasa+1Vpdp1O/CVjUZmm7145+F1pDD08UTt3Im3RosCgYAoHq+hZYGKMK1q+s6SiwRCARccbDAM+xqc8oGJD2+lsH4LcQG+NYWl3feJ7xi+U3ffkAK7fynHngEnrFD7ZKT0Ta00l7A7rYadsKaqdAbCfp8ppkd5txKhdpl0eit4Ckd2FtzZwOHZNLqKWAvbDKUkP2I6b3JcT3w4j02MqR0lxA==

公钥publickey.pem:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRxdttlFHIIZbcwV+MuiSfxQL5jRvhs3BDBpHnNqSpi3pCeCIYGGVbocfINQd7GmWChZ/7g9HYITQP0BpiqAR8ckVngQWoFGsZUbMBWO7bt0x9Ka+6HSssUH9z/vKndYrrvpagErFsKIjUD6MuBu/iF+f5qQ3BTmlXRger27ori5DS+hqG7f0YofY7CEKFCnaklyQZqbWa6PhKrbF/XY4pGjFA7sVSHozNeRJ0GyLLJJnQD9S4vRBfShMqVmXYOgFAM8cXuvtpXjupsquDJ1yCUMGHE5cNBWhck3ywtIg8g9/WqXUgvenjwxTrC7ZoxK0yNazh9FLkOpVZzJeuDpcQIDAQAB

源文件:original.xml

<?xml version="1.0"?>
<student>
    <desc id="request">
        <head>
            <id>ididididi</id>
            <name>Your Name here</name>
        </head>
        <course>
            <english>37</english>
            <math>99</math>
            <art>0</art>
        </course>
    </desc>
</student>

数字签名后文件:originalWithSign.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?><student>
    <desc id="request">
        <head>
            <id>ididididi</id>
            <name>Your Name here</name>
        </head>
        <course>
            <english>37</english>
            <math>99</math>
            <art>0</art>
        </course>
    </desc>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>cOI6m7AfoVFkF4DV5ZINFtoldbUKtSHgQtk/NhtvU9U=</DigestValue></Reference></SignedInfo><SignatureValue>HgvGkJzuZVJ57yHrk0xf3FRvZ7lpQT2nj7BShOOxOvIXRlyGGbkox9JahMh/BJTfP/xv1hDyg+o1
h5uPtuowGjG1Pg+LSrl46v9Byxr0KJXZcZ6UotoO6rndkp63fIrBNlzTaLutiBsxRggZFubSboAJ
svbQk+k6oIbWhfa85KwyxeN/2rPIMo72Lz4qkhkwIZjv+4b3ye0+rRZrFvsKzDjxUnwXn5jPjasn
/8OrQGn/uGEGKA9lj0uJobAiVFAaDyKY12FmSz1Htik12mCh7TwEScHFtWHvB6JHEbOTGxn3bX3H
LDEb5Zroh1sPArECIh/uW+5YZWXfIRPU/gMAzg==</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>nRxdttlFHIIZbcwV+MuiSfxQL5jRvhs3BDBpHnNqSpi3pCeCIYGGVbocfINQd7GmWChZ/7g9HYIT
QP0BpiqAR8ckVngQWoFGsZUbMBWO7bt0x9Ka+6HSssUH9z/vKndYrrvpagErFsKIjUD6MuBu/iF+
f5qQ3BTmlXRger27ori5DS+hqG7f0YofY7CEKFCnaklyQZqbWa6PhKrbF/XY4pGjFA7sVSHozNeR
J0GyLLJJnQD9S4vRBfShMqVmXYOgFAM8cXuvtpXjupsquDJ1yCUMGHE5cNBWhck3ywtIg8g9/WqX
UgvenjwxTrC7ZoxK0yNazh9FLkOpVZzJeuDpcQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature></student>

base64 endoce 后签名文件originalWithSignBase64.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?><student>
    <desc id="request">
        <head>
            <id>ididididi</id>
            <name>Your Name here</name>
        </head>
        <course>
            <english>37</english>
            <math>99</math>
            <art>0</art>
        </course>
    </desc>
<signature>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48U2lnbmF0dXJlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48U2lnbmVkSW5mbz48Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnL1RSLzIwMDEvUkVDLXhtbC1jMTRuLTIwMDEwMzE1Ii8+PFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxSZWZlcmVuY2UgVVJJPSIiPjxUcmFuc2Zvcm1zPjxUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PERpZ2VzdFZhbHVlPmNPSTZtN0Fmb1ZGa0Y0RFY1WklORnRvbGRiVUt0U0hnUXRrL05odHZVOVU9PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8+PFNpZ25hdHVyZVZhbHVlPkhndkdrSnp1WlZKNTd5SHJrMHhmM0ZSdlo3bHBRVDJuajdCU2hPT3hPdklYUmx5R0dia294OUphaE1oL0JKVGZQL3h2MWhEeWcrbzENCmg1dVB0dW93R2pHMVBnK0xTcmw0NnY5Qnl4cjBLSlhaY1o2VW90b082cm5ka3A2M2ZJckJObHpUYUx1dGlCc3hSZ2daRnViU2JvQUoNCnN2YlFrK2s2b0liV2hmYTg1S3d5eGVOLzJyUElNbzcyTHo0cWtoa3dJWmp2KzRiM3llMCtyUlpyRnZzS3pEanhVbndYbjVqUGphc24NCi84T3JRR24vdUdFR0tBOWxqMHVKb2JBaVZGQWFEeUtZMTJGbVN6MUh0aWsxMm1DaDdUd0VTY0hGdFdIdkI2SkhFYk9UR3huM2JYM0gNCkxERWI1WnJvaDFzUEFyRUNJaC91Vys1WVpXWGZJUlBVL2dNQXpnPT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxLZXlWYWx1ZT48UlNBS2V5VmFsdWU+PE1vZHVsdXM+blJ4ZHR0bEZISUlaYmN3VitNdWlTZnhRTDVqUnZoczNCREJwSG5OcVNwaTNwQ2VDSVlHR1Zib2NmSU5RZDdHbVdDaFovN2c5SFlJVA0KUVAwQnBpcUFSOGNrVm5nUVdvRkdzWlViTUJXTzdidDB4OUthKzZIU3NzVUg5ei92S25kWXJydnBhZ0VyRnNLSWpVRDZNdUJ1L2lGKw0KZjVxUTNCVG1sWFJnZXIyN29yaTVEUytocUc3ZjBZb2ZZN0NFS0ZDbmFrbHlRWnFiV2E2UGhLcmJGL1hZNHBHakZBN3NWU0hvek5lUg0KSjBHeUxMSkpuUUQ5UzR2UkJmU2hNcVZtWFlPZ0ZBTThjWHV2dHBYanVwc3F1REoxeUNVTUdIRTVjTkJXaGNrM3l3dElnOGc5L1dxWA0KVWd2ZW5qd3hUckM3Wm94SzB5TmF6aDlGTGtPcFZaekpldURwY1E9PTwvTW9kdWx1cz48RXhwb25lbnQ+QVFBQjwvRXhwb25lbnQ+PC9SU0FLZXlWYWx1ZT48L0tleVZhbHVlPjwvS2V5SW5mbz48L1NpZ25hdHVyZT4=</signature></student>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值