18034727885
2026-05-23 293183266e941692005262d76f0bcff05d6384d6
修复安全漏洞问题
登录登出优化
1个文件已添加
16个文件已修改
648 ■■■■■ 已修改文件
app/build.gradle.kts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/MyApp.kt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/extension/ViewExtension.kt 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/LoginModel.kt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserApi.kt 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserRepository.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/push/MessageHandler.kt 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/protocol/ProtocolActivity.kt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetVM.kt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/car/detail/StandardDescriptionActivity.kt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_find_psw.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/CommonDialog.kt 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/utils/MMKVManager.kt 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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)
}
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)
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
    )
}
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
)
/**
 * 用户信息实体类
 * 包含用户基本信息、所属车队、角色列表等完整信息
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>>
}
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()
}
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",
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)
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",
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)
            })
        }
    }
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)
            }
        }
    }
}
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",
app/src/main/java/com/xw/yunwei/utils/HtmlSanitizer.kt
New file
@@ -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_\\-]+$"))
    }
}
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"
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()
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") // 是否升级
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" }