- android - 多次调用 OnPrimaryClipChangedListener
- android - 无法更新 RecyclerView 中的 TextView 字段
- android.database.CursorIndexOutOfBoundsException : Index 0 requested, 光标大小为 0
- android - 使用 AppCompat 时,我们是否需要明确指定其 UI 组件(Spinner、EditText)颜色
在 Java 12 中开发基于 SSL(非 HTTP)的服务器时,我遇到了让服务器和客户端一起对话的意想不到的困难。连接总是因 handshake_failure 错误而中断。经过大量努力(最初我认为我的证书有罪),由于套接字级别的 HelloWorld SSL 客户端/服务器对,我能够隔离问题:握手失败是由启用的协议(protocol)中存在 TLSv1.3 引起的默认情况下在 JDK 中。
到目前为止,我避免握手错误的解决方案是完全禁用 TLSv1.3(服务器端),但我对此解决方案并不满意。我想了解发生了什么(假设它不是 JDK 或系统中的错误,而是我的应用程序或我的环境中的错误)。
我在下面提供了一些代码。谁能检查并告诉我我做错了什么?谢谢并致以最诚挚的问候。
这是我的配置:OSX 10.14 (Mojave)我已经使用以下版本的 Java 进行了测试:
编辑:我将 Java 11 添加到我的测试中,这个版本也会出现问题
// HelloSSLServer.java
import java.net.*;
import javax.net.*;
import javax.net.ssl.*;
import java.io.*;
public class HelloSSLServer {
public static void main(String args[]) throws Exception {
int port = 1234;
boolean needClientAuth = false;
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket ss = factory.createServerSocket(port)) {
SSLServerSocket ssl = (SSLServerSocket) ss;
ssl.setNeedClientAuth(needClientAuth);
ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); // fails with TLSv1.3 (or with the default, i.e. no call)
while (true) {
try (Socket socket = ss.accept()) {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("Hello World!");
}
}
}
}
}
// HelloSSLClient.java
import java.net.*;
import javax.net.*;
import javax.net.ssl.*;
import java.io.*;
public class HelloSSLClient {
public static void main(String args[]) throws Exception {
String host = "localhost";
int port = 1234;
SocketFactory factory = SSLSocketFactory.getDefault();
try (Socket connection = factory.createSocket(host, port)) {
SSLSocket ssl = (SSLSocket) connection;
SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
ssl.setSSLParameters(sslParams);
BufferedReader input =
new BufferedReader(new InputStreamReader(connection.getInputStream()));
String message = input.readLine();
System.out.println("Got the message: " + message);
}
}
}
(服务器的 keystore 和客户端的信任库中也需要服务器的证书以测试任何 SSL 应用程序)
这是结果(发生错误时):
$ java -Djavax.net.ssl.keyStore=serverkeystore.ks -Djavax.net.ssl.keyStorePassword=xxxxxxx HelloSSLServer
$ java -Djavax.net.ssl.trustStore=clienttruststore.ks -Djavax.net.ssl.trustStorePassword=xxxxxx HelloSSLClient
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:307)
at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:285)
at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:180)
at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1180)
at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1091)
at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:402)
at java.base/sun.security.ssl.SSLSocketImpl.ensureNegotiated(SSLSocketImpl.java:721)
at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:804)
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.base/java.io.InputStreamReader.read(InputStreamReader.java:185)
at java.base/java.io.BufferedReader.fill(BufferedReader.java:161)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:326)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:392)
at HelloSSLClient.main(HelloSSLClient.java:21)
正如@SvetlinZarev 和@user207421 所建议的,这里是一些启用了 -Djavax.net.debug=ssl:handshake 的输出片段。由于代码在强制使用 TLSv1.2 协议(protocol)时有效,因此更有趣的是看看当简单地使用默认协议(protocol)时发生了什么(对 ssl.setEnabledProtocols 的调用被注释掉):
注意:该项目是个人的和实验性的,服务器的证书是假的,自签名的,并添加到客户端的信任库( keystore 和信任库也是虚拟的),并且无论如何都是公开的,所以我不要害怕发布任何敏感信息
从客户端:
java -Djavax.net.debug=ssl:handshake -Djavax.net.ssl.trustStore=clienttruststore.ks -Djavax.net.ssl.trustStorePassword=xxxxxxxxx HelloSSLClient
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.229 CEST|SSLCipher.java:463|jdk.tls.keyLimits: entry = AES/GCM/NoPadding KeyUpdate 2^37. AES/GCM/NOPADDING:KEYUPDATE = 137438953472
javax.net.ssl|WARNING|01|main|2019-08-22 09:54:22.451 CEST|ServerNameExtension.java:261|Unable to indicate server name
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.451 CEST|SSLExtensions.java:257|Ignore, context unavailable extension: server_name
javax.net.ssl|WARNING|01|main|2019-08-22 09:54:22.455 CEST|SignatureScheme.java:282|Signature algorithm, ed25519, is not supported by the underlying providers
javax.net.ssl|WARNING|01|main|2019-08-22 09:54:22.455 CEST|SignatureScheme.java:282|Signature algorithm, ed448, is not supported by the underlying providers
javax.net.ssl|INFO|01|main|2019-08-22 09:54:22.459 CEST|AlpnExtension.java:161|No available application protocols
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.460 CEST|SSLExtensions.java:257|Ignore, context unavailable extension: application_layer_protocol_negotiation
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.460 CEST|SSLExtensions.java:257|Ignore, context unavailable extension: cookie
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.477 CEST|SSLExtensions.java:257|Ignore, context unavailable extension: renegotiation_info
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.477 CEST|PreSharedKeyExtension.java:633|No session to resume.
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.478 CEST|SSLExtensions.java:257|Ignore, context unavailable extension: pre_shared_key
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.482 CEST|ClientHello.java:653|Produced ClientHello handshake message (
"ClientHello": {
"client version" : "TLSv1.2",
"random" : "8D 95 FA BE D8 F4 BC AC E6 15 36 FE FE A2 57 C4 DD EF F6 53 B8 54 1D 4A ED AE C6 0A CD 92 E0 A4",
"session id" : "CC EE 0F 29 F8 9A 3B 72 61 61 99 46 AA 69 CF 23 4F E9 05 13 2A 52 B8 1D 34 18 FA DF 26 1B 46 87",
"cipher suites" : "[TLS_AES_128_GCM_SHA256(0x1301), TLS_AES_256_GCM_SHA384(0x1302), TLS_CHACHA20_POLY1305_SHA256(0x1303), TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B), TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xCCA9), TLS_DHE_DSS_WITH_AES_128_CBC_SHA256(0x0040),
[...]
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA(0xC00E), TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033), TLS_DHE_DSS_WITH_AES_128_CBC_SHA(0x0032), TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00FF)]",
[...]
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.528 CEST|ServerHello.java:871|Consuming ServerHello handshake message (
"ServerHello": {
"server version" : "TLSv1.2",
"random" : "B5 27 FE 28 29 85 AC 1A C4 62 57 28 45 12 63 BA 4D CC 4B E0 02 A4 A9 7A ED 9F A3 8D A6 98 85 BE",
"session id" : "CC EE 0F 29 F8 9A 3B 72 61 61 99 46 AA 69 CF 23 4F E9 05 13 2A 52 B8 1D 34 18 FA DF 26 1B 46 87",
"cipher suite" : "TLS_AES_128_GCM_SHA256(0x1301)",
"compression methods" : "00",
"extensions" : [
"supported_versions (43)": {
"selected version": [TLSv1.3]
},
"key_share (51)": {
"server_share": {
[...]
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.528 CEST|SSLExtensions.java:189|Consumed extension: supported_versions
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.529 CEST|ServerHello.java:967|Negotiated protocol version: TLSv1.3
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.530 CEST|SSLExtensions.java:160|Ignore unsupported extension: server_name
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.530 CEST|SSLExtensions.java:160|Ignore unsupported extension: max_fragment_length
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.530 CEST|SSLExtensions.java:160|Ignore unsupported extension: status_request
[...]
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.582 CEST|SSLExtensions.java:170|Ignore unavailable extension: server_name
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.582 CEST|SSLExtensions.java:170|Ignore unavailable extension: max_fragment_length
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.583 CEST|SSLExtensions.java:189|Consumed extension: supported_groups
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.583 CEST|SSLExtensions.java:204|Ignore unavailable extension: server_name
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.583 CEST|SSLExtensions.java:204|Ignore unavailable extension: max_fragment_length
javax.net.ssl|WARNING|01|main|2019-08-22 09:54:22.583 CEST|SSLExtensions.java:212|Ignore impact of unsupported extension: supported_groups
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.583 CEST|SSLExtensions.java:204|Ignore unavailable extension: application_layer_protocol_negotiation
javax.net.ssl|DEBUG|01|main|2019-08-22 09:54:22.585 CEST|Alert.java:238|Received alert message (
"Alert": {
"level" : "fatal",
"description": "handshake_failure"
}
)
javax.net.ssl|ERROR|01|main|2019-08-22 09:54:22.586 CEST|TransportContext.java:312|Fatal (HANDSHAKE_FAILURE): Received fatal alert: handshake_failure (
"throwable" : {
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
这是服务器端:
java -Djavax.net.debug=ssl,handshake -Djavax.net.ssl.keyStore=serverkeystore.ks -Djavax.net.ssl.keyStorePassword=xxxxxxxxx HelloSSLServer
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:55.869 CEST|SSLCipher.java:463|jdk.tls.keyLimits: entry = AES/GCM/NoPadding KeyUpdate 2^37. AES/GCM/NOPADDING:KEYUPDATE = 137438953472
javax.net.ssl|WARNING|01|main|2019-08-22 12:23:59.697 CEST|SignatureScheme.java:282|Signature algorithm, ed25519, is not supported by the underlying providers
javax.net.ssl|WARNING|01|main|2019-08-22 12:23:59.698 CEST|SignatureScheme.java:282|Signature algorithm, ed448, is not supported by the underlying providers
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.704 CEST|ClientHello.java:809|Consuming ClientHello handshake message (
"ClientHello": {
"client version" : "TLSv1.2",
"random" : "0D E6 53 8A B0 E4 E7 9A 80 93 49 84 AD 88 0A 5F D5 7F 29 37 C3 86 A7 28 A7 D9 C6 7D EB DF 6A 3D",
"session id" : "5B 9A 18 25 31 65 8C 8F E8 E6 93 DA F5 AA 50 45 A8 C9 20 D1 9D 67 35 9B 7B D3 46 D5 CA C0 FC 85",
"cipher suites" : "[TLS_AES_128_GCM_SHA256(0x1301), TLS_AES_256_GCM_SHA384(0x1302), TLS_CHACHA20_POLY1305_SHA256(0x1303), TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B),
[...]
TLS_DHE_DSS_WITH_AES_128_CBC_SHA(0x0032), TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00FF)]",
"compression methods" : "00",
"extensions" : [
"status_request (5)": {
"certificate status type": ocsp
"OCSP status request": {
"responder_id": <empty>
"request extensions": {
<empty>
}
}
},
"supported_groups (10)": {
"versions": [secp256r1, secp384r1, secp521r1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, secp256k1, ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192]
},
"ec_point_formats (11)": {
"formats": [uncompressed]
},
"signature_algorithms (13)": {
"signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha224, rsa_sha224, dsa_sha224, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
},
"signature_algorithms_cert (50)": {
"signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha224, rsa_sha224, dsa_sha224, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
},
[...]
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.704 CEST|SSLExtensions.java:189|Consumed extension: supported_versions
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.705 CEST|ClientHello.java:839|Negotiated protocol version: TLSv1.3
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.705 CEST|SSLExtensions.java:189|Consumed extension: psk_key_exchange_modes
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.706 CEST|PreSharedKeyExtension.java:805|Handling pre_shared_key absence.
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.706 CEST|SSLExtensions.java:170|Ignore unavailable extension: server_name
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.706 CEST|SSLExtensions.java:170|Ignore unavailable extension: max_fragment_length
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.706 CEST|SSLExtensions.java:189|Consumed extension: status_request
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.707 CEST|SSLExtensions.java:189|Consumed extension: supported_groups
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.707 CEST|SSLExtensions.java:160|Ignore unsupported extension: ec_point_formats
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.707 CEST|SSLExtensions.java:189|Consumed extension: signature_algorithms
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.707 CEST|SSLExtensions.java:189|Consumed extension: signature_algorithms_cert
[...]
javax.net.ssl|DEBUG|01|main|2019-08-22 12:23:59.739 CEST|ServerHello.java:576|Produced ServerHello handshake message (
"ServerHello": {
"server version" : "TLSv1.2",
"random" : "FD FD 39 0C 3A D8 F0 E8 38 F8 08 D8 19 94 7A FA 4A 68 71 F8 4C 32 EB 7A D0 53 96 E5 9F E8 0A 3B",
"session id" : "5B 9A 18 25 31 65 8C 8F E8 E6 93 DA F5 AA 50 45 A8 C9 20 D1 9D 67 35 9B 7B D3 46 D5 CA C0 FC 85",
"cipher suite" : "TLS_AES_128_GCM_SHA256(0x1301)",
"compression methods" : "00",
"extensions" : [
"supported_versions (43)": {
"selected version": [TLSv1.3]
},
"key_share (51)": {
"server_share": {
[...]
javax.net.ssl|ALL|01|main|2019-08-22 12:23:59.785 CEST|X509Authentication.java:243|No X.509 cert selected for EC
javax.net.ssl|WARNING|01|main|2019-08-22 12:23:59.785 CEST|CertificateMessage.java:1055|Unavailable authentication scheme: ecdsa_sha1
javax.net.ssl|ALL|01|main|2019-08-22 12:23:59.785 CEST|X509Authentication.java:243|No X.509 cert selected for RSA
javax.net.ssl|WARNING|01|main|2019-08-22 12:23:59.785 CEST|CertificateMessage.java:1055|Unavailable authentication scheme: rsa_pkcs1_sha1
javax.net.ssl|WARNING|01|main|2019-08-22 12:23:59.785 CEST|CertificateMessage.java:1065|No available authentication scheme
javax.net.ssl|ERROR|01|main|2019-08-22 12:23:59.788 CEST|TransportContext.java:312|Fatal (HANDSHAKE_FAILURE): No available authentication scheme (
"throwable" : {
javax.net.ssl.SSLHandshakeException: No available authentication scheme
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
[...]
最佳答案
这是解决方案:需要使用 keytool 的 -keyalg 标志来生成证书,否则, key 将使用旧的默认 DSA 加密,这在 TLS1.3 中不再被允许。使用 RSA 就可以了。
结论:
因此建议始终使用显式 -keyalg RSA(或任何其他受支持的 TLS1.3 算法)生成证书以避免意外。
编辑 2020 年 11 月 19 日:keytool 的默认算法问题很久以前就在 Red Hat 的 Bugzilla 上讨论过 (https://bugzilla.redhat.com/show_bug.cgi?id=1582253)。 RSA 似乎是 OpenJDK 14 中 key 工具的新默认算法,但据我所知,它不能保证其他 JDK 实现(除非 JDK 规范对此有任何说明)。
关于启用 TLSv1.3 的 Java 11 和 12 SSL 套接字因 handshake_failure 错误而失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57601284/
我想知道如何首先检测 Javascript 是否启用/禁用。在此站点上禁用 javascript 表明 stackoverflow 使用了称为标签的东西。 这是标准的做法吗?它适用于所有浏览器吗?它不
这个问题在这里已经有了答案: 关闭 11 年前。 Possible Duplicate: How to enable PHP short tags ? Enable short open tags
在 VSTS (Azure DevOps) 上拥有私有(private)存储库我尝试通过将以下内容添加到 .csproj 文件来启用 SourceLink:https://github.com/dot
我创建了一个 wx.Frame(我们称之为 mainFrame)。该框架上包含一个按钮,当单击该按钮时,会创建一个新框架(我们称之为 childFrame)。 我想知道如何在创建 childFrame
当我禁用 WPF 中的控件时,比如说一个菜单项 MenuItem aMenuItem = ... aMenuItem.IsEnabled = false; MenuItem 中的文本仍然处于事件状态,
我想在我的 nginx 服务器上启用 gzip 压缩。 nginx.conf 文件在这里: http { # Enable Gzip server { location ~* \.(?
我正在使用免费的 heroku 附加 PG 备份并遵循这些 instructions .我找到了安装应用程序的命令:heroku addons:add pgbackups:auto-week。但是我想
我想知道脚本是否可以使用某种切换按钮启用/禁用页面上的所有输入元素。 我用谷歌搜索了它,但除了这个之外没有发现任何有用的东西: http://www.codetoad.com/javascript/e
在我的 php 文件中,我想使用 jQuery Datepicker。 当我的文件加载时,我创建了禁用的日期选择器。 然后,当我的 php 文件(它是一个表单)中的一个特殊字段被填充时,我想启用日期选
我有一个按钮,如下所示: RadButton lnkAdd = new RadButton(); lnkAdd.ID = "BtnAdd"; lnkAdd.CommandName = RadGrid.
public static void ToggleTaskManager(string keyValue) { RegistryKey objRegistryK
我正在 Azure 中使用事件网格订阅,该订阅在创建 Blob 时触发。然而,我们有很多文件进入这个 blob,比如说 1000 多个。 如果我发现任何文件有任何错误,我想做的是禁用事件订阅。 最佳答
我的网站上有几个使用 HTML5 contentEditable 属性的 div。目标是让用户能够开始编写日记条目,并将保存按钮从禁用更改为启用。 这是我目前拥有的 HTML: Write
我有一个范围输入,其定义如下: @Html.LabelFor(m => Model.Quality, Resources.CompressionQuality) 和一个下拉菜单: @Html.Lab
我正在尝试创建一个启用/禁用按钮的下拉菜单,并且我正在关注此 example 但它已经有 4 年历史了,而且该功能似乎无法在我的 xhtml 页面上运行。 任何帮助都将被适当 最佳答案 这是一个现场演
我正在 Azure 中使用事件网格订阅,该订阅在创建 Blob 时触发。然而,我们有很多文件进入这个 blob,比如说 1000 多个。 如果我发现任何文件有任何错误,我想做的是禁用事件订阅。 最佳答
我在这里遇到一个非常奇怪的情况:我编写了一个应用程序,除其他外,将连接的代理从打开切换到关闭,反之亦然。通过更改注册表中的值来完成此操作: public void SetUpProxy(string
我需要调整一堆 PVC 的大小。似乎最简单的方法是通过ExpandPersistentVolumes 功能。但是我无法获得配置合作。 ExpandPersistentVolumes feature g
如果我的TextField为空,则应禁用该按钮,并且该按钮的textColor和borderColor应该为灰色。但是,启用按钮后,颜色应为蓝色。 更改textColor很容易: button.Set
您好,我的问题是:我无法从另一个类启用表单的按钮。我的表单类是 public class FileSending { //Function for enabling the button
我是一名优秀的程序员,十分优秀!