You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何处理AppleSignInAuthenticator结果以在OpenIdDict中实现登录/注册

解决方案:移动端Apple登录转OpenIddict令牌端点

核心思路

你需要完成三个关键步骤:验证Apple提供的凭证合法性、关联/创建本地用户、生成OpenIddict令牌返回。以下是具体实现:


1. 定义请求模型

首先创建一个模型接收移动端传来的Apple登录凭证:

public class AppleMobileLoginRequest
{
    // 二选一:移动端获取的授权码
    public string AuthorizationCode { get; set; }
    // 二选一:移动端获取的ID Token
    public string IdToken { get; set; }
    // 你的应用客户端ID,用于验证请求合法性
    public string ClientId { get; set; }
}

2. 实现端点逻辑

替换你原来的ExchangeMobile方法,以下是完整的可运行代码:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using System.Text.Json;

[HttpPost("~/connect/token/apple"), IgnoreAntiforgeryToken, Produces("application/json")]
public async Task<IActionResult> ExchangeMobile([FromBody] AppleMobileLoginRequest request)
{
    var provider = "apple";
    var appleOptions = (await _authenticationSchemeProvider.GetSchemeAsync(provider)).Options as AppleOptions;
    if (appleOptions == null) return BadRequest("Apple认证配置未找到");

    // 验证客户端ID合法性(可选但推荐)
    if (request.ClientId != appleOptions.ClientId) return BadRequest("无效的客户端ID");

    JwtSecurityToken validatedIdToken = null;
    string appleUserId = null;
    string email = null;

    #region 步骤1:验证Apple凭证的合法性
    if (!string.IsNullOrEmpty(request.IdToken))
    {
        // 验证Apple ID Token的签名、签发者、受众等
        var tokenHandler = new JwtSecurityTokenHandler();
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://appleid.apple.com",
            ValidateAudience = true,
            ValidAudience = appleOptions.ClientId,
            ValidateLifetime = true,
            IssuerSigningKeys = await GetAppleSigningKeysAsync(), // 获取Apple的公钥用于验证签名
            ClockSkew = TimeSpan.FromMinutes(5) // 允许少量时间偏差
        };

        try
        {
            var claimsPrincipal = tokenHandler.ValidateToken(request.IdToken, validationParameters, out var validatedToken);
            validatedIdToken = validatedToken as JwtSecurityToken;
            appleUserId = validatedIdToken.Payload["sub"].ToString();
            email = validatedIdToken.Payload.ContainsKey("email") ? validatedIdToken.Payload["email"].ToString() : null;
        }
        catch (Exception ex)
        {
            return BadRequest($"Apple ID Token验证失败:{ex.Message}");
        }
    }
    else if (!string.IsNullOrEmpty(request.AuthorizationCode))
    {
        // 用授权码向Apple交换Token(更安全,推荐移动端传递授权码而非ID Token)
        var tokenResponse = await ExchangeAppleAuthorizationCodeAsync(request.AuthorizationCode, appleOptions);
        if (tokenResponse == null) return BadRequest("授权码无效或已过期");

        validatedIdToken = new JwtSecurityToken(tokenResponse.IdToken);
        appleUserId = validatedIdToken.Payload["sub"].ToString();
        email = validatedIdToken.Payload.ContainsKey("email") ? validatedIdToken.Payload["email"].ToString() : null;
    }
    else
    {
        return BadRequest("必须提供AuthorizationCode或IdToken");
    }
    #endregion

    #region 步骤2:关联或创建本地用户
    var user = await _userManager.FindByLoginAsync(provider, appleUserId);
    if (user == null)
    {
        if (string.IsNullOrEmpty(email)) return BadRequest("无法获取用户邮箱信息");

        user = new ApplicationUser
        {
            UserName = email,
            Email = email,
            EmailConfirmed = true,
            CreatedOn = DateTime.UtcNow,
            ChangedOn = DateTime.UtcNow
        };

        var createResult = await _userManager.CreateAsync(user);
        if (!createResult.Succeeded) return BadRequest($"创建用户失败:{string.Join(", ", createResult.Errors.Select(e => e.Description))}");

        // 添加Apple登录关联
        var loginInfo = new UserLoginInfo(provider, appleUserId, "Apple");
        var addLoginResult = await _userManager.AddLoginAsync(user, loginInfo);
        if (!addLoginResult.Succeeded) return BadRequest($"关联登录失败:{string.Join(", ", addLoginResult.Errors.Select(e => e.Description))}");
    }
    #endregion

    #region 步骤3:生成OpenIddict令牌
    // 创建用户的身份凭证
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(OpenIddictConstants.Claims.Subject, user.Id)
    };

    var identity = new ClaimsIdentity(claims, provider);
    var principal = new ClaimsPrincipal(identity);

    // 设置OpenIddict的令牌属性
    principal.SetScopes(new[]
    {
        OpenIddictConstants.Scopes.OpenId,
        OpenIddictConstants.Scopes.Email,
        OpenIddictConstants.Scopes.Profile,
        OpenIddictConstants.Scopes.OfflineAccess // 如果需要刷新令牌
    });

    principal.SetResource(request.ClientId);

    // 使用OpenIddict生成令牌
    var tokenDescriptor = new OpenIddictTokenDescriptor
    {
        Subject = user.Id,
        ClientId = request.ClientId,
        IssuedAt = DateTime.UtcNow,
        ExpiresAt = DateTime.UtcNow.AddHours(1), // 访问令牌有效期
        RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(7), // 刷新令牌有效期
        Scopes = principal.GetScopes(),
        Claims = principal.Claims.ToDictionary(c => c.Type, c => (object)c.Value)
    };

    var token = await _openIddictTokenManager.CreateAsync(tokenDescriptor);

    // 返回令牌响应
    return Ok(new
    {
        access_token = await _openIddictTokenManager.GetAccessTokenAsync(token),
        refresh_token = await _openIddictTokenManager.GetRefreshTokenAsync(token),
        token_type = "Bearer",
        expires_in = (int)TimeSpan.FromHours(1).TotalSeconds,
        scope = string.Join(" ", principal.GetScopes())
    });
    #endregion
}

// 辅助方法:获取Apple的公钥用于验证签名
private async Task<IEnumerable<SecurityKey>> GetAppleSigningKeysAsync()
{
    var httpClient = _httpClientFactory.CreateClient();
    var response = await httpClient.GetAsync("https://appleid.apple.com/auth/keys");
    response.EnsureSuccessStatusCode();
    var json = await response.Content.ReadAsStringAsync();
    var keys = new JsonWebKeySet(json);
    return keys.GetSigningKeys();
}

// 辅助方法:用授权码向Apple交换Token
private async Task<AppleTokenResponse> ExchangeAppleAuthorizationCodeAsync(string authorizationCode, AppleOptions options)
{
    var httpClient = _httpClientFactory.CreateClient();
    var requestContent = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["client_id"] = options.ClientId,
        ["client_secret"] = GenerateAppleClientSecret(options), // 需要生成Apple的客户端密钥
        ["code"] = authorizationCode,
        ["grant_type"] = "authorization_code",
        ["redirect_uri"] = options.RedirectUri // 注意:这里要和移动端授权时用的redirect_uri一致(移动端可能用urn:ietf:wg:oauth:2.0:oob)
    });

    var response = await httpClient.PostAsync("https://appleid.apple.com/auth/token", requestContent);
    if (!response.IsSuccessStatusCode) return null;

    var json = await response.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<AppleTokenResponse>(json);
}

// 辅助方法:生成Apple客户端密钥(和你原来配置的SetSigningKey逻辑一致)
private string GenerateAppleClientSecret(AppleOptions options)
{
    var securityKey = options.SigningKey as ECDsaSecurityKey;
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

    var tokenHandler = new JwtSecurityTokenHandler();
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Issuer = options.TeamId,
        Audience = "https://appleid.apple.com",
        Claims = new Dictionary<string, object>
        {
            ["sub"] = options.ClientId
        },
        Expires = DateTime.UtcNow.AddDays(180), // Apple允许最长6个月
        SigningCredentials = credentials
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

// Apple Token响应模型
public class AppleTokenResponse
{
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; }

    [JsonPropertyName("token_type")]
    public string TokenType { get; set; }

    [JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }

    [JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; }

    [JsonPropertyName("id_token")]
    public string IdToken { get; set; }
}

关键注意事项

  • 优先使用AuthorizationCode:相比直接传递ID Token,用授权码交换更安全,避免ID Token被篡改或泄露的风险。
  • 必须验证ID Token签名:不能直接解析JWT内容,必须用Apple的公钥验证签名,确保令牌是Apple签发的。
  • 客户端ID验证:添加客户端ID验证可以防止非法请求,确保只有你的移动端应用能调用该端点。
  • Apple客户端密钥生成:客户端密钥需要用你的Team ID、Client ID和私钥生成,有效期最长6个月,你可以复用原来配置中的签名逻辑。

内容的提问来源于stack exchange,提问作者Jacob Clark

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 17:27:03