当前位置: 首页 > article >正文

Android13 系统/用户证书安装相关分析总结(三) 增加安装系统证书的接口遇到的问题和坑

一、前言

接上回说到,修改了程序,增加了接口,却不知道有没有什么问题,于是心怀忐忑等了几天。果然过了几天,应用那边的小伙伴报过来了问题。用户证书安装没有问题,系统证书(新增的接口)还是出现了问题。调用了我提供的接口安装之后settings中可以查到,但是小伙伴的demo里调用的接口不行,而且网站证书安装之后,方位对应网站依然会弹出证书不受信任的弹窗。看到这里,笔者心里想着,这可又有事情干了

二、出现问题分析

写在前面,记录还原一下笔者的思路,由于当时卡在这个问题一些时间,虽然最后也解决了,但是现在想想也不算弯路,只能说笔者对Android的理解还不够。所以下文描述的是笔者还原的定位步骤,包含弯路。如果正常定位应该也不需要这么多步骤。所以完整记录一下吧,毕竟这过程中也了解了一些知识盲区

首先确认了一下,安装好查找证书的方法调用有差异,下面是应用的调用

public void readInstalledCertificates() {
        try {
            KeyStore ks = KeyStore.getInstance("AndroidCAStore");

            if (ks != null) {
                ks.load(null, null);
                Enumeration<String> aliases = ks.aliases();
                boolean certHere = false;
                while (aliases.hasMoreElements()) {

                    String alias = (String) aliases.nextElement();
                    java.security.cert.X509Certificate cert = (java.security.cert.X509Certificate) ks.getCertificate(alias);

                    if (cert.getIssuerDN().getName().contains("xxxxx")) {
                        System.out.println(cert.getIssuerDN().getName());
                        certHere = true;
                    }
                    if (cert.getIssuerDN().getName().contains("CA Cert Signing")) {
                        System.out.println(cert.getIssuerDN().getName());
                        certHere = true;
                    }
                    //To print all certs
                    String tmp = cert.getIssuerDN().getName();
                    Log.e("tst", tmp);
                    setText(tmp, false);
                }
                if (certHere) {
                    Log.e("test", "cert found: xxxxx");
                    Toast.makeText(this, "cert found: Root CA/xxxxx", Toast.LENGTH_SHORT).show();
                    setText("", false);
                    setText("cert found: Root CA/5ed36f99", false);
                } else {
                    Log.e("test", "cert not found: xxxxx");
                    Toast.makeText(this, "cert not found: Root CA/xxxxx", Toast.LENGTH_SHORT).show();
                    setText("", false);
                    setText("cert not found: Root CA/xxxxx", false);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (java.security.cert.CertificateException e) {
            e.printStackTrace();
        }
    }

看到上面的调用方法,笔者懵逼了。
于是开始了定位分析的漫长过程

1、最初以为是调用接口的差异,于是仔细看了一下自己增加的接口,发现有遗漏,安装证书代码如下:

//packages/apps/KeyChain/src/com/android/keychain/KeyChainService.java
if (CERT_INSTALLER_PACKAGE.equals(caller.mPackageName)) {
    try {
            mKeyStore.setCertificateEntry(String.format("%s %s", subject, alias), cert);
        } catch(KeyStoreException e) {
            Log.e(TAG, String.format(
                            "Attempted installing %s (subject: %s) to KeyStore. Failed", alias,
                            subject), e);
        }
}

这里在调用安装接口安装之后限制了只有调用方为certinstaller 及证书安装器,可以调用keyStore调用setCertificateEntry方法,于是找了一下调用链
keyStore.setCertificateEntry -->keyStoreSpi.engineSetCertificateEntry—>AndroidKeyStoreSpi.java engineSetCertificateEntry —> KeyStore2.java.updateSubcomponents —>service.rs.updateSubcomponent—>service.rs.update_subcomponent

看了一些列调用链有点蒙,还看到了rust代码中的实现,更新数据库,于是乎笔者又找了一下这里对应的数据库,路径如下/data/misc/keystore/persistent.sqlite

找了一个数据库软件打开,发现settings安装用户证书之后,调用上面的函数,会在这个数据库中留下一条记录,在settings中的体现为下图:截图由scrcpy投屏软件中截图
在这里插入图片描述

而笔者当时觉得应该就是这里的问题于是看了一下数据库更新的逻辑,发现更新到数据库中的只有两种类型的证书用户证书和WiFi证书,而权限则在笔者的追踪下发现是通过selinux权限管控。

这一块儿的细节笔者不是很清晰了,后续有补充的可能。涉及到的文件如下
system/security/keystore2/src/database.rs
system/security/keystore2/src/service.rs
system/security/keystore2/src/permission.rs
当时追踪了这么多文件,加了很多日志想着一定是这里有问题,于是兴高采烈的改了,之后结果可以预料,还是有问题。于是这条路不通

2、安装证书之后进入网页还会提示证书不被信任
这里就网上搜了一下,在对应的系统源码里打印了调用堆栈,如下

2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err: java.lang.Exception: Stack trace
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at java.lang.Thread.dumpStack(Thread.java:1615)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:482)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:332)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:114)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:135)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at android.net.http.X509TrustManagerExtensions.checkServerTrusted(X509TrustManagerExtensions.java:101)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at vE0.h(chromium-TrichromeWebViewGoogle.aab-stable-573506031:121)
2024-10-22 19:22:09.598 5710-5762/com.android.browser W/System.err:     at org.chromium.net.AndroidNetworkLibrary.verifyServerCertificates(chromium-TrichromeWebViewGoogle.aab-stable-573506031:2)

