From 293183266e941692005262d76f0bcff05d6384d6 Mon Sep 17 00:00:00 2001
From: 18034727885 <973609879@qq.com>
Date: 星期六, 23 五月 2026 18:13:28 +0800
Subject: [PATCH] 修复安全漏洞问题 登录登出优化

---
 app/build.gradle.kts                                                              |   15 
 app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt         |   11 
 app/src/main/res/layout/activity_find_psw.xml                                     |    2 
 common/src/main/java/com/xw/common/CommonDialog.kt                                |   40 +++
 app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt                        |  174 +++++++++++++++
 app/src/main/java/com/xw/yunwei/net/UserApi.kt                                    |    4 
 app/src/main/java/com/xw/yunwei/MyApp.kt                                          |   12 
 gradle/libs.versions.toml                                                         |    2 
 app/src/main/java/com/xw/yunwei/net/UserRepository.kt                             |    3 
 app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt                            |  209 +++++++++++++++++++
 app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt                   |   15 -
 app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt                          |   88 ++++---
 common/src/main/java/com/xw/common/utils/MMKVManager.kt                           |    1 
 app/src/main/java/com/xw/yunwei/model/LoginModel.kt                               |    9 
 app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt                         |   21 +
 app/src/main/java/com/xw/yunwei/push/MessageHandler.kt                            |   34 +-
 app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt |    8 
 17 files changed, 558 insertions(+), 90 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a4fe101..c786d2b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -10,9 +10,6 @@
 }
 
 val APPLICATION_ID = "com.xw.yunwei"
-// 阿里云推送
-val PUSH_KEY = "335666352"
-val PUSH_SECRET = "0252ebde2dc84f63aebd41f757f213d7"
 
 android {
     namespace = APPLICATION_ID
@@ -30,9 +27,6 @@
             // 选择要添加的对应cpu类型的.so库。此处仅为示意,推送支持所有主流类型,请根据实际需求选择
             abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))//, "x86", "x86_64"
         }
-
-        buildConfigField("String", "PUSH_KEY", "\"${PUSH_KEY}\"")
-        buildConfigField("String", "PUSH_SECRET", "\"${PUSH_SECRET}\"")
 
     }
 
@@ -86,15 +80,15 @@
             manifestPlaceholders["APP_NAME"] = "@string/app_name_dev"
             manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
 
-            buildConfigField("String", "DEV_API_HOST", "\"http://221.182.45.100:8000/\"")
+            buildConfigField("String", "DEV_API_HOST", "\"\"")
             buildConfigField("String", "API_HOST", "\"http://rt-admin.test.xiaopeng.local/\"")
             buildConfigField("String", "WS_URL", "\"ws://221.182.45.100:8888/websocket\"")
             //翼虎appId
-            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin-test\"")
+            buildConfigField("String", "YIHU_APPID", "\"RoboTaxi-admin\"")
             //翼虎appSecret
-            buildConfigField("String", "YIHU_APP_SECRET", "\"SXa7vEe7V23Xh3RN\"")
+            buildConfigField("String", "YIHU_APP_SECRET", "\"9UvdqMuZRRTNcpeA\"")
             //翼虎公钥
-            buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjT9Pl5kqJeinJyAjvW95ulxtON34ABhX pTRiPczVRJSVSTQzqLyr6UNRU6AvR2diZ4eGRHbeJ+NNtiuSverAZ+xu4B05xWhERThV0SETigkR 9IaJ9glucmAL6S6CNE5JCwWYYDfCgSIN3fzWhC48hGwEL8nauBIHrg2TRs0x4/rtmUR5oEMrm6RA eafdHDD62Dtsc7zm7uIxaOfn90cG35DKsp3iIcw7BLFT/HmBSVML2SLTruNkgDhH2qIZ1t6YQEJj C0uO+jiHXoluKRLWDMg9+SuoJHrsntCzRdwx246g6nMKfxtyC/jrPB6KUdrSR9xaxXZPcNQLb0ip y6mTkwIDAQAB\"")
+            buildConfigField("String", "YIHU_PUBLIC_KEY", "\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAko2oXAtrtl2BGUZjjDZBbEtbYd8pddah hqTbQ68F59no7eZGdfE4PQyKnPZTJSR0XApwydqB6nKEID58bej3F7p/2mL7i/s9+9zdepMSpg9b 1nTi730Fs1pzEalew7V839/Wg/RPFr6xZEqU8oaKWT6fe4anRAOI7aCgA8CxEB8YwQvrMTYetFnE QRGHlziKKDHmurKXToGdu2SSXzYLy8Cl2X79z3rBUBQpMZI63g93wcRIToSP8DUkbQFY64cXa6cT ZyBrt72K2Dx4IfuLb7K1BZbZxXswN/Lx4rQ7anQ8HFtA5bE7sNznj16pgoI3aUKjVm45JWtxTAWa XrSwTQIDAQAB\"")
             //翼虎网络域名
             buildConfigField("String", "YIHU_API_HOST", "\"https://sso.test.xiaopeng.com/sso/\"")
         }
@@ -170,5 +164,6 @@
     implementation(project(":common"))
     implementation(libs.dialog.pickerview)
     implementation(libs.huawei.agconnect.core)
+    implementation(libs.jsoup)
 }
 
