android avalonia oidc 登录

流程与桌面站类似,登录时启用浏览器(使用 Custom Tabs,在应用内部打开一个轻量级的浏览器)跳转至登录链接, 用户操作登录,后端服务器跳转到本地应用。跳转会有所区别,桌面应用是跳转回本地监听地址,android 中应用会注册一个 scheme (App Links 技术),浏览器跳转到该 scheme 时,系统会唤醒对应应用的 Activity 并传入参数

跳转时有两种方式

  • Scheme (Deep Link)(如 myapp://):配置简单,但是跳转时会弹窗口提示是否要跳转。而且如果两个 App 注册了同一个 Scheme,会弹出选择框。
  • App Links(如 https://login.example.com/login):通过域名验证实现唯一跳转,安全性更高。跳转时不会询问。

使用 Avalonia 进行登录时,可以使用 MauiEssentialsMicrosoft.Maui.Essentials包)组件,它内部已经实现了跳转浏览器及浏览器回处理的 CallbackActivity。 其它的认证处理还是由 Duende.IdentityModelDuende.IdentityModel.OidcClient 包进行处理

  • Duende.IdentityModel.OidcClient oidc 登录过程中的链接生成及 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 中配置入该地址
};

上一篇
下一篇