移动端sso
This commit is contained in:
1 parent
18151fda81
commit
f08cd94924
10 files changed
+154
-10
No files matched your search
@@ -194,6 +194,14 @@ Sso__CallbackUrl=https://jiaowu.example.edu.cn/signin-keycloak
|
|||||||
显示当前生效的完整回调地址。多实例部署应配置 Redis,以便任意实例都能兑换两分钟内
|
显示当前生效的完整回调地址。多实例部署应配置 Redis,以便任意实例都能兑换两分钟内
|
||||||
有效、使用后即删除的 SSO 登录码及五分钟内有效的绑定意图。
|
有效、使用后即删除的 SSO 登录码及五分钟内有效的绑定意图。
|
||||||
|
|
||||||
|
Android App 使用系统浏览器完成 Keycloak 登录,再通过 `https://eis.biss.click/sso/callback`
|
||||||
|
或 `/sso/bind` 的 Android App Link 回到应用;Keycloak 的 Valid redirect URI 仍然只配置
|
||||||
|
`Sso__CallbackUrl`(即 `/signin-keycloak`),不要配置 `mingxu://`。正式发布前,将 Play
|
||||||
|
App Signing 证书的 SHA-256 指纹写入 `web/public/.well-known/assetlinks.json`,并确保该文件
|
||||||
|
以 `application/json` 在 `https://eis.biss.click/.well-known/assetlinks.json` 可匿名访问。当前
|
||||||
|
配置的包名为 `edu.mingxu.jiaowu`;站点域名或正式签名证书变更时必须同时更新此文件和 Android
|
||||||
|
Manifest 后重新签名发布 APK/AAB,此类变更不能通过 OTA 下发。
|
||||||
|
|
||||||
用户登录后可从页面右上角进入“个人账户”,主动绑定或解除 Keycloak 账号。主动绑定先
|
用户登录后可从页面右上角进入“个人账户”,主动绑定或解除 Keycloak 账号。主动绑定先
|
||||||
使用当前 JWT 创建五分钟有效的一次性绑定意图,再跳转 Keycloak;回调只能绑定到发起该
|
使用当前 JWT 创建五分钟有效的一次性绑定意图,再跳转 Keycloak;回调只能绑定到发起该
|
||||||
意图的本地账号。解绑需要再次验证本地密码,避免仅凭未锁屏的登录会话解除身份关联。
|
意图的本地账号。解绑需要再次验证本地密码,避免仅凭未锁屏的登录会话解除身份关联。
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public sealed class SsoController(
|
|||||||
ILogger<SsoController> logger) : ControllerBase
|
ILogger<SsoController> logger) : ControllerBase
|
||||||
{
|
{
|
||||||
private const string BindingIntentProperty = "sso-binding-intent";
|
private const string BindingIntentProperty = "sso-binding-intent";
|
||||||
|
private const string NativeAppProperty = "sso-native-app";
|
||||||
private readonly SsoOptions _options = options.Value;
|
private readonly SsoOptions _options = options.Value;
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
@@ -42,6 +43,7 @@ public sealed class SsoController(
|
|||||||
public async Task<IActionResult> Login(
|
public async Task<IActionResult> Login(
|
||||||
[FromQuery] string? returnUrl = null,
|
[FromQuery] string? returnUrl = null,
|
||||||
[FromQuery] string? bindingIntent = null,
|
[FromQuery] string? bindingIntent = null,
|
||||||
|
[FromQuery] bool nativeApp = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!_options.Enabled)
|
if (!_options.Enabled)
|
||||||
@@ -49,6 +51,8 @@ public sealed class SsoController(
|
|||||||
|
|
||||||
var safeReturnUrl = NormalizeReturnUrl(returnUrl);
|
var safeReturnUrl = NormalizeReturnUrl(returnUrl);
|
||||||
var properties = new AuthenticationProperties();
|
var properties = new AuthenticationProperties();
|
||||||
|
if (nativeApp)
|
||||||
|
properties.Items[NativeAppProperty] = bool.TrueString;
|
||||||
if (!string.IsNullOrWhiteSpace(bindingIntent))
|
if (!string.IsNullOrWhiteSpace(bindingIntent))
|
||||||
{
|
{
|
||||||
var targetUserId = await cache.GetStringAsync(
|
var targetUserId = await cache.GetStringAsync(
|
||||||
@@ -108,6 +112,10 @@ public sealed class SsoController(
|
|||||||
out var storedBindingIntent)
|
out var storedBindingIntent)
|
||||||
? storedBindingIntent
|
? storedBindingIntent
|
||||||
: null;
|
: null;
|
||||||
|
var nativeApp = authentication.Properties is { } externalProperties &&
|
||||||
|
externalProperties.Items.TryGetValue(NativeAppProperty, out var nativeAppValue) &&
|
||||||
|
bool.TryParse(nativeAppValue, out var isNativeApp) &&
|
||||||
|
isNativeApp;
|
||||||
if (!string.IsNullOrWhiteSpace(bindingIntent))
|
if (!string.IsNullOrWhiteSpace(bindingIntent))
|
||||||
{
|
{
|
||||||
var targetUserId = await cache.GetStringAsync(
|
var targetUserId = await cache.GetStringAsync(
|
||||||
@@ -165,7 +173,7 @@ public sealed class SsoController(
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
|
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
|
||||||
|
|
||||||
var bindingPage = BuildFrontendUrl("/sso/bind") +
|
var bindingPage = BuildFrontendUrl("/sso/bind", nativeApp) +
|
||||||
$"?code={Uri.EscapeDataString(bindingCode)}" +
|
$"?code={Uri.EscapeDataString(bindingCode)}" +
|
||||||
$"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
|
$"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
|
||||||
return Redirect(bindingPage);
|
return Redirect(bindingPage);
|
||||||
@@ -190,7 +198,7 @@ public sealed class SsoController(
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
|
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
|
||||||
|
|
||||||
var callback = BuildFrontendUrl("/sso/callback") +
|
var callback = BuildFrontendUrl("/sso/callback", nativeApp) +
|
||||||
$"?code={Uri.EscapeDataString(exchangeCode)}" +
|
$"?code={Uri.EscapeDataString(exchangeCode)}" +
|
||||||
$"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
|
$"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
|
||||||
return Redirect(callback);
|
return Redirect(callback);
|
||||||
@@ -344,7 +352,8 @@ public sealed class SsoController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpPost("prepare-binding")]
|
[HttpPost("prepare-binding")]
|
||||||
public async Task<ActionResult<SsoBindingStartResponse>> PrepareBinding(
|
public async Task<ActionResult<SsoBindingStartResponse>> PrepareBinding(
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
[FromQuery] bool nativeApp = false)
|
||||||
{
|
{
|
||||||
if (!_options.Enabled)
|
if (!_options.Enabled)
|
||||||
return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);
|
return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);
|
||||||
@@ -373,7 +382,8 @@ public sealed class SsoController(
|
|||||||
values: new
|
values: new
|
||||||
{
|
{
|
||||||
returnUrl = "/account",
|
returnUrl = "/account",
|
||||||
bindingIntent = intentCode
|
bindingIntent = intentCode,
|
||||||
|
nativeApp
|
||||||
})!;
|
})!;
|
||||||
return new SsoBindingStartResponse(loginUrl);
|
return new SsoBindingStartResponse(loginUrl);
|
||||||
}
|
}
|
||||||
@@ -417,10 +427,19 @@ public sealed class SsoController(
|
|||||||
? returnUrl
|
? returnUrl
|
||||||
: "/dashboard";
|
: "/dashboard";
|
||||||
|
|
||||||
private string BuildFrontendUrl(string path) =>
|
private string BuildFrontendUrl(string path, bool requireAbsoluteUrl = false)
|
||||||
|
{
|
||||||
|
if (requireAbsoluteUrl && string.IsNullOrWhiteSpace(_options.FrontendBaseUrl))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"原生单点登录需要配置 Sso:FrontendBaseUrl 为已验证的 HTTPS 地址。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
string.IsNullOrWhiteSpace(_options.FrontendBaseUrl)
|
string.IsNullOrWhiteSpace(_options.FrontendBaseUrl)
|
||||||
? path
|
? path
|
||||||
: _options.FrontendBaseUrl.TrimEnd('/') + path;
|
: _options.FrontendBaseUrl.TrimEnd('/') + path;
|
||||||
|
}
|
||||||
|
|
||||||
private RedirectResult RedirectToFrontendError(
|
private RedirectResult RedirectToFrontendError(
|
||||||
string error,
|
string error,
|
||||||
|
|||||||
@@ -29,6 +29,26 @@
|
|||||||
<data android:scheme="mingxu" android:host="open" />
|
<data android:scheme="mingxu" android:host="open" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="eis.biss.click"
|
||||||
|
android:pathPrefix="/sso/callback" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="eis.biss.click"
|
||||||
|
android:pathPrefix="/sso/bind" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
|
|||||||
Loaded 3 of 10 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user