diff --git a/app/src/main/java/com/xw/yunwei/MyApp.kt b/app/src/main/java/com/xw/yunwei/MyApp.kt
index 0cf6693..90fad43 100644
--- a/app/src/main/java/com/xw/yunwei/MyApp.kt
+++ b/app/src/main/java/com/xw/yunwei/MyApp.kt
@@ -26,6 +26,7 @@
 import com.xw.common.custom.ClassicsFooter11
 import com.xw.common.custom.ClassicsHeader11
 import com.xw.common.utils.ActivityManager
+import com.xw.common.utils.KeyPreferences
 import com.xw.common.utils.LogUtils
 import com.xw.common.utils.LogUtils.useThreeLog
 import com.xw.common.utils.MMKVManager
@@ -33,12 +34,15 @@
 import com.xw.yunwei.di.networkModule
 import com.xw.yunwei.model.AppPermissionModel
 import com.xw.yunwei.model.TaskModel
+import com.xw.yunwei.model.ThirdInfo
+import com.xw.yunwei.model.UserInfo
 import com.xw.yunwei.model.event.TaskEvent
 import com.xw.yunwei.net.TaskRepository
 import com.xw.yunwei.ui.dialog.ApplyForReassignReasonDialog
 import com.xw.yunwei.ui.dialog.NewTaskDialog
 import com.xw.yunwei.ui.task.accident.detail.AccidentDetailActivity
 import com.xw.yunwei.ui.task.normal.NormalTaskDetailActivity
+import com.xw.yunwei.utils.DeviceId
 import com.xw.yunwei.utils.TTSController
 import com.xw.yunwei.websocket.WebSocketManager
 import kotlinx.coroutines.CompletableDeferred
@@ -50,7 +54,6 @@
 import org.koin.android.ext.android.inject
 import org.koin.android.ext.koin.androidContext
 import org.koin.core.context.GlobalContext.startKoin
-import com.xw.yunwei.utils.DeviceId
 
 class MyApp : Application() {
 
@@ -222,11 +225,12 @@
         })
     }
 