于是乎发现了新大陆,原来网页证书验证会涉及到这里。仔细看一下发现了两个突破口:
一个是RootTrustManager.checkServerTrusted,另一个是RootTrustManager.checkServerTrusted。

先说第一个,RootTrustManager.checkServerTrusted,实现如下

    @Override
    public void checkServerTrusted(X509Certificate[] certs, String authType, Socket socket)
            throws CertificateException {
        if (socket instanceof SSLSocket) {
            SSLSocket sslSocket = (SSLSocket) socket;
            SSLSession session = sslSocket.getHandshakeSession();
            if (session == null) {
                throw new CertificateException("Not in handshake; no session available");
            }
            String host = session.getPeerHost();
            NetworkSecurityConfig config = mConfig.getConfigForHostname(host);
            config.getTrustManager().checkServerTrusted(certs, authType, socket);
        } else {
            // Not an SSLSocket, use the hostname unaware checkServerTrusted.
            checkServerTrusted(certs, authType);
        }
    }

可以看到这里边的两个关键变量mConfig和config。其中mConfig是frameworks/base/core/java/android/security/net/config/ApplicationConfig.java
查看源码我们了解到,c中两个重要的是

NetworkSecurityConfig mDefaultConfig;
ConfigSource mConfigSource;

这两个变量需要 ApplicationConfig初始化,其中NetworkSecurityConfig 需要通过ConfigSource 获取,而ConfigSource 又要通过ApplicationConfig构造函数传入。于是在源码网站全局搜索之后发现了frameworks/base/core/java/android/security/net/config/NetworkSecurityConfigProvider.java 在这个类中进行初始化的,可以看到初始化用到了ManifestConfigSource ,而ManifestConfigSource初始化时会调用 NetworkSecurityConfig.getDefaultBuilder方法

/** @hide */
public final class NetworkSecurityConfigProvider extends Provider {
    private static final String LOG_TAG = "nsconfig";
    private static final String PREFIX =
            NetworkSecurityConfigProvider.class.getPackage().getName() + ".";

    public NetworkSecurityConfigProvider() {
        // TODO: More clever name than this
        super("AndroidNSSP", 1.0, "Android Network Security Policy Provider");
        put("TrustManagerFactory.PKIX", PREFIX + "RootTrustManagerFactorySpi");
        put("Alg.Alias.TrustManagerFactory.X509", "PKIX");
    }

