流程与桌面站类似,登录时启用浏览器(使用 Custom Tabs,在应用内部打开一个轻量级的浏览器)跳转至登录链接, 用户操作登录,后端服务器跳转到本地应用。跳转会有所区别,桌面应用是跳转回本地监听地址,android 中应用会注册一个 scheme (App Links 技术),浏览器跳转到该 scheme 时,系统会唤醒对应应用的 Activity 并传入参数
跳转时有两种方式
- Scheme (Deep Link)(如
myapp://):配置简单,但是跳转时会弹窗口提示是否要跳转。而且如果两个 App 注册了同一个 Scheme,会弹出选择框。 - App Links(如
https://login.example.com/login):通过域名验证实现唯一跳转,安全性更高。跳转时不会询问。
使用 Avalonia 进行登录时,可以使用 MauiEssentials (Microsoft.Maui.Essentials包)组件,它内部已经实现了跳转浏览器及浏览器回处理的 CallbackActivity。 其它的认证处理还是由 Duende.IdentityModel 与 Duende.IdentityModel.OidcClient 包进行处理
-
Duende.IdentityModel.OidcClientoidc登录过程中的链接生成及 token 计算解析 -
Duende.IdentityModel扩展了httpclient提供一些与登录无关的调用函数,比如RevokeToken,因为OidcClient中只提供了登录相关的处理流程。 -
Microsoft.Maui.Essentials用来处理启用浏览器及接收跳转参数的流程,内部的WebAuthenticatorCallbackActivity
接入 Microsoft.Maui.Essentials
1. 添加包 Microsoft.Maui.Essentials
2. 项目中加入 <UseMauiEssentials>true</UseMauiEssentials>
3. 系统接入,在 MainActivity 中加入
using Microsoft.Maui.ApplicationModel;
protected override void OnCreate(Bundle? savedInstanceState) {
Platform.Init(this, savedInstanceState);
base.OnCreate(savedInstanceState);
}
protected override void OnResume() {
base.OnResume();
Platform.OnResume();
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) {
Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
配置 WebAuthenticatorCallbackActivity
WebAuthenticatorCallbackActivity用来处理系统传入的回调值,可以直接在AndroidManifest.xml中配置该activity但是可能在应用编译时被自动剪裁,所以下例中直接定义了一个子类, 程序在启动时会自动扫描(自动生成的AndroidManifest)并调用它来接收回调值
// 通过特性自动生成 AndroidManifest
// 定义接收浏览器登录结果的回调
// LaunchMode 定义为单例
// DataScheme 定义了回调地址为 myapp://callback
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTask, Exported = true)]
[IntentFilter(["android.intent.action.VIEW",], // 使用字符串常量修复错误
Categories = ["android.intent.category.DEFAULT", "android.intent.category.BROWSABLE",],
DataScheme = "myapp")]
public class IoxWebAuthenticatorCallbackActivity : WebAuthenticatorCallbackActivity
{
}
调用代码
桌面端与
android调用代码相同, 都是调用OidcClient进行登录,区别是要实现一个IBrowser用在不同系统下的浏览器处理,android下的话可以使用Microsoft.Maui.Authentication.WebAuthenticator来处理, 而桌面端可以监听本地端口,Process.Start 启动默认浏览器来处理
登录代码
public async Task < string ? > LoginAsync() {
using CancellationTokenSource ts = new(TimeSpan.FromMinutes(5));
try {
OidcClientOptions options = new() {
Authority = "https://login.example.com",
ClientId = "iox-linkedin-app",
RedirectUri = "myapp://callback", // 上面定义的 IoxWebAuthenticatorCallbackActivity 中绑定的 scheme, 并要加入 authelia 的配置中
Scope = "openid offline_access appinfo_scope", // offline_access 可以得到 refresh_token
Browser = new AndroidBrowser(), // 处理浏览器操作相关内容
// LoggerFactory = loggerFactory, // 输出日志服务
DisablePushedAuthorization = true, // 禁用 Pushed Authorization, authelia 应该不支持这个, 要关闭掉
};
OidcClient client = new(options);
// 进行登录并返回结果
LoginResult loginResult = await client.LoginAsync(cancellationToken: ts.Token);
if (loginResult.IsError) {
throw new Exception($"登录失败, {loginResult.Error}");
}
// 登录相关的 token
string ? accessToken = loginResult.AccessToken;
string ? refreshToken = loginResult.RefreshToken;
string ? idToken = loginResult.IdentityToken;
// 使用登录信息获取用户信息
UserInfoResult userInfo = await client.GetUserInfoAsync(accessToken, ts.Token);
// 查找邮箱并返回
return userInfo.Claims.FirstOrDefault(it => it.Type == "email")?.Value;
} catch (TaskCanceledException) {
logger.LogInformation("用户关闭了浏览器");
// 用户关闭了浏览器
return null;
} catch (Exception exception) {
logger.LogError(exception, "登录失败");
return null;
}
}
浏览器调用
这里交给
WebAuthenticatorCallbackActivity处理, 如果是桌面版本中,这里就是启动浏览器并监听回调端口
// 处理浏览器相关内容
public class AndroidBrowser: IBrowser {
// 实现接口,接收浏览器访问地址及回调地址,并返回浏览器处理结果
public async Task < BrowserResult > InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = new()) {
try {
// 这里交给
WebAuthenticatorResult ? result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(options.StartUrl), // 浏览器访问地址
new Uri(options.EndUrl), // 回调地址
cancellationToken
);
// 返回浏览器处理结果 result.CallbackUri
return new BrowserResult {
ResultType = BrowserResultType.Success, Response = $ "{result.CallbackUri}",
};
} catch (Exception exception) when(cancellationToken.IsCancellationRequested) {
return new BrowserResult {
ResultType = BrowserResultType.UserCancel, Response = exception.Message,
};
}
catch (Exception exception) {
return new BrowserResult {
ResultType = BrowserResultType.UnknownError, Response = exception.Message,
};
}
}
}
App Links 跳转
原理是在 https://login.example.com/.well-known/assetlinks.json 提供一个认证文件,提示应用包名及签名进行验证, 如果验证通过的话,则应用可以使用该域名作为回调地址,而系统会在检测到该回调地址的时候拉起应用并通过 Intent 传入验证后的参数。 目前测试有一个问题,不是所有浏览器都支持该功能,特别是手机厂家自带浏览器。所以推荐使用 Scheme 方案
为应用生成证书
# 生成证书
keytool -genkey -v -keystore iox-app-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias iox-app-key -storepass 123456
# 查看签名值 hash 值
keytool -list -v -keystore iox-app-release-key.jks -alias iox-app-key -storepass 123456
# 项目配置在编译时进行签名
<PropertyGroup>
<!-- 签名 key-->
<AndroidKeyStore>True</AndroidKeyStore>
<AndroidSigningKeyStore>iox-app-release-key.jks</AndroidSigningKeyStore>
<AndroidSigningStorePass>zA6*qoFwXmYz</AndroidSigningStorePass>
<AndroidSigningKeyAlias>iox-app-key</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>123456</AndroidSigningKeyPass>
</PropertyGroup>
-
Duende.IdentityModel扩展了httpclient提供一些与登录无关的调用函数,比如RevokeToken,因为OidcClient中只提供了登录相关的处理流程。
生成 assetlinks.json 文件
-
Google 在下面的链接中生成 assetlinks.json 内容,并进行验证
https://developers.google.com/digital-asset-links/tools/generator -
nginx 中配置入文件
location = /.well-known/assetlinks.json { # 这里的路径必须是 Nginx 容器内部能访问到的路径 alias /data/static_html/auth/assetlinks.json; # 强制指定 Content-Type 为 JSON,这对 Android 校验至关重要 default_type application/json; # 允许所有来源访问(跨域需求) add_header Access-Control-Allow-Origin *; # 确保不会被某些 Nginx 配置中的隐藏文件规则拦截 allow all; } -
在手机中进行验证
或是应用属性中查看有一个 支持的链接 其中列出跳转域名
adb shell am resend-app-link-verify com.iox.KebooApp adb shell dumpsys package d | grep com.iox.KebooApp -A 10
配置 WebAuthenticatorCallbackActivity
可以与
Scheme方案同时存在,哪个生效会自动调用哪个
# AutoVerify 要打开, 不然以 Deep Link 方式(不验证域名)运行而不是 App Links
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTask, Exported = true)]
[IntentFilter(["android.intent.action.VIEW", ], // 使用字符串常量修复错误
Categories = ["android.intent.category.DEFAULT", "android.intent.category.BROWSABLE", ],
DataScheme = "https",
DataHost = "login.example.com",
DataPathPrefix = "/kebooapp/callback",
AutoVerify = true)]
public class IoxWebAuthenticatorCallbackActivity: WebAuthenticatorCallbackActivity {
}
调用代码
与前面的一样,就是回调用地址有区别
OidcClientOptions options = new() {
RedirectUri = "https://login.example.com/kebooapp/callback", // 记得在 authelia 中配置入该地址
};