-    private fun initPush() {
+    fun initPush() {
+        val third = MMKVManager.getInstance().getModel<ThirdInfo?>(KeyPreferences.THIRD) ?: return
         val pushInitConfig = PushInitConfig.Builder()
             .application(this)
-            .appKey(BuildConfig.PUSH_KEY)
-            .appSecret(BuildConfig.PUSH_SECRET)
+            .appKey(third.pushKey)
+            .appSecret(third.pushSecret)
             .build()
 
         PushServiceFactory.init(pushInitConfig)
diff --git a/app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt b/app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt
index 1225b4e..2c5a3b0 100644
--- a/app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt
+++ b/app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt
@@ -1,6 +1,14 @@
 package com.xw.yunwei.extension
 
+import android.util.Log
 import android.view.View
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import com.xw.yunwei.utils.HtmlSanitizer
+import java.net.HttpURLConnection
+import java.net.URL
 
 //扩展函数 设置防重复点击监听器
 fun View.setOnDurationClickListener(duration: Long = 1000L, listener: View.OnClickListener) {
@@ -12,4 +20,170 @@
             listener.onClick(it)
         }
     }
+}
+
+//扩展函数 网页加载控件安全设置
+fun WebView.setSafeSetting() {
+    apply {
+        settings.allowFileAccess = false //设置不可以访问文件
+        webViewClient = object : WebViewClient() {
+            override fun shouldInterceptRequest(
+                view: WebView,
+                request: WebResourceRequest
+            ): WebResourceResponse? {
+                val url = request.url.toString()
+                //访问文件危险操作
+                val dangerousOperation = listOf("file://", "content://", "android_asset")
+                // 阻止文件访问
+                if (dangerousOperation.any { url.contains(it) }) {
+                    return null // 或返回空响应
+                }
+                // 只对HTML文档添加CSP
+                if (isHtmlDocument(request)) {
+                    return getResponseWithCSP(request)
+                }
+                return super.shouldInterceptRequest(view, request)
+            }
+
+            private fun isHtmlDocument(request: WebResourceRequest): Boolean {
+                val url = request.url.toString()
+                val contentType = request.requestHeaders?.get("Accept")
+                val urlPatterns = listOf(".html", ".htm", ".php", ".jsp", ".asp")
+
+                return urlPatterns.any { url.endsWith(it) } ||
+                        contentType?.contains("text/html") == true
+            }
+
+            private fun getResponseWithCSP(request: WebResourceRequest): WebResourceResponse? {
+                try {
+                    val urlConnection =
+                        URL(request.url.toString()).openConnection() as HttpURLConnection
+
+                    // 设置请求头
+                    request.requestHeaders?.forEach { (key, value) ->
+                        urlConnection.setRequestProperty(key, value)
+                    }
+
+                    urlConnection.requestMethod = request.method
+                    val responseCode = urlConnection.responseCode
+
+                    if (responseCode == HttpURLConnection.HTTP_OK) {
+                        val mimeType =
+                            urlConnection.contentType?.split(";")?.firstOrNull() ?: "text/html"
+                        val encoding = urlConnection.contentEncoding ?: "UTF-8"
+                        val inputStream = urlConnection.inputStream
+
+                        // 创建带有CSP头的响应
+                        val headers = mutableMapOf<String, String>()
+
+                        // 收集原始响应头
+                        urlConnection.headerFields.forEach { (key, valueList) ->
+                            if (key != null && valueList != null && valueList.isNotEmpty()) {
+                                headers[key] = valueList.joinToString(", ")
+                            }
+                        }
+
+                        // 添加或覆盖CSP头
+                        headers["Content-Security-Policy"] = buildCSPPolicy()
+
+                        return WebResourceResponse(
+                            mimeType, encoding, responseCode,
+                            urlConnection.responseMessage, headers, inputStream
+                        )
+                    }
+                } catch (e: Exception) {
+                    e.printStackTrace()
+                    Log.e("CSP", "Failed to add CSP header")
+                }
+                return null
+            }
+
+            private fun buildCSPPolicy(): String {
+                return """
+            default-src 'self';
+            script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted-cdn.com;
+            style-src 'self' 'unsafe-inline' https://trusted-cdn.com;
+            img-src 'self' data: https:;
+            font-src 'self' https://fonts.gstatic.com;
+            connect-src 'self' https://api.example.com;
+            frame-src 'self' https://trusted-iframe.com;
+            report-uri /csp-report-endpoint
+        """.trimIndent().replace("\n", " ")
+            }
+
+            override fun onPageFinished(view: WebView?, url: String?) {
+                super.onPageFinished(view, url)
+                // 注入安全监控脚本
+                injectSecurityMonitor(view)
+            }
+
+            private fun injectSecurityMonitor(view: WebView?) {
+                val securityScript = """
+                (function() {
+                    'use strict';
+                    
+                    // 监控DOM变化
+                    const observer = new MutationObserver(function(mutations) {
+                        mutations.forEach(function(mutation) {
+                            mutation.addedNodes.forEach(function(node) {
+                                if (node.nodeName === 'SCRIPT') {
+                                    console.warn('Dynamic script added:', node.src || node.textContent.substring(0, 100));
+                                }
+                                if (node.nodeName === 'IFRAME') {
+                                    console.warn('Dynamic iframe added:', node.src);
+                                }
+                            });
+                        });
+                    });
+                    
+                    observer.observe(document.documentElement, {
+                        childList: true,
+                        subtree: true
+                    });
+                    
+                    // 拦截危险API调用
+                    const originalFetch = window.fetch;
+                    window.fetch = function() {
+                        const url = arguments[0];
+                        if (typeof url === 'string') {
+                            if (url.startsWith('file://') || url.includes('localhost')) {
+                                console.error('Blocked fetch to local file:', url);
+                                return Promise.reject(new Error('Access to local files is blocked'));
+                            }
+                        }
+                        return originalFetch.apply(this, arguments);
+                    };
+                    
+                    // 监控表单提交
+                    document.addEventListener('submit', function(e) {
+                        const form = e.target;
+                        const action = form.getAttribute('action') || '';
+                        if (action.startsWith('file://')) {
+                            e.preventDefault();
+                            console.error('Blocked form submission to file:', action);
+                        }
+                    });
+                })();
+            """.trimIndent()
+                view?.evaluateJavascript(securityScript, null)
+            }
+        }
+    }
+}
+
+//扩展函数 安全加载本地数据
+fun WebView.safeLoadDataWithBaseURL(
+    baseUrl: String?,
+    data: String,
+    mimeType: String?,
+    encoding: String?,
+    historyUrl: String?
+) {
+    loadDataWithBaseURL(
+        baseUrl,
+        HtmlSanitizer().sanitizeUserContent(data),
+        mimeType,
+        encoding,
+        historyUrl
+    )
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/xw/yunwei/model/LoginModel.kt b/app/src/main/java/com/xw/yunwei/model/LoginModel.kt
index 8ffd8d9..889e177 100644
--- a/app/src/main/java/com/xw/yunwei/model/LoginModel.kt
+++ b/app/src/main/java/com/xw/yunwei/model/LoginModel.kt
@@ -2,7 +2,8 @@
 
 data class LoginModel(
     val info: Info?,
-    val token: Token?
+    val token: Token?,
+    val thirdInfo: ThirdInfo?
 )
 
 data class Info(
@@ -44,6 +45,12 @@
     val updateTime: String?
 )
 
+//第三方sdk信息
+data class ThirdInfo(
+    val pushKey: String,//阿里推送appKey
+    val pushSecret: String,//阿里推送appSecret
+)
+
 /**
  * 用户信息实体类
  * 包含用户基本信息、所属车队、角色列表等完整信息
diff --git a/app/src/main/java/com/xw/yunwei/net/UserApi.kt b/app/src/main/java/com/xw/yunwei/net/UserApi.kt
index 8c587fc..11cf942 100644
--- a/app/src/main/java/com/xw/yunwei/net/UserApi.kt
+++ b/app/src/main/java/com/xw/yunwei/net/UserApi.kt
@@ -111,4 +111,8 @@
     /*个人中心-查询证件上传详情*/
     @POST("account/maintenance-app/api/v1/getCertificate")
     fun getCertificate(): Call<HttpResponse<CertificateAuditInfo>>
+
+    /*个人中心-退出登录*/
+    @POST("account/maintenance-app/ssoAuthSdk/appLoginOut")
+    fun appLoginOut(): Call<HttpResponse<Any>>
 }
diff --git a/app/src/main/java/com/xw/yunwei/net/UserRepository.kt b/app/src/main/java/com/xw/yunwei/net/UserRepository.kt
index 0ece703..05a1754 100644
--- a/app/src/main/java/com/xw/yunwei/net/UserRepository.kt
+++ b/app/src/main/java/com/xw/yunwei/net/UserRepository.kt
@@ -89,4 +89,7 @@
 
     /*个人中心-查询证件上传详情*/
     suspend fun getCertificate() = api.getCertificate().interceptAwait()
