Android开发——V1及V2签名原理简析

2023-11-02

Android为了保证系统及应用的安全性,在安装APK的时候需要校验包的完整性,同时,对于覆盖安装的场景还要校验新旧是否匹配,这两者都是通过Android签名机制来进行保证的,本文就简单看下Android的签名与校验原理,分一下几个部分分析下:

  • APK签名是什么
  • APK签名如何保证APK信息完整性
  • 如何为APK签名
  • APK签名怎么校验

Android的APK签名是什么

签名是摘要与非对称密钥加密相相结合的产物,摘要就像内容的一个指纹信息,一旦内容被篡改,摘要就会改变,签名是摘要的加密结果,摘要改变,签名也会失效。Android APK签名也是这个道理,如果APK签名跟内容对应不起来,Android系统就认为APK内容被篡改了,从而拒绝安装,以保证系统的安全性。目前Android有三种签名V1、V2(N)、V3(P),本文只看前两种V1跟V2,对于V3的轮密先不考虑。先看下只有V1签名后APK的样式:

再看下只有V2签名的APK包样式:

可以看到,如果只有V2签名,那么APK包内容几乎是没有改动的,META_INF中不会有新增文件,按Google官方文档:在使用v2签名方案进行签名时,会在APK文件中插入一个APK签名分块,该分块位于zip中央目录部分之前并紧邻该部分。在APK签名分块内,签名和签名者身份信息会存储在APK签名方案v2分块中,保证整个APK文件不可修改,如下图:

而V1签名是通过META-INF中的三个文件保证签名及信息的完整性:

APK签名如何保证APK信息完整性

V1签名是如何保证信息的完整性呢?V1签名主要包含三部分内容,如果狭义上说签名跟公钥的话,仅仅在.rsa文件中,V1签名的三个文件其实是一套机制,不能单单拿一个来说事.

MANIFEST.MF:摘要文件,存储文件名与文件SHA1摘要(Base64格式)键值对,格式如下,其主要作用是保证每个文件的完整性.

如果对APK中的资源文件进行了替换,那么该资源的摘要必定发生改变,如果没有修改MANIFEST.MF中的信息,那么在安装时候V1校验就会失败,无法安装,不过如果篡改文件的同时,也修改其MANIFEST.MF中的摘要值,那么MANIFEST.MF校验就可以绕过。

CERT.SF:二次摘要文件,存储文件名与MANIFEST.MF摘要条目的SHA1摘要(Base64格式)键值对,格式如下

CERT.SF个人觉得有点像冗余,更像对文件完整性的二次保证,同绕过MANIFEST.MF一样,.SF校验也很容易被绕过。

CERT.RSA 证书(公钥)及签名文件,存储keystore的公钥、发行信息、以及对CERT.SF文件摘要的签名信息(利用keystore的私钥进行加密过)

CERT.RSA与CERT.SF是相互对应的,两者名字前缀必须一致,不知道算不算一个无聊的标准。看下CERT.RSA文件内容:

CERT.RSA文件里面存储了证书公钥、过期日期、发行人、加密算法等信息,根据公钥及加密算法,Android系统就能计算出CERT.SF的摘要信息,其严格的格式如下:

从CERT.RSA中,我们能获的证书的指纹信息,在微信分享、第三方SDK申请的时候经常用到,其实就是公钥+开发者信息的一个签名:

除了CERT.RSA文件,其余两个签名文件其实跟keystore没什么关系,主要是文件自身的摘要及二次摘要,用不同的keystore进行签名,生成的MANIFEST.MF与CERT.SF都是一样的,不同的只有CERT.RSA签名文件。也就是说前两者主要保证各个文件的完整性,CERT.RSA从整体上保证APK的来源及完整性,不过META_INF中的文件不在校验范围中,这也是V1的一个缺点。V2签名又是如何保证信息的完整性呢?

V2签名块如何保证APK的完整性

前面说过V1签名中文件的完整性很容易被绕过,可以理解单个文件完整性校验的意义并不是很大,安装的时候反而耗时,不如采用更加简单的便捷的校验方式。V2签名就不针对单个文件校验了,而是针对APK进行校验,将APK分成1M的块,对每个块计算值摘要,之后针对所有摘要进行摘要,再利用摘要进行签名。

 

