一、生成client_secret
安装jose-jwt,示例代码如下
///
/// 生成JWT
///
///
private string CreateAppleClientSecret()
{
var iat = Math.Round((DateTime.UtcNow.AddMinutes(-1) - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds, 0);
var exp = Math.Round((DateTime.UtcNow.AddMinutes(30) - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds, 0);
string keyId = "8D7EEULXX4";
var extraHeader = new Dictionary()
{
{ "alg", "ES256" },
{ "kid", keyId }, //key id
{ "typ", "JWT" }
};
var payload = new Dictionary()
{
{ "sub" , bundleIdentifier }, //iOS App的Bundle Identifier
{ "exp", exp },
{ "iat", iat },
{ "iss", "2K26LU5HS2" }, //team ID
{ "aud", "https://appleid.apple.com" },
{ "origin", "https://appleid.apple.com" }
};
var keyString = GetApplePrivateKeyText();
CngKey privateKey = CngKey.Import(Convert.FromBase64String(keyString), CngKeyBlobFormat.Pkcs8PrivateBlob);
string token = JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeader);
return token;
}
///
/// 获取苹果p8文件内容
///
///
private string GetApplePrivateKeyText()
{
string filePath = "iOSFiles/AuthKey_976XVM7U2S.p8"; //iOS提供的p8文件,可以不用文件,直接复制文件里边的内容
string path = Server.MapPath(filePath);
string text = System.IO.File.ReadAllText(path);
//去头去尾的方法:
var lines = text.Split('\n');
var privateKeyText = string.Join("", lines.Skip(1).Take(lines.Length - 2).Select(l => l.Trim()));
return privateKeyText;
}
二、发送请求
1.请求地址为 https://appleid.apple.com/auth/token;
2.method为POST;
3.ContentType为application/x-www-form-urlencoded;
4.所需参数为
①client_id : "" //iOS App的Bundle Identifier
②grant_type : "authorization_code" //固定值
③code : 苹果验证返回authorizationCode
④client_secret : CreateAppleClientSecret() //上面步骤一生成的token
示例代码
///
/// 苹果登录
///
/// iOS App的Bundle Identifier
/// 苹果验证返回authorizationCode
/// 苹果验证返回user
///
public string SignInWithApple(string bundleIdentifier, string authorizationCode, string appleUserId)
{
try
{
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback((object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) =>
{
return true; //总是接受
});
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://appleid.apple.com/auth/token");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
var datas = new Dictionary()
{
{ "client_id", bundleIdentifier },
{ "grant_type", "authorization_code"},//固定authorization_code
{ "code", authorizationCode },//授权码,前端验证登录给予
{ "client_secret", CreateAppleClientSecret() } //client_secret,后面方法生成
};
string postData = JsonUrlEncode(JsonConvert.SerializeObject(datas));
byte[] buf = Encoding.Default.GetBytes(postData);
request.ContentLength = buf.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(buf, 0, buf.Length);
requestStream.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream stream = response.GetResponseStream();
StreamReader streamReader = new StreamReader(stream, encoding: Encoding.UTF8);
responseString = streamReader.ReadToEnd();
HlllAppleAuthTokenResultModel tokenResultModel = JsonConvert.DeserializeObject(responseString);
HlllAppleJwtPlayloadModel playloadModel = DecodeAppleJwtPlayload(tokenResultModel.id_token);
if (!playloadModel.Aud.Equals(bundleIdentifier) || !playloadModel.Sub.Equals(appleUserId))
{
// "信息不正确,请重试";
}
else //你的服务器处理逻辑,服务器数据库添加苹果账号ID验证appleUserId
{
}
}
catch (Exception ex)
{
}
}
///
/// json转urlencode
///
///
public static string JsonUrlEncode(string json)
{
Dictionary dic = JsonConvert.DeserializeObject>(json);
StringBuilder builder = new StringBuilder();
foreach (KeyValuePair item in dic)
{
builder.Append(GetFormDataContent(item, ""));
}
return builder.ToString().TrimEnd('&');
}
///
/// 递归转formdata
///
///
///
///
private static string GetFormDataContent(KeyValuePair item, string preStr)
{
StringBuilder builder = new StringBuilder();
if (string.IsNullOrEmpty(item.Value?.ToString()))
{
builder.AppendFormat("{0}={1}", string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]"), System.Web.HttpUtility.UrlEncode((item.Value == null ? "" : item.Value.ToString()).ToString()));
builder.Append("&");
}
else
{
//如果是数组
if (item.Value.GetType().Name.Equals("JArray"))
{
var children = JsonConvert.DeserializeObject>(item.Value.ToString());
for (int j = 0; j < children.Count; j++)
{
Dictionary childrendic = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(children[j]));
foreach (var row in childrendic)
{
builder.Append(GetFormDataContent(row, string.IsNullOrEmpty(preStr) ? (item.Key + "[" + j + "]") : (preStr + "[" + item.Key + "][" + j + "]")));
}
}
}
//如果是对象
else if (item.Value.GetType().Name.Equals("JObject"))
{
Dictionary children = JsonConvert.DeserializeObject>(item.Value.ToString());
foreach (var row in children)
{
builder.Append(GetFormDataContent(row, string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]")));
}
}
//字符串、数字等
else
{
builder.AppendFormat("{0}={1}", string.IsNullOrEmpty(preStr) ? item.Key : (preStr + "[" + item.Key + "]"), System.Web.HttpUtility.UrlEncode((item.Value == null ? "" : item.Value.ToString()).ToString()));
builder.Append("&");
}
}
return builder.ToString();
}
private HlllAppleJwtPlayloadModel DecodeAppleJwtPlayload(string jwtString)
{
try
{
var code = jwtString.Split('.')[1];
code = code.Replace('-', '+').Replace('_', '/').PadRight(4 * ((code.Length + 3) / 4), '=');
var bytes = Convert.FromBase64String(code);
var decode = Encoding.UTF8.GetString(bytes);
return JsonConvert.DeserializeObject(decode);
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
///
/// 苹果接口返回数据
///
public class HlllAppleAuthTokenResultModel
{
///
/// access_token
///
public string access_token { set; get; } = "";
///
/// token_type
///
public string token_type { set; get; } = "";
///
/// expires_in
///
public long expires_in { set; get; } = 0;
///
/// refresh_token
///
public string refresh_token { set; get; } = "";
///
/// id_token
///
public string id_token { set; get; } = "";
}
///
/// 苹果接口返回的id_token解析
///
public class HlllAppleJwtPlayloadModel
{
///
/// "https://appleid.apple.com"
///
[JsonProperty("iss")]
public string Iss { get; set; }
///
/// 这个是你的app的bundle identifier
///
[JsonProperty("aud")]
public string Aud { get; set; }
///
///
///
[JsonProperty("exp")]
public long Exp { get; set; }
///
///
///
[JsonProperty("iat")]
public long Iat { get; set; }
///
/// 用户ID
///
[JsonProperty("sub")]
public string Sub { get; set; }
///
///
///
[JsonProperty("at_hash")]
public string AtHash { get; set; }
///
///
///
[JsonProperty("email")]
public string Email { get; set; }
///
///
///
[JsonProperty("email_verified")]
public bool EmailVerified { get; set; }
///
///
///
[JsonProperty("is_private_email")]
public bool IsPrivateEmail { get; set; }
///
///
///
[JsonProperty("auth_time")]
public long AuthTime { get; set; }
///
///
///
[JsonProperty("nonce_supported")]
public bool NonceSupported { get; set; }
}
常见问题
- POST请求返回400
1.可能参数不正确;
2.每次授权信息只能请求一次,成功之后,第二次之后请求均返回400。
- 本地调试没问题,发布到服务器报错,错误为:
系统找不到指定的文件。\r\n【具体信息】 在 System.Security.Cryptography.NCryptNative.ImportKey(SafeNCryptProviderHandle provider, Byte[] keyBlob, String format)\r\n 在 System.Security.Cryptography.CngKey.Import(Byte[] keyBlob, String curveName, CngKeyBlobFormat format, CngProvider provider)
解决方案
1.在服务器以管理员身份运行 C:\Windows\System32\cmd.exe ;
2.输入以下命令,注意修改name='hlll'部分,单引号部分为你的IIS上面的网站名称。
c:\windows\system32\inetsrv\appcmd.exe set config -section:applicationPools "/[name='你的网站名称'].processModel.loadUserProfile:true"
- 本地调试没问题,发布到服务器报错,错误为:
用于 ECDsaCng 算法的密钥必须具有 ECDsa 算法组(英文报错为 Keys used with the ECDsaCng algorithm must have an algorithm group of ECDsa)
解决方案
服务器.NET framework升级到4.6.2即可解决问题。