+
+    /*个人中心-退出登录*/
+    suspend fun appLoginOut() = api.appLoginOut().interceptAwait()
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/xw/yunwei/push/MessageHandler.kt b/app/src/main/java/com/xw/yunwei/push/MessageHandler.kt
index b7f687f..9487c3b 100644
--- a/app/src/main/java/com/xw/yunwei/push/MessageHandler.kt
+++ b/app/src/main/java/com/xw/yunwei/push/MessageHandler.kt
@@ -10,7 +10,6 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.databinding.DataBindingUtil
 import androidx.lifecycle.lifecycleScope
-import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.customview.customView
 import com.afollestad.materialdialogs.customview.getCustomView
 import com.google.android.material.tabs.TabLayout
@@ -21,6 +20,8 @@
 import com.xw.common.utils.ToastUtilCommon
 import com.xw.yunwei.MyApp
 import com.xw.yunwei.databinding.DialogProtocolBinding
+import com.xw.yunwei.extension.safeLoadDataWithBaseURL
+import com.xw.yunwei.extension.setSafeSetting
 import com.xw.yunwei.model.SwitchTabMain
 import com.xw.yunwei.model.SwitchTabTask
 import com.xw.yunwei.net.UserRepository
@@ -38,7 +39,6 @@
 import org.json.JSONObject
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
-import kotlin.getValue
 
 object MessageHandler : KoinComponent {
     val userRepository: UserRepository by inject()
@@ -48,7 +48,7 @@
             handlePushEvent(JSONObject(content ?: ""), isPush)
         } catch (e: Exception) {
             ToastUtilCommon.showError("未获取到消息详情")
-            LogUtils.e("zmc-MessageHandler","handlePushEvent content = $content error = $e")
+            LogUtils.e("zmc-MessageHandler", "handlePushEvent content = $content error = $e")
         }
     }
 
