.NET MAUI应用安全与身份验证完全指南:从OAuth 2.0到代码保护实战

从OAuth 2.0+PKCE身份验证、SecureStorage安全存储、Certificate Pinning证书固定到NativeAOT编译和代码混淆,全面覆盖.NET MAUI移动应用安全的每个关键层面,附完整代码示例和平台配置。

MAUI安全认证完整指南2026:OAuth+代码保护

为什么你的.NET MAUI应用需要认真对待安全?

做移动端开发的同学可能都有个误区——觉得安全是后端的事,前端只要调调API就行了。说实话,我之前也这么想过。但现实给了我一巴掌:移动应用是攻击者最喜欢的突破口之一

.NET MAUI应用编译后的程序集包含丰富的元数据和IL中间代码,用ILSpy这类工具几分钟就能反编译出来。如果你的应用涉及用户隐私、支付流程或者企业数据,安全绝不是可选项。(别问我怎么知道的,问就是被甲方安全审计教做人了。)

2026年的移动安全形势更加严峻了。根据OWASP Mobile Top 10,最常见的安全风险包括:不安全的认证机制、不当的凭据存储、不充分的输入验证以及代码逆向工程。

所以这篇文章会从身份验证、安全存储、网络通信、代码保护四个维度,给你一套完整的.NET MAUI安全实践方案。每个部分都有可以直接拿来用的代码,不搞纸上谈兵。

OAuth 2.0 + PKCE身份验证:用WebAuthenticator实现安全登录

现代移动应用的身份验证标准是OAuth 2.0 + Authorization Code + PKCE。PKCE(Proof Key for Code Exchange)是专门为移动端和公共客户端设计的安全扩展——简单来说,它通过生成一对code_verifier和code_challenge来防止授权码被截获后滥用。

PKCE工作原理

整个PKCE流程其实不复杂,分为这几步:

  1. 客户端生成一个随机字符串code_verifier
  2. code_verifier做SHA-256哈希后Base64编码,得到code_challenge
  3. code_challenge随授权请求一起发送给认证服务器
  4. 服务器返回授权码(authorization code)
  5. 客户端用授权码 + 原始code_verifier换取access token
  6. 服务器验证code_verifier的哈希是否匹配之前收到的code_challenge

关键点在于:即使攻击者拦截了授权码,没有code_verifier也换不到token。这就是目前移动端认证的最佳实践。

实现PKCE辅助类

先封装一个PKCE工具类,后面会反复用到:

using System.Security.Cryptography;
using System.Text;

namespace MyMauiApp.Security;