也就是说,V2摘要签名分两级,第一级是对APK文件的1、3 、4 部分进行摘要,第二级是对第一级的摘要集合进行摘要,然后利用秘钥进行签名。安装的时候,块摘要可以并行处理,这样可以提高校验速度。

 

简单的APK签名流程(签名原理)

APK是先摘要,再签名,先看下摘要的定义:Message Digest:摘要是对消息数据执行一个单向Hash,从而生成一个固定长度的Hash值,这个值就是消息摘要,至于常听到的MD5、SHA1都是摘要算法的一种。理论上说,摘要一定会有碰撞,但只要保证有限长度内碰撞率很低就可以,这样就能利用摘要来保证消息的完整性,只要消息被篡改,摘要一定会发生改变。但是,如果消息跟摘要同时被修改,那就无从得知了。

而数字签名是什么呢(公钥数字签名),利用非对称加密技术,通过私钥对摘要进行加密,产生一个字符串,这个字符串+公钥证书就可以看做消息的数字签名,如RSA就是常用的非对称加密算法。在没有私钥的前提下,非对称加密算法能确保别人无法伪造签名,因此数字签名也是对发送者信息真实性的一个有效证明。不过由于Android的keystore证书是自签名的,没有第三方权威机构认证,用户可以自行生成keystore,Android签名方案无法保证APK不被二次签名。

知道了摘要跟签名的概念后,再来看看Android的签名文件怎么来的?如何影响原来APK包?通过sdk中的apksign来对一个APK进行签名的命令如下:

 ./apksigner sign  --ks   keystore.jks  --ks-key-alias keystore  --ks-pass pass:XXX  --key-pass pass:XXX  --out output.apk input.apk

其主要实现在 android/platform/tools/apksig 文件夹中,主体是ApkSigner.java的sign函数,函数比较长,分几步分析