@@ -60,7 +60,7 @@
 
     @OptIn(DelicateCoroutinesApi::class)
     fun handlePushEvent(jsob: JSONObject, isPush: Boolean) {
-        LogUtils.i("zmc-MessageHandler","handlePushEvent $jsob isPush = $isPush")
+        LogUtils.i("zmc-MessageHandler", "handlePushEvent $jsob isPush = $isPush")
         TaskVM.needRefresh.value = true
         /**
          * content:消息内容
@@ -82,10 +82,15 @@
         }
     }
 
-    private fun handleMsg(currentShowActivity: Activity, strategy: Int, id: String, isPush: Boolean) {
+    private fun handleMsg(
+        currentShowActivity: Activity,
+        strategy: Int,
+        id: String,
+        isPush: Boolean
+    ) {
         when (strategy) {
             1, 2, 3, 5 -> { // 1=新增事故,2=事故即将超时提示,3=事故超时提示,5=事故状态变更 点击都跳事故详情
-                if(strategy == 5 && isPush) return
+                if (strategy == 5 && isPush) return
                 currentShowActivity.startActivity(
                     Intent(currentShowActivity, AccidentDetailActivity::class.java).apply {
                         putExtra("taskId", id.toLong())
@@ -120,7 +125,7 @@
             }
 
             8, 12 -> { // 升级
-                if(strategy == 8 && isPush) return
+                if (strategy == 8 && isPush) return
                 showUpdateDialog(currentShowActivity)
             }
 
@@ -177,9 +182,12 @@
 //                    val tab1 = bind.protocolLayout.tabLayout.newTab().apply { text = "用户协议" }
 //                    val tab2 = bind.protocolLayout.tabLayout.newTab().apply { text = "隐私条款" }
 
-                    val tab1 = bind.protocolLayout.tabLayout.newTab().apply { text = "小鹏Robotaxi...隐私政策" }
-                    val tab2 = bind.protocolLayout.tabLayout.newTab().apply { text = "第三方共享信息清单" }
-                    val tab3 = bind.protocolLayout.tabLayout.newTab().apply { text = "个人信息收集清单" }
+                    val tab1 = bind.protocolLayout.tabLayout.newTab()
+                        .apply { text = "小鹏Robotaxi...隐私政策" }
+                    val tab2 =
+                        bind.protocolLayout.tabLayout.newTab().apply { text = "第三方共享信息清单" }
+                    val tab3 =
+                        bind.protocolLayout.tabLayout.newTab().apply { text = "个人信息收集清单" }
                     val tab4 = bind.protocolLayout.tabLayout.newTab().apply { text = "权限调用" }
 
                     bind.protocolLayout.tabLayout.addTab(tab1, true)
@@ -244,7 +252,6 @@
 
         //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
         webSettings.javaScriptEnabled = true //允许js调用
-        webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等
         webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许
 
         //缩放操作
@@ -254,17 +261,18 @@
 
         //其他细节操作
         webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
-        webSettings.allowFileAccess = true //设置可以访问文件
         webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
         webSettings.loadsImagesAutomatically = true //支持自动加载图片
         webSettings.defaultTextEncodingName = "utf-8" //设置编码格式
+
+        webView.setSafeSetting()//安全设置
     }
 
     @SuppressLint("SetJavaScriptEnabled")
     private fun setContent(webView: WebView, content: String?) {
         content?.let {
             // 加载并显示HTML代码
-            webView.loadDataWithBaseURL(
+            webView.safeLoadDataWithBaseURL(
                 null,
                 content,
                 "text/html",
diff --git a/app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt b/app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
index edd49c2..915850c 100644
--- a/app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
+++ b/app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
@@ -104,6 +104,9 @@
                             Constants.token = it.token?.access_token ?: ""
                             MMKVManager.getInstance()
                                 .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
+                            MMKVManager.getInstance()
+                                .saveModel(KeyPreferences.THIRD, it.thirdInfo)
+                            MyApp.myApp.initPush()
                             MyApp.myApp.connectWebSocket()
                             startActivity(MainActivity::class.java)
                             finish()
@@ -153,32 +156,35 @@
             ToastUtilCommon.showError("登录失败次数过多,请1分钟后重试")
             return
         }
-        if (BuildConfig.DEBUG) {
-            launchRequestFlow(
-                tips = "登录中...",
-                flowBlock = {
-                    val map =
-                        mapOf("phoneNumber" to phone.value, "password" to psw, "deviceType" to 1)
-                    userRepository.loginByPassword(map)
-                },
-                error = { code, msg ->
-                    ToastUtilCommon.showError(msg)
-                    loginFailCount++
-                    lastFailTime = System.currentTimeMillis()
-                },
-                onCollect = { data ->
-                    data?.let {
-                        Constants.token = it.token?.access_token ?: ""
-                        MMKVManager.getInstance()
-                            .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
-                        MyApp.myApp.connectWebSocket()
-                        startActivity(MainActivity::class.java)
-                        finish()
-                    }
-                }
-            )
-            return
-        }
+//        if (BuildConfig.DEBUG) {
+//            launchRequestFlow(
+//                tips = "登录中...",
+//                flowBlock = {
+//                    val map =
+//                        mapOf("phoneNumber" to phone.value, "password" to psw, "deviceType" to 1)
+//                    userRepository.loginByPassword(map)
+//                },
+//                error = { code, msg ->
+//                    ToastUtilCommon.showError(msg)
+//                    loginFailCount++
+//                    lastFailTime = System.currentTimeMillis()
+//                },
+//                onCollect = { data ->
+//                    data?.let {
+//                        Constants.token = it.token?.access_token ?: ""
+//                        MMKVManager.getInstance()
+//                            .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
+//                        MMKVManager.getInstance()
+//                            .saveModel(KeyPreferences.THIRD, it.thirdInfo)
+//                        MyApp.myApp.initPush()
+//                        MyApp.myApp.connectWebSocket()
+//                        startActivity(MainActivity::class.java)
+//                        finish()
+//                    }
+//                }
+//            )
+//            return
+//        }
         launchRequestFlow(
             tips = "登录中...",
             flowBlock = {
@@ -202,6 +208,9 @@
                             Constants.token = it.token?.access_token ?: ""
                             MMKVManager.getInstance()
                                 .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
+                            MMKVManager.getInstance()
+                                .saveModel(KeyPreferences.THIRD, it.thirdInfo)
+                            MyApp.myApp.initPush()
                             MyApp.myApp.connectWebSocket()
                             startActivity(MainActivity::class.java)
                             finish()
@@ -275,20 +284,29 @@
             ToastUtilCommon.showError("请输入密码!")
             return
         }
-        if (psw.value!!.length < 6) {
-            ToastUtilCommon.showError("密码长度至少6位!")
-            return
-        }
         if (pswConfirm.value.isNullOrEmpty()) {
             ToastUtilCommon.showError("请再次输入密码!")
             return
         }
-        if (pswConfirm.value!!.length < 6) {
-            ToastUtilCommon.showError("密码长度至少6位!")
-            return
-        }
         if (psw.value != pswConfirm.value) {
             ToastUtilCommon.showError("两次密码不一致!")
+            return
+        }
+        val psw = psw.value ?: ""
+        if (psw.length < 8) {
+            ToastUtilCommon.showError("密码长度至少8位!")
+            return
+        }
+        if (!psw.matches(Regex(".*[A-Z].*"))) {
+            ToastUtilCommon.showError("密码需包含大写字母!")
+            return
+        }
+        if (!psw.matches(Regex(".*[a-z].*"))) {
+            ToastUtilCommon.showError("密码需包含小写字母!")
+            return
+        }
+        if (!psw.matches(Regex(".*[0-9].*"))) {
+            ToastUtilCommon.showError("密码需包含数字!")
             return
         }
 
@@ -296,7 +314,7 @@
             val map = mapOf(
                 "phoneNumber" to phone.value,
                 "verificationCode" to code.value,
-                "newPassword" to psw.value,
+                "newPassword" to psw,
                 "confirmPassword" to pswConfirm.value,
             )
             userRepository.modifierPsw(map)
diff --git a/app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt b/app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt
index b098a81..2825398 100644
--- a/app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt
+++ b/app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt
@@ -7,8 +7,10 @@
 import com.xw.yunwei.BR
 import com.xw.yunwei.BaseBusinessActivity
 import com.xw.yunwei.databinding.ActivityProtocolBinding
+import com.xw.yunwei.extension.safeLoadDataWithBaseURL
+import com.xw.yunwei.extension.setSafeSetting
 
-class ProtocolActivity: BaseBusinessActivity<ActivityProtocolBinding, ProtocolVM>() {
+class ProtocolActivity : BaseBusinessActivity<ActivityProtocolBinding, ProtocolVM>() {
     override fun getBindingVariable() = BR.viewModel
 
     override fun init() {
@@ -48,6 +50,7 @@
                     }
                 }
             }
+
             override fun onTabUnselected(tab: TabLayout.Tab?) {}
             override fun onTabReselected(tab: TabLayout.Tab?) {}
         })
@@ -62,7 +65,6 @@
 
         //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
         webSettings.javaScriptEnabled = true //允许js调用
-        webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等
         webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许
 
         //缩放操作
@@ -72,17 +74,18 @@
 
         //其他细节操作
         webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
-        webSettings.allowFileAccess = true //设置可以访问文件
         webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
         webSettings.loadsImagesAutomatically = true //支持自动加载图片
         webSettings.defaultTextEncodingName = "utf-8" //设置编码格式
+
+        webView.setSafeSetting()//安全设置
     }
 
     @SuppressLint("SetJavaScriptEnabled")
     private fun setContent(webView: WebView, content: String?) {
         content?.let {
             // 加载并显示HTML代码
-            webView.loadDataWithBaseURL(
+            webView.safeLoadDataWithBaseURL(
                 null,
                 content,
                 "text/html",
diff --git a/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt b/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt
index 103e7eb..8b90229 100644
--- a/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt
+++ b/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt
@@ -7,21 +7,16 @@
 import android.provider.Settings
 import androidx.core.app.NotificationManagerCompat
 import androidx.core.content.ContextCompat
-import androidx.lifecycle.lifecycleScope
 import com.blankj.utilcode.util.AppUtils
 import com.hjq.permissions.OnPermissionCallback
 import com.hjq.permissions.Permission
 import com.hjq.permissions.XXPermissions
 import com.xw.common.CommonDialog
 import com.xw.common.net.UpdateManager
-import com.xw.common.utils.MMKVManager
 import com.xw.common.utils.ToastUtilCommon
 import com.xw.yunwei.BR
 import com.xw.yunwei.BaseBusinessActivity
-import com.xw.yunwei.MyApp
 import com.xw.yunwei.databinding.ActivitySetBinding
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 
 /**
  * 设置
@@ -106,14 +101,8 @@
         }
 
         binding.logOut.setOnClickListener {
-            CommonDialog.showTipsDialog(this, "确认退出当前账号登录吗?", rightBlock = {
-                MMKVManager.getInstance().clear()
-                MyApp.myApp.disconnectWebSocket()
-                lifecycleScope.launch {
-                    delay(100)
-                    AppUtils.relaunchApp(true)
-                }
-                true
+            CommonDialog.showCommonTipsDialog(this, "确认退出当前账号登录吗?", rightBlock = {
+                viewModel.loginOut(it)
             })
         }
     }
diff --git a/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt b/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt
index 273e1c5..1273bab 100644
--- a/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt
+++ b/app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt
@@ -1,9 +1,16 @@
 package com.xw.yunwei.ui.user.mine.set
 
 import android.app.Application
+import androidx.lifecycle.viewModelScope
+import com.afollestad.materialdialogs.MaterialDialog
+import com.blankj.utilcode.util.AppUtils
 import com.xw.common.base.BaseViewModel
+import com.xw.common.utils.MMKVManager
+import com.xw.yunwei.MyApp
 import com.xw.yunwei.model.AppVersionModel
 import com.xw.yunwei.net.UserRepository
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import org.koin.core.component.inject
 
 class SetVM(application: Application) : BaseViewModel(application) {
@@ -18,4 +25,18 @@
             }
         }
     }
+
+    //退出登录
+    fun loginOut(dialog: MaterialDialog) {
+        launchRequest {
+            val resultOk = userRepository.appLoginOut()
+            dialog.dismiss()
+            MMKVManager.getInstance().clear()
+            MyApp.myApp.disconnectWebSocket()
+            viewModelScope.launch {
+                delay(100)
+                AppUtils.relaunchApp(true)
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt b/app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt
index 8e014b0..1cab42e 100644
--- a/app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt
+++ b/app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt
@@ -6,6 +6,8 @@
 import com.xw.yunwei.BR
 import com.xw.yunwei.BaseBusinessActivity
 import com.xw.yunwei.databinding.ActivityStandardDescriptionBinding
+import com.xw.yunwei.extension.safeLoadDataWithBaseURL
+import com.xw.yunwei.extension.setSafeSetting
 
 /**
  * 标准说明
@@ -48,7 +50,6 @@
 
         //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
         webSettings.javaScriptEnabled = true //允许js调用
-        webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等
         webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许
 
         //缩放操作
@@ -58,17 +59,18 @@
 
         //其他细节操作
         webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
-        webSettings.allowFileAccess = true //设置可以访问文件
         webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
         webSettings.loadsImagesAutomatically = true //支持自动加载图片
         webSettings.defaultTextEncodingName = "utf-8" //设置编码格式
+
+        webView.setSafeSetting()//安全设置
     }
 
     @SuppressLint("SetJavaScriptEnabled")
     private fun setContent(webView: WebView, content: String?) {
         content?.let {
             // 加载并显示HTML代码
-            webView.loadDataWithBaseURL(
+            webView.safeLoadDataWithBaseURL(
                 null,
                 content,
                 "text/html",
diff --git a/app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt b/app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt
new file mode 100644
index 0000000..46dd24f
--- /dev/null
+++ b/app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt
@@ -0,0 +1,209 @@
+package com.xw.yunwei.utils
+
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.safety.Safelist
+import java.util.Locale
+
+class HtmlSanitizer {
+
+    // 定义不同安全等级的白名单
+    object Whitelists {
+        // 1. 基本文本(最严格)
+        val BASIC_TEXT = Safelist.none()
+            .addTags("p", "br", "b", "i", "strong", "em", "blockquote")
+            .addAttributes("p", "class")
+
+        // 2. 简单文档
+        val SIMPLE_DOCUMENT = Safelist.simpleText()
+            .addTags("h1", "h2", "h3", "h4", "h5", "h6", "hr", "ul", "ol", "li")
+            .addAttributes("a", "href", "title", "target")
+            .addProtocols("a", "href", "http", "https", "mailto")
+
+        // 3. 富文本(允许格式和媒体)
+        val RICH_TEXT = Safelist.relaxed()
+            .addTags("div", "span", "table", "tr", "td", "th", "tbody", "thead")
+            .addAttributes("div", "class", "style", "id")
+            .addAttributes("span", "class", "style")
+            .addAttributes("table", "border", "cellpadding", "cellspacing")
+            .addAttributes("img", "src", "alt", "title", "width", "height")
+            .addProtocols("img", "src", "http", "https", "data")
+            .removeTags("iframe", "object", "embed", "script", "style")
+
+        // 4. 自定义白名单(根据业务需求)
+        val CUSTOM = Safelist.none()
+            .addTags(
+                "p", "br", "b", "i", "u", "strong", "em",
+                "h1", "h2", "h3", "h4", "h5", "h6",
+                "ul", "ol", "li", "a", "img", "div", "span"
+            )
+            .addAttributes("a", "href", "target", "rel")
+            .addAttributes("img", "src", "alt", "width", "height")
+            .addAttributes("div", "class", "id")
+            .addProtocols("a", "href", "http", "https", "mailto", "tel")
+            .addProtocols("img", "src", "http", "https", "data")
+            .addEnforcedAttribute("a", "rel", "nofollow noopener noreferrer")
+    }
+
+    /**
+     * 基础清洗方法
+     */
+    fun sanitizeBasic(html: String, whitelist: Safelist = Whitelists.SIMPLE_DOCUMENT): String {
+        return Jsoup.clean(html, whitelist)
+    }
+
+    /**
+     * 高级清洗:包含额外安全检查和转换
+     */
+    fun sanitizeAdvanced(
+        html: String,
+        baseUri: String = ""
+    ): String {
+        val document = Jsoup.parse(html, baseUri)
+
+        // 1. 移除危险标签
+        val dangerousTags = listOf("script", "iframe", "object", "embed",
+            "applet", "meta", "link", "base", "frame")
+        dangerousTags.forEach { tag ->
+            document.select(tag).remove()
+        }
+
+        // 2. 移除危险属性
+        val dangerousAttrs = listOf(
+            "onclick", "onload", "onerror", "onmouseover", "onmouseout",
+            "onkeydown", "onkeypress", "onkeyup", "onfocus", "onblur",
+            "onchange", "onsubmit", "href"  // 特别处理href
+        )
+
+        document.allElements.forEach { element ->
+            val attributes = element.attributes()
+            attributes.forEach { attr ->
+                // 移除以"on"开头的事件处理器
+                if (attr.key.startsWith("on")) {
+                    attributes.remove(attr.key)
+                }
+                // 移除javascript:协议
+                if (attr.key == "href" || attr.key == "src") {
+                    val value = attr.value.lowercase(Locale.ROOT)
+                    if (value.startsWith("javascript:") ||
+                        value.startsWith("data:text/html") ||
+                        value.startsWith("file://")) {
+                        attributes.remove(attr.key)
+                    }
+                }
+            }
+        }
+
+        // 3. 净化链接(确保外部链接有安全属性)
+        document.select("a[href]").forEach { link ->
+            val href = link.attr("href")
+            if (href.startsWith("http://") || href.startsWith("https://")) {
+                link.attr("target", "_blank")
+                link.attr("rel", "noopener noreferrer")
+            }
+        }
+
+        // 4. 净化图片(防止盗链和XSS)
+        document.select("img[src]").forEach { img ->
+            val src = img.attr("src")
+            // 检查data:协议图片
+            if (src.startsWith("data:image")) {
+                // 可以限制data URL大小
+                if (src.length > 10000) { // 限制10KB
+                    img.remove()
+                }
+            }
+        }
+
+        // 5. 应用白名单
+        val cleaned = Jsoup.clean(document.body().html(), Whitelists.RICH_TEXT)
+
+        return cleaned
+    }
+
+    /**
+     * 针对用户输入的富文本编辑器内容清洗
+     */
+    fun sanitizeUserContent(html: String): String {
+        // 解析文档
+        val doc = Jsoup.parseBodyFragment(html)
+
+        // 创建自定义白名单
+        val whitelist = Whitelists.CUSTOM
+
+        // 特别处理style属性
+        doc.select("*[style]").forEach { element ->
+            val style = element.attr("style")
+            val sanitizedStyle = sanitizeCss(style)
+            if (sanitizedStyle.isNotEmpty()) {
+                element.attr("style", sanitizedStyle)
+            } else {
+                element.removeAttr("style")
+            }
+        }
+
+        // 清理class属性
+        doc.select("*[class]").forEach { element ->
+            val classes = element.attr("class").split(" ")
+            val safeClasses = classes.filter { isValidClassName(it) }
+            if (safeClasses.isNotEmpty()) {
+                element.attr("class", safeClasses.joinToString(" "))
+            } else {
+                element.removeAttr("class")
+            }
+        }
+
+        // 最终应用白名单
+        return Jsoup.clean(doc.body().html(), whitelist)
+    }
+
+    private fun sanitizeCss(css: String): String {
+        // 只允许安全的CSS属性
+        val allowedProperties = listOf(
+            "color", "background-color", "font-size", "font-weight",
+            "font-family", "text-align", "margin", "padding",
+            "border", "width", "height", "display", "position"
+        )
+
+        val pattern = Regex("""([a-z\-]+)\s*:\s*([^;]+)""")
+        val sanitized = StringBuilder()
+
+        pattern.findAll(css).forEach { match ->
+            val property = match.groupValues[1]
+            val value = match.groupValues[2]
+
+            if (allowedProperties.contains(property) ||
+                property.startsWith("margin-") ||
+                property.startsWith("padding-") ||
+                property.startsWith("border-")) {
+
+                // 检查值是否安全
+                if (isSafeCssValue(property, value)) {
+                    if (sanitized.isNotEmpty()) sanitized.append("; ")
+                    sanitized.append("$property: $value")
+                }
+            }
+        }
+
+        return sanitized.toString()
+    }
+
+    private fun isSafeCssValue(property: String, value: String): Boolean {
+        // 防止CSS注入攻击
+        val unsafePatterns = listOf(
+            Regex("expression\\s*\\("),
+            Regex("url\\s*\\(.*javascript:"),
+            Regex("javascript:"),
+            Regex("data:text/html")
+        )
+
+        return unsafePatterns.none { pattern ->
+            pattern.containsMatchIn(value)
+        }
+    }
+
+    private fun isValidClassName(className: String): Boolean {
+        // 只允许字母、数字、连字符、下划线
+        return className.matches(Regex("^[a-zA-Z0-9_\\-]+$"))
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_find_psw.xml b/app/src/main/res/layout/activity_find_psw.xml
index c9dc6d0..49003b7 100644
--- a/app/src/main/res/layout/activity_find_psw.xml
+++ b/app/src/main/res/layout/activity_find_psw.xml
@@ -105,7 +105,7 @@
             android:layout_marginHorizontal="@dimen/dp_32"
             android:background="@null"
             android:gravity="center_vertical"
-            android:hint="请输入密码(最少6位)"
+            android:hint="请输入密码(最少8位)"
             android:inputType="textPassword"
             android:text="@={viewModel.psw}"
             android:textColor="@color/gray_10"
diff --git a/common/src/main/java/com/xw/common/CommonDialog.kt b/common/src/main/java/com/xw/common/CommonDialog.kt
index 3747823..6359086 100644
--- a/common/src/main/java/com/xw/common/CommonDialog.kt
+++ b/common/src/main/java/com/xw/common/CommonDialog.kt
@@ -36,6 +36,38 @@
         leftBlock: () -> Boolean = { true },
         rightBlock: () -> Boolean = { true }
     ) {
+        showCommonTipsDialog(
+            context = context,
+            content = content,
+            leftText = leftText,
+            rightText = rightText,
+            leftBlock = { dialog ->
+                if (leftBlock.invoke()) {
+                    dialog.dismiss()
+                }
+            },
+            rightBlock = { dialog ->
+                if (rightBlock.invoke()) {
+                    dialog.dismiss()
+                }
+            },
+        )
+    }
+
+    /**
+     * 通用提示弹窗
+     * @param content 内容
+     * @param leftBlock 回调事件  返回true dismiss
+     * @param rightBlock 回调事件  返回true dismiss
+     */
+    fun showCommonTipsDialog(
+        context: Context,
+        content: String,
+        leftText: String? = "取消",
+        rightText: String? = "确定",
+        leftBlock: (MaterialDialog) -> Unit = { },
+        rightBlock: (MaterialDialog) -> Unit = { }
+    ) {
         Handler(Looper.getMainLooper()).post {
             val dialog = MaterialDialog(context).apply {
                 if (!BuildConfig.DEBUG) {
@@ -57,14 +89,10 @@
                 btnLeft.isVisible = !leftText.isNullOrEmpty()
                 btnRight.isVisible = !rightText.isNullOrEmpty()
                 btnLeft.setOnClickListener {
-                    if (leftBlock()) {
-                        dismiss()
-                    }
+                    leftBlock(this)
                 }
                 btnRight.setOnClickListener {
-                    if (rightBlock()) {
-                        dismiss()
-                    }
+                    rightBlock(this)
                 }
             }
             dialog.show()
diff --git a/common/src/main/java/com/xw/common/utils/MMKVManager.kt b/common/src/main/java/com/xw/common/utils/MMKVManager.kt
index bc2d5a7..c574e5c 100644
--- a/common/src/main/java/com/xw/common/utils/MMKVManager.kt
+++ b/common/src/main/java/com/xw/common/utils/MMKVManager.kt
@@ -88,6 +88,7 @@
 enum class KeyPreferences(val value: String) {
     TOKEN("token"),
     USER("user"),
+    THIRD("third"),
     ACCIDENT("accident"),
     NEED_READ("needRead"), // 是否需要阅读协议
     NEED_UPDATE("needUpdate") // 是否升级
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0eca6bd..e64fadc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -39,6 +39,7 @@
 crashreport = "latest.release"
 alicloud = "3.10.1"
 shadowLayout = "3.4.5"
+jsoup = "1.22.2"
 
 
 [libraries]
@@ -115,6 +116,7 @@
 alicloud = { module = "com.aliyun.ams:alicloud-android-push", version.ref = "alicloud" }
 shadowLayout = { module = "com.github.lihangleo2:ShadowLayout", version.ref = "shadowLayout" }
 huawei-agconnect-core = { module = "com.huawei.agconnect:agconnect-core", version.ref = "huawei" }
+jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }

--
Gitblit v1.7.1