WebRTC技术研究:Android WebView兼容性问题与Crossover框架实践
研究背景
2019年,随着电子签名和远程办公的快速发展,在线视频签约成为金融、法律、房地产等行业的重要需求。在我们的在线视频签约平台项目中,需要实现以下核心功能:
- 远程视频签约:支持签约双方实时音视频沟通并完成电子签名
- 身份验证:通过视频通话进行人脸识别和身份核验
- 合同签署:在视频见证下完成电子合同的签署流程
- 过程记录:录制完整的签约过程作为法律证据
项目技术栈背景:
- 前端:Vue.js + WebRTC + 电子签名SDK
- 移动端:Android WebView + Crossover框架
- 后端:Node.js + Socket.IO + WebRTC SFU服务器 + 区块链存证
WebRTC技术概述
1. WebRTC核心组件
视频签约场景的WebRTC架构:
┌─────────────────────────────────────┐
│ 视频签约应用界面 │
│ (合同展示、电子签名、视频通话) │
├─────────────────────────────────────┤
│ WebRTC APIs │
│ (媒体采集、PeerConnection、录制) │
├─────────────────────────────────────┤
│ 移动浏览器 │
│ (Android WebView) │
├─────────────────────────────────────┤
│ 网络传输 │
│ (STUN/TURN、ICE、网络自适应) │
└─────────────────────────────────────┘
核心API实现:
// 1. 视频签约专用媒体采集
async function getSigningMediaStream() {
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
facingMode: "user" // 自拍摄像头
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
// 签约场景需要高清晰度音频
latency: 0,
sampleRate: 48000
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// 添加屏幕共享用于合同展示(可选)
if (isContractSharingEnabled) {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "always" },
audio: false
});
// 合并媒体流
stream.addTrack(screenStream.getVideoTracks()[0]);
}
return stream;
} catch (error) {
console.error('获取签约媒体流失败:', error);
throw new Error('无法启动视频签约,请检查摄像头和麦克风权限');
}
}
// 2. 签约专用PeerConnection建立
function createSigningPeerConnection() {
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'signing-platform',
credential: 'secure-signing-key'
}
],
// 签约场景的特殊配置
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "all"
};
const pc = new RTCPeerConnection(configuration);
// 处理ICE候选
pc.onicecandidate = (event) => {
if (event.candidate) {
// 发送ICE候选到对方
sendToSignalingServer({
type: 'candidate',
candidate: event.candidate,
sessionId: getCurrentSessionId()
});
}
};
// 处理远程流
pc.ontrack = (event) => {
const remoteStream = event.streams[0];
// 显示签约对方视频
const videoElement = document.getElementById('counterpartyVideo');
videoElement.srcObject = remoteStream;
videoElement.play();
// 开始录制签约过程
if (isRecordingEnabled) {
startSessionRecording(remoteStream);
}
};
// 处理数据通道(用于签约状态同步)
pc.ondatachannel = (event) => {
const channel = event.channel;
setupSigningDataChannel(channel);
};
return pc;
}
// 3. 签约数据通道设置
function setupSigningDataChannel(channel) {
channel.onopen = () => {
console.log('签约数据通道已建立');
// 发送签约准备状态
channel.send(JSON.stringify({
type: 'signing_status',
status: 'ready',
timestamp: Date.now()
}));
};
channel.onmessage = (event) => {
const message = JSON.parse(event.data);
handleSigningMessage(message);
};
channel.onclose = () => {
console.log('签约数据通道已关闭');
handleSigninlose();
};
channel.onerror = (error) => {
console.error('签约数据通道错误:', error);
handleSigningError(error);
};
}
2. 视频签约信令服务器设计
签约专用信令协议:
class SigningSignalingClient {
constructor(serverUrl) {
this.ws = new WebSocket(serverUrl);
this.sessionId = null;
this.setupEventHandlers();
}
setupEventHandlers() {
this.ws.onopen = () => {
console.log('签约信令服务器连接成功');
this.authenticateUser();
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onerror = (error) => {
console.error('签约信令服务器错误:', error);
this.emit('signaling:error', error);
};
}
authenticateUser() {
// 发送用户身份验证
this.send({
type: 'authenticate',
userId: getCurrentUserId(),
sessionId: generateSessionId(),
timestamp: Date.now()
});
}
handleMessage(message) {
switch (message.type) {
case 'authenticated':
this.sessionId = message.sessionId;
this.emit('authenticated', message);
break;
case 'offer':
this.handleOffer(message);
break;
case 'answer':
this.handleAnswer(message);
break;
case 'candidate':
this.handleCandidate(message);
break;
case 'counterparty_joined':
this.emit('counterparty:joined', message);
break;
case 'contract_signed':
this.emit('contract:signed', message);
break;
case 'session_ended':
this.emit('session:ended', message);
break;
}
}
// 发送签约Offer
sendOffer(offer, contractInfo) {
this.send({
type: 'offer',
sdp: offer,
contractInfo: contractInfo,
sessionId: this.sessionId,
timestamp: Date.now()
});
}
// 发送签约Answer
sendAnswer(answer) {
this.send({
type: 'answer',
sdp: answer,
sessionId: this.sessionId,
timestamp: Date.now()
});
}
// 发送ICE Candidate
sendCandidate(candidate) {
this.send({
type: 'candidate',
candidate: candidate,
sessionId: this.sessionId
});
}
// 发送合同签署状态
sendContractStatus(status, documentId, signature) {
this.send({
type: 'contract_status',
status: status,
documentId: documentId,
signature: signature,
sessionId: this.sessionId,
timestamp: Date.now()
});
}
// 结束签约会话
endSession(reason) {
this.send({
type: 'end_session',
reason: reason,
sessionId: this.sessionId,
timestamp: Date.now()
});
}
}
Android WebView兼容性问题
1. 签约场景权限配置
问题分析: 在线视频签约对权限要求更高,需要确保摄像头、麦克风、存储等权限的正确配置。
解决方案:
class SigningWebRTCAwareWebView : WebView {
constructor(context: Context) : super(context) {
initWebView()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initWebView()
}
private fun initWebView() {
// 启用JavaScript和媒体权限
settings.javaScriptEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.domStorageEnabled = true
settings.databaseEnabled = true
// 签约场景特殊配置
settings.allowUniversalAccessFromFileURLs = true
settings.allowFileAccessFromFileURLs = true
// 设置签约专用用户代理
settings.userAgentString = "${settings.userAgentString} VideoSigningApp/1.0"
// 启用混合内容(HTTPS页面加载HTTP资源)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
webViewClient = SigningWebViewClient()
webChromeClient = SigningWebChromeClient()
}
private inner class SigningWebViewClient : WebViewClient() {
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
// 签约场景SSL处理
when (error?.primaryError) {
SslError.SSL_UNTRUSTED -> {
if (BuildConfig.DEBUG || isTrustedCertificate(error)) {
handler?.proceed()
} else {
showSigningSSLError("证书不受信任,无法保证签约安全")
handler?.cancel()
}
}
else -> {
showSigningSSLError("SSL证书验证失败")
handler?.cancel()
}
}
}
private fun isTrustedCertificate(error: SslError?): Boolean {
// 检查是否为签约平台信任的证书
return true // 实际实现中需要具体的证书验证逻辑
}
private fun showSigningSSLError(message: String) {
AlertDialog.Builder(context)
.setTitle("签约安全警告")
.setMessage(message)
.setPositiveButton("退出签约") { _, _ ->
// 结束签约会话
(context as Activity).finish()
}
.setNegativeButton("继续") { _, _ ->
// 在某些情况下允许继续
}
.show()
}
}
private inner class SigningWebChromeClient : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest?) {
val resources = request?.resources ?: return
val grantedResources = mutableListOf<String>()
for (resource in resources) {
when (resource) {
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> {
if (hasCameraPermission() && verifyCameraForSigning()) {
grantedResources.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
}
}
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> {
if (hasAudioPermission() && verifyAudioForSigning()) {
grantedResources.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE)
}
}
PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> {
if (hasMediaPermission()) {
grantedResources.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)
}
}
}
}
if (grantedResources.size == resources.size) {
request.grant(grantedResources.toTypedArray())
} else {
request.deny()
}
}
}
private fun verifyCameraForSigning(): Boolean {
// 验证摄像头是否适合签约场景
return try {
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraId = cameraManager.cameraIdList[0]
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
// 检查摄像头质量是否满足签约要求
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
capabilities?.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) == true
} catch (e: Exception) {
false
}
}
private fun verifyAudioForSigning(): Boolean {
// 验证麦克风是否适合签约场景
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
private fun hasMediaPermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
}
2. 签约场景WebRTC兼容性
问题:Android WebView对WebRTC支持不完整,影响签约体验
解决方案:
// 视频签约WebRTC兼容性检测
class VideoSigningWebRTCSupportChecker {
static checkSupport() {
const support = {
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
peerConnection: !!window.RTCPeerConnection,
dataChannel: !!window.RTCDataChannel,
mediaRecording: !!window.MediaRecorder,
screenCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
// 签约特殊需求
highQualityVideo: this.supportsHighQualityVideo(),
echoCancellation: this.supportsEchoCancellation(),
noiseSuppression: this.supportsNoiseSuppression()
};
return support;
}
static supportsHighQualityVideo() {
try {
const canvas = document.createElement('canvas');
canvas.width = 1920;
canvas.height = 1080;
return canvas.toDataURL().length > 0;
} catch (e) {
return false;
}
}
static supportsEchoCancellation() {
return typeof MediaStreamTrackProcessor !== 'undefined';
}
static supportsNoiseSuppression() {
return typeof AudioContext !== 'undefined';
}
static loadSigningPolyfills() {
// 加载签约专用polyfill
const scripts = [
'https://webrtc.github.io/adapter/adapter-latest.js',
'/js/signing-webrtc-polyfill.js' // 签约专用polyfill
];
scripts.forEach(src => {
const script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
}
}
// 签约专用WebRTC初始化
class VideoSigningWebRTCManager {
constructor() {
this.localStream = null;
this.remoteStream = null;
this.peerConnection = null;
this.signalingClient = null;
this.recordingManager = null;
this.contractManager = null;
}
async initialize() {
// 检查兼容性
const support = VideoSigningWebRTCSupportChecker.checkSupport();
if (!support.getUserMedia) {
throw new Error('您的设备不支持视频签约,请升级到最新版本的浏览器');
}
// 加载polyfill
VideoSigningWebRTCSupportChecker.loadSigningPolyfills();
// 初始化组件
await this.initializeSignaling();
await this.initializeContract();
}
async initializeSignaling() {
this.signalingClient = new SigningSignalingClient(this.getSignalingServerUrl());
this.signalingClient.on('authenticated', () => {
console.log('签约信令认证成功');
});
}
async initializeContract() {
this.contractManager = new ContractManager();
await this.contractManager.initialize();
}
async startSigningSession(contractInfo) {
try {
// 获取高质量媒体流
this.localStream = await this.getHighQualityMediaStream();
// 创建签约专用PeerConnection
this.peerConnection = this.createSigningPeerConnection();
// 添加本地流
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
// 创建Offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
// 发送签约邀请
this.signalingClient.sendOffer(offer, contractInfo);
return offer;
} catch (error) {
console.error('启动签约会话失败:', error);
throw new Error('无法启动视频签约会话');
}
}
async getHighQualityMediaStream() {
const constraints = {
video: {
width: { ideal: 1920, max: 1920 },
height: { ideal: 1080, max: 1080 },
frameRate: { ideal: 30, max: 30 },
facingMode: "user"
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
latency: 0,
sampleRate: 48000,
channelCount: 2
}
};
return await navigator.mediaDevices.getUserMedia(constraints);
}
}
Crossover框架集成
1. 签约场景Crossover配置
Android端签约功能集成:
class VideoSigningActivity : AppCompatActivity() {
private lateinit var webView: SigningWebRTCAwareWebView
private lateinit var crossover: Crossover
private lateinit var signingManager: VideoSigningManager
private var currentSessionId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_signing)
initWebView()
initCrossover()
initSigningManager()
loadSigningPage()
}
private fun initWebView() {
webView = findViewById(R.id.webView)
}
private fun initCrossover() {
crossover = Crossover.Builder()
.withWebView(webView)
.withLogging(BuildConfig.DEBUG)
.build()
registerSigningMethods()
}
private fun initSigningManager() {
signingManager = VideoSigningManager(this)
signingManager.setOnSessionStateChangeListener { state ->
handleSessionStateChange(state)
}
}
private fun registerSigningMethods() {
// 获取签约设备信息
crossover.register("getSigningDeviceInfo") { params ->
val deviceInfo = JSONObject().apply {
put("platform", "Android")
put("version", Build.VERSION.RELEASE)
put("model", Build.MODEL)
put("manufacturer", Build.MANUFACTURER)
put("isFrontCameraAvailable", isFrontCameraAvailable())
put("isAudioQualityGood", isAudioQualityGood())
}
CompletableFuture.completedFuture(deviceInfo)
}
// 检查签约权限
crossover.register("checkSigningPermissions") { params ->
val hasCamera = hasCameraPermission()
val hasAudio = hasAudioPermission()
val hasStorage = hasStoragePermission()
val result = JSONObject().apply {
put("camera", hasCamera)
put("audio", hasAudio)
put("storage", hasStorage)
put("allGranted", hasCamera && hasAudio && hasStorage)
}
CompletableFuture.completedFuture(result)
}
// 开始签约会话
crossover.register("startSigningSession") { params ->
val contractId = params.getString("contractId")
val sessionId = startSigningSession(contractId)
CompletableFuture.completedFuture(sessionId)
}
// 结束签约会话
crossover.register("endSigningSession") { params ->
endCurrentSession()
CompletableFuture.completedFuture(true)
}
// 电子签名集成
crossover.register("performElectronicSignature") { params ->
val documentId = params.getString("documentId")
val signatureData = params.getString("signatureData")
val result = performElectronicSignature(documentId, signatureData)
CompletableFuture.completedFuture(result)
}
// 身份验证
crossover.register("verifyIdentity") { params ->
val idCardNumber = params.getString("idCardNumber")
val realName = params.getString("realName")
val result = verifyIdentity(idCardNumber, realName)
CompletableFuture.completedFuture(result)
}
// 录制签约过程
crossover.register("startRecording") { params ->
val recordingPath = startSessionRecording()
CompletableFuture.completedFuture(recordingPath)
}
// 停止录制
crossover.register("stopRecording") { params ->
val recordingPath = stopSessionRecording()
CompletableFuture.completedFuture(recordingPath)
}
}
private fun startSigningSession(contractId: String): String {
currentSessionId = generateSessionId()
signingManager.startSession(currentSessionId!!, contractId)
return currentSessionId!!
}
private fun endCurrentSession() {
currentSessionId?.let { sessionId ->
signingManager.endSession(sessionId)
currentSessionId = null
}
}
private fun performElectronicSignature(documentId: String, signatureData: String): Boolean {
return try {
val signatureResult = signingManager.performSignature(documentId, signatureData)
crossover.call("onSignatureCompleted", mapOf(
"documentId" to documentId,
"success" to signatureResult.success,
"signatureId" to signatureResult.signatureId
))
signatureResult.success
} catch (e: Exception) {
crossover.call("onSignatureFailed", mapOf(
"documentId" to documentId,
"error" to e.message
))
false
}
}
private fun verifyIdentity(idCardNumber: String, realName: String): Boolean {
return try {
val verificationResult = signingManager.verifyIdentity(idCardNumber, realName)
crossover.call("onIdentityVerified", mapOf(
"success" to verificationResult.success,
"verifiedName" to verificationResult.verifiedName,
"confidence" to verificationResult.confidence
))
verificationResult.success
} catch (e: Exception) {
crossover.call("onIdentityVerificationFailed", mapOf(
"error" to e.message
))
false
}
}
private fun startSessionRecording(): String {
return signingManager.startRecording(currentSessionId!!)
}
private fun stopSessionRecording(): String {
return signingManager.stopRecording()
}
private fun handleSessionStateChange(state: SessionState) {
when (state) {
SessionState.CONNECTING -> {
showLoading("正在连接签约对方面...")
}
SessionState.CONNECTED -> {
hideLoading()
crossover.call("onCounterpartyConnected")
}
SessionState.FAILED -> {
showError("签约连接失败")
crossover.call("onSessionFailed")
}
SessionState.ENDED -> {
showSuccess("签约会话已结束")
crossover.call("onSessionEnded")
}
}
}
private fun isFrontCameraAvailable(): Boolean {
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
return try {
cameraManager.cameraIdList.any { id ->
val characteristics = cameraManager.getCameraCharacteristics(id)
val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)
lensFacing == CameraMetadata.LENS_FACING_FRONT
}
} catch (e: Exception) {
false
}
}
private fun isAudioQualityGood(): Boolean {
return true // 实际实现中需要具体的音频质量检测
}
private fun hasStoragePermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
private fun loadSigningPage() {
val contractId = intent.getStringExtra("contractId") ?: ""
val url = "https://your-signing-platform.com/signing?contractId=$contractId"
webView.loadUrl(url)
}
companion object {
private const val REQUEST_PERMISSIONS_CODE = 1001
}
}
JavaScript端签约功能:
class VideoSigningWebRTCManager {
constructor() {
this.crossover = null;
this.peerConnection = null;
this.localStream = null;
this.signalingClient = null;
this.contractInfo = null;
this.sessionId = null;
}
async initialize() {
// 初始化Crossover
this.crossover = await Crossover.initialize();
// 检查签约设备权限
const permissions = await this.crossover.call('checkSigningPermissions');
if (!permissions.allGranted) {
await this.requestSigningPermissions();
}
// 初始化信令客户端
this.signalingClient = new SigningSignalingClient(this.getSignalingServerUrl());
// 设置事件监听
this.setupEventListeners();
}
async requestSigningPermissions() {
try {
await this.crossover.call('requestSigningPermissions');
console.log('签约权限请求成功');
} catch (error) {
console.error('签约权限请求失败:', error);
throw new Error('无法获取签约所需的设备权限');
}
}
setupEventListeners() {
// 监听签约会话状态变化
this.crossover.on('onCounterpartyConnected', () => {
this.emit('counterparty:connected');
showSuccess('签约对方已连接');
});
this.crossover.on('onSessionFailed', () => {
this.emit('session:failed');
showError('签约连接失败,请重试');
});
this.crossover.on('onSessionEnded', () => {
this.emit('session:ended');
showSuccess('签约会话已结束');
});
// 监听签名完成事件
this.crossover.on('onSignatureCompleted', (data) => {
this.emit('signature:completed', data);
showSuccess('电子签名已完成');
});
this.crossover.on('onSignatureFailed', (data) => {
this.emit('signature:failed', data);
showError('电子签名失败: ' + data.error);
});
// 监听身份验证结果
this.crossover.on('onIdentityVerified', (data) => {
this.emit('identity:verified', data);
if (data.success) {
showSuccess(`身份验证通过 (${data.verifiedName})`);
}
});
this.crossover.on('onIdentityVerificationFailed', (data) => {
this.emit('identity:failed', data);
showError('身份验证失败: ' + data.error);
});
// 监听录制事件
this.crossover.on('onRecordingStarted', (data) => {
this.emit('recording:started', data);
showInfo('签约过程开始录制');
});
this.crossover.on('onRecordingStopped', (data) => {
this.emit('recording:stopped', data);
showSuccess('签约录制已完成');
});
}
async startSigningSession(contractInfo) {
try {
// 获取本地媒体流
this.localStream = await this.getSigningMediaStream();
// 创建PeerConnection
this.peerConnection = this.createSigningPeerConnection();
// 添加本地流
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
// 创建Offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
// 开始签约会话
this.sessionId = await this.crossover.call('startSigningSession', {
contractId: contractInfo.id
});
// 发送Offer到签约对方
this.signalingClient.sendOffer(offer, contractInfo);
return offer;
} catch (error) {
console.error('启动签约会话失败:', error);
throw error;
}
}
async performElectronicSignature(documentId, signatureData) {
try {
const result = await this.crossover.call('performElectronicSignature', {
documentId: documentId,
signatureData: signatureData
});
if (result) {
// 签名成功,继续后续流程
this.handleSignatureSuccess(documentId);
} else {
throw new Error('电子签名失败');
}
return result;
} catch (error) {
console.error('执行电子签名失败:', error);
throw error;
}
}
async verifyIdentity(idCardNumber, realName) {
try {
const result = await this.crossover.call('verifyIdentity', {
idCardNumber: idCardNumber,
realName: realName
});
return result;
} catch (error) {
console.error('身份验证失败:', error);
throw error;
}
}
async startSessionRecording() {
try {
const recordingPath = await this.crossover.call('startRecording');
return recordingPath;
} catch (error) {
console.error('开始录制失败:', error);
throw error;
}
}
async stopSessionRecording() {
try {
const recordingPath = await this.crossover.call('stopRecording');
return recordingPath;
} catch (error) {
console.error('停止录制失败:', error);
throw error;
}
}
endSigningSession() {
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
if (this.sessionId) {
this.crossover.call('endSigningSession');
this.sessionId = null;
}
}
}
SSL问题解决
1. 签约场景SSL证书配置
问题:视频签约涉及敏感信息,对SSL安全性要求极高
解决方案:
Android端签约SSL处理:
class SigningSSLUtils {
companion object {
fun getSigningSSLSocketFactory(): SSLSocketFactory {
try {
// 创建信任管理器
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
// 签约场景:严格验证客户端证书
validateClientCertificate(chain)
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
// 签约场景:严格验证服务器证书
validateServerCertificate(chain)
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
})
val sslContext = SSLContext.getInstance("TLSv1.2")
sslContext.init(null, trustAllCerts, SecureRandom())
return sslContext.socketFactory
} catch (e: Exception) {
throw RuntimeException("签约SSL配置失败: ${e.message}")
}
}
private fun validateClientCertificate(chain: Array<X509Certificate>) {
// 签约客户端证书验证逻辑
if (chain.isEmpty()) {
throw CertificateException("客户端证书链为空")
}
// 验证证书有效期
val cert = chain[0]
cert.checkValidity()
// 验证证书颁发机构
if (!isTrustedCA(cert.issuerDN.name)) {
throw CertificateException("客户端证书由不受信任的CA颁发")
}
}
private fun validateServerCertificate(chain: Array<X509Certificate>) {
// 签约服务器证书验证逻辑
if (chain.isEmpty()) {
throw CertificateException("服务器证书链为空")
}
// 验证证书有效期
val cert = chain[0]
cert.checkValidity()
// 验证证书域名匹配
if (!isDomainMatch(cert)) {
throw CertificateException("服务器证书域名不匹配")
}
// 验证证书链
validateCertificateChain(chain)
}
private fun isTrustedCA(issuerName: String): Boolean {
// 检查是否为签约平台信任的CA
val trustedCAs = listOf(
"CN=GlobalSign Root CA,OU=Root CA,O=GlobalSign nv-sa,C=BE",
"CN=DigiCert TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US"
)
return trustedCAs.any { trusted -> issuerName.contains(trusted) }
}
private fun isDomainMatch(cert: X509Certificate): Boolean {
// 验证证书域名是否匹配签约平台域名
val signingDomains = listOf(
"signing-platform.com",
"secure-signing.com",
"video-sign.com"
)
// 简化实现:检查Subject Alternative Name
return signingDomains.any { domain ->
cert.subjectDN.name.contains(domain)
}
}
private fun validateCertificateChain(chain: Array<X509Certificate>) {
// 验证证书链的完整性和可信度
for (i in 0 until chain.size - 1) {
try {
chain[i].verify(chain[i + 1].publicKey)
} catch (e: Exception) {
throw CertificateException("证书链验证失败: ${e.message}")
}
}
}
fun getSigningHostnameVerifier(): HostnameVerifier {
return HostnameVerifier { hostname, session ->
// 签约专用域名验证
val signingHostnames = setOf(
"signing-platform.com",
"secure-signing.com",
"video-sign.com",
"localhost" // 开发环境
)
signingHostnames.any { hostname.endsWith(it) }
}
}
}
}
签约WebView SSL错误处理:
class SigningWebViewClient : WebViewClient() {
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
when (error?.primaryError) {
SslError.SSL_UNTRUSTED -> {
// 签约场景:证书不受信任是严重安全问题
showSigningSecurityAlert(
"证书不受信任",
"检测到不受信任的SSL证书,这可能危及您的签约安全。请不要继续操作。"
)
handler?.cancel()
}
SslError.SSL_EXPIRED -> {
// 签约场景:过期证书不允许继续
showSigningSecurityAlert(
"证书已过期",
"SSL证书已过期,无法保证签约过程的安全性。"
)
handler?.cancel()
}
SslError.SSL_IDMISMATCH -> {
// 签约场景:域名不匹配需要严格处理
showSigningSecurityAlert(
"证书域名不匹配",
"SSL证书域名与签约平台域名不匹配,可能存在安全风险。"
)
handler?.cancel()
}
SslError.SSL_NOTYETVALID -> {
// 签约场景:证书未生效
showSigningSecurityAlert(
"证书无效",
"SSL证书尚未生效,无法建立安全连接。"
)
handler?.cancel()
}
else -> {
showSigningSecurityAlert(
"SSL证书错误",
"SSL证书验证过程中发生未知错误,为保证签约安全,请不要继续操作。"
)
handler?.cancel()
}
}
}
private fun showSigningSecurityAlert(title: String, message: String) {
AlertDialog.Builder(view.context)
.setTitle("签约安全警告")
.setIcon(R.drawable.ic_security_warning)
.setMessage(message)
.setPositiveButton("退出签约") { _, _ ->
// 记录安全事件
SecurityLogger.logSSLError(title, message)
// 结束签约会话
(view.context as Activity).finish()
}
.setNegativeButton("查看证书") { _, _ ->
// 显示证书详细信息
showCertificateDetails()
}
.setCancelable(false) // 签约场景不允许忽略安全警告
.show()
}
private fun showCertificateDetails() {
// 显示SSL证书详细信息的对话框
// 包括证书颁发者、有效期、域名等信息
}
}
2. 签约数据加密
问题:签约过程中的敏感数据需要额外加密保护
解决方案:
class SigningDataEncryption {
companion object {
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val KEY_SIZE = 256
private const val IV_SIZE = 12
fun encryptSignData(data: String, sessionId: String): String {
return try {
val key = generateSessionKey(sessionId)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = generateIv()
cipher.init(Cipher.ENCRYPT_MODE, key, iv)
val encryptedData = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val ivBytes = iv.iv
// 将IV和加密数据合并
val combinedData = ByteArray(ivBytes.size + encryptedData.size)
System.arraycopy(ivBytes, 0, combinedData, 0, ivBytes.size)
System.arraycopy(encryptedData, 0, combinedData, ivBytes.size, encryptedData.size)
// Base64编码
android.util.Base64.encodeToString(combinedData, android.util.Base64.DEFAULT)
} catch (e: Exception) {
throw EncryptionException("签约数据加密失败", e)
}
}
fun decryptSignData(encryptedData: String, sessionId: String): String {
return try {
val combinedData = android.util.Base64.decode(encryptedData, android.util.Base64.DEFAULT)
val key = generateSessionKey(sessionId)
// 提取IV
val iv = IvParameterSpec(combinedData, 0, IV_SIZE / 8)
// 提取加密数据
val encryptedBytes = ByteArray(combinedData.size - IV_SIZE / 8)
System.arraycopy(combinedData, IV_SIZE / 8, encryptedBytes, 0, encryptedBytes.size)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, key, iv)
val decryptedData = cipher.doFinal(encryptedBytes)
String(decryptedData, Charsets.UTF_8)
} catch (e: Exception) {
throw DecryptionException("签约数据解密失败", e)
}
}
private fun generateSessionKey(sessionId: String): SecretKey {
val keyMaterial = ("SigningKey" + sessionId + BuildConfig.SIGNING_SECRET_KEY).toByteArray(Charsets.UTF_8)
val sha256 = MessageDigest.getInstance("SHA-256")
val keyHash = sha256.digest(keyMaterial)
val keyBytes = ByteArray(KEY_SIZE / 8)
System.arraycopy(keyHash, 0, keyBytes, 0, keyBytes.size)
return SecretKeySpec(keyBytes, "AES")
}
private fun generateIv(): SecureRandom {
val iv = ByteArray(IV_SIZE / 8)
SecureRandom().nextBytes(iv)
return SecureRandom(iv)
}
}
}
// 签约异常类
class EncryptionException(message: String, cause: Throwable) : Exception(message, cause)
class DecryptionException(message: String, cause: Throwable) : Exception(message, cause)
实际应用案例
1. 在线视频签约平台集成
签约平台架构:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Web前端 │ │ Android App │ │ 签约后端 │
│ (Vue.js) │<──>│ (WebView) │<──>│ (Node.js) │
│ │ │ │ │ │
│ • 合同展示 │ │ • WebView容器 │ │ • 信令服务器 │
│ • 视频签约 │ │ • Crossover桥接 │ │ • 区块链存证 │
│ • 电子签名 │ │ • 权限管理 │ │ • 身份验证 │
│ • 录制回放 │ │ • SSL安全处理 │ │ • 数据加密 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
核心签约流程:
class VideoSigningWorkflow {
private var signingSession: SigningSession? = null
private var contractManager: ContractManager? = null
private var identityVerifier: IdentityVerifier? = null
private var recordingManager: RecordingManager? = null
suspend fun startCompleteSigningProcess(contractId: String, userId: String) {
try {
// 1. 合同验证
val contract = verifyContract(contractId)
// 2. 用户身份验证
val identityResult = verifyUserIdentity(userId)
// 3. 启动视频签约会话
val session = startVideoSigningSession(contract, userId)
// 4. 视频验证和沟通
val videoVerificationResult = performVideoVerification(session)
// 5. 电子签名
val signatureResult = performElectronicSignature(session, contract)
// 6. 录制和存证
val evidenceResult = createSigningEvidence(session, contract)
// 7. 完成签约
completeSigningProcess(session, evidenceResult)
} catch (e: Exception) {
handleSigningError(e)
throw e
}
}
private suspend fun verifyContract(contractId: String): Contract {
// 合同真实性验证
return contractManager?.verifyContract(contractId)
?: throw IllegalStateException("合同管理器未初始化")
}
private suspend fun verifyUserIdentity(userId: String): IdentityVerificationResult {
// 用户身份验证
return identityVerifier?.verifyIdentity(userId)
?: throw IllegalStateException("身份验证器未初始化")
}
private suspend fun startVideoSigningSession(contract: Contract, userId: String): SigningSession {
// 启动视频签约会话
val session = SigningSession(
sessionId = generateSessionId(),
contract = contract,
userId = userId,
startTime = System.currentTimeMillis()
)
// 通知前端开始签约
notifyFrontend("signing:session:started", session)
return session
}
private suspend fun performVideoVerification(session: SigningSession): VideoVerificationResult {
// 视频身份验证
return identityVerifier?.performVideoVerification(session.sessionId)
?: throw IllegalStateException("身份验证器未初始化")
}
private suspend fun performElectronicSignature(session: SigningSession, contract: Contract): SignatureResult {
// 电子签名流程
val signatureData = collectSignatureData()
val signatureResult = signingManager.performSignature(contract.id, signatureData)
// 通知前端签名完成
notifyFrontend("signing:signature:completed", signatureResult)
return signatureResult
}
private suspend fun createSigningEvidence(session: SigningSession, contract: Contract): SigningEvidence {
// 创建签约证据
val recordingPath = recordingManager?.stopRecording() ?: ""
return SigningEvidence(
sessionId = session.sessionId,
contractId = contract.id,
recordingPath = recordingPath,
signatureId = session.signatureResult?.signatureId,
verificationResult = session.videoVerificationResult,
timestamp = System.currentTimeMillis(),
blockchainHash = createBlockchainEvidence(recordingPath)
)
}
private suspend fun completeSigningProcess(session: SigningSession, evidence: SigningEvidence) {
// 完成签约流程
session.endTime = System.currentTimeMillis()
session.evidence = evidence
// 保存签约记录
saveSigningRecord(session)
// 通知前端签约完成
notifyFrontend("signing:completed", session)
// 发送确认邮件/短信
sendConfirmationNotification(session)
}
}
2. 性能优化
签约场景性能调优:
class VideoSigningPerformanceOptimizer {
companion object {
fun optimizeSigningPeerConnectionConfig(): PeerConnectionConfiguration {
return PeerConnectionConfiguration().apply {
// 签约场景的ICE配置优化
iceServers = listOf(
IceServer("stun:stun.l.google.com:19302"),
IceServer("turn:signing-turn-server.com:3478", "signing-platform", "secure-turn-key")
)
// 签约场景的传输策略
bundlePolicy = BundlePolicy.MAX_BUNDLE
rtcpMuxPolicy = RtcpMuxPolicy.REQUIRE
// 签约场景的编码器配置
videoCodec = VideoCodec.H264 // 签约场景优先选择H264保证兼容性
audioCodec = AudioCodec.OPUS
// 签约场景的带宽限制
maxVideoBitrate = 1500 // kbps - 签约需要更高清晰度
maxAudioBitrate = 256 // kbps - 签约需要更高音频质量
// 签约场景的延迟优化
enableDscp = true
enableCpuOveruseDetection = true
enableVideoFrameDrop = false // 签约不允许丢帧
}
}
fun optimizeSigningMediaConstraints(): MediaConstraints {
return MediaConstraints().apply {
// 签约场景的视频约束
video = MediaConstraints.VideoConstraints().apply {
minWidth = 1280
maxWidth = 1920
minHeight = 720
maxHeight = 1080
minFrameRate = 24
maxFrameRate = 30
// 签约场景需要更高的视频质量
enableHighQuality = true
}
// 签约场景的音频约束
audio = MediaConstraints.AudioConstraints().apply {
echoCancellation = true
noiseSuppression = true
autoGainControl = true
highPassFilter = true
// 签约场景的特殊音频处理
enableNoiseSuppression = true
enableEchoCancellation = true
}
}
}
fun optimizeSigningNetworkConfiguration(): NetworkConfiguration {
return NetworkConfiguration().apply {
// 签约场景的网络超时配置
connectionTimeout = 15000 // 15秒 - 签约需要更长的连接时间
signalingTimeout = 10000 // 10秒
iceCandidateTimeout = 30000 // 30秒 - 签约需要更长的ICE收集时间
// 签约场景的重连策略
maxReconnectionAttempts = 5 // 签约允许更多重连尝试
reconnectionDelay = 3000 // 3秒重连间隔
enableFastReconnection = true
}
}
}
}
技术栈总结(2019年11月)
前端技术栈:
Vue.js 2.6
WebRTC API
Socket.IO 2.3
Crossover 1.0
电子签名SDK
Android技术栈:
Android SDK 28 (9.0)
WebView + WebRTC
Crossover Bridge
OkHttp 3.14
Retrofit 2.6
安全加密库
后端技术栈:
Node.js 12.x
WebRTC SFU Server
Socket.IO Server
区块链存证服务
身份验证服务
经验总结
1. 技术收获
WebRTC深度理解:
- 掌握了WebRTC在金融级应用中的安全要求
- 理解了视频签约场景的特殊性能需求
- 学习了移动端WebRTC的安全配置最佳实践
移动端WebRTC实践:
- 解决了Android WebView中金融级应用的兼容性问题
- 掌握了Crossover框架在敏感业务场景中的使用
- 学习了移动端音视频处理的安全最佳实践
安全和合规:
- 理解了金融级应用的SSL/TLS安全要求
- 掌握了敏感数据的加密传输和存储
- 学习了区块链存证技术的应用
2. 项目价值
技术突破:
- 成功在Android WebView中实现了金融级的视频签约功能
- 解决了移动端WebRTC在金融场景中的安全性和合规性问题
- 建立了完整的视频签约技术栈和安全体系
业务价值:
- 为金融、法律、房地产等行业提供了安全可靠的在线签约解决方案
- 大幅降低了传统签约的物理成本和时间成本
- 提升了用户体验和业务处理效率
3. 最佳实践
// 视频签约最佳实践总结
object VideoSigningBestPractices {
// 1. 安全要求
const val MIN_SSL_VERSION = "TLSv1.2"
const val MIN_VIDEO_QUALITY = "720p"
const val MIN_AUDIO_QUALITY = "48kHz"
const val MAX_SESSION_TIMEOUT = 30 * 60 * 1000 // 30分钟
// 2. 性能要求
const val MAX_CONNECTION_DELAY = 5000 // 5秒
const val MAX_VIDEO_BITRATE = 1500 // kbps
const val MAX_AUDIO_BITRATE = 256 // kbps
// 3. 合规要求
val REQUIRED_DOCUMENTS = setOf("身份证", "合同", "签名")
val REQUIRED_VERIFICATIONS = setOf("人脸识别", "活体检测", "身份核验")
val REQUIRED_EVIDENCES = setOf("视频录制", "区块链存证", "操作日志")
// 4. 错误处理
val CRITICAL_ERRORS = setOf(
"SSL_CERTIFICATE_ERROR",
"IDENTITY_VERIFICATION_FAILED",
"CONNECTION_SECURITY_BREACH",
"ELECTRONIC_SIGNATURE_FAILED"
)
}
后续规划
- 技术升级:跟进WebRTC最新标准和金融安全规范
- 功能扩展:支持多签、批量签约、智能合同等高级功能
- 合规认证:通过等保三级、GDPR等安全合规认证
- AI集成:集成AI能力如智能风控、语音识别、文档识别等
- 区块链深化:深化区块链技术在电子存证中的应用
总结
2019年的WebRTC视频签约技术研究是一次非常有价值的技术探索。通过这次实践,我们不仅解决了Android WebView中WebRTC的兼容性问题,还深入理解了金融级应用的安全要求和技术实现。
技术突破:
- 成功实现了移动端金融级视频签约的完整功能
- 解决了Crossover框架在金融场景中的SSL安全配置问题
- 建立了完整的视频签约安全技术栈
经验积累:
- 掌握了WebRTC在金融级应用中的安全最佳实践
- 学习了移动端音视频处理的安全合规要求
- 积累了金融级应用开发的宝贵经验
这次WebRTC视频签约研究为后续的金融科技项目奠定了坚实的技术基础,也让我对安全合规的实时通信技术有了更深的认识。
WebRTC视频签约研究感悟:
- 金融级应用对安全性的要求远超普通应用
- 技术实现必须兼顾功能性和合规性
- 用户信任建立在技术安全的基础之上
- 持续学习和适应安全标准的变化是必要的