检测签名之间对已签名PDF所做的更改

本文介绍了检测签名之间对已签名PDF所做的更改的处理方法,对大家解决问题具有一定的参考价值

问题描述

我正在开发一个应该验证pdf文件签名的应用程序。应用程序应在应用每个签名之前检测对文件内容进行的更新的完整历史记录。 例如:

  1. 签名者%1签署了纯pdf文件
  2. 签名者2向签名文件添加了注释,然后对其签名

应用程序如何检测到签名者2在其签名之前添加了注释。

我已经尝试使用itext和pdfbox

推荐答案

已经在comment中解释过,iText和PDFBox都没有提供高级接口,告诉您在增量更新过程中在UI对象(注释、文本内容等)方面发生了哪些变化。

您可以使用它们将PDF的不同修订呈现为位图并比较这些图像。

或者您可以使用它们告诉您低级COS对象(字典、数组、数字、字符串等)的变化。

但分析这些图像或低级别对象中的更改并确定它们在用户界面对象方面的含义,例如添加了评论和仅添加了评论,这是非常重要的。

In response您要求

能否解释一下,如何检测低级COS对象的变化。

要比较的内容和要考虑的更改

首先,您必须清楚可以比较哪些文档状态来检测更改。

PDF格式允许在所谓的增量更新中将更改追加到PDF。这允许对签名文档进行更改,而无需加密破坏这些签名,因为原始签名字节保持原样:

但是,中间可能有更多未签名的增量更新;例如,版本2的更改可能包括多个增量更新。

可以考虑比较由任意增量更新创建的修订。然而,这里的问题是,如果没有签名,您无法识别应用增量更新的人。

因此,通常更有意义的做法是只比较签名的修订版,并让每个签名者负责自上一个签名修订版以来的所有更改。此处唯一的例外是整个文件,因为它是PDF的当前版本,即使没有覆盖所有文件的签名也是特别重要的。

接下来,您必须决定您认为什么是更改。特别是:

  • 增量更新中的每个对象覆盖都是更改吗?即使是用完全相同的副本覆盖原始对象的对象?

  • 使直接对象成为间接对象(反之亦然)但保持所有内容和引用不变的更改怎么办?

  • 添加未从标准结构中的任何位置引用的新对象怎么办?

  • 添加未从交叉引用流或表引用的对象怎么办?

  • 添加根本不遵循PDF语法的数据怎么办?

如果您也对此类更改感兴趣,现有的现成PDF库通常不会为您提供确定更改的方法;您很可能至少必须更改它们的代码以遍历交叉引用表/流的链,甚至直接分析更新中的文件字节。

如果您对此类更改不感兴趣,则通常不需要更改或替换库例程。

由于由符合规范的PDF处理器处理PDF时,列举的和类似的更改没有区别,因此通常可以忽略此类更改。

如果这也是您的立场,下面的示例工具可能会为您提供一个起点。

基于iText 7的示例工具

使用上述限制,您可以在不更改库的情况下使用iText 7比较PDF的签名修订版,方法是将要比较的修订版加载到单独的PdfDocument实例中,并从尾部开始递归比较PDF对象。

我曾经将其作为个人使用的小型辅助工具来实现(因此它还没有完全完成,更多的工作正在进行中)。首先是允许比较两个任意文档的基类:

public class PdfCompare {
    public static void main(String[] args) throws IOException {
        System.out.printf("Comparing:
* %s
* %s
", args[0], args[1]);
        try (   PdfDocument pdfDocument1 = new PdfDocument(new PdfReader(args[0]));
                PdfDocument pdfDocument2 = new PdfDocument(new PdfReader(args[1]))  ) {
            PdfCompare pdfCompare = new PdfCompare(pdfDocument1, pdfDocument2);
            pdfCompare.compare();

            List<Difference> differences = pdfCompare.getDifferences();
            if (differences == null || differences.isEmpty()) {
                System.out.println("No differences found.");
            } else {
                System.out.printf("%d differences found:
", differences.size());
                for (Difference difference : pdfCompare.getDifferences()) {
                    for (String element : difference.getPath()) {
                        System.out.print(element);
                    }
                    System.out.printf(" - %s
", difference.getDescription());
                }
            }
        }
    }

    public interface Difference {
        List<String> getPath();
        String getDescription();
    }

    public PdfCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
        trailer1 = pdfDocument1.getTrailer();
        trailer2 = pdfDocument2.getTrailer();
    }

    public void compare() {
        LOGGER.info("Starting comparison");
        try {
            compared.clear();
            differences.clear();
            LOGGER.info("START COMPARE");
            compare(trailer1, trailer2, Collections.singletonList("trailer"));
            LOGGER.info("START SHORTEN PATHS");
            shortenPaths();
        } finally {
            LOGGER.info("Finished comparison and shortening");
        }
    }

    public List<Difference> getDifferences() {
        return differences;
    }

    class DifferenceImplSimple implements Difference {
        DifferenceImplSimple(PdfObject object1, PdfObject object2, List<String> path, String description) {
            this.pair = Pair.of(object1, object2);
            this.path = path;
            this.description = description;
        }

        @Override
        public List<String> getPath() {
            List<String> byPair = getShortestPath(pair);
            return byPair != null ? byPair : shorten(path);
        }
        @Override public String getDescription()    { return description;           }

        final Pair<PdfObject, PdfObject> pair;
        final List<String> path;
        final String description;
    }

    void compare(PdfObject object1, PdfObject object2, List<String> path) {
        LOGGER.debug("Comparing objects at {}.", path);
        if (object1 == null && object2 == null)
        {
            LOGGER.debug("Both objects are null at {}.", path);
            return;
        }
        if (object1 == null) {
            differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 1"));
            LOGGER.info("Object in document 1 is missing at {}.", path);
            return;
        }
        if (object2 == null) {
            differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 2"));
            LOGGER.info("Object in document 2 is missing at {}.", path);
            return;
        }

        if (object1.getType() != object2.getType()) {
            differences.add(new DifferenceImplSimple(object1, object2, path,
                    String.format("Type difference, %s in document 1 and %s in document 2",
                            getTypeName(object1.getType()), getTypeName(object2.getType()))));
            LOGGER.info("Objects have different types at {}, {} and {}.", path, getTypeName(object1.getType()), getTypeName(object2.getType()));
            return;
        }

        switch (object1.getType()) {
        case PdfObject.ARRAY:
            compareContents((PdfArray) object1, (PdfArray) object2, path);
            break;
        case PdfObject.DICTIONARY:
            compareContents((PdfDictionary) object1, (PdfDictionary) object2, path);
            break;
        case PdfObject.STREAM:
            compareContents((PdfStream)object1, (PdfStream)object2, path);
            break;
        case PdfObject.BOOLEAN:
        case PdfObject.INDIRECT_REFERENCE:
        case PdfObject.LITERAL:
        case PdfObject.NAME:
        case PdfObject.NULL:
        case PdfObject.NUMBER:
        case PdfObject.STRING:
            compareContentsSimple(object1, object2, path);
            break;
        default:
            differences.add(new DifferenceImplSimple(object1, object2, path, "Unknown object type " + object1.getType() + "; cannot compare"));
            LOGGER.warn("Unknown object type at {}, {}.", path, object1.getType());
            break;
        }
    }

    void compareContents(PdfArray array1, PdfArray array2, List<String> path) {
        int count1 = array1.size();
        int count2 = array2.size();
        if (count1 < count2) {
            differences.add(new DifferenceImplSimple(array1, array2, path, "Document 1 misses " + (count2-count1) + " array entries"));
            LOGGER.info("Array in document 1 is missing {} entries at {} for {}.", (count2-count1), path);
        }
        if (count1 > count2) {
            differences.add(new DifferenceImplSimple(array1, array2, path, "Document 2 misses " + (count1-count2) + " array entries"));
            LOGGER.info("Array in document 2 is missing {} entries at {} for {}.", (count1-count2), path);
        }

        if (alreadyCompared(array1, array2, path)) {
            return;
        }

        int count = Math.min(count1, count2);
        for (int i = 0; i < count; i++) {
            compare(array1.get(i), array2.get(i), join(path, String.format("[%d]", i)));
        }
    }

    void compareContents(PdfDictionary dictionary1, PdfDictionary dictionary2, List<String> path) {
        List<PdfName> missing1 = new ArrayList<PdfName>(dictionary2.keySet());
        missing1.removeAll(dictionary1.keySet());
        if (!missing1.isEmpty()) {
            differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 1 misses dictionary entries for " + missing1));
            LOGGER.info("Dictionary in document 1 is missing entries at {} for {}.", path, missing1);
        }

        List<PdfName> missing2 = new ArrayList<PdfName>(dictionary1.keySet());
        missing2.removeAll(dictionary2.keySet());
        if (!missing2.isEmpty()) {
            differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 2 misses dictionary entries for " + missing2));
            LOGGER.info("Dictionary in document 2 is missing entries at {} for {}.", path, missing2);
        }

        if (alreadyCompared(dictionary1, dictionary2, path)) {
            return;
        }

        List<PdfName> common = new ArrayList<PdfName>(dictionary1.keySet());
        common.retainAll(dictionary2.keySet());
        for (PdfName name : common) {
            compare(dictionary1.get(name), dictionary2.get(name), join(path, name.toString()));
        }
    }

    void compareContents(PdfStream stream1, PdfStream stream2, List<String> path) {
        compareContents((PdfDictionary)stream1, (PdfDictionary)stream2, path);

        byte[] bytes1 = stream1.getBytes();
        byte[] bytes2 = stream2.getBytes();
        if (!Arrays.equals(bytes1, bytes2)) {
            differences.add(new DifferenceImplSimple(stream1, stream2, path, "Stream contents differ"));
            LOGGER.info("Stream contents differ at {}.", path);
        }
    }

    void compareContentsSimple(PdfObject object1, PdfObject object2, List<String> path) {
        // vvv--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
        if (object1 instanceof PdfNumber)
            ((PdfNumber)object1).getValue();
        if (object2 instanceof PdfNumber)
            ((PdfNumber)object2).getValue();
        // ^^^--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
        if (!object1.equals(object2)) {
            if (object1 instanceof PdfString) {
                String string1 = object1.toString();
                if (string1.length() > 40)
                    string1 = string1.substring(0, 40) + 'u22EF';
                string1 = sanitize(string1);
                String string2 = object2.toString();
                if (string2.length() > 40)
                    string2 = string2.substring(0, 40) + 'u22EF';
                string2 = sanitize(string2);
                differences.add(new DifferenceImplSimple(object1, object2, path, String.format("String values differ, '%s' and '%s'", string1, string2)));
                LOGGER.info("String values differ at {}, '{}' and '{}'.", path, string1, string2);
            } else {
                differences.add(new DifferenceImplSimple(object1, object2, path, String.format("Object values differ, '%s' and '%s'", object1, object2)));
                LOGGER.info("Object values differ at {}, '{}' and '{}'.", path, object1, object2);
            }
        }
    }

    String sanitize(CharSequence string) {
        char[] sanitized = new char[string.length()];
        for (int i = 0; i < sanitized.length; i++) {
            char c = string.charAt(i);
            if (c >= 0 && c < ' ')
                c = 'uFFFD';
            sanitized[i] = c;
        }
        return new String(sanitized);
    }

    String getTypeName(byte type) {
        switch (type) {
        case PdfObject.ARRAY:               return "ARRAY";
        case PdfObject.BOOLEAN:             return "BOOLEAN";
        case PdfObject.DICTIONARY:          return "DICTIONARY";
        case PdfObject.LITERAL:             return "LITERAL";
        case PdfObject.INDIRECT_REFERENCE:  return "REFERENCE";
        case PdfObject.NAME:                return "NAME";
        case PdfObject.NULL:                return "NULL";
        case PdfObject.NUMBER:              return "NUMBER";
        case PdfObject.STREAM:              return "STREAM";
        case PdfObject.STRING:              return "STRING";
        default:
            return "UNKNOWN";
        }
    }

    List<String> join(List<String> path, String element) {
        String[] array = path.toArray(new String[path.size() + 1]);
        array[array.length-1] = element;
        return Arrays.asList(array);
    }

    boolean alreadyCompared(PdfObject object1, PdfObject object2, List<String> path) {
        Pair<PdfObject, PdfObject> pair = Pair.of(object1, object2);
        if (compared.containsKey(pair)) {
            //LOGGER.debug("Objects already compared at {}, previously at {}.", path, compared.get(pair));
            Set<List<String>> paths = compared.get(pair);
            boolean alreadyPresent = false;
//            List<List<String>> toRemove = new ArrayList<>();
//            for (List<String> formerPath : paths) {
//                for (int i = 0; ; i++) {
//                    if (i == path.size()) {
//                        toRemove.add(formerPath);
//                        System.out.print('.');
//                        break;
//                    }
//                    if (i == formerPath.size()) {
//                        alreadyPresent = true;
//                        System.out.print(':');
//                        break;
//                    }
//                    if (!path.get(i).equals(formerPath.get(i)))
//                        break;
//                }
//            }
//            paths.removeAll(toRemove);
            if (!alreadyPresent)
                paths.add(path);
            return true;
        }
        compared.put(pair, new HashSet<>(Collections.singleton(path)));
        return false;
    }

    List<String> getShortestPath(Pair<PdfObject, PdfObject> pair) {
        Set<List<String>> paths = compared.get(pair);
        //return (paths == null) ? null : Collections.min(paths, pathComparator);
        return (paths == null || paths.isEmpty()) ? null : shortened.get(paths.stream().findFirst().get());
    }

    void shortenPaths() {
        List<Map<List<String>, SortedSet<List<String>>>> data = new ArrayList<>();
        for (Set<List<String>> set : compared.values()) {
            SortedSet<List<String>> sortedSet = new TreeSet<List<String>>(pathComparator);
            sortedSet.addAll(set);
            for (List<String> path : sortedSet) {
                while (path.size() >= data.size()) {
                    data.add(new HashMap<>());
                }
                SortedSet<List<String>> former = data.get(path.size()).put(path, sortedSet);
                if (former != null) {
                    LOGGER.error("Path not well-defined for {}", path);
                }
            }
        }
        for (int pathSize = 3; pathSize < data.size(); pathSize++) {
            for (Map.Entry<List<String>, SortedSet<List<String>>> pathEntry : data.get(pathSize).entrySet()) {
                List<String> path = pathEntry.getKey();
                SortedSet<List<String>> equivalents = pathEntry.getValue();
                for (int subpathSize = 2; subpathSize < pathSize; subpathSize++) {
                    List<String> subpath = path.subList(0, subpathSize);
                    List<String> remainder = path.subList(subpathSize, pathSize); 
                    SortedSet<List<String>> subequivalents = data.get(subpathSize).get(subpath);
                    if (subequivalents != null && subequivalents.size() > 1) {
                        List<String> subequivalent = subequivalents.first();
                        if (subequivalent.size() < subpathSize) {
                            List<String> replacement = join(subequivalent, remainder);
                            if (equivalents.add(replacement)) {
                                data.get(replacement.size()).put(replacement, equivalents);
                            }
                        }
                    }
                }
            }
        }

        shortened.clear();
        for (Map<List<String>, SortedSet<List<String>>> singleLengthData : data) {
            for (Map.Entry<List<String>, SortedSet<List<String>>> entry : singleLengthData.entrySet()) {
                List<String> path = entry.getKey();
                List<String> shortenedPath = entry.getValue().first();
                shortened.put(path, shortenedPath);
            }
        }
    }

    List<String> join(List<String> path, List<String> elements) {
        String[] array = path.toArray(new String[path.size() + elements.size()]);
        for (int i = 0; i < elements.size(); i++) {
            array[path.size() + i] = elements.get(i);
        }
        return Arrays.asList(array);
    }

    List<String> shorten(List<String> path) {
        List<String> shortPath = path;
        for (int subpathSize = path.size(); subpathSize > 2; subpathSize--) {
            List<String> subpath = path.subList(0, subpathSize);
            List<String> shortSubpath = shortened.get(subpath);
            if (shortSubpath != null && shortSubpath.size() < subpathSize) {
                List<String> remainder = path.subList(subpathSize, path.size());
                List<String> replacement = join(shortSubpath, remainder);
                if (replacement.size() < shortPath.size())
                    shortPath = replacement;
            }
        }
        return shortPath;
    }

    final static Logger LOGGER = LoggerFactory.getLogger(PdfCompare.class);
    final PdfDictionary trailer1;
    final PdfDictionary trailer2;
    final Map<Pair<PdfObject, PdfObject>, Set<List<String>>> compared = new HashMap<>();
    final List<Difference> differences = new ArrayList<>();
    final Map<List<String>, List<String>> shortened = new HashMap<>();
    final static Comparator<List<String>> pathComparator = new Comparator<List<String>>() {
        @Override
        public int compare(List<String> o1, List<String> o2) {
            int compare = Integer.compare(o1.size(), o2.size());
            if (compare != 0)
                return compare;
            for (int i = 0; i < o1.size(); i++) {
                compare = o1.get(i).compareTo(o2.get(i));
                if (compare != 0)
                    return compare;
            }
            return 0;
        }
    };
}

(PdfCompare.java)

使用此代码进行修订比较的工具是其子类:

public class PdfRevisionCompare extends PdfCompare {
    public static void main(String[] args) throws IOException {
        for (String arg : args) {
            System.out.printf("
Comparing revisions of: %s
***********************
", args[0]);
            try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(arg))) {
                SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
                List<String> signatureNames = signatureUtil.getSignatureNames();
                if (signatureNames.isEmpty()) {
                    System.out.println("No signed revisions detected. (no AcroForm)");
                    continue;
                }
                String previousRevision = signatureNames.get(0);
                PdfDocument previousDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(previousRevision)));
                System.out.printf("* Initial signed revision: %s
", previousRevision);
                for (int i = 1; i < signatureNames.size(); i++) {
                    String currentRevision = signatureNames.get(i);
                    PdfDocument currentDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(currentRevision)));
                    showDifferences(previousDocument, currentDocument);
                    System.out.printf("* Next signed revision (%d): %s
", i+1, currentRevision);
                    previousDocument.close();
                    previousDocument = currentDocument;
                    previousRevision = currentRevision;
                }
                if (signatureUtil.signatureCoversWholeDocument(previousRevision)) {
                    System.out.println("No unsigned updates.");
                } else {
                    showDifferences(previousDocument, pdfDocument);
                    System.out.println("* Final unsigned revision");
                }
                previousDocument.close();
            }
        }
    }

    static void showDifferences(PdfDocument previousDocument, PdfDocument currentDocument) {
        PdfRevisionCompare pdfRevisionCompare = new PdfRevisionCompare(previousDocument, currentDocument);
        pdfRevisionCompare.compare();
        List<Difference> differences = pdfRevisionCompare.getDifferences();
        if (differences == null || differences.isEmpty()) {
            System.out.println("No differences found.");
        } else {
            System.out.printf("%d differences found:
", differences.size());
            for (Difference difference : differences) {
                for (String element : difference.getPath()) {
                    System.out.print(element);
                }
                System.out.printf(" - %s
", difference.getDescription());
            }
        }
    }

    public PdfRevisionCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
        super(pdfDocument1, pdfDocument2);
    }
}