private void sign(
        DataSource inputApk,
        DataSink outputApkOut,
        DataSource outputApkIn)
                throws IOException, ApkFormatException, NoSuchAlgorithmException,
                        InvalidKeyException, SignatureException {
    // Step 1. Find input APK's main ZIP sections
    ApkUtils.ZipSections inputZipSections;
    <!--根据zip包的结构,找到APK中包内容Object-->
    try {
        inputZipSections = ApkUtils.findZipSections(inputApk);
    ...

先来看这一步,ApkUtils.findZipSections,这个函数主要是解析APK文件,获得ZIP格式的一些简单信息,并返回一个ZipSections,

public static ZipSections findZipSections(DataSource apk)
            throws IOException, ZipFormatException {
        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
        long eocdOffset = eocdAndOffsetInFile.getSecond();
        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
        ...
        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
        long cdEndOffset = cdStartOffset + cdSizeBytes;
        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
        return new ZipSections(
                cdStartOffset,
                cdSizeBytes,
                cdRecordCount,
                eocdOffset,
                eocdBuf);
    }

ZipSections包含了ZIP文件格式的一些信息,比如中央目录信息、中央目录结尾信息等,对比到zip文件格式如下:

获取到 ZipSections之后,就可以进一步解析APK这个ZIP包,继续走后面的签名流程,

 long inputApkSigningBlockOffset = -1;
    DataSource inputApkSigningBlock = null;
    <!--检查V2签名是否存在-->
    try {
        Pair<DataSource, Long> apkSigningBlockAndOffset =
                V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
        inputApkSigningBlock = apkSigningBlockAndOffset.getFirst();
        inputApkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
    } catch (V2SchemeVerifier.SignatureNotFoundException e) {
    <!--V2签名不存在也没什么问题,非必须-->
}
 <!--获取V2签名以外的信息区域-->
 DataSource inputApkLfhSection =
            inputApk.slice(
                    0,
                    (inputApkSigningBlockOffset != -1)
                            ? inputApkSigningBlockOffset
                            : inputZipSections.getZipCentralDirectoryOffset());

可以看到先进行了一个V2签名的检验,这里是用来签名,为什么先检验了一次?第一次签名的时候会直接走这个异常逻辑分支,重复签名的时候才能获到取之前的V2签名,怀疑这里获取V2签名的目的应该是为了排除V2签名,并获取V2签名以外的数据块,因为签名本身不能被算入到签名中,之后会解析中央目录区,构建一个DefaultApkSignerEngine用于签名

      <!--解析中央目录区,目的是为了解析AndroidManifest-->
    // Step 2. Parse the input APK's ZIP Central Directory
    ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
    List<CentralDirectoryRecord> inputCdRecords =
            parseZipCentralDirectory(inputCd, inputZipSections);

    // Step 3. Obtain a signer engine instance
    ApkSignerEngine signerEngine;
    if (mSignerEngine != null) {
        signerEngine = mSignerEngine;
    } else {
        // Construct a signer engine from the provided parameters
        ...
        List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
                new ArrayList<>(mSignerConfigs.size());
        <!--一般就一个-->
        for (SignerConfig signerConfig : mSignerConfigs) {
            engineSignerConfigs.add(
                    new DefaultApkSignerEngine.SignerConfig.Builder(
                            signerConfig.getName(),
                            signerConfig.getPrivateKey(),
                            signerConfig.getCertificates())
                            .build());
        }
        <!--默认V1 V2都启用-->
        DefaultApkSignerEngine.Builder signerEngineBuilder =
                new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
                        .setV1SigningEnabled(mV1SigningEnabled)
                        .setV2SigningEnabled(mV2SigningEnabled)
                        .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved);
        if (mCreatedBy != null) {
            signerEngineBuilder.setCreatedBy(mCreatedBy);
        }
        signerEngine = signerEngineBuilder.build();
    }

先解析中央目录区,获取AndroidManifest文件,获取minSdkVersion(影响签名算法),并构建DefaultApkSignerEngine,默认情况下V1 V2签名都是打开的。

    // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any)
    <!--忽略这一步-->
    if (inputApkSigningBlock != null) {
        signerEngine.inputApkSigningBlock(inputApkSigningBlock);
    }

    // Step 5. Iterate over input APK's entries and output the Local File Header + data of those
    // entries which need to be output. Entries are iterated in the order in which their Local
    // File Header records are stored in the file. This is to achieve better data locality in
    // case Central Directory entries are in the wrong order.
    List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
            new ArrayList<>(inputCdRecords);
    Collections.sort(
            inputCdRecordsSortedByLfhOffset,
            CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
    int lastModifiedDateForNewEntries = -1;
    int lastModifiedTimeForNewEntries = -1;
    long inputOffset = 0;
    long outputOffset = 0;
    Map<String, CentralDirectoryRecord> outputCdRecordsByName =
            new HashMap<>(inputCdRecords.size());
    ...

    // Step 6. Sort output APK's Central Directory records in the order in which they should
    // appear in the output
    List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
    for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
        String entryName = inputCdRecord.getName();
        CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
        if (outputCdRecord != null) {
            outputCdRecords.add(outputCdRecord);
        }
    }

第五步与第六步的主要工作是:apk的预处理,包括目录的一些排序之类的工作,应该是为了更高效处理签名,预处理结束后,就开始签名流程,首先做的是V1签名(默认存在,除非主动关闭):

    // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
    // Header + data entries and add to the list of output Central Directory records.
    ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
            signerEngine.outputJarEntries();
    if (outputJarSignatureRequest != null) {
        if (lastModifiedDateForNewEntries == -1) {
            lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
            lastModifiedTimeForNewEntries = 0;
        }
        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
                outputJarSignatureRequest.getAdditionalJarEntries()) {
            String entryName = entry.getName();
            byte[] uncompressedData = entry.getData();
            ZipUtils.DeflateResult deflateResult =
                    ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
            byte[] compressedData = deflateResult.output;
            long uncompressedDataCrc32 = deflateResult.inputCrc32;

            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                    signerEngine.outputJarEntry(entryName);
            if (inspectEntryRequest != null) {
                inspectEntryRequest.getDataSink().consume(
                        uncompressedData, 0, uncompressedData.length);
                inspectEntryRequest.done();
            }

            long localFileHeaderOffset = outputOffset;
            outputOffset +=
                    LocalFileRecord.outputRecordWithDeflateCompressedData(
                            entryName,
                            lastModifiedTimeForNewEntries,
                            lastModifiedDateForNewEntries,
                            compressedData,
                            uncompressedDataCrc32,
                            uncompressedData.length,
                            outputApkOut);


            outputCdRecords.add(
                    CentralDirectoryRecord.createWithDeflateCompressedData(
                            entryName,
                            lastModifiedTimeForNewEntries,
                            lastModifiedDateForNewEntries,
                            uncompressedDataCrc32,
                            compressedData.length,
                            uncompressedData.length,
                            localFileHeaderOffset));
        }
        outputJarSignatureRequest.done();
    }

    // Step 8. Construct output ZIP Central Directory in an in-memory buffer
    long outputCentralDirSizeBytes = 0;
    for (CentralDirectoryRecord record : outputCdRecords) {
        outputCentralDirSizeBytes += record.getSize();
    }
    if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
        throw new IOException(
                "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
                        + " bytes");
    }
    ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
    for (CentralDirectoryRecord record : outputCdRecords) {
        record.copyTo(outputCentralDir);
    }
    outputCentralDir.flip();
    DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
    long outputCentralDirStartOffset = outputOffset;
    int outputCentralDirRecordCount = outputCdRecords.size();

    // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
    ByteBuffer outputEocd =
            EocdRecord.createWithModifiedCentralDirectoryInfo(
                    inputZipSections.getZipEndOfCentralDirectory(),
                    outputCentralDirRecordCount,
                    outputCentralDirDataSource.size(),
                    outputCentralDirStartOffset);

