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的身份服务。
技术架构概览
环境准备与依赖配置
安装必要的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. 网络连接问题
// 添加网络状态检查
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客户端凭证流认证。关键要点包括:
- 架构现代化: 从WCF RIA Services迁移到RESTful WebAPI
- 安全升级: 采用标准的OAuth 2.0协议
- 跨平台兼容: 确保在UWP、WebAssembly、iOS、Android等平台正常工作
- 用户体验: 保持与原有Silverlight应用类似的登录流程
这种迁移方案不仅解决了技术债务问题,还为应用未来的扩展和维护奠定了坚实的基础。
下一步建议:实现令牌的自动刷新机制和离线登录支持,进一步提升用户体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