(PdfRevisionCompare.java)

这篇关于检测签名之间对已签名PDF所做的更改的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,WP2

admin_action_{$_REQUEST[‘action’]}

do_action( "admin_action_{$_REQUEST[‘action’]}" )动作钩子::在发送“Action”请求变量时激发。Action Hook: Fires when an ‘action’ request variable is sent.目录锚点:#说明#源码说明(Description)钩子名称的动态部分$_REQUEST['action']引用从GET或POST请求派生的操作。源码(Source)更新版本源码位置使用被使用2.6.0 wp-admin/admin.php:...

日期:2020-09-02 17:44:16 浏览:1164

admin_footer-{$GLOBALS[‘hook_suffix’]}

do_action( "admin_footer-{$GLOBALS[‘hook_suffix’]}", string $hook_suffix )操作挂钩:在默认页脚脚本之后打印脚本或数据。Action Hook: Print scripts or data after the default footer scripts.目录锚点:#说明#参数#源码说明(Description)钩子名的动态部分,$GLOBALS['hook_suffix']引用当前页的全局钩子后缀。参数(Parameters)参数类...

日期:2020-09-02 17:44:20 浏览:1067

customize_save_{$this->id_data[‘base’]}

do_action( "customize_save_{$this-&gt;id_data[‘base’]}", WP_Customize_Setting $this )动作钩子::在调用WP_Customize_Setting::save()方法时激发。Action Hook: Fires when the WP_Customize_Setting::save() method is called.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分,$this->id_data...

