Uno项目Silverlight迁移指南:实现身份服务客户端

Uno项目Silverlight迁移指南:实现身份服务客户端

还在为Silverlight应用的现代化迁移而头疼?本文手把手教你如何在Uno Platform中实现完整的身份认证服务客户端,解决企业级应用的安全访问难题。

迁移背景与挑战

随着Silverlight技术的退役,越来越多的企业面临将现有Silverlight业务应用迁移到现代技术栈的需求。Uno Platform作为跨平台UI框架,提供了从Silverlight到多平台(Windows、WebAssembly、iOS、Android等)的平滑迁移路径。

在Silverlight Business Application模板中,身份认证通常依赖于WCF RIA Services和ASP.NET表单认证。迁移到Uno Platform后,我们需要采用现代的认证方案,如基于OAuth 2.0和OpenID Connect的身份服务。

技术架构概览

mermaid

环境准备与依赖配置

安装必要的NuGet包

首先需要为项目添加必要的依赖包:

<!-- 在项目文件中添加 -->
<PackageReference Include="IdentityModel" Version="5.2.0" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />

配置链接器设置(WebAssembly)

对于WASM项目,需要在LinkerConfig.xml中添加以下配置以防止必要的程序集被裁剪:

<linker>
    <assembly fullname="YourApp.Wasm" />
    <assembly fullname="Uno.UI" />
    <assembly fullname="System.Text.Json" />
    <assembly fullname="IdentityModel" />
    <assembly fullname="System.Core">
        <type fullname="System.Linq.Expressions*" />
    </assembly>
</linker>

核心服务实现

1. IdentityServer客户端实现

using IdentityModel.Client;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Uno.Extensions;

public sealed class IdentityServerClient
{
    private static HttpClient _client;
    private readonly string _identityServerBaseAddress;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string _scope;

    static IdentityServerClient()
    {
        _client = new HttpClient();
    }

    public IdentityServerClient(
        string identityServerBaseAddress, 
        string clientId, 
        string clientSecret, 
        string scope)
    {
        _identityServerBaseAddress = identityServerBaseAddress;
        _clientId = clientId;
        _clientSecret = clientSecret;
        _scope = scope;
    }

    public async Task<string> GetAccessTokenAsync()
    {
        var discoveryResponse = await _client.GetDiscoveryDocumentAsync(
            address: _identityServerBaseAddress);

        if (discoveryResponse.IsError)
        {
            this.Log().LogError(discoveryResponse.Error);
            throw new Exception(discoveryResponse.Error);
        }

        var tokenResponse = await _client.RequestClientCredentialsTokenAsync(
            new ClientCredentialsTokenRequest
            {
                Address = discoveryResponse.TokenEndpoint,
                ClientId = _clientId,
                ClientSecret = _clientSecret,
                Scope = _scope
            });

        if (tokenResponse.IsError)
        {
            this.Log().LogError(tokenResponse.Error);
            throw new Exception(tokenResponse.Error);
        }

        return tokenResponse.AccessToken;
    }
}

2. 单例模式基类

using System;
using System.Reflection;

public abstract class SingletonBase<T> where T : class
{
    private static readonly Lazy<T> _instance = new Lazy<T>(() =>
    {
        var constructor = typeof(T).GetConstructor(
            BindingFlags.Instance |
            BindingFlags.Public |
            BindingFlags.NonPublic,
            null, Type.EmptyTypes, null);
        return (T)constructor.Invoke(null);
    });

    public static T Instance => _instance.Value;
}

3. 令牌服务实现

using System.Threading.Tasks;

public sealed class TokenService : SingletonBase<TokenService>
{
    private readonly IdentityServerClient _identityServerClient;
    
    public string AccessToken { get; private set; }
    public Task Initialization { get; private set; }

    private TokenService()
    {
        _identityServerClient = new IdentityServerClient(
            identityServerBaseAddress: "https://localhost:5001",
            clientId: "YourApp",
            clientSecret: "YourClientSecret",
            scope: "YourApiScope");

        Initialization = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
        AccessToken = await _identityServerClient.GetAccessTokenAsync();
    }

