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