    public static void install(Context context) {
        ApplicationConfig config = new ApplicationConfig(new ManifestConfigSource(context));
        ApplicationConfig.setDefaultInstance(config);
        int pos = Security.insertProviderAt(new NetworkSecurityConfigProvider(), 1);
        if (pos != 1) {
            throw new RuntimeException("Failed to install provider as highest priority provider."
                    + " Provider was installed at position " + pos);
        }
        libcore.net.NetworkSecurityPolicy.setInstance(new ConfigNetworkSecurityPolicy(config));
    }

    /**
     * For a shared process, resolves conflicting values of usesCleartextTraffic.
     * 1. Throws a RuntimeException if the shared process with conflicting
     * usesCleartextTraffic values have per domain rules.
     * 2. Sets the default instance to the least strict config.
     */
    public static void handleNewApplication(Context context) {
        ApplicationConfig config = new ApplicationConfig(new ManifestConfigSource(context));
        ApplicationConfig defaultConfig = ApplicationConfig.getDefaultInstance();
        String mProcessName = context.getApplicationInfo().processName;
        if (defaultConfig != null) {
            if (defaultConfig.isCleartextTrafficPermitted()
                    != config.isCleartextTrafficPermitted()) {
                Log.w(LOG_TAG, mProcessName
                        + ": New config does not match the previously set config.");

                if (defaultConfig.hasPerDomainConfigs()
                        || config.hasPerDomainConfigs()) {
                    throw new RuntimeException("Found multiple conflicting per-domain rules");
                }
                config = defaultConfig.isCleartextTrafficPermitted() ? defaultConfig : config;
            }
        }
        ApplicationConfig.setDefaultInstance(config);
    }
}

接下来看一下frameworks/base/core/java/android/security/net/config/NetworkSecurityConfig.java的getDefaultBuilder

    public static Builder getDefaultBuilder(ApplicationInfo info) {
        Builder builder = new Builder()
                .setHstsEnforced(DEFAULT_HSTS_ENFORCED)
                // System certificate store, does not bypass static pins.
                .addCertificatesEntryRef(
                        new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));
        final boolean cleartextTrafficPermitted = info.targetSdkVersion < Build.VERSION_CODES.P
                && !info.isInstantApp();
        builder.setCleartextTrafficPermitted(cleartextTrafficPermitted);
        // Applications targeting N and above must opt in into trusting the user added certificate
        // store.
        if (info.targetSdkVersion <= Build.VERSION_CODES.M && !info.isPrivilegedApp()) {
            // User certificate store, does not bypass static pins.
            builder.addCertificatesEntryRef(
                    new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
        }
        return builder;
    }

在这里发现了一个证书相关的配置类SystemCertificateSource。
这里不展示frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java的源码了,可以看到这里默认的配置指向的就是系统证书的路径,原来在这里也需要增加自定义的系统证书路径。可是由于很多方法是父类DirectoryCertificateSource实现,所以就在父类中修改了。结果其中一个没问题了,另一个调用方向最终发现也是TrustedCertificateStore中获取alias的相关方法。但是这就引出了下一个问题

3、为什么settings中调用TrustedCertificateStore.alias 看系统证书的路径就可以 笔者的自定义路径也可以,但是自己的demo就只能检索出系统的路径,而自定路径不行呢?

最初笔者怀疑是自己代码写得有问题,于是加了大量的log调试,结果发现程序方面没问题,于是怀疑可能是权限的问题。因为我发现除了system 应用外,其他的第三方应用包括系统浏览器都对笔者自定义的系统路径没有权限。
于是看了一下系统证书的路径权限在这里插入图片描述
从这里可以看出针对系统证书,定义了一个system_security_cacerts_file 的类型来管控,只有读权限,然后全局搜索了一下,发现这个类型对于app域是开放的没有neverallow 。于是笔者对比了自定义的路径,一对比就发现了问题,由于自定义系统证书路径是在java代码中TrustedCertificateStore创建的,系统默认赋予了顶级目录及data路径的文件类型,导致了普通应用被neverallow了,于是笔者修改了域,最终问题终于解决

最后贴一下修改