步骤7、8、9都可以看做是V1签名的处理逻辑,主要在V1SchemeSigner中处理,其中包括创建META-INFO文件夹下的一些签名文件,更新中央目录、更新中央目录结尾等,流程不复杂,不在赘述,简单流程就是:

这里特殊提一下重复签名的问题:对一个已经V1签名的APK再次V1签名不会有任何问题,原理就是:再次签名的时候,会排除之前的签名文件。

  public static boolean isJarEntryDigestNeededInManifest(String entryName) {
        // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File

        // Entries which represent directories sould not be listed in the manifest.
        if (entryName.endsWith("/")) {
            return false;
        }

        // Entries outside of META-INF must be listed in the manifest.
        if (!entryName.startsWith("META-INF/")) {
            return true;
        }
        // Entries in subdirectories of META-INF must be listed in the manifest.
        if (entryName.indexOf('/', "META-INF/".length()) != -1) {
            return true;
        }

        // Ignored file names (case-insensitive) in META-INF directory:
        //   MANIFEST.MF
        //   *.SF
        //   *.RSA
        //   *.DSA
        //   *.EC
        //   SIG-*
        String fileNameLowerCase =
                entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
        if (("manifest.mf".equals(fileNameLowerCase))
                || (fileNameLowerCase.endsWith(".sf"))
                || (fileNameLowerCase.endsWith(".rsa"))
                || (fileNameLowerCase.endsWith(".dsa"))
                || (fileNameLowerCase.endsWith(".ec"))
                || (fileNameLowerCase.startsWith("sig-"))) {
            return false;
        }
        return true;
    }

可以看到目录、META-INF文件夹下的文件、sf、rsa等结尾的文件都不会被V1签名进行处理,所以这里不用担心多次签名的问题。接下来就是处理V2签名。

    // Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
    // insert an APK Signing Block just before the output's ZIP Central Directory
    ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
            signerEngine.outputZipSections(
                    outputApkIn,
                    outputCentralDirDataSource,
                    DataSources.asDataSource(outputEocd));
    if (outputApkSigingBlockRequest != null) {
        byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
        outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
        ZipUtils.setZipEocdCentralDirectoryOffset(
                outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
        outputApkSigingBlockRequest.done();
    }

    // Step 11. Output ZIP Central Directory and ZIP End of Central Directory
    outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
    outputApkOut.consume(outputEocd);
    signerEngine.outputDone();
}

V2SchemeSigner处理V2签名,逻辑比较清晰,直接对V1签名过的APK进行分块摘要,再集合签名,V2签名不会改变之前V1签名后的任何信息,签名后,在中央目录前添加V2签名块,并更新中央目录结尾信息,因为V2签名后,中央目录的偏移会再次改变:

APK签名怎么校验

签名校验的过程可以看做签名的逆向,只不过覆盖安装可能还要校验公钥及证书信息一致,否则覆盖安装会失败。签名校验的入口在PackageManagerService的install里,安装官方文档,7.0以上的手机优先检测V2签名,如果V2签名不存在,再校验V1签名,对于7.0以下的手机,不存在V2签名校验机制,只会校验V1,所以,如果你的App的miniSdkVersion<24(N),那么你的签名方式必须内含V1签名:

校验流程就是签名的逆向,了解签名流程即可,本文不求甚解,有兴趣自己去分析,只是额外提下覆盖安装,覆盖安装除了检验APK自己的完整性以外,还要校验证书是否一致只有证书一致(同一个keystore签名),才有可能覆盖升级。覆盖安装同全新安装相比较多了几个校验

  • 包名一致
  • 证书一致
  • versioncode不能降低

这里只关心证书部分:

    // Verify: if target already has an installer package, it must
    // be signed with the same cert as the caller.
    if (targetPackageSetting.installerPackageName != null) {
        PackageSetting setting = mSettings.mPackages.get(
                targetPackageSetting.installerPackageName);
        // If the currently set package isn't valid, then it's always
        // okay to change it.
        if (setting != null) {
            if (compareSignatures(callerSignature,
                    setting.signatures.mSignatures)
                    != PackageManager.SIGNATURE_MATCH) {
                throw new SecurityException(
                        "Caller does not have same cert as old installer package "
                        + targetPackageSetting.installerPackageName);
            }
        }
    }

V1、V2签名下美团多渠道打包的切入点

  • V1签名:META_INFO文件夹下增加文件不会对校验有任何影响,则是美团V1多渠道打包方案的切入点
  • V2签名:V2签名块中可以添加一些附属信息,不会对签名又任何影响,这是V2多渠道打包的切入点。

总结

  • V1签名靠META_INFO文件夹下的签名文件
  • V2签名依靠中央目录前的V2签名快,ZIP的目录结构不会改变,当然结尾偏移要改。
  • V1 V2签名可以同时存在(miniSdkVersion 7.0以下如果没有V1签名是不可以的)
  • 多去到打包的切入点原则:附加信息不影响签名验证

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android开发——V1及V2签名原理简析 的相关文章

  • 简单Java前后端分离项目部署

    返回导航页 返回导航页 简单部署 直接运行jar包 1 需要配置安全组 2 服务器需要防火墙开启端口 涉及到的相关命令解释 firewall cmd zone public add port 8080 tcp permanent 开放808
  • Games101第三次作业(渲染管线分析、着色模型分析、双线性插值)

    文章目录 前言 一 作业要求 二 渲染管线分析 1 main函数 1 1main cpp的功能 2 draw函数 2 1draw函数的作用 2 2viewspace下的顶点的法向量 3 rasterize triangle函数 3 1ras
  • VirtualBox上安装Linux的基本配置

    这里主要记录下网络方面的配置 一般情况下都是如上的配置 只有界面名称那里 根据实际情况而定 如果你是有线网 那就要变一下 当系统安装好了 输入可以查询ip地址的命令 一般新安装的linux都不支持ifconfig命令查看 所以直接用 ip
  • java-快速查找文件,文件内容工具

    背景 工作日积月累 PC电脑文件越来越多 有时想找一个文件都不知道在哪 很烦恼 因此写了一个工具可以帮快速查找一些文件 减少查找文件的时间 应用场景 查找关键字文件名 文件目录 查找关键字文件内容 查找关键字特殊文档内容 xml doc p
  • web移动端-2-流动布局-案例学习

    总结 参考 黑马 移动web day3 总结京东M站 1 适配问题 移动端设备尺寸不一致 2 流式布局 随着浏览器尺寸的改变做自适应 掌握 3 当放到移动端预览 通过谷歌浏览器的模拟器 真机调试 服务器和手机设备在同一个局域网内容即可 4
  • 【Google】免翻!Google 最强插件下载中心!

    文章目录 一 极简插件 二 GugeAPPs 三 Extfans 四 插件安装方法 今天我送大家3款超强 Google 浏览器下载插件网站 可以完美解决 Google 浏览器应用中心在国内无法登陆的尴尬境遇 整理不易 点赞多多支持 一 极简
  • DataAnalysis-Maggie-Lecture1

    Lec1 5 31 目标 学习多种数据分析算法和技巧 数据分析应用 期中 project 期末 术语区别 数据科学是领域 统计分析是数学子学科 ML是让电脑自己通过数据和经验改进算法 AI是一种模仿人类的ML 数据挖掘 是处理数据然后发现模
  • Source Insight 4.0安装后首次打开报错

    老早就听说过代码阅读神器Source Insight一直没使用过 今天终于要用到了 兴冲冲的去官网下载安装了Source Insight4 0 安装一切顺利 好感动 Linux上经常各种缺依赖 我都怕了 可能是我比较菜 双击打开 报错 不对
  • Linux ps命令常见实战用法

    文章目录 一 基本介绍 1 1 基本介绍 1 2 常用参数 1 3 字段含义 二 常见用法 2 1 查看所有进程 2 2 查看特定进程信息 参考资料 Linux中的ps命令是Process Status的缩写 当程序运行在系统上时 我们称之
  • 毕业设计-基于机器视觉人脸识别技术课堂签到系统的设计与实现-OpenCV

    目录 前言 课题背景和意义 实现技术思路 一 软件设计 二 系统实现 实现效果图样例 最后 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备 一边要为毕业设计耗费大量精力 近几年各个学校要求的毕设项目越
  • 【机器学习】机器学习算法的随机数据生成

    文章目录 一 前言 二 numpy随机数据生成API 2 1 rand d 0 d 0 d0
  • BeanUtils数据封装与表单JavaBean

    一 BeanUtils工具的解释 1 Apache的Commons组件中 提供了一个实用的工具类BeanUtils 利用它可以方便的将表单数据值填充值Bean中 2 javax servlet ServletRequest getParam

