You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Multiplatform:如何提取WebView中SAML认证返回的JSON值?

问题描述

我使用WebView加载Google SAML身份验证URL完成用户登录,成功认证后WebView会返回如下JSON响应:

{
  "status": 1,
  "msg": "user Authenticated !!",
  "user": {
    "userFirstName": "John",
    "userLastName": "Doe",
    "userEmail": "john@example.in",
    "userType": "manager",
    "manager": true,
    "employee": false,
    "teamLead": false
  },
  "token": "eyJhbGciOiJI................."
}

我需要从WebView中提取这些信息并进行本地存储。

我在Kotlin Multiplatform中使用compose-webview-multiplatform库实现WebView,当前代码如下:

@Composable
internal fun LoginWebView(navHostController: NavHostController? = null) {
    val state = rememberWebViewState(url = SAML_DEV)
    val navigator = rememberWebViewNavigator()
    val jsBridge = rememberWebViewJsBridge()

    jsBridge.register(LoginMessageHandler())
    state.webSettings.apply {
        isJavaScriptEnabled = true
    }

    DisposableEffect(Unit) {
        state.webSettings.apply {
            logSeverity = KLogSeverity.Verbose
        }

        onDispose { }
    }


    MaterialTheme {
        Column {
            TopAppBar(
                title = { Text(text = "Login") },
                navigationIcon = {
                    IconButton(onClick = {
                        if (navigator.canGoBack) {
                            navigator.navigateBack()
                        } else {
                            navHostController?.popBackStack()
                        }
                    }) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "Back",
                        )
                    }
                },
            )

            val loadingState = state.loadingState
            if (loadingState is LoadingState.Loading) {
                LinearProgressIndicator(
                    color = MaterialTheme.colorScheme.onPrimary,
                    modifier = Modifier
                        .height(6.dp)
                        .fillMaxWidth(),
                )
            }

            WebView(
                state = state,
                modifier = Modifier.fillMaxSize(),
                navigator = navigator,
                webViewJsBridge = jsBridge,
            )

            LaunchedEffect(state, navigator) {
                snapshotFlow { state.loadingState }
                    .filter { it is LoadingState.Finished }
                    .collect {
                        navigator.evaluateJavaScript(
                            """
                                try {
                                        // Select the <pre> element in the DOM
                                        const preElement = document.querySelector('pre');
                                        if (preElement) {
                                            // Extract the JSON content from the <pre> element
                                            const jsonResponse = preElement.innerText;
                                            console.log("Extracted JSON response:", jsonResponse);
                                            // Send the response to the native side using the bridge
                                            window.JsBridge.postMessage('login', jsonResponse);
                                        } else {
                                            console.error("<pre> element not found");
                                        }
                                    } catch (e) {
                                        console.error('Error extracting JSON response:', e);
                                    }
                            """.trimIndent()
                        )
                    }
            }

        }
    }
}

class LoginMessageHandler : IJsMessageHandler {
    override fun handle(
        message: JsMessage,
        navigator: WebViewNavigator?,
        callback: (String) -> Unit
    ) {
        println("LoginMessageHandler handle: $message")

        Logger.i {
            "Greet Handler Get Message: $message"
        }
        //val param = processParams<User>(message)
        callback(message.params)
    }

    override fun methodName(): String {
        return "login"
    }
}

解决方案

1. 定义JSON对应的数据类

首先创建与返回JSON结构匹配的可序列化数据类,方便后续解析:

import kotlinx.serialization.Serializable

@Serializable
data class AuthResponse(
    val status: Int,
    val msg: String,
    val user: UserInfo,
    val token: String
)

@Serializable
data class UserInfo(
    val userFirstName: String,
    val userLastName: String,
    val userEmail: String,
    val userType: String,
    val manager: Boolean,
    val employee: Boolean,
    val teamLead: Boolean
)

2. 完善消息处理器的解析与存储逻辑

修改LoginMessageHandler,将收到的JSON字符串解析为数据类,再执行本地存储操作:

import kotlinx.serialization.json.Json

class LoginMessageHandler : IJsMessageHandler {
    // 配置JSON解析器,忽略未知字段避免解析失败
    private val jsonParser = Json { ignoreUnknownKeys = true }

    override fun handle(
        message: JsMessage,
        navigator: WebViewNavigator?,
        callback: (String) -> Unit
    ) {
        try {
            // 将JSON字符串解析为AuthResponse对象
            val authData = jsonParser.decodeFromString<AuthResponse>(message.params)
            
            // 执行本地存储
            persistAuthData(authData)
            
            Logger.i { "登录信息已成功解析并存储:${authData.user.userEmail}" }
            callback("success")
        } catch (e: Exception) {
            Logger.e { "解析登录响应失败:${e.message}" }
            callback("error: ${e.message.orEmpty()}")
        }
    }

    override fun methodName(): String {
        return "login"
    }

    // 本地存储实现(根据项目选择KMP兼容的存储方案)
    private fun persistAuthData(authData: AuthResponse) {
        // 示例:使用MMKV(多平台高性能键值存储)
        // val mmkv = MMKV.defaultMMKV()
        // mmkv.encode("auth_token", authData.token)
        // mmkv.encode("user_profile", jsonParser.encodeToString(authData.user))
        
        // 示例:使用Multiplatform DataStore
        // val dataStore = context.createDataStore<AuthResponse>("auth_store")
        // dataStore.updateData { authData }
        
        // 请根据项目实际依赖替换为对应存储逻辑
    }
}

3. 优化JS注入时机

原代码中每次页面加载完成都会执行提取逻辑,容易重复触发。建议增加URL判断,仅在登录成功的回调页面执行:

LaunchedEffect(state, navigator) {
    snapshotFlow { Pair(state.loadingState, state.content.getCurrentUrl()) }
        .filter { (loadingState, url) -> 
            // 替换为实际的登录成功回调URL的特征关键词
            loadingState is LoadingState.Finished && url?.contains("auth-callback") == true 
        }
        .collect {
            navigator.evaluateJavaScript(
                """
                    try {
                        const preElement = document.querySelector('pre');
                        if (preElement) {
                            const jsonResponse = preElement.innerText;
                            console.log("Extracted JSON response:", jsonResponse);
                            window.JsBridge.postMessage('login', jsonResponse);
                        } else {
                            console.error("pre元素未找到");
                        }
                    } catch (e) {
                        console.error('提取JSON响应出错:', e);
                    }
                """.trimIndent()
            )
        }
}

4. 添加依赖配置

若使用kotlinx.serialization解析JSON,需在项目中添加对应依赖:

  • 根目录build.gradle.kts添加插件:
plugins {
    kotlin("plugin.serialization") version "1.9.20" // 匹配你的Kotlin版本
}
  • 模块build.gradle.kts添加依赖:
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

5. 选择合适的KMP本地存储方案

推荐几种Kotlin Multiplatform兼容的存储方案:

  • MMKV:腾讯开源的高性能键值存储,集成简单,支持多平台
  • Multiplatform DataStore:基于Flow的结构化存储,适合小量配置/用户数据
  • SQLDelight:关系型数据库,适合存储大量结构化数据

根据项目需求选择对应方案,完成persistAuthData方法的实现即可。


内容的提问来源于stack exchange,提问作者Cypher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 01:32:03