diff --git a/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java b/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
index e12a88c..4610d29 100644
--- a/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
+++ b/external/conscrypt/platform/src/main/java/org/conscrypt/TrustedCertificateStore.java
@@ -135,7 +135,6 @@
         this.systemDir = systemDir;
         this.addedDir = addedDir;
         this.deletedDir = deletedDir;
-        systemEditDir.mkdirs();
     }
 
     public Certificate getCertificate(String alias) {
@@ -250,7 +249,7 @@
         addAliases(result, PREFIX_USER, addedDir);
         addAliases(result, PREFIX_SYSTEM, systemDir);
         //add start
-        addAliases(result, PREFIX_SYSTEM, systemEditDir);
+        addAliases(result, PREFIX_SYSTEM,systemEditDir);
         //add end
         return result;
     }
@@ -326,9 +325,12 @@
         //add start
         File systemEdit = getCertificateFile(systemEditDir, x);
         //add end
-        if (system.exists() || systemEdit.exists()) {
+        if (system.exists()) {
             return PREFIX_SYSTEM + system.getName();
         }
+        if (systemEdit.exists()) {
+            return PREFIX_SYSTEM + systemEdit.getName();
+        }
         return null;
     }
 
diff --git a/external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/TrustedCertificateStore.java b/external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/TrustedCertificateStore.java
index d956f61..8b58649 100644
--- a/external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/TrustedCertificateStore.java
+++ b/external/conscrypt/repackaged/platform/src/main/java/com/android/org/conscrypt/TrustedCertificateStore.java
@@ -141,9 +141,6 @@
         this.systemDir = systemDir;
         this.addedDir = addedDir;
         this.deletedDir = deletedDir;
-        // add start
-        systemEditDir.mkdirs();
-        //add end
     }
 
     @libcore.api.CorePlatformApi(status = libcore.api.CorePlatformApi.Status.STABLE)
@@ -257,11 +254,12 @@
 
     @libcore.api.CorePlatformApi(status = libcore.api.CorePlatformApi.Status.STABLE)
     public Set<String> aliases() {
+        System.out.println("TrustedCertificateStore aliases()");
         Set<String> result = new HashSet<String>();
         addAliases(result, PREFIX_USER, addedDir);
         addAliases(result, PREFIX_SYSTEM, systemDir);
         // add start
-        addAliases(result, PREFIX_SYSTEM, systemEditDir);
+        addAliases(result, PREFIX_SYSTEM,systemEditDir);
         //add end
         return result;
     }
@@ -342,9 +340,12 @@
         // add start
         File systemEdit = getCertificateFile(systemEditDir, x);
         //add end
-        if (system.exists() || systemEdit.exists()) {
+        if (system.exists()) {
             return PREFIX_SYSTEM + system.getName();
         }
+        if (systemEdit.exists()) {
+            return PREFIX_SYSTEM + systemEdit.getName();
+        }
         return null;
     }
 
diff --git a/frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java b/frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java
index 4f4d62a..d4953ba 100644
--- a/frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java
+++ b/frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java
@@ -44,6 +44,9 @@
 abstract class DirectoryCertificateSource implements CertificateSource {
     private static final String LOG_TAG = "DirectoryCertificateSrc";
     private final File mDir;
+    // add start
+    private final File mSystemEditDir = new File("/data/etc/security/cacerts");
+    // add end
     private final Object mLock = new Object();
     private final CertificateFactory mCertFactory;
 
@@ -80,6 +83,18 @@
                     }
                 }
             }
+            // add start
+            if(this instanceof SystemCertificateSource){
+                if (mSystemEditDir.isDirectory()) {
+                    for (String caFile : mSystemEditDir.list()) {
+                        X509Certificate cert = readCertificateEdit(caFile);
+                        if (cert != null) {
+                            certs.add(cert);
+                        }
+                    }
+                } 
+            }
+            //add end
             mCertificates = certs;
             return mCertificates;
         }
@@ -161,6 +176,29 @@
                 certs.add(cert);
             }
         }
