simplify SSO: read JWT from localStorage, bypass OAuth2 authorize flow entirely

This commit is contained in:
2026-08-31 17:39:29 +08:00
parent cb5573da73
commit b335430c18
2 changed files with 63 additions and 109 deletions

View File

@@ -566,17 +566,6 @@ class MainActivity : Activity() {
@Suppress("SetJavaScriptEnabled")
private fun createSsoWebView(): View {
ssoPhase = 0
val authorizeUrl = Uri.parse(SSO_AUTHORIZE_URL).buildUpon()
.appendQueryParameter("client_id", SSO_CLIENT_ID)
.appendQueryParameter("redirect_uri", SSO_REDIRECT_URI)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", SSO_SCOPE)
.appendQueryParameter("state", "oral-trainer")
.build()
.toString()
val container = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(COLOR_BACKGROUND)
@@ -597,108 +586,65 @@ class MainActivity : Activity() {
ssoWebView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url ?: return false
if (ssoPhase >= 1 && url.toString().startsWith(SSO_REDIRECT_URI)) {
val code = url.getQueryParameter("code")
handleSsoCallback(code)
return true
}
return false
}
}
// 阶段 1:直接打开第一课堂登录页(绕过授权接口的错误重定向)
loadUrl(SSO_LOGIN_URL)
}
container.addView(ssoWebView, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f
))
// ── 底部:登录完成后手动点继续 ──
// ── 底部:登录完成后点继续,读 JWT 发给后端 ──
container.addView(Button(this).apply {
text = "已登录,点击继续授权"
text = "已登录,点击继续"
background = rounded(COLOR_ACCENT, 8f)
setTextColor(Color.WHITE)
setOnClickListener {
ssoPhase = 1
ssoWebView?.loadUrl(authorizeUrl)
readJwtAndLogin()
}
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 48.dp).withMargins(8.dp, 8.dp, 8.dp, 8.dp)
})
return container
}
private fun handleSsoCallback(code: String?) {
if (code.isNullOrBlank()) {
showingSsoWebView = false
ssoWebView = null
setRootView()
return
}
Thread {
try {
val accessToken = exchangeSsoCodeForToken(code)
sdk.userApi.ssoExchange(accessToken) { result ->
result.fold(
onSuccess = { ssoResult ->
showingSsoWebView = false
ssoWebView = null
ssoPendingToken = ssoResult.token
if (!ssoResult.phoneBound) {
showingPhoneBinding = true
setRootView()
} else {
saveAuthSession(AuthSession(ssoResult.token, ssoResult.user))
}
},
onFailure = { error ->
showingSsoWebView = false
ssoWebView = null
setRootView()
runOnUiThread {
authStatusText?.text = "SSO 登录失败:${error.message}"
}
},
)
}
} catch (e: Exception) {
private fun readJwtAndLogin() {
ssoWebView?.evaluateJavascript(
"(function(){ try { return localStorage.getItem('token') || sessionStorage.getItem('token') || ''; } catch(e) { return ''; } })()"
) { result ->
val jwt = result?.removeSurrounding("\"").orEmpty()
if (jwt.isBlank() || jwt == "null") {
runOnUiThread {
showingSsoWebView = false
ssoWebView = null
setRootView()
authStatusText?.text = "SSO 登录失败:${e.message}"
android.widget.Toast.makeText(this, "未找到登录凭证,请先在页面中登录", android.widget.Toast.LENGTH_SHORT).show()
}
return@evaluateJavascript
}
}.start()
sendJwtToBackend(jwt)
}
}
private fun exchangeSsoCodeForToken(code: String): String {
val url = URL(SSO_TOKEN_URL)
val connection = url.openConnection() as HttpURLConnection
try {
connection.requestMethod = "POST"
connection.doOutput = true
connection.connectTimeout = 15_000
connection.readTimeout = 15_000
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
val body = "grant_type=authorization_code" +
"&code=$code" +
"&client_id=$SSO_CLIENT_ID" +
"&client_secret=$SSO_CLIENT_SECRET" +
"&redirect_uri=${Uri.encode(SSO_REDIRECT_URI)}"
connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
val statusCode = connection.responseCode
val stream = if (statusCode in 200..299) connection.inputStream else connection.errorStream
val text = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }.orEmpty()
if (statusCode !in 200..299) throw RuntimeException("Token exchange failed: HTTP $statusCode $text")
val json = JSONObject(text)
return json.getString("access_token")
} finally {
connection.disconnect()
private fun sendJwtToBackend(jwt: String) {
sdk.userApi.ssoExchange(jwt) { result ->
result.fold(
onSuccess = { ssoResult ->
runOnUiThread {
showingSsoWebView = false
ssoWebView = null
ssoPendingToken = ssoResult.token
if (!ssoResult.phoneBound) {
showingPhoneBinding = true
setRootView()
} else {
saveAuthSession(AuthSession(ssoResult.token, ssoResult.user))
}
}
},
onFailure = { error ->
runOnUiThread {
showingSsoWebView = false
ssoWebView = null
setRootView()
authStatusText?.text = "SSO 登录失败:${error.message}"
}
},
)
}
}