    public async Task<string> GetAccessTokenAsync()
    {
        await Initialization;
        if (Initialization.IsCompleted && 
            Initialization.Status == TaskStatus.RanToCompletion)
        {
            return AccessToken;
        }

        throw new InvalidOperationException("AccessToken is unavailable");
    }
}

4. 用户模型定义

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;

public class User : IPrincipal, IIdentity
{
    public int Id { get; set; }
    public bool IsAuthenticated { get; set; }
    public string Name { get; set; }
    public IEnumerable<string> Roles { get; set; }
    public string FriendlyName { get; set; }
    
    public string DisplayName => 
        !string.IsNullOrEmpty(FriendlyName) ? FriendlyName : Name;
    
    public string AuthenticationType => "Custom";
    public IIdentity Identity => this;
    
    public bool IsInRole(string role) => 
        Roles?.Contains(role) ?? false;
}

5. Web API基础类

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

public abstract class WebApiBase
{
    protected async Task<string> GetAsync(
        string url, 
        Dictionary<string, string> headers = null)
    {
        using var client = CreateHttpClient(headers);
        var response = await client.GetAsync(url);
        return await HandleResponse(response);
    }

    protected async Task<string> PostAsync(
        string url, 
        string content, 
        Dictionary<string, string> headers = null)
    {
        using var client = CreateHttpClient(headers);
        var response = await client.PostAsync(
            url, 
            new StringContent(content, Encoding.UTF8, "application/json"));
        return await HandleResponse(response);
    }

    private HttpClient CreateHttpClient(Dictionary<string, string> headers)
    {
        var client = new HttpClient();
        if (headers != null)
        {
            foreach (var header in headers)
            {
                client.DefaultRequestHeaders.Add(header.Key, header.Value);
            }
        }
        return client;
    }

    private async Task<string> HandleResponse(HttpResponseMessage response)
    {
        if (response.IsSuccessStatusCode)
        {
            return await response.Content.ReadAsStringAsync();
        }
        throw new HttpRequestException(
            $"HTTP request failed with status code: {response.StatusCode}");
    }
}

6. 身份认证API客户端

using System.Collections.Generic;
using System.Security.Principal;
using System.Text.Json;
using System.Threading.Tasks;

public sealed class IdentityApi : WebApiBase
{
    private readonly Dictionary<string, string> _defaultHeaders;
    private readonly string _identityServiceBaseAddress;

    public IdentityApi(string identityServiceBaseAddress, string accessToken)
    {
        _identityServiceBaseAddress = identityServiceBaseAddress;
        _defaultHeaders = new Dictionary<string, string>
        {
            {"accept", "application/json"},
            {"Authorization", $"Bearer {accessToken}"}
        };
    }

    public async Task<bool> ValidateUser(string userName, string password)
    {
        var result = await PostAsync(
            $"{_identityServiceBaseAddress}/identity/validateuser",
            JsonSerializer.Serialize(new
            {
                userName,
                password
            }),
            _defaultHeaders);

        return result != null && JsonSerializer.Deserialize<bool>(result);
    }

    public async Task<User> GetAuthenticatedUser(string userName)
    {
        var result = await PostAsync(
            $"{_identityServiceBaseAddress}/identity/getauthenticateduser",
            JsonSerializer.Serialize(new { userName }),
            _defaultHeaders);

        return result != null ? 
            JsonSerializer.Deserialize<User>(result) : null;
    }
}

7. 认证服务封装

using Microsoft.Extensions.Logging;
using System;
using System.Security.Principal;
using System.Threading.Tasks;
using Uno.Extensions;

public sealed class AuthenticationService : SingletonBase<AuthenticationService>
{
    public event EventHandler LoggedIn;
    public event EventHandler LoggedOut;
    public event EventHandler LoginFailed;
    
    public IPrincipal CurrentPrincipal { get; private set; }

    private AuthenticationService() { }

    public void Logout()
    {
        CurrentPrincipal = null;
        LoggedOut?.Invoke(this, EventArgs.Empty);
    }