日期:2020-08-15 15:47:24 浏览:803

customize_value_{$this->id_data[‘base’]}

apply_filters( "customize_value_{$this-&gt;id_data[‘base’]}", mixed $default )过滤器::过滤未作为主题模式或选项处理的自定义设置值。Filter Hook: Filter a Customize setting value not handled as a theme_mod or option.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分,$this->id_date['base'],指的是设置...

日期:2020-08-15 15:47:24 浏览:893

get_comment_author_url

过滤钩子:过滤评论作者的URL。Filter Hook: Filters the comment author’s URL.目录锚点:#源码源码(Source)更新版本源码位置使用被使用 wp-includes/comment-template.php:32610...

日期:2020-08-10 23:06:14 浏览:928

network_admin_edit_{$_GET[‘action’]}

do_action( "network_admin_edit_{$_GET[‘action’]}" )操作挂钩:启动请求的处理程序操作。Action Hook: Fires the requested handler action.目录锚点:#说明#源码说明(Description)钩子名称的动态部分$u GET['action']引用请求的操作的名称。源码(Source)更新版本源码位置使用被使用3.1.0 wp-admin/network/edit.php:3600...

日期:2020-08-02 09:56:09 浏览:874

network_sites_updated_message_{$_GET[‘updated’]}

apply_filters( "network_sites_updated_message_{$_GET[‘updated’]}", string $msg )筛选器挂钩:在网络管理中筛选特定的非默认站点更新消息。Filter Hook: Filters a specific, non-default site-updated message in the Network admin.目录锚点:#说明#参数#源码说明(Description)钩子名称的动态部分$_GET['updated']引用了非默认的...