public static class PkceHelper
{
    public static string CreateCodeVerifier()
    {
        var bytes = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    public static string CreateCodeChallenge(string codeVerifier)
    {
        using var sha256 = SHA256.Create();
        var challengeBytes = sha256.ComputeHash(
            Encoding.ASCII.GetBytes(codeVerifier));
        return Convert.ToBase64String(challengeBytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}

使用WebAuthenticator发起OAuth登录

.NET MAUI内置了WebAuthenticator API,它会启动系统浏览器完成认证流程,然后通过自定义URI scheme把结果回调给应用。这比在WebView里做登录安全太多了:

using Microsoft.Maui.Authentication;

namespace MyMauiApp.Services;

public class AuthService
{
    private const string AuthorizeUrl = "https://your-auth-server.com/oauth2/authorize";
    private const string TokenUrl = "https://your-auth-server.com/oauth2/token";
    private const string ClientId = "your-client-id";
    private const string RedirectUri = "myapp://callback";
    private const string Scope = "openid profile email";

    private string? _codeVerifier;

    public async Task LoginAsync()
    {
        _codeVerifier = PkceHelper.CreateCodeVerifier();
        var codeChallenge = PkceHelper.CreateCodeChallenge(_codeVerifier);
        var state = Guid.NewGuid().ToString("N");

        var authUrl = $"{AuthorizeUrl}?" +
            $"client_id={ClientId}" +
            $"&redirect_uri={Uri.EscapeDataString(RedirectUri)}" +
            $"&response_type=code" +
            $"&scope={Uri.EscapeDataString(Scope)}" +
            $"&code_challenge={codeChallenge}" +
            $"&code_challenge_method=S256" +
            $"&state={state}";

        try
        {
            var result = await WebAuthenticator.Default.AuthenticateAsync(
                new Uri(authUrl),
                new Uri(RedirectUri));

            var code = result?.Get("code");
            if (string.IsNullOrEmpty(code))
                return null;

            return await ExchangeCodeForTokenAsync(code);
        }
        catch (TaskCanceledException)
        {
            // 用户取消了登录
            return null;
        }
    }

    private async Task ExchangeCodeForTokenAsync(string code)
    {
        using var httpClient = new HttpClient();
        var tokenRequest = new Dictionary
        {
            ["grant_type"] = "authorization_code",
            ["code"] = code,
            ["redirect_uri"] = RedirectUri,
            ["client_id"] = ClientId,
            ["code_verifier"] = _codeVerifier!
        };

        var response = await httpClient.PostAsync(
            TokenUrl,
            new FormUrlEncodedContent(tokenRequest));

        if (!response.IsSuccessStatusCode)
            return null;

        var json = await response.Content.ReadAsStringAsync();
        var tokenResponse = System.Text.Json.JsonSerializer
            .Deserialize(json);

        // 安全存储token
        await SecureStorage.Default.SetAsync(
            "access_token", tokenResponse!.AccessToken);
        await SecureStorage.Default.SetAsync(
            "refresh_token", tokenResponse.RefreshToken);

        return tokenResponse.AccessToken;
    }
}

public class TokenResponse
{
    [System.Text.Json.Serialization.JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = "";

    [System.Text.Json.Serialization.JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; } = "";

    [System.Text.Json.Serialization.JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }
}

Android平台配置:Intent Filter

Android端需要注册一个Activity来处理回调URI。在Platforms/Android目录下创建就行:

using Android.App;
using Android.Content.PM;

namespace MyMauiApp.Platforms.Android;

[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { global::Android.Content.Intent.ActionView },
    Categories = new[] {
        global::Android.Content.Intent.CategoryDefault,
        global::Android.Content.Intent.CategoryBrowsable
    },
    DataScheme = "myapp")]
public class WebAuthCallbackActivity
    : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity
{
}

iOS平台配置:URL Scheme

iOS这边需要在Info.plist里注册自定义URI scheme:

CFBundleURLTypes

    
        CFBundleURLSchemes
        
            myapp
        
    

同时在AppDelegate.cs中确保正确处理回调:

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    public override bool OpenUrl(UIApplication application,
        NSUrl url, NSDictionary options)
    {
        if (Platform.OpenUrl(application, url, options))
            return true;
        return base.OpenUrl(application, url, options);
    }
}

MSAL集成:微软身份平台认证

如果你的应用需要接入Microsoft Entra ID(原Azure AD)做企业级身份验证,那MSAL.NET(Microsoft Authentication Library)是官方推荐方案。老实说,它帮你处理了token获取、缓存、刷新这些烦人的事情,省了不少心。

配置MSAL

先装NuGet包:

dotnet add package Microsoft.Identity.Client

然后封装一个单例包装器。注意看代码里针对Android和iOS的平台差异处理:

using Microsoft.Identity.Client;

namespace MyMauiApp.Security;

public class MsalAuthService
{
    private static readonly Lazy _instance =
        new(() => new MsalAuthService());
    public static MsalAuthService Instance => _instance.Value;

    private IPublicClientApplication _pca;

    private const string ClientId = "your-azure-client-id";
    private const string TenantId = "your-tenant-id";
    private const string Authority =
        $"https://login.microsoftonline.com/{TenantId}";
    private static readonly string[] Scopes =
        new[] { "User.Read", "openid", "profile" };

    private MsalAuthService()
    {
        _pca = PublicClientApplicationBuilder
            .Create(ClientId)
            .WithAuthority(Authority)
            .WithRedirectUri($"msal{ClientId}://auth")
#if ANDROID
            .WithParentActivityOrWindow(() =>
                Platform.CurrentActivity!)
#elif IOS
            .WithIosKeychainSecurityGroup(
                "com.yourcompany.myapp")
#endif
            .Build();
    }

    public async Task SignInAsync()
    {
        try
        {
            // 先尝试静默获取(使用缓存的token)
            var accounts = await _pca.GetAccountsAsync();
            var firstAccount = accounts.FirstOrDefault();

            if (firstAccount != null)
            {
                return await _pca
                    .AcquireTokenSilent(Scopes, firstAccount)
                    .ExecuteAsync();
            }
        }
        catch (MsalUiRequiredException)
        {
            // 需要用户交互
        }

        // 交互式登录
        return await _pca
            .AcquireTokenInteractive(Scopes)
            .ExecuteAsync();
    }

    public async Task SignOutAsync()
    {
        var accounts = await _pca.GetAccountsAsync();
        foreach (var account in accounts)
        {
            await _pca.RemoveAsync(account);
        }
    }
}

在ViewModel中使用MSAL

实际用起来很简单,配合CommunityToolkit.Mvvm几行代码搞定:

public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    private string userName = "";

    [ObservableProperty]
    private bool isLoggedIn;

    [RelayCommand]
    private async Task LoginAsync()
    {
        var result = await MsalAuthService.Instance.SignInAsync();
        if (result != null)
        {
            UserName = result.Account.Username;
            IsLoggedIn = true;

            // 安全存储access token供API调用使用
            await SecureStorage.Default.SetAsync(
                "access_token", result.AccessToken);
        }
    }

    [RelayCommand]
    private async Task LogoutAsync()
    {
        await MsalAuthService.Instance.SignOutAsync();
        SecureStorage.Default.Remove("access_token");
        UserName = "";
        IsLoggedIn = false;
    }
}

SecureStorage安全存储:别把Token随便放

token怎么存,这个问题真的很关键。

我见过不少项目直接用Preferences甚至文件系统存token——这是非常不安全的做法。Android上root一下就能看到明文,iOS上越狱后也一样。.NET MAUI提供了SecureStorage,它在不同平台用的是操作系统级别的加密机制:

  • Android:使用EncryptedSharedPreferences,基于AES-256-GCM加密
  • iOS:使用Keychain,这是iOS系统最安全的存储方式
  • Windows:打包应用使用ApplicationData.Current.LocalSettings,数据经DPAPI加密

封装安全Token管理器

下面这个封装类帮你处理了token过期检查和自动刷新的逻辑,算是一个比较实用的起点:

namespace MyMauiApp.Security;

public class SecureTokenManager
{
    private const string AccessTokenKey = "access_token";
    private const string RefreshTokenKey = "refresh_token";
    private const string TokenExpiryKey = "token_expiry";

    public async Task StoreTokensAsync(
        string accessToken,
        string refreshToken,
        DateTimeOffset expiry)
    {
        await SecureStorage.Default.SetAsync(
            AccessTokenKey, accessToken);
        await SecureStorage.Default.SetAsync(
            RefreshTokenKey, refreshToken);
        await SecureStorage.Default.SetAsync(
            TokenExpiryKey, expiry.ToUnixTimeSeconds().ToString());
    }

    public async Task GetAccessTokenAsync()
    {
        var expiryStr = await SecureStorage.Default.GetAsync(
            TokenExpiryKey);

        if (!string.IsNullOrEmpty(expiryStr)
            && long.TryParse(expiryStr, out var unixTime))
        {
            var expiry = DateTimeOffset.FromUnixTimeSeconds(unixTime);
            if (expiry < DateTimeOffset.UtcNow.AddMinutes(-5))
            {
                // Token即将过期或已过期,尝试刷新
                return await RefreshAccessTokenAsync();
            }
        }

        return await SecureStorage.Default.GetAsync(AccessTokenKey);
    }

    private async Task RefreshAccessTokenAsync()
    {
        var refreshToken = await SecureStorage.Default.GetAsync(
            RefreshTokenKey);
        if (string.IsNullOrEmpty(refreshToken))
            return null;

        // 调用token端点用refresh_token换取新的access_token
        // 具体实现取决于你的认证服务器
        // ...
        return null;
    }

    public void ClearAllTokens()
    {
        SecureStorage.Default.Remove(AccessTokenKey);
        SecureStorage.Default.Remove(RefreshTokenKey);
        SecureStorage.Default.Remove(TokenExpiryKey);
    }
}

Android Auto Backup的坑

这里有个很多人会踩的坑。Android 6.0+默认会自动备份应用数据,包括SharedPreferences。问题来了:当用户换机恢复数据时,加密的SecureStorage数据在新设备上是无法解密的——因为加密密钥是设备绑定的。结果就是应用直接崩溃。

解决方法是在AndroidManifest.xml中排除这些数据:


然后创建Platforms/Android/Resources/xml/auto_backup_rules.xml



    

API通信安全:Certificate Pinning与HttpClient配置

即使用了HTTPS,如果用户设备被装了恶意CA证书(企业设备上挺常见的),中间人攻击依然有可能发生。Certificate Pinning(证书固定)就是为了防这个——让你的应用只信任特定的服务器证书或公钥。

Android平台实现:Network Security Config

Android原生就支持通过Network Security Configuration做证书固定,配置起来很方便。创建Platforms/Android/Resources/xml/network_security_config.xml



    
        
            api.yourserver.com
        
        
            
                base64编码的证书公钥SHA-256哈希值=
            
            
            
                备份证书的SHA-256哈希值=
            
        
    

然后在AndroidManifest.xml中引用一下就好:


iOS平台实现

iOS这边稍微麻烦一点,需要通过自定义HttpMessageHandler来做:

using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace MyMauiApp.Platforms.iOS;

public class PinnedHttpHandler : NSUrlSessionHandler
{
    private readonly HashSet _pinnedKeys;

    public PinnedHttpHandler(IEnumerable pinnedPublicKeyHashes)
    {
        _pinnedKeys = new HashSet(pinnedPublicKeyHashes);

        TrustOverrideForUrl = (sender, url, trust) =>
        {
            if (trust == null) return false;

            var serverCert = trust.GetResult();
            // 验证服务器证书的公钥哈希
            // 与预存的pin值比较
            return ValidatePins(trust);
        };
    }

    private bool ValidatePins(Security.SecTrust trust)
    {
        var certCount = trust.Count;
        for (int i = 0; i < certCount; i++)
        {
            var cert = trust[i];
            var publicKey = cert.GetPublicKey();
            // 计算公钥的SHA-256并与pinnedKeys比较
            // 匹配任意一个即可
        }
        return false;
    }
}

构建安全的HttpClient

接下来把上面的安全策略整合到一个统一的HttpClient工厂里。这样整个应用只需要注入这个client就行了:

namespace MyMauiApp.Services;

public static class SecureHttpClientFactory
{
    public static HttpClient CreateSecureClient(
        SecureTokenManager tokenManager)
    {
        HttpMessageHandler handler;

#if ANDROID
        handler = new Xamarin.Android.Net.AndroidMessageHandler
        {
            // Android自动使用network_security_config
        };
#elif IOS
        handler = new Platforms.iOS.PinnedHttpHandler(
            new[] { "你的证书pin值" });
#else
        handler = new HttpClientHandler();
#endif

        var client = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://api.yourserver.com"),
            Timeout = TimeSpan.FromSeconds(30)
        };

        // 禁止跟随重定向到非HTTPS地址
        if (handler is HttpClientHandler httpHandler)
        {
            httpHandler.AllowAutoRedirect = true;
            httpHandler.MaxAutomaticRedirections = 3;
        }

        return client;
    }
}

// 在DI容器中注册
// MauiProgram.cs
builder.Services.AddSingleton();
builder.Services.AddSingleton(sp =>
    SecureHttpClientFactory.CreateSecureClient(
        sp.GetRequiredService()));

自动附加Bearer Token的DelegatingHandler

每个API请求都手动加token?那也太累了。写一个DelegatingHandler自动搞定:

public class AuthenticatedHttpHandler : DelegatingHandler
{
    private readonly SecureTokenManager _tokenManager;

    public AuthenticatedHttpHandler(
        SecureTokenManager tokenManager,
        HttpMessageHandler innerHandler) : base(innerHandler)
    {
        _tokenManager = tokenManager;
    }

    protected override async Task SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await _tokenManager.GetAccessTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new System.Net.Http.Headers
                    .AuthenticationHeaderValue("Bearer", token);
        }

        var response = await base.SendAsync(
            request, cancellationToken);

        // 401时清除token,提示用户重新登录
        if (response.StatusCode ==
            System.Net.HttpStatusCode.Unauthorized)
        {
            _tokenManager.ClearAllTokens();
            // 触发重新登录流程
        }

        return response;
    }
}

代码保护与反逆向工程

.NET MAUI应用编译后的DLL包含完整的元数据和IL代码,攻击者用ILSpy之类的工具几分钟就能看到你的源码。这不是吓唬你——我亲手试过反编译自己的项目,出来的代码几乎跟原始版本一模一样。

对于涉及商业机密、加密逻辑或许可证验证的应用,代码保护是必须做的

多层防御策略

有效的代码保护不是靠单一手段,而是多层叠加:

  • 代码混淆(Obfuscation)——重命名类、方法、字段,打乱控制流
  • 字符串加密(String Encryption)——防止敏感字符串被直接搜索到
  • NativeAOT编译——直接编译为原生机器码,压根没有IL可以反编译
  • 篡改检测(Tamper Detection)——运行时校验应用完整性
  • Root/越狱检测——发现设备被破解时采取保护措施

NativeAOT:最强编译级保护

.NET 10对MAUI的NativeAOT支持已经相当成熟了。NativeAOT会把C#代码直接编译为原生机器码,完全不包含IL代码和元数据,从根本上堵死了反编译这条路。

在项目文件中启用也很简单:


    true
    
    
        -all
    

NativeAOT除了安全性,还有个好处:启动速度和内存占用都有明显改善。实测.NET 10 + NativeAOT的启动时间能缩短30%-50%,算是意外之喜。

代码混淆工具集成

即使用了NativeAOT,对于无法完全AOT编译的场景(比如需要反射的部分),代码混淆仍然很有必要。目前主流的工具有:

  • Dotfuscator(PreEmptive):微软官方推荐,支持重命名、控制流混淆、字符串加密、篡改检测
  • Babel Obfuscator:比较轻量,对.NET MAUI项目兼容性不错
  • ByteHide Shield:提供代码虚拟化等高级保护功能

以Dotfuscator为例,在MSBuild中集成:



    
    

Root/越狱检测

在被root或越狱的设备上,应用更容易被调试和篡改。虽然这不是万能的(后面FAQ会讲到),但作为防御层之一还是值得加上。

namespace MyMauiApp.Security;

public static class DeviceSecurityChecker
{
    public static bool IsDeviceCompromised()
    {
#if ANDROID
        return CheckAndroidRoot();
#elif IOS
        return CheckIosJailbreak();
#else
        return false;
#endif
    }

#if ANDROID
    private static bool CheckAndroidRoot()
    {
        var rootIndicators = new[]
        {
            "/system/app/Superuser.apk",
            "/system/xbin/su",
            "/system/bin/su",
            "/sbin/su",
            "/data/local/bin/su",
            "/data/local/xbin/su"
        };

        foreach (var path in rootIndicators)
        {
            if (System.IO.File.Exists(path))
                return true;
        }

        // 检查危险属性
        try
        {
            var process = Java.Lang.Runtime.GetRuntime()?
                .Exec(new[] { "getprop", "ro.debuggable" });
            if (process != null)
            {
                using var reader = new System.IO.StreamReader(
                    process.InputStream!);
                var result = reader.ReadToEnd().Trim();
                if (result == "1") return true;
            }
        }
        catch { }

        return false;
    }
#endif

#if IOS
    private static bool CheckIosJailbreak()
    {
        var jailbreakPaths = new[]
        {
            "/Applications/Cydia.app",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt",
            "/private/var/lib/apt/"
        };

        foreach (var path in jailbreakPaths)
        {
            if (System.IO.File.Exists(path))
                return true;
        }

        // 检查是否能写入系统目录
        try
        {
            System.IO.File.WriteAllText(
                "/private/test_jailbreak.txt", "test");
            System.IO.File.Delete("/private/test_jailbreak.txt");
            return true; // 如果能写入说明越狱了
        }
        catch { }

        return false;
    }
#endif
}

在应用启动时调用检测:

// App.xaml.cs
protected override void OnStart()
{
    if (DeviceSecurityChecker.IsDeviceCompromised())
    {
        // 根据业务需求选择策略:
        // 1. 显示警告但允许继续
        // 2. 禁用敏感功能
        // 3. 直接退出应用
        MainPage = new ContentPage
        {
            Content = new Label
            {
                Text = "检测到不安全设备,部分功能已限制",
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center
            }
        };
    }
}

生物识别认证:指纹和面部识别

对于支付确认、敏感数据查看这类操作,光有OAuth登录还不够,再加一层生物识别会安心很多。.NET MAUI本身没有内置生物识别API,不过社区有很好用的Plugin.Maui.Biometric包:

// 安装 Plugin.Maui.Biometric NuGet包
// dotnet add package Plugin.Maui.Biometric

using Plugin.Maui.Biometric;

namespace MyMauiApp.Services;

public class BiometricService
{
    private readonly IBiometric _biometric;

    public BiometricService(IBiometric biometric)
    {
        _biometric = biometric;
    }

    public async Task AuthenticateAsync(string reason)
    {
        var request = new AuthenticationRequest
        {
            Title = "身份验证",
            Subtitle = reason,
            NegativeText = "取消"
        };

        var result = await _biometric.AuthenticateAsync(
            request, CancellationToken.None);

        return result.Status == BiometricResponseStatus.Success;
    }

    public async Task IsBiometricAvailableAsync()
    {
        var result = await _biometric.GetAuthenticationStatusAsync();
        return result == BiometricHwStatus.Success;
    }
}

// MauiProgram.cs中注册
builder.Services.AddSingleton(BiometricAuthenticationService.Default);
builder.Services.AddSingleton();

安全架构最佳实践总结

好了,到这里各个安全层面都讲到了。来把它们整合到依赖注入容器中,看看完整的样子:

// MauiProgram.cs — 完整安全配置
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSans");
        });

    // 安全服务注册
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();

    // 安全HttpClient注册
    builder.Services.AddSingleton(sp =>
    {
        var tokenManager = sp.GetRequiredService();
        return SecureHttpClientFactory.CreateSecureClient(tokenManager);
    });

    // ViewModels
    builder.Services.AddTransient();

    return builder.Build();
}

完整的安全防护体系应该覆盖下面这些层面:

安全层面 方案 关键技术
身份验证 OAuth 2.0 + PKCE WebAuthenticator、MSAL
凭据存储 操作系统级加密 SecureStorage(Keychain/EncryptedSharedPrefs)
网络通信 Certificate Pinning Network Security Config、NSUrlSessionHandler
API安全 Bearer Token自动管理 DelegatingHandler、Token刷新
代码保护 多层混淆 + AOT NativeAOT、Dotfuscator
运行时保护 环境检测 Root/越狱检测、篡改检测
二次验证 生物识别 指纹/面部识别

常见问题(FAQ)

.NET MAUI应用应该使用哪种身份验证方式?

推荐OAuth 2.0 + Authorization Code + PKCE,这是OWASP和RFC 8252推荐的移动端标准方案。如果后端是Microsoft Entra ID,直接用MSAL.NET最省事——token管理、缓存、刷新它全包了。自建认证服务器的话,WebAuthenticator + PKCE就够了。

SecureStorage的数据在换机后能恢复吗?

不能直接恢复。Android的EncryptedSharedPreferences和iOS的Keychain加密密钥都是绑定到具体设备的,换了设备密钥就不同了,旧数据解不开。正确的做法是设计好token刷新机制,让用户在新设备重新登录就行。别忘了配置auto_backup_rules.xml排除加密数据,不然备份恢复后应用会崩。

NativeAOT编译对.NET MAUI应用有什么限制?

最大的限制是不支持运行时动态代码生成(Reflection.Emit),某些重度依赖反射的库可能需要调整或者显式标注保留类型。好消息是截至.NET 10,NativeAOT在iOS上已经比较成熟了,Android上也可以用。启用后应用体积可能变大,但启动速度会有明显提升,一般来说这个取舍是值得的。

如何防止.NET MAUI应用被中间人攻击?

Certificate Pinning是最有效的防御手段。Android上用network_security_config.xml声明pin值,iOS上通过自定义HttpMessageHandler验证服务器证书公钥。另外别忘了:始终用HTTPS、禁止明文流量、在pin配置里包含备份pin以应对证书轮换、定期更新pin值并设好过期时间。

Root或越狱检测被绕过了怎么办?

说实话,Root/越狱检测本来就不是银弹。高级攻击者可以hook检测函数轻松绕过。所以它应该是多层防御中的一部分,而不是你唯一的依仗。把代码混淆(让攻击者难以找到检测逻辑)、运行时完整性校验(检测代码是否被篡改)和服务端验证(在API层面做额外安全检查)组合起来,构建纵深防御才是正道。

关于作者 Sofia Marchetti

Sofia is a mobile platform engineer with eleven years across native iOS, Xamarin, and now .NET MAUI. She spent three years at Spotify in Stockholm working on internal tooling for the mobile build infrastructure, then joined a fintech in Milan where she leads the mobile foundations team responsible for a MAUI app that handles around 2 million monthly active users across iOS and Android. Most of what she writes about lives in the build and release layer: deterministic builds, fastlane integration for MAUI, code signing on macOS runners, MAUI .NET 9 upgrade postmortems, and benchmarking startup time on cheap Android hardware. She co-organizes the Milano .NET meetup and gave a talk at NDC Oslo 2025 on shrinking a MAUI Android APK from 84 MB to 31 MB without losing features.