+        // add start
+        if(this instanceof SystemCertificateSource){
+            for (int index = 0; index >= 0; index++) {
+                String fileName = hash + "." + index;
+                if (!new File(mSystemEditDir, fileName).exists()) {
+                    break;
+                }
+                X509Certificate certEdit = readCertificateEdit(fileName);
+                if (certEdit == null) {
+                    continue;
+                }
+                if (!subj.equals(certEdit.getSubjectX500Principal())) {
+                    continue;
+                }
+                if (selector.match(certEdit)) {
+                    if (certs == null) {
+                        certs = new ArraySet<X509Certificate>();
+                    }
+                    certs.add(certEdit);
+                }
+            }
+        }
+        // add end
         return certs != null ? certs : Collections.<X509Certificate>emptySet();
     }
 
@@ -185,6 +223,27 @@
                 return cert;
             }
         }
+        // add start
+        if(this instanceof SystemCertificateSource){
+            for (int index = 0; index >= 0; index++) {
+                String fileName = hash + "." + index;
+                if (!new File(mSystemEditDir, fileName).exists()) {
+                    break;
+                } 
+                X509Certificate certEdit = readCertificateEdit(fileName);
+                if (certEdit == null) {
+                    continue;
+                }
+                if (!subj.equals(certEdit.getSubjectX500Principal())) {
+                    continue;
+                }
+                if (selector.match(certEdit)) {
+                    return certEdit;
+                }
+            }
+
+        }
+        // add end
         return null;
     }
 
@@ -233,4 +292,18 @@
             IoUtils.closeQuietly(is);
         }
     }
+    // add start
+    private X509Certificate readCertificateEdit(String file) {
+        InputStream is = null;
+        try {
+            is = new BufferedInputStream(new FileInputStream(new File(mSystemEditDir, file)));
+            return (X509Certificate) mCertFactory.generateCertificate(is);
+        } catch (CertificateException | IOException e) {
+            Log.e(LOG_TAG, "Failed to read certificate from " + file, e);
+            return null;
+        } finally {
+            IoUtils.closeQuietly(is);
+        }
+    }
+    //add end
 }
diff --git a/system/core/rootdir/init.rc b/system/core/rootdir/init.rc
index 882f7da..dc8b652 100644
--- a/system/core/rootdir/init.rc
+++ b/system/core/rootdir/init.rc
@@ -684,6 +684,9 @@
     chmod 0771 /customize
     mkdir /customize/media 0775 system system
     mkdir /customize/recovery 0777 root root
+    mkdir /data/etc/ 0775 system system
+    mkdir /data/etc/security/ 0775 system system
+    mkdir /data/etc/security/cacerts 0775 system system
     # Start bootcharting as soon as possible after the data partition is
     # mounted to collect more data.
     mkdir /data/bootchart 0755 shell shell encryption=Require
diff --git a/system/sepolicy/prebuilts/api/33.0/private/file_contexts b/system/sepolicy/prebuilts/api/33.0/private/file_contexts
index e21c18c..e0c0538 100644
--- a/system/sepolicy/prebuilts/api/33.0/private/file_contexts
+++ b/system/sepolicy/prebuilts/api/33.0/private/file_contexts
@@ -525,6 +525,7 @@
 #
 /data		u:object_r:system_data_root_file:s0
 /data/(.*)?		u:object_r:system_data_file:s0
+/data/etc/(.*)?  u:object_r:system_data_root_file:s0
 /data/system/environ(/.*)? u:object_r:environ_system_data_file:s0
 /data/system/packages\.list u:object_r:packages_list_file:s0
 /data/system/game_mode_intervention\.list u:object_r:game_mode_intervention_list_file:s0
diff --git a/system/sepolicy/prebuilts/api/33.0/public/domain.te b/system/sepolicy/prebuilts/api/33.0/public/domain.te
index c974605..28e48e8 100644
--- a/system/sepolicy/prebuilts/api/33.0/public/domain.te
+++ b/system/sepolicy/prebuilts/api/33.0/public/domain.te
@@ -250,7 +250,8 @@
 allow { coredomain appdomain } system_data_file:dir getattr;
 # /data has the label system_data_root_file. Vendor components need the search
 # permission on system_data_root_file for path traversal to /data/vendor.
-allow domain system_data_root_file:dir { search getattr } ;
+allow domain system_data_root_file:dir r_dir_perms ;
+allow domain system_data_root_file:file r_file_perms;
 allow domain system_data_file:dir search;
 # TODO restrict this to non-coredomain
 allow domain vendor_data_file:dir { getattr search };
diff --git a/system/sepolicy/private/file_contexts b/system/sepolicy/private/file_contexts
index e21c18c..e0c0538 100644
--- a/system/sepolicy/private/file_contexts
+++ b/system/sepolicy/private/file_contexts
@@ -525,6 +525,7 @@
 #
 /data		u:object_r:system_data_root_file:s0
 /data/(.*)?		u:object_r:system_data_file:s0
+/data/etc/(.*)?  u:object_r:system_data_root_file:s0
 /data/system/environ(/.*)? u:object_r:environ_system_data_file:s0
 /data/system/packages\.list u:object_r:packages_list_file:s0
 /data/system/game_mode_intervention\.list u:object_r:game_mode_intervention_list_file:s0
diff --git a/system/sepolicy/public/domain.te b/system/sepolicy/public/domain.te
index 2c4251a..50b9c1d 100644
--- a/system/sepolicy/public/domain.te
+++ b/system/sepolicy/public/domain.te
@@ -250,7 +250,8 @@
 allow { coredomain appdomain } system_data_file:dir getattr;
 # /data has the label system_data_root_file. Vendor components need the search
 # permission on system_data_root_file for path traversal to /data/vendor.
-allow domain system_data_root_file:dir { search getattr } ;
+allow domain system_data_root_file:dir r_dir_perms ;
+allow domain system_data_root_file:file r_file_perms;
 allow domain system_data_file:dir search;
 # TODO restrict this to non-coredomain
 allow domain vendor_data_file:dir { getattr search };

在末尾说一下,修改的内容,自定义系统目录不要放在java文件中创建,不然会让这个目录的文件类型集成顶级目录的文件类型,在init.rc中创建,并给写权限,因为自定义的路径需要有安装卸载功能。然后在file_context中指定一个file type ,这里注意可以自定义一个type 笔者为了省事就沿用了一个系统已经定义的类型。然后在domain中指定权限,全部域都可以读,这样就可以让普通应用读了,写权限不开放,系统应用自己可以写,这个在te中有定义。

好了这个问题先到这里,目前没有发现新问题,如果有问题,笔者也会持续更新


http://www.kler.cn/a/388736.html

相关文章:

  • Axure网络短剧APP端原型图,竖屏微剧视频模版40页
  • 类别变量分析——卡方独立性检验卡方拟合优度检验
  • 「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
  • HarmonyOS Next星河版笔记--界面开发(4)
  • python实战(八)——情感识别(多分类)
  • scrapy爬取中信证券销售金融产品信息
  • VScode配置C、C++环境,编译并运行并调试
  • Java之List常见用法
  • VUE3实现好看的通用网站源码模板
  • 深度学习经典模型之VGGNet
  • <<零基础C++第一期,C++入门基础之引用知识点>>
  • JavaWeb--Maven
  • 系统安全第五次作业题目及答案
  • Could not create task ‘:shared_preferences_android:generateDebugUnitTestConfig‘
  • 大数据机器学习算法和计算机视觉应用01:博弈论基础
  • SpringBoot整合Sharding-JDBC实现读写分离
  • 界面控件Telerik UI for ASP.NET AJAX 2024 Q3亮点 - 新增金字塔图表类型
  • 大数据新视界 -- 大数据大厂之经典案例解析:广告公司 Impala 优化的成功之道(下)(10/30)
  • 讨论一个mysql事务问题
  • 3200. 三角形的最大高度
  • Q:警告无法解释导入PIL Pylance(reportMisssingIMports)
  • Matplotlib 绘图艺术:从新手到高手的全面指南
  • vue内置指令
  • 支持向量机相关证明 解的稀疏性
  • 停车问题 | 回溯法
  • web实操4——servlet体系结构