随机推荐

  • Java垃圾回收机制

    众所周知 Java是一个编译一次就可在不同系统上运行的语言 这主要得益于JVM虚拟机 JVM担任着Java内存自动管理的重任 JVM 的 动内存管理主要是进 对象内存的分配与回收 最核 的功能是 堆 内存中对象的分配 与回收 堆 是垃圾收集
  • Solr应用之电商商品搜索备忘

    把以前做电商商品搜索的经验归档一下 电商的搜索功能大体上比较相同 从京东 苏宁 易讯等大型电商都可以观察出来 电商搜索功能大致分为几块 1 商品搜索 列表的展示 带排序功能 可能有些产品会要求一个商品不同规格也聚合成一个展示 2 类别导航区
  • Android Studio SVN 使用

    如何安装配置SVN 请直接参考 SVN在Android Studio中的配置 http www cnblogs com songmeng p 4389446 html 如何使用SVN 本文主要参考了 AndroidStudio配置SVN以及
  • CDN加速下如何识别和限制访问来源

    前言 大多数提供CDN服务的云厂商 基本上都为客户提供了黑白名单 限制单个ip或网段 地域限制 限制某些地区的访问 鉴权 使只有符合规则的用户才可访问 或异常流量的监控和限制等功能 实现了基本的加速网站访问和安全的功能 但是有时候可能会因为
  • 立创EDA专业版修改图纸尺寸

    emmm 修改图纸尺寸 对于大多是软件还是很简单的 也没有什么奇怪的小虫 bug 但 接下来我说下遇到的一些情况和解决方法 遇到的情况 在我原先画的原理图中 我只需要在属性一栏修改下图纸尺寸即可 但最近的一次就不行了 情况变成了这样 emm
  • go 进阶 协程相关: 五. 协程底层从Main方法到调度器

    目录 三 通过runtime mstart SB 启动调度循环了解调度底层 再简单复习一下m p g的关系与调度过程简介 mstart1 初始化设置 schedule 调度函数 1 globrunqget 通过全局队列获取可运行G 通过全局
  • 在线生日快乐网站,可以在线访问,通过网址!

    在线生日快乐网站 可以在线访问 通过网址 详情可私信 详情可私信 点击查看详情
  • js逆向播放量增加,增加视频热度,uuid,sid,buvid3,aid,b_lsid, b_nut 还原实现过程

    本次记录尝试逆向某比里比里视频平台播放量 主要思路 不断debug 观察代码设计还原实现 1 播放量增加必定是点击播放后的记录 2 清空监视器 点击播放开始debug 截取到的就代表包含了增加播放量的包 3 验证截取到的包 测试看哪一个是包
  • C++学习(四九四)cmake从list从删除文件

    生成文件列表 file GLOB RECURSE lib srcs c 输出文件列表 注意lib srcs的写法 需要 MESSAGE STATUS lib srcs 从文件列表中删除文件 注意lib srcs的写法 不需要 注意文件路径
  • 【pyq文案】合理但有病の自拍文案

    1 丑一眼 2 强子 妈发自拍了 3 真是方向失了南北 美的有点东西 4 妈的看自己就烦 800块出了 完美无瑕 5 拍了拍自己 6 这张脸 全是这双手给的 7 糟糕 没有酷起来 8 制造美女我比女娲还牛 9 注意看 无论从正面还是侧面 这
  • 如何在vscode中显示markdow大纲

    你可以在 Visual Studio Code 中使用 Markdown All in One 扩展来显示 Markdown 大纲 首先 你需要在 Visual Studio Code 中安装 Markdown All in One 扩展
  • 现代操作系统 第七章

    虚拟化和云 虚拟化的主要思想是虚拟化监控程序 virtual Machine Monitor VMM 在同一物理硬件上创建出有多台虚拟机器的假象 VMM又称作虚拟机管理程序 hypervisor 这种方法的好处是一台虚拟机的故障不会影响其他
  • 【IDEA使用教程】利用教育邮箱免费激活Jetbrains系列产品

    如果是学生并且你们学校给你们注册了edu后缀的教育邮箱 那么恭喜你 可以免费激活并使用idea等软件了 1 进入网站JetBrains 学习产品https www jetbrains com shop eform students 2 填写
  • Java IO技术

    Java IO技术 java io包为我们提供了IO相关的API 实现了对所有外部系统的输入输出操作 数据源 数据源data source 提供数据的原始媒介 常见的数据源有 数据库 文件 其他程序 内存 网络连接 IO设备 数据源分为 源
  • MIB基本概念

    MIB的概念 MIB的定义 MIB中的OID OID的表示方式 SMI 对象数据类型 MIB 2中的文本规定 MIB和SMI关系 MIB编写示例 more 一 MIB的概念 MIB全称Management Information Base
  • 【详解如何一步步实现三子棋】

    相信大家都玩过五子棋 三子棋也是一样的道理 行列三子 对角线三子获得胜利 想要实现三子棋小游戏需要哪几步 1 三子棋首先我们要创建棋盘 创建一个二维数组三行三列 然后将棋盘初始化为全空格 2 如何将棋盘转换为网格状棋盘 如下图 3 玩家下棋
  • e-charts 图例过多问题

    饼图的图例 如果过多 需要增加 分页按钮 注意 如果测试用例数量不够 则分页按钮不会出现 会默认将画面填满后 分页按钮才会出现 我之前只用了两三个 总是不出现 气死了 legend top 15 type scroll orient ver
  • nginx代理获取ip为127.0.0.1解决方法

    原因 我们访问互联网上的服务时 大多数时 客户端并不是直接访问到服务端的 而是客户端首先请求到反向代理 反向代理再转发到服务端实现服务访问 通过反向代理实现路由 负载均衡等策略 这样在服务端拿到的客户端IP将是反向代理IP 而不是真实客户端
  • LeetCode:用栈实现队列(纯C语言)可CV

    题目链接 232 用栈实现队列 力扣 Leetcode 还是老套路二话不说 先上代码 typedef char STDataType typedef struct Stack STDataType a int top int capacit
  • Android开发——V1及V2签名原理简析

    Android为了保证系统及应用的安全性 在安装APK的时候需要校验包的完整性 同时 对于覆盖安装的场景还要校验新旧是否匹配 这两者都是通过Android签名机制来进行保证的 本文就简单看下Android的签名与校验原理 分一下几个部分分析