simplify SSO: read JWT from localStorage, bypass OAuth2 authorize flow entirely
This commit is contained in:
@@ -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}"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user