日期:2020-08-02 09:56:03 浏览:860

pre_wp_is_site_initialized

过滤器::过滤在访问数据库之前是否初始化站点的检查。Filter Hook: Filters the check for whether a site is initialized before the database is accessed.目录锚点:#源码源码(Source)更新版本源码位置使用被使用 wp-includes/ms-site.php:93910...

日期:2020-07-29 10:15:38 浏览:829

WordPress 的SEO 教学:如何在网站中加入关键字(Meta Keywords)与Meta 描述(Meta Description)?

你想在WordPress 中添加关键字和meta 描述吗?关键字和meta 描述使你能够提高网站的SEO。在本文中,我们将向你展示如何在WordPress 中正确添加关键字和meta 描述。为什么要在WordPress 中添加关键字和Meta 描述?关键字和说明让搜寻引擎更了解您的帖子和页面的内容。关键词是人们寻找您发布的内容时,可能会搜索的重要词语或片语。而Meta Description则是对你的页面和文章的简要描述。如果你想要了解更多关于中继标签的资讯,可以参考Google的说明。Meta 关键字和描...

日期:2020-10-03 21:18:25 浏览:1710

谷歌的SEO是什么

SEO (Search Engine Optimization)中文是搜寻引擎最佳化,意思近于「关键字自然排序」、「网站排名优化」。简言之,SEO是以搜索引擎(如Google、Bing)为曝光媒体的行销手法。例如搜寻「wordpress教学」,会看到本站的「WordPress教学:12个课程…」排行Google第一:关键字:wordpress教学、wordpress课程…若搜寻「网站架设」,则会看到另一个网页排名第1:关键字:网站架设、架站…以上两个网页,每月从搜寻引擎导入自然流量,达2万4千:每月「有机搜...

日期:2020-10-30 17:23:57 浏览:1304