.net Sign in with apple
苹果应用市场发出规定,所有上架的app,只要有第三方登录的功能,必须加上使用苹果手机登录这一功能。由于我是个.neter,主要做的是后端,所以登录验证这块记录一下,以供参考!
1.Token结构
var jwt = "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA";
这块是前端从苹果服务器获取到的token,这个token是jwt的格式的,我们需要对jwt有一些了解
JWT构成
jwt最多由3部分组成,"{header}.{payload}.{signature}"由“.”区分开来。
第一部分我们称它为头部(header),第二部分称为载荷(payload),第三部分是签证(signature)
header
jwt的头部承载两部分信息:
1、声明类型,这里是jwt
2、声明加密的算法通常是SHA256
完整的头部json如下
{ 'typ': 'JWT', 'alg': 'HS256' }
然后将头部进行base64加密,构成第一部分
ewogICd0eXAnOiAnSldUJywKICAnYWxnJzogJ0hTMjU2Jwp9
playload
载荷就是存放有效信息的地方。其中包括(包括但不限于
)如下几个部分:
1、iss:jwt的签发者
2、sub:jwt所面向的用户
3、aud:接收jwt的一方
4、exp:jwt的过期时间,这个日期必须大于签发时间
5、nbf:定义在什么时间之前,该jwt都是不可用的
6、iat:jwt的签发时间
7、jti:jwt的唯一身份标识,主要用来作为一次性token
json信息如下:
{ "sub":"123456", "aud":"Tom" }
然后进行base64加密,构成第二部分
ewogICJzdWIiOiIxMjM0NTYiLAogICJhdWQiOiJUb20iCn0=
signature
jwt的第三个部分是一个签证信息,这个信息有三部分组成:
1、header(base64后的)
2、payload(base64后的)
3、secret
这个部分需要base64加密后的header和payload使用"."连接组成的字符串,然后通过头部信息的加密方式进行加盐secret组合加密构成了jwt的第三部分。
如何验证苹果传过来的token的有效性?
苹果官方文档给了我们两种验证token的方式


这里我们选择第一种方式验证。先去获取公钥,通过访问网址https://appleid.apple.com/auth/keys我们得到如下信息

首先我们需要通过头部信息来找到对应的公钥,同时我们知道signature信息是由头部和payload加上钥匙组成的。我们这里可以使用RSACryptoServiceProvider类库导入公钥,然后来验证数据的SHA256的摘要即可
using (var rsa = new RSACryptoServiceProvider()) { // 导入RSA公钥 rsa.ImportParameters(new RSAParameters() { Exponent = e, Modulus = n }); // 验证数据的SHA256摘要 var signatureVerified = rsa.VerifyData(signedOver, signagure, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); if (!signatureVerified) { issss = false; } }
完整代码如下:
namespace jwt { class Program { static void Main(string[] args) { // jwt最多由3部分组成,"{header}.{payload}.{signature}" var jwt = "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA"; var parts = jwt.Split('.'); // heaer 类似 {"kid": "eXaunmL", "alg": "RS256"}, kid是key的标志,用来从一堆keys里拿到响应的钥匙 var header = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(FromBase64(parts[0]))); // payload 类似 {... "sub": "000401.a5bb0bec6491440bb61fa314a50b2a13.0737", "email": "hj7a4dp4ua@privaterelay.appleid.com", ...} var payload = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(FromBase64(parts[1]))); // 第三部分是验证码,用来验证数据"{header}.{payload}" var signagure = FromBase64(parts[2]); var signedOver = Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]); // 下载钥匙,并用kid找到响应的签名公钥 var keysJson = Http_Get("https://appleid.apple.com/auth/keys"); var keys = JsonConvert.DeserializeObject<JObject>(keysJson)["keys"] as JArray; var key = keys.OfType<JObject>().FirstOrDefault(x => (string)x["kid"] == (string)header["kid"]); // 这里只支持RS256签名。RS256就是使用RSA算法,用一个RSA公钥,来验证数据的SHA256摘要。 var alg = (string)key["alg"]; if (alg != "RS256") throw new NotImplementedException(); var n = FromBase64((string)key["n"]); var e = FromBase64((string)key["e"]); bool issss = true; using (var rsa = new RSACryptoServiceProvider()) { // 导入RSA公钥 rsa.ImportParameters(new RSAParameters() { Exponent = e, Modulus = n }); // 验证数据的SHA256摘要 var signatureVerified = rsa.VerifyData(signedOver, signagure, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); if (!signatureVerified) { issss = false; } } DateTime exp = ConvertStringToDateTime(Convert.ToInt32(payload["exp"])); var iat = ConvertStringToDateTime(Convert.ToInt32(payload["iat"])); //验证token的的有效期 if (DateTime.Now > exp || DateTime.Now < iat) { issss = false; } } static byte[] FromBase64(string base64WithoutPadding) { var base64 = base64WithoutPadding.Length % 4 == 0 ? base64WithoutPadding : base64WithoutPadding + new string('=', 4 - base64WithoutPadding.Length % 4); return Convert.FromBase64String(base64.Replace("-", "+").Replace('_', '/')); } static DateTime ConvertStringToDateTime(int timeStamp) { DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); long lTime = ((long)timeStamp * 10000000); TimeSpan toNow = new TimeSpan(lTime); DateTime targetDt = dtStart.Add(toNow); return targetDt; } public static string Http_Get(string url, int timeout = 25000, string contentType = "application/json; charset=utf-8", bool keepAlive = false) { HttpWebRequest request = CreateWebRequest(url, "GET", timeout, contentType, keepAlive, false); request.ContentLength = 0; try { using (var response = request.GetResponse()) { Stream stream = response.GetResponseStream(); StreamReader reader = new StreamReader(stream, Encoding.UTF8); string retString = reader.ReadToEnd(); reader.Close(); stream.Close(); return retString; } } catch (Exception ex) { throw ex; } finally { if (request != null) { request.Abort(); } } } private static HttpWebRequest CreateWebRequest(string url, string method, int timeout, string contentType, bool keepAlive, bool isnocache = true) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; request.ContentType = contentType; request.Accept = "application/xml,application/xhtml+xml, application/json,text/html, text/javascript;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; if (isnocache) { request.Headers["Pragma"] = "no-cache"; } request.Timeout = timeout; request.AllowAutoRedirect = true; request.KeepAlive = keepAlive; request.ServicePoint.Expect100Continue = false; request.Proxy = null; return request; } } }
撰写博客主在学习记录,如有不足欢迎指出!
浙公网安备 33010602011771号