    public async Task<bool> LoginUser(string userName, string password)
    {
        try
        {
            var api = new IdentityApi(
                "https://localhost:6001",
                await TokenService.Instance.GetAccessTokenAsync());

            var validUser = await api.ValidateUser(userName, password);
            if (!validUser)
            {
                LoginFailed?.Invoke(this, EventArgs.Empty);
                return false;
            }

            var authUser = await api.GetAuthenticatedUser(userName);
            if (authUser == null)
            {
                LoginFailed?.Invoke(this, EventArgs.Empty);
                return false;
            }

            CurrentPrincipal = authUser;
            LoggedIn?.Invoke(this, EventArgs.Empty);
            return true;
        }
        catch (Exception e)
        {
            this.Log().LogError($"Login failed: {e.Message}");
            LoginFailed?.Invoke(this, EventArgs.Empty);
            return false;
        }
    }
}

应用集成与使用

应用启动初始化

在App.xaml.cs中进行服务初始化:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    // 启动令牌服务初始化
    var tokenTask = TokenService.Instance;
    tokenTask.Initialization.ContinueWith(
        ct =>
        {
            this.Log().LogCritical(
                $"Token service initialization failed: {ct.Exception.Message}");
            // 显示错误对话框
        },
        TaskContinuationOptions.OnlyOnFaulted);
}

登录页面使用示例

public sealed partial class LoginPage : Page
{
    public LoginPage()
    {
        InitializeComponent();
        AuthenticationService.Instance.LoggedIn += OnLoggedIn;
        AuthenticationService.Instance.LoginFailed += OnLoginFailed;
    }

    private async void LoginButton_Click(object sender, RoutedEventArgs e)
    {
        var userName = UsernameTextBox.Text;
        var password = PasswordBox.Password;
        
        var isLoggedIn = await AuthenticationService.Instance
            .LoginUser(userName, password);
        
        if (isLoggedIn)
        {
            Frame.Navigate(typeof(MainPage));
        }
    }

    private void OnLoggedIn(object sender, EventArgs e)
    {
        // 更新UI状态
        LoginStatusText.Text = "登录成功";
    }

    private void OnLoginFailed(object sender, EventArgs e)
    {
        // 显示错误信息
        ErrorTextBlock.Text = "用户名或密码错误";
    }
}

迁移注意事项

平台特定配置

平台配置要求注意事项
UWP添加企业认证能力需要在Package.appxmanifest中配置
WebAssembly配置CORS确保服务器允许跨域请求
Android/iOS网络权限在清单文件中声明网络访问权限

安全最佳实践

  1. 令牌管理: 实现令牌刷新机制,处理过期情况
  2. 错误处理: 完善的异常处理和重试逻辑
  3. 日志记录: 详细的认证过程日志记录
  4. 输入验证: 客户端和服务端的双重验证

性能优化建议

mermaid

常见问题解决

1. 网络连接问题

// 添加网络状态检查
public static async Task<bool> CheckNetworkAvailability()
{
    try
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
        var response = await client.GetAsync("https://www.example.com");
        return response.IsSuccessStatusCode;
    }
    catch
    {
        return false;
    }
}

2. 令牌过期处理

public class TokenServiceWithRefresh : TokenService
{
    private DateTime _tokenExpiry;
    
    public async Task<string> GetValidAccessTokenAsync()
    {
        if (DateTime.UtcNow >= _tokenExpiry)
        {
            await RefreshTokenAsync();
        }
        return await GetAccessTokenAsync();
    }
    
    private async Task RefreshTokenAsync()
    {
        // 实现令牌刷新逻辑
        AccessToken = await _identityServerClient.GetAccessTokenAsync();
        _tokenExpiry = DateTime.UtcNow.AddMinutes(55); // 提前5分钟刷新
    }
}

总结

通过本文的指导,你可以成功将Silverlight应用的身份认证系统迁移到Uno Platform,实现现代化的OAuth 2.0客户端凭证流认证。关键要点包括:

  1. 架构现代化: 从WCF RIA Services迁移到RESTful WebAPI
  2. 安全升级: 采用标准的OAuth 2.0协议
  3. 跨平台兼容: 确保在UWP、WebAssembly、iOS、Android等平台正常工作
  4. 用户体验: 保持与原有Silverlight应用类似的登录流程

这种迁移方案不仅解决了技术债务问题,还为应用未来的扩展和维护奠定了坚实的基础。

下一步建议:实现令牌的自动刷新机制和离线登录支持,进一步提升用户体验。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值