feat: 支持免密连接模式及允许免密连接开关
This commit is contained in:
@@ -24,6 +24,9 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
|||||||
/** 启动本 Activity 时携带的发起方(控制端)设备 ID。 */
|
/** 启动本 Activity 时携带的发起方(控制端)设备 ID。 */
|
||||||
public static final String EXTRA_CONTROLLER_ID = "controller_id";
|
public static final String EXTRA_CONTROLLER_ID = "controller_id";
|
||||||
|
|
||||||
|
/** 启动本 Activity 时携带的鉴权类型(NONE=免密连接 / CODE / PASSWORD)。 */
|
||||||
|
public static final String EXTRA_AUTH_TYPE = "auth_type";
|
||||||
|
|
||||||
/** 无响应自动拒绝的超时时间(毫秒)。 */
|
/** 无响应自动拒绝的超时时间(毫秒)。 */
|
||||||
private static final long AUTO_REJECT_TIMEOUT = 60_000L;
|
private static final long AUTO_REJECT_TIMEOUT = 60_000L;
|
||||||
|
|
||||||
@@ -38,6 +41,8 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
|||||||
String controllerId = getIntent() != null
|
String controllerId = getIntent() != null
|
||||||
? getIntent().getStringExtra(EXTRA_CONTROLLER_ID)
|
? getIntent().getStringExtra(EXTRA_CONTROLLER_ID)
|
||||||
: null;
|
: null;
|
||||||
|
String authType = getIntent() != null
|
||||||
|
? getIntent().getStringExtra(EXTRA_AUTH_TYPE) : null;
|
||||||
|
|
||||||
// 服务实例不存在或缺少必要参数时,直接结束,不做任何连接处理。
|
// 服务实例不存在或缺少必要参数时,直接结束,不做任何连接处理。
|
||||||
if (ScreenCaptureService.getInstance() == null || controllerId == null) {
|
if (ScreenCaptureService.getInstance() == null || controllerId == null) {
|
||||||
@@ -48,7 +53,13 @@ public class ConnectionRequestActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setTitle(R.string.connection_request_title);
|
builder.setTitle(R.string.connection_request_title);
|
||||||
builder.setMessage(getString(R.string.connection_request_message, controllerId));
|
|
||||||
|
// 免密连接时在提示中标注,便于用户知晓无需验证码/密码
|
||||||
|
String message = getString(R.string.connection_request_message, controllerId);
|
||||||
|
if ("NONE".equalsIgnoreCase(authType)) {
|
||||||
|
message += "\n\n" + getString(R.string.connection_request_mode_none);
|
||||||
|
}
|
||||||
|
builder.setMessage(message);
|
||||||
builder.setCancelable(false);
|
builder.setCancelable(false);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.connection_accept, (d, which) -> onAccept());
|
builder.setPositiveButton(R.string.connection_accept, (d, which) -> onAccept());
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
|||||||
binding.tvAuthHint.setText("固定密码已保存");
|
binding.tvAuthHint.setText("固定密码已保存");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 允许免密连接开关:默认开启;关闭后控制端必须使用验证码或密码
|
||||||
|
binding.switchAllowNoAuth.setChecked(AuthSettings.isNoAuthAllowed(this));
|
||||||
|
binding.switchAllowNoAuth.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
AuthSettings.setNoAuthAllowed(this, isChecked);
|
||||||
|
binding.tvAuthHint.setText(isChecked ? R.string.allow_no_auth_on : R.string.allow_no_auth_off);
|
||||||
|
});
|
||||||
|
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -464,28 +464,40 @@ public class ScreenCaptureService extends Service {
|
|||||||
? controllerName : controllerId;
|
? controllerName : controllerId;
|
||||||
pendingOfferSdp = offerSdp;
|
pendingOfferSdp = offerSdp;
|
||||||
|
|
||||||
// 鉴权:动态验证码(CODE) / 固定密码(PASSWORD)。
|
// 鉴权模式:免密(NONE) / 动态验证码(CODE) / 固定密码(PASSWORD)。
|
||||||
// 服务器仅做转发,真实校验在此完成(密钥不离开被控端)。
|
// 服务器仅做转发,真实校验在此完成(密钥不离开被控端)。
|
||||||
String authType = message.getAuthType();
|
String rawAuthType = message.getAuthType();
|
||||||
String authValue = message.getAuthValue();
|
String authType = (rawAuthType == null || rawAuthType.trim().isEmpty())
|
||||||
if (authType != null && !authType.trim().isEmpty()) {
|
? "NONE" : rawAuthType.trim();
|
||||||
String failReason = verifyAuth(authType.trim(), authValue);
|
boolean isNoAuth = "NONE".equalsIgnoreCase(authType);
|
||||||
if (failReason == null) {
|
if (isNoAuth) {
|
||||||
// 校验通过:自动接受,直接建立连接(无需手动确认)
|
// 免密连接:若被控端已关闭免密,则直接拒绝,要求使用验证码/密码
|
||||||
Log.i(TAG, "Auth passed (" + authType + "), auto-accepting " + controllerId);
|
if (!AuthSettings.isNoAuthAllowed(this)) {
|
||||||
mainHandler.post(this::acceptConnection);
|
Log.w(TAG, "Reject 免密连接 from " + controllerId + ": 被控端已关闭免密连接");
|
||||||
} else {
|
sendAuthRejected("被控端已关闭免密连接,请使用动态验证码或固定密码");
|
||||||
Log.w(TAG, "Auth failed (" + authType + "): " + failReason + " from " + controllerId);
|
return;
|
||||||
sendAuthRejected(failReason);
|
|
||||||
}
|
}
|
||||||
|
// 免密连接:弹出手动确认弹窗,由用户决定是否接受
|
||||||
|
Log.i(TAG, "免密连接,转交用户手动确认: " + controllerId);
|
||||||
|
Intent dialogIntent = new Intent(this, ConnectionRequestActivity.class);
|
||||||
|
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId);
|
||||||
|
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_AUTH_TYPE, "NONE");
|
||||||
|
startActivity(dialogIntent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未携带鉴权信息:沿用原有手动确认弹窗
|
// 携带鉴权信息:被控端本地校验,通过则自动接受
|
||||||
Intent dialogIntent = new Intent(this, ConnectionRequestActivity.class);
|
String authValue = message.getAuthValue();
|
||||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
String failReason = verifyAuth(authType, authValue);
|
||||||
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId);
|
if (failReason == null) {
|
||||||
startActivity(dialogIntent);
|
// 校验通过:自动接受,直接建立连接(无需手动确认)
|
||||||
|
Log.i(TAG, "Auth passed (" + authType + "), auto-accepting " + controllerId);
|
||||||
|
mainHandler.post(this::acceptConnection);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Auth failed (" + authType + "): " + failReason + " from " + controllerId);
|
||||||
|
sendAuthRejected(failReason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public class AuthSettings {
|
|||||||
private static final String PREF_NAME = "controlled_auth_prefs";
|
private static final String PREF_NAME = "controlled_auth_prefs";
|
||||||
private static final String KEY_DYNAMIC_CODE = "dynamic_code";
|
private static final String KEY_DYNAMIC_CODE = "dynamic_code";
|
||||||
private static final String KEY_FIXED_PASSWORD = "fixed_password";
|
private static final String KEY_FIXED_PASSWORD = "fixed_password";
|
||||||
|
/** 是否允许“免密连接”(被控端手动确认)。默认开启。 */
|
||||||
|
private static final String KEY_ALLOW_NO_AUTH = "allow_no_auth";
|
||||||
|
|
||||||
private static final String CODE_CHARS =
|
private static final String CODE_CHARS =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -57,4 +59,16 @@ public class AuthSettings {
|
|||||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
.edit().putString(KEY_FIXED_PASSWORD, password == null ? "" : password).apply();
|
.edit().putString(KEY_FIXED_PASSWORD, password == null ? "" : password).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否允许免密连接(被控端手动确认)。默认开启。 */
|
||||||
|
public static boolean isNoAuthAllowed(Context context) {
|
||||||
|
return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getBoolean(KEY_ALLOW_NO_AUTH, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置是否允许免密连接。 */
|
||||||
|
public static void setNoAuthAllowed(Context context, boolean allowed) {
|
||||||
|
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(KEY_ALLOW_NO_AUTH, allowed).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,39 @@
|
|||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:textColor="#888888" />
|
android:textColor="#888888" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/allow_no_auth_label"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/allow_no_auth_desc"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#888888" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_allow_no_auth"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_start"
|
android:id="@+id/btn_start"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -15,4 +15,11 @@
|
|||||||
<string name="accessibility_dialog_cancel">取消</string>
|
<string name="accessibility_dialog_cancel">取消</string>
|
||||||
<string name="resolution_label">采集分辨率</string>
|
<string name="resolution_label">采集分辨率</string>
|
||||||
<string name="resolution_changed_message">采集分辨率已切换为 %1$d×%2$d</string>
|
<string name="resolution_changed_message">采集分辨率已切换为 %1$d×%2$d</string>
|
||||||
|
|
||||||
|
<!-- 免密连接 -->
|
||||||
|
<string name="connection_request_mode_none">连接方式:免密连接(被控端手动确认)</string>
|
||||||
|
<string name="allow_no_auth_label">允许免密连接</string>
|
||||||
|
<string name="allow_no_auth_desc">关闭后,控制端必须使用动态验证码或固定密码才能连接</string>
|
||||||
|
<string name="allow_no_auth_on">已允许免密连接</string>
|
||||||
|
<string name="allow_no_auth_off">已关闭免密连接</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private TextView tvStatusControl;
|
private TextView tvStatusControl;
|
||||||
private TextView tvStats;
|
private TextView tvStats;
|
||||||
private Spinner spinnerResolution;
|
private Spinner spinnerResolution;
|
||||||
|
private Button btnDisconnectControl;
|
||||||
|
|
||||||
private WebSocketClient wsClient;
|
private WebSocketClient wsClient;
|
||||||
private WebRtcClient webRtcClient;
|
private WebRtcClient webRtcClient;
|
||||||
@@ -102,6 +103,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
tvStatusControl = findViewById(R.id.tv_status_control);
|
tvStatusControl = findViewById(R.id.tv_status_control);
|
||||||
tvStats = findViewById(R.id.tv_stats);
|
tvStats = findViewById(R.id.tv_stats);
|
||||||
spinnerResolution = findViewById(R.id.spinner_resolution);
|
spinnerResolution = findViewById(R.id.spinner_resolution);
|
||||||
|
btnDisconnectControl = findViewById(R.id.btn_disconnect_control);
|
||||||
|
|
||||||
// 默认服务器地址
|
// 默认服务器地址
|
||||||
etServerUrl.setText("ws://175.178.213.60:8088/ws/signal");
|
etServerUrl.setText("ws://175.178.213.60:8088/ws/signal");
|
||||||
@@ -111,6 +113,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
btnConnect.setOnClickListener(v -> showAuthDialog());
|
btnConnect.setOnClickListener(v -> showAuthDialog());
|
||||||
btnDisconnect.setOnClickListener(v -> disconnect());
|
btnDisconnect.setOnClickListener(v -> disconnect());
|
||||||
|
btnDisconnectControl.setOnClickListener(v -> disconnect());
|
||||||
|
|
||||||
// 初始化 EGL 和远端视频渲染
|
// 初始化 EGL 和远端视频渲染
|
||||||
eglBase = EglBase.create();
|
eglBase = EglBase.create();
|
||||||
@@ -214,7 +217,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 弹出鉴权输入对话框:选择动态验证码或固定密码,输入后发起连接。 */
|
/** 弹出鉴权输入对话框:选择免密连接、动态验证码或固定密码,输入后发起连接。 */
|
||||||
private void showAuthDialog() {
|
private void showAuthDialog() {
|
||||||
String serverUrl = etServerUrl.getText().toString().trim();
|
String serverUrl = etServerUrl.getText().toString().trim();
|
||||||
String target = etTargetDeviceId.getText().toString().trim();
|
String target = etTargetDeviceId.getText().toString().trim();
|
||||||
@@ -229,13 +232,22 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
EditText et = view.findViewById(R.id.et_auth_value);
|
EditText et = view.findViewById(R.id.et_auth_value);
|
||||||
TextView tip = view.findViewById(R.id.tv_auth_tip);
|
TextView tip = view.findViewById(R.id.tv_auth_tip);
|
||||||
|
|
||||||
|
// 根据选中的鉴权方式切换输入框与提示文案。默认选中“免密连接”,输入框隐藏。
|
||||||
rg.setOnCheckedChangeListener((group, checkedId) -> {
|
rg.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
if (checkedId == R.id.rb_password) {
|
if (checkedId == R.id.rb_none) {
|
||||||
et.setHint("请输入固定密码");
|
et.setVisibility(View.GONE);
|
||||||
tip.setText("固定密码为被控端设置的固定密码");
|
tip.setVisibility(View.VISIBLE);
|
||||||
|
tip.setText(R.string.auth_none_tip);
|
||||||
|
} else if (checkedId == R.id.rb_password) {
|
||||||
|
et.setVisibility(View.VISIBLE);
|
||||||
|
et.setHint(R.string.auth_password_hint);
|
||||||
|
tip.setVisibility(View.VISIBLE);
|
||||||
|
tip.setText(R.string.auth_password_tip);
|
||||||
} else {
|
} else {
|
||||||
et.setHint("请输入动态验证码");
|
et.setVisibility(View.VISIBLE);
|
||||||
tip.setText("动态验证码为被控端界面显示的 6 位大小写字母与数字组合");
|
et.setHint(R.string.auth_code_hint);
|
||||||
|
tip.setVisibility(View.VISIBLE);
|
||||||
|
tip.setText(R.string.auth_code_tip);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,15 +261,22 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// 校验失败时不关闭对话框,引导用户重新输入
|
// 校验失败时不关闭对话框,引导用户重新输入
|
||||||
dialog.setOnShowListener(d -> dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
dialog.setOnShowListener(d -> dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
.setOnClickListener(v -> {
|
.setOnClickListener(v -> {
|
||||||
boolean isPassword = (rg.getCheckedRadioButtonId() == R.id.rb_password);
|
int checkedId = rg.getCheckedRadioButtonId();
|
||||||
String authType = isPassword ? "PASSWORD" : "CODE";
|
// 免密连接(NONE)无需输入验证码/密码
|
||||||
String authValue = et.getText().toString().trim();
|
if (checkedId == R.id.rb_none) {
|
||||||
if (authValue.isEmpty()) {
|
pendingAuthType = "NONE";
|
||||||
Toast.makeText(MainActivity.this, "请输入验证码或密码", Toast.LENGTH_SHORT).show();
|
pendingAuthValue = "";
|
||||||
return;
|
} else {
|
||||||
|
boolean isPassword = (checkedId == R.id.rb_password);
|
||||||
|
String authType = isPassword ? "PASSWORD" : "CODE";
|
||||||
|
String authValue = et.getText().toString().trim();
|
||||||
|
if (authValue.isEmpty()) {
|
||||||
|
Toast.makeText(MainActivity.this, "请输入验证码或密码", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAuthType = authType;
|
||||||
|
pendingAuthValue = authValue;
|
||||||
}
|
}
|
||||||
pendingAuthType = authType;
|
|
||||||
pendingAuthValue = authValue;
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
connectToControlled();
|
connectToControlled();
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -143,6 +143,15 @@
|
|||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:backgroundTint="#80FFFFFF" />
|
android:backgroundTint="#80FFFFFF" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_disconnect_control"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:backgroundTint="#E53935"
|
||||||
|
android:text="断开连接"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -12,17 +12,23 @@
|
|||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
android:id="@+id/rb_code"
|
android:id="@+id/rb_none"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="动态验证码" />
|
android:text="@string/auth_none" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rb_code"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/auth_code" />
|
||||||
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
android:id="@+id/rb_password"
|
android:id="@+id/rb_password"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="固定密码" />
|
android:text="@string/auth_password" />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
@@ -30,15 +36,16 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:hint="请输入动态验证码"
|
android:hint="@string/auth_input_hint"
|
||||||
android:inputType="textVisiblePassword" />
|
android:inputType="textVisiblePassword"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_auth_tip"
|
android:id="@+id/tv_auth_tip"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:text="动态验证码为被控端界面显示的 6 位大小写字母与数字组合"
|
android:text="@string/auth_none_tip"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:textColor="#888888" />
|
android:textColor="#888888" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WebRTC控制端</string>
|
<string name="app_name">WebRTC控制端</string>
|
||||||
|
|
||||||
|
<!-- 连接鉴权 -->
|
||||||
|
<string name="auth_none">免密连接</string>
|
||||||
|
<string name="auth_code">动态验证码</string>
|
||||||
|
<string name="auth_password">固定密码</string>
|
||||||
|
<string name="auth_none_tip">免密连接:被控端将弹出手动确认对话框,无需输入验证码或密码。</string>
|
||||||
|
<string name="auth_code_tip">动态验证码为被控端界面显示的 6 位大小写字母与数字组合</string>
|
||||||
|
<string name="auth_password_tip">固定密码为被控端设置的固定密码</string>
|
||||||
|
<string name="auth_code_hint">请输入动态验证码</string>
|
||||||
|
<string name="auth_password_hint">请输入固定密码</string>
|
||||||
|
<string name="auth_input_hint">请输入验证码或密码</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
|||||||
import { store, connectSignaling, disconnectSignaling, refreshDevices, connectToDevice, disconnectDevice } from '../store/controllerStore';
|
import { store, connectSignaling, disconnectSignaling, refreshDevices, connectToDevice, disconnectDevice } from '../store/controllerStore';
|
||||||
|
|
||||||
const selected = ref('');
|
const selected = ref('');
|
||||||
const authType = ref('CODE');
|
const authType = ref('NONE');
|
||||||
const authValue = ref('');
|
const authValue = ref('');
|
||||||
|
|
||||||
function onConnectDevice() {
|
function onConnectDevice() {
|
||||||
@@ -11,11 +11,11 @@ function onConnectDevice() {
|
|||||||
store.error = '请选择要连接的设备';
|
store.error = '请选择要连接的设备';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!authValue.value.trim()) {
|
if (authType.value !== 'NONE' && !authValue.value.trim()) {
|
||||||
store.error = '请输入验证码或密码';
|
store.error = '请输入验证码或密码';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
connectToDevice(selected.value, authType.value, authValue.value.trim());
|
connectToDevice(selected.value, authType.value, authType.value === 'NONE' ? '' : authValue.value.trim());
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -63,10 +63,14 @@ function onConnectDevice() {
|
|||||||
|
|
||||||
<div class="section-title" style="margin-top:14px">连接鉴权</div>
|
<div class="section-title" style="margin-top:14px">连接鉴权</div>
|
||||||
<div class="auth-row">
|
<div class="auth-row">
|
||||||
|
<label class="auth-radio"><input type="radio" value="NONE" v-model="authType" /> 免密连接</label>
|
||||||
<label class="auth-radio"><input type="radio" value="CODE" v-model="authType" /> 动态验证码</label>
|
<label class="auth-radio"><input type="radio" value="CODE" v-model="authType" /> 动态验证码</label>
|
||||||
<label class="auth-radio"><input type="radio" value="PASSWORD" v-model="authType" /> 固定密码</label>
|
<label class="auth-radio"><input type="radio" value="PASSWORD" v-model="authType" /> 固定密码</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div v-if="authType === 'NONE'" class="hint">
|
||||||
|
免密连接:被控端将弹出手动确认对话框,无需输入验证码或密码。
|
||||||
|
</div>
|
||||||
|
<div class="field" v-else>
|
||||||
<input class="input" v-model="authValue" :placeholder="authType === 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码'" />
|
<input class="input" v-model="authValue" :placeholder="authType === 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鉴权类型分类:动态验证码(CODE) / 固定密码(PASSWORD)。
|
// 鉴权类型分类:动态验证码(CODE) / 固定密码(PASSWORD) / 免密(NONE)。
|
||||||
// 服务器只做分类与转发,真实校验在被控端完成(服务器不保存任何密钥)。
|
// 服务器只做分类与转发,真实校验在被控端完成(服务器不保存任何密钥)。
|
||||||
String authType = message.getAuthType();
|
String authType = message.getAuthType();
|
||||||
if (authType != null && !authType.trim().isEmpty()) {
|
if (authType != null && !authType.trim().isEmpty()) {
|
||||||
@@ -197,11 +197,17 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
|||||||
logger.info("连接请求 {} -> {} 使用【动态验证码】鉴权", fromDeviceId, toDeviceId);
|
logger.info("连接请求 {} -> {} 使用【动态验证码】鉴权", fromDeviceId, toDeviceId);
|
||||||
} else if ("PASSWORD".equalsIgnoreCase(authType)) {
|
} else if ("PASSWORD".equalsIgnoreCase(authType)) {
|
||||||
logger.info("连接请求 {} -> {} 使用【固定密码】鉴权", fromDeviceId, toDeviceId);
|
logger.info("连接请求 {} -> {} 使用【固定密码】鉴权", fromDeviceId, toDeviceId);
|
||||||
|
} else if ("NONE".equalsIgnoreCase(authType)) {
|
||||||
|
// 显式标记为免密,便于被控端识别
|
||||||
|
message.setAuthType("NONE");
|
||||||
|
logger.info("连接请求 {} -> {} 使用【免密连接】鉴权(被控端手动确认)", fromDeviceId, toDeviceId);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("连接请求 {} -> {} 携带未知鉴权类型 authType={}", fromDeviceId, toDeviceId, authType);
|
logger.warn("连接请求 {} -> {} 携带未知鉴权类型 authType={}", fromDeviceId, toDeviceId, authType);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info("连接请求 {} -> {} 未携带鉴权(走被控端手动确认)", fromDeviceId, toDeviceId);
|
// 未携带 authType 也视为免密连接(向后兼容),规范化为 NONE
|
||||||
|
message.setAuthType("NONE");
|
||||||
|
logger.info("连接请求 {} -> {} 未携带鉴权,按【免密连接】处理(被控端手动确认)", fromDeviceId, toDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 去重:短时间内重复 OFFER 直接忽略,避免被控端反复弹窗
|
// 2. 去重:短时间内重复 OFFER 直接忽略,避免被控端反复弹窗
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String selectedType = 'CODE';
|
String selectedType = 'NONE';
|
||||||
final valueController = TextEditingController();
|
final valueController = TextEditingController();
|
||||||
|
|
||||||
await showCupertinoDialog<void>(
|
await showCupertinoDialog<void>(
|
||||||
@@ -129,6 +129,10 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
CupertinoSegmentedControl<String>(
|
CupertinoSegmentedControl<String>(
|
||||||
children: const {
|
children: const {
|
||||||
|
'NONE': Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text('免密连接'),
|
||||||
|
),
|
||||||
'CODE': Padding(
|
'CODE': Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text('动态验证码'),
|
child: Text('动态验证码'),
|
||||||
@@ -142,12 +146,18 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
onValueChanged: (v) => setDialogState(() => selectedType = v),
|
onValueChanged: (v) => setDialogState(() => selectedType = v),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
CupertinoTextField(
|
if (selectedType == 'NONE')
|
||||||
controller: valueController,
|
const Text(
|
||||||
placeholder: '请输入验证码或密码',
|
'免密连接:被控端将弹出手动确认对话框,无需输入验证码或密码。',
|
||||||
obscureText: true,
|
style: TextStyle(fontSize: 13, color: CupertinoColors.systemGrey),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
)
|
||||||
),
|
else
|
||||||
|
CupertinoTextField(
|
||||||
|
controller: valueController,
|
||||||
|
placeholder: selectedType == 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码',
|
||||||
|
obscureText: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -159,12 +169,15 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
child: const Text('连接'),
|
child: const Text('连接'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final val = valueController.text.trim();
|
final val = valueController.text.trim();
|
||||||
if (val.isEmpty) {
|
if (selectedType != 'NONE' && val.isEmpty) {
|
||||||
_showAlert('请输入验证码或密码');
|
_showAlert('请输入验证码或密码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
_connect(authType: selectedType, authValue: val);
|
_connect(
|
||||||
|
authType: selectedType,
|
||||||
|
authValue: selectedType == 'NONE' ? '' : val,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user