2020年4月之后,上架App Store得应用必须集成apple账号得登录。
近期博主刚好配合前端IOS集成apple登录,网上找了不少文章教程,发现基本都是网页集成登录或者是java代码,比较少纯后端net验证,期间也走了不少弯路,在这分享给大家实现思路和需要得注意事项。
文章开始前先说明一下此文环境为netcore3.1环境代码编写,IOS相关配置和文章请参考文末链接。
整体思路为:前端调用苹果接口获取到userID和authorizationCode,后端通过authorizationCode调用苹果接口验证,若检验成功会返回相关信息;
以下为apple官方接口文档说明:https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
根据文档可知,调用授权码验证需要传递6个参数,其中3个必填client_id,client_secret和grant_type。
client_id和client_secret为apple验证请求方是否合法,client_secret得生成也本文得重点讲解。
grant_type 我理解为操作方式,固定为验证授权码authorization_code和刷新token refresh_token,因为我们是验证授权码,所以只需传递authorization_code
这里注意Content-Type: application/x-www-form-urlencoded,若使用postman工具尝试请求Body请记得选x-www-form-urlencoded,否则请求格式失败必定“invalid_client”;
先说请求的几种错误的返回格式:
| 返回 | 原因 |
| invalid_client | client_id或client_secret错误,请复制下面代码生成 |
|
invalid_grant
|
authorization_code 授权码错误,可以去怼前端了 |
|
unsupported_grant_type
|
grant_type错误,嗯请固定authorization_code,别问我为什么知道,当然是特意去请求尝试给你们看的啦 |

说了这么多先贴一下请求代码:
只要成功返回,解析返回值的IdToken,jwt解析第二段验证Aud和clientId,Sub与userID一致即可:
1 /// <summary>
2 /// 检验生成的授权码是正确的,需要给出正确的授权码
3 /// </summary>
4 /// <param name="authorizationCode">授权码</param>
5 /// <param name="appUserId">apple用户ID</param>
6 /// <returns></returns>
7 public async Task TestAppleSign(string authorizationCode, string appUserId)
8 {
9 var httpClientHandler = new HttpClientHandler
10 {
11 ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true
12 };
13 var httpClient = new HttpClient(httpClientHandler, true)
14 {
15 //超时时间设置长一点点,有时候响应超过3秒,根据情况设置
16 //我这里是单元测试,防止异常所以写30秒
17 Timeout = TimeSpan.FromSeconds(30)
18 };
19 var newToken = CreateNewClientSecret();
20 var clientId = "bundleId";//找IOS要
21 var datas = new Dictionary<string, string>()
22 {
23 {"client_id", clientId},
24 {"grant_type", "authorization_code"},//固定authorization_code
25 {"code",authorizationCode },//授权码,前端验证登录给予
26 {"client_secret",newToken} //client_secret,后面方法生成
27 };
28 //x-www-form-urlencoded 使用FormUrlEncodedContent
29 var formdata = new FormUrlEncodedContent(datas);
30 var result = await httpClient.PostAsync("https://appleid.apple.com/auth/token", formdata);
31 var re = await result.Content.ReadAsStringAsync();
32 if (result.IsSuccessStatusCode)
33 {
34 var deserializeObject = JsonConvert.DeserializeObject<TokenResult>(re);
35 var jwtPlayload = DecodeJwtPlayload(deserializeObject.IdToken);
36 if (!jwtPlayload.Aud.Equals(clientId) || !jwtPlayload.Sub.Equals(appUserId))//appUserId,前端验证登录给予
37 {
38
39 }
40 }
41 else
42 {
43 //请根据re的返回值,查看上面的错误表格
44 }
45 }
生成ClientSecret代码如下:需要注意的是IssuedAt和NotBefore时间需要留些余地,我就是在windos开发成功,部署到服务器环境后一致报invalid_client,之前发生过加密错误,还以为又是加密方式导致,经过一段时间的排除后发现是时间的坑
1 public static string CreateNewClientSecret()
2 {
3 var handler = new JwtSecurityTokenHandler();
4 var subject = new Claim("sub", "bundleId");//找IOS要
5 var tokenDescriptor = new SecurityTokenDescriptor()
6 {
7 Audience = "https://appleid.apple.com",//固定值
8 Expires = DateTime.Now.AddMonths(5),//ClientSecret超时时间,可以设置长一点,也可以根据需要设置有效时间
9 Issuer = "team ID",//team ID,找IOS要
10 //防止服务器时间比apple时间晚
11 //签发时间
12 IssuedAt = DateTime.Now.AddDays(-1),
13 //防止服务器时间比apple时间晚
14 //生效时间
15 NotBefore = DateTime.Now.AddDays(-1),
16 Subject = new ClaimsIdentity(new[] { subject }),
17 };
18 byte[] keyBlob = GetPrivateKeyBytesAsync();
19 var algorithm = CreateAlgorithm(keyBlob);
20 {
21 tokenDescriptor.SigningCredentials = CreateSigningCredentials("KeyID", algorithm);//p8私钥文件得Key,找IOS要
22
23 var clientSecret = handler.CreateEncodedJwt(tokenDescriptor);
24
25 return clientSecret;
26 }
27 }
28 public static byte[] GetPrivateKeyBytesAsync()
29 {
30 //p8文件内容
31 string content = @"-----BEGIN PRIVATE KEY-----
32 ********************************
33 *******************************
34 ***********************************
35 *************
36 ---- - END -----";
37
38 if (content.StartsWith("-----BEGIN PRIVATE KEY-----", StringComparison.Ordinal))
39 {
40 string[] keyLines = content.Split('\n');
41 content = string.Join(string.Empty, keyLines.Skip(1).Take(keyLines.Length - 2));
42 }
43
44 return Convert.FromBase64String(content);
45 }
46 private static ECDsa CreateAlgorithm(byte[] keyBlob)
47 {
48 var algorithm = ECDsa.Create();
49
50 try
51 {
52 algorithm.ImportPkcs8PrivateKey(keyBlob, out int _);
53 return algorithm;
54 }
55 catch (Exception)
56 {
57 algorithm?.Dispose();
58 throw;
59 }
60 }
61 private static SigningCredentials CreateSigningCredentials(string keyId, ECDsa algorithm)
62 {
63 var key = new ECDsaSecurityKey(algorithm) { KeyId = keyId };
64 return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256Signature);
65 }
只要成功返回,解析返回值的IdToken,jwt解析第二段验证Aud和clientId,Sub与userID一致即可,附反序列化返回值和jwt解析:
1 /// <summary>
2 /// 解析jwt第二部分
3 /// </summary>
4 /// <param name="jwtString"></param>
5 /// <returns></returns>
6 private JwtPlayload DecodeJwtPlayload(string jwtString)
7 {
8 try
9 {
10 var code = jwtString.Split('.')[1];
11 code = code.Replace('-', '+').Replace('_', '/').PadRight(4 * ((code.Length + 3) / 4), '=');
12 var bytes = Convert.FromBase64String(code);
13 var decode = Encoding.UTF8.GetString(bytes);
14 return JsonConvert.DeserializeObject<JwtPlayload>(decode);
15 }
16 catch (Exception e)
17 {
18 throw new Exception(e.Message);
19 }
20 }
21 /// <summary>
22 /// 接口返回值
23 /// </summary>
24 public class TokenResult
25 {
26 /// <summary>
27 /// 一个token
28 /// </summary>
29 [JsonProperty("access_token")]
30 public string AccessToken { get; set; }
31 /// <summary>
32 /// Bearer
33 /// </summary>
34 [JsonProperty("token_type")]
35 public string TokenType { get; set; }
36 /// <summary>
37 ///
38 /// </summary>
39 [JsonProperty("expires_in")]
40 public long ExpiresIn { get; set; }
41 /// <summary>
42 /// 一个token
43 /// </summary>
44 [JsonProperty("refresh_token")]
45 public string RefreshToken { get; set; }
46 /// <summary>
47 /// "结果是JWT,字符串形式,identityToken 解析后和客户端端做比对
48 /// </summary>
49 [JsonProperty("id_token")]
50 public string IdToken { get; set; }
51 }
52 /// <summary>
53 /// jwt第二部分
54 /// </summary>
55 private class JwtPlayload
56 {
57 /// <summary>
58 /// "https://appleid.apple.com"
59 /// </summary>
60 [JsonProperty("iss")]
61 public string Iss { get; set; }
62 /// <summary>
63 /// 这个是你的app的bundle identifier
64 /// </summary>
65 [JsonProperty("aud")]
66 public string Aud { get; set; }
67 /// <summary>
68 ///
69 /// </summary>
70 [JsonProperty("exp")]
71 public long Exp { get; set; }
72 /// <summary>
73 ///
74 /// </summary>
75 [JsonProperty("iat")]
76 public long Iat { get; set; }
77 /// <summary>
78 /// 用户ID
79 /// </summary>
80 [JsonProperty("sub")]
81 public string Sub { get; set; }
82 /// <summary>
83 ///
84 /// </summary>
85 [JsonProperty("at_hash")]
86 public string AtHash { get; set; }
87 /// <summary>
88 ///
89 /// </summary>
90 [JsonProperty("email")]
91 public string Email { get; set; }
92 /// <summary>
93 ///
94 /// </summary>
95 [JsonProperty("email_verified")]
96 public bool EmailVerified { get; set; }
97 /// <summary>
98 ///
99 /// </summary>
100 [JsonProperty("is_private_email")]
101 public bool IsPrivateEmail { get; set; }
102 /// <summary>
103 ///
104 /// </summary>
105 [JsonProperty("auth_time")]
106 public long AuthTime { get; set; }
107 /// <summary>
108 ///
109 /// </summary>
110 [JsonProperty("nonce_supported")]
111 public bool NonceSupported { get; set; }
112 }
113
下面得生成client_secret方式是由文章参考而来:https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core
但是这个方式只适用于windos,使用容器方式部署到linux时发现CnKey加密方式会报平台错误,若只在windos部署可直接使用该方式
public static string CreateNewToken()
{
const string iss = "62QM29578N"; // your account's team ID found in the dev portal
const string aud = "https://appleid.apple.com";
const string sub = "com.scottbrady91.authdemo.service"; // same as client_id
const string privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgnbfHJQO9feC7yKOenScNctvHUP+Hp3AdOKnjUC3Ee9GgCgYIKoZIzj0DAQehRANCAATMgckuqQ1MhKALhLT/CA9lZrLA+VqTW/iIJ9GKimtC2GP02hCc5Vac8WuN6YjynF3JPWKTYjg2zqex5Sdn9Wj+"; // contents of .p8 file
var cngKey = CngKey.Import(
Convert.FromBase64String(privateKey),
CngKeyBlobFormat.Pkcs8PrivateBlob);
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
issuer: iss,
audience: aud,
subject: new ClaimsIdentity(new List<Claim> {new Claim("sub", sub)}),
expires: DateTime.UtcNow.AddMinutes(5), // expiry can be a maximum of 6 months
issuedAt: DateTime.UtcNow,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(
new ECDsaSecurityKey(new ECDsaCng(cngKey)), SecurityAlgorithms.EcdsaSha256));
return handler.WriteToken(token);
}
参考资料:
https://www.jianshu.com/p/e1284bd8c72a
苹果授权登陆后端验证:
https://blog.csdn.net/wpf199402076118/article/details/99677412
netcore验证相关文章:
https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core
https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
来源:oschina
链接:https://my.oschina.net/u/4401867/blog/4480711