苹果登陆及后端Node 验证

2019 年苹果推出了自己家的登陆,要求集成第三方登陆的app, 必须在2020年4月份前集成苹果登陆, 所以我们来简单说一说

  • 废话不多说, 直接上代码

  • 这里不采用苹果提供的Button, 只提供方法

要求:

  • iOS13 或以上
  • Xcode11
  • enables Sign in with Apple features on Apple developer website , 开启这个后, 你的开发者网站会默认也会开启Sign in with Apple
import UIKit
import AuthenticationServices

public typealias CallbackType = (Any?) -> ()

* 加objc 是为了方便OC 可以直接调用, 看需要加不加
@objc open class SignInWithAppleTool: NSObject {
    var viewController: UIViewController! = nil
    private var callback_: CallbackType? = nil
    required public init(viewController: UIViewController) {
        self.viewController = viewController
    }
    
    @objc public var canShowButton: Bool {
        var r = false
        if #available(iOS 13.0, *) {r = true}
        return r
    }
    
    @objc public func login(callback: @escaping CallbackType ) {
        
        if !canShowButton {return};
        
        guard #available(iOS 13.0, *) else {
            callback(["state" : -1, "errCode" : "0","errDesc": "only ios13 or above"])
            return
        }
        callback_ = callback
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]   //需要的权限
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()

    }
    
    @objc public func checkState(withUserID userID: String,callback: @escaping CallbackType) {
        guard #available(iOS 13.0, *) else {return}
        
        let provider = ASAuthorizationAppleIDProvider()
        provider.getCredentialState(forUserID: userID) { (credentialState, error) in
            switch(credentialState){
            case .authorized:
                // Apple ID Credential is valid
                callback(["state":1,"errDesc": "Apple ID Credential is valid"])
                break
            case .revoked:
                // Apple ID Credential revoked, handle unlink
                callback(["state":-1, "errDesc": "Apple ID Credential revoked, handle unlink"])
                fallthrough
            case .notFound:
                // Credential not found, show login UI
                callback(["state":-2, "errDesc": "Credential not found, show login UI"])
                break
            default:
                callback(["state":-3, "errDesc": "Other"])
                break
            }
        }
    }
    
}
extension SignInWithAppleTool: ASAuthorizationControllerDelegate {
    
    /*
    ∙ User ID: Unique, stable, team-scoped user ID,苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。

    ∙ Verification data: Identity token, code,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证本次授权登录请求数据的有效性和真实性,详见 Sign In with Apple REST API。如果验证成功,可以根据 userIdentifier 判断账号是否已存在,若存在,则返回自己账号系统的登录态,若不存在,则创建一个新的账号,并返回对应的登录态给 App。

    ∙ Account information: Name, verified email,苹果用户信息,包括全名、邮箱等。

    ∙ Real user indicator: High confidence indicator that likely real user,用于判断当前登录的苹果账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal。*/
    @available(iOS 13.0, *)
    public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        switch authorization.credential {
        case let appleIdCredential as ASAuthorizationAppleIDCredential:
            let state = appleIdCredential.state ?? ""
            let userIdentifier = appleIdCredential.user
            let familyName = appleIdCredential.fullName?.familyName ?? ""
            let givenName = appleIdCredential.fullName?.givenName ?? ""
            let nickname = appleIdCredential.fullName?.nickname ?? ""
            let middleName = appleIdCredential.fullName?.middleName ?? ""
            let namePrefix = appleIdCredential.fullName?.namePrefix ?? ""
            let nameSuffix = appleIdCredential.fullName?.nameSuffix ?? ""
            
            let familyName_phone = appleIdCredential.fullName?.phoneticRepresentation?.familyName ?? ""
            let givenName_phone = appleIdCredential.fullName?.phoneticRepresentation?.givenName ?? ""
            let nickname_phone = appleIdCredential.fullName?.phoneticRepresentation?.nickname ?? ""
            let namePrefix_phone = appleIdCredential.fullName?.phoneticRepresentation?.namePrefix ?? ""
            let nameSuffix_phone = appleIdCredential.fullName?.phoneticRepresentation?.nameSuffix ?? ""
            let middleName_phone = appleIdCredential.fullName?.phoneticRepresentation?.middleName ?? ""
            
            let email = appleIdCredential.email ?? ""
            let identityToken = String(bytes: appleIdCredential.identityToken ?? Data(), encoding: .utf8) ?? ""
            let authCode = String(bytes: appleIdCredential.authorizationCode ?? Data(), encoding: .utf8) ?? ""
            let realUserStatus = appleIdCredential.realUserStatus.rawValue
            let info = [
                "state": state,
                "userIdentifier": userIdentifier,
                "familyName": familyName,
                "givenName": givenName,
                "nickname": nickname,
                "middleName": middleName,
                "namePrefix": namePrefix,
                "nameSuffix": nameSuffix,
                "familyName_phone": familyName_phone,
                "givenName_phone": givenName_phone,
                "nickname_phone": nickname_phone,
                "namePrefix_phone": namePrefix_phone,
                "nameSuffix_phone": nameSuffix_phone,
                "middleName_phone": middleName_phone,
                "email": email,
                "identityToken": identityToken,
                "authCode": authCode,
                "realUserStatus": realUserStatus
                ] as [String : Any]
            print("success:=\(info)")
            let p = ["state" : 1, "errCode" : "", "info": info] as [String : Any]
            callback_?(p)
        default:
            let p = ["state" : -1, "errCode" : "0",] as [String : Any]
            callback_?(p)
            break
        }
    }

    @available(iOS 13.0, *)
    public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print("error:\(error.localizedDescription)")
        let err = error as NSError
        var errCode = 0;
        var errDesc = "Other"
        switch err.code {
        case ASAuthorizationError.canceled.rawValue:
            print("用户取消了授权请求")
            errCode = -1;
            errDesc = "User cancelled authorization request"
            break;
        case ASAuthorizationError.failed.rawValue:
            print("授权请求失败")
            errCode = -2;
            errDesc = "Authorization request failed"
            break;
        case ASAuthorizationError.invalidResponse.rawValue:
            print("授权请求无响应")
            errCode = -3;
            errDesc = "Authorization request is not responding"
            break;
        case ASAuthorizationError.notHandled.rawValue:
            print("未能处理授权请求")
            errCode = -4;
            errDesc = "Failed to process authorization request"
            break;
        case ASAuthorizationError.unknown.rawValue:
            print("授权请求失败未知原因")
            errCode = -5;
            errDesc = "Authorization request failed : unknown reason"
            break;
        default:
            print("other")
            break;
        }
        let p = ["state" : -1, "errCode" : errCode, "errDesc": errDesc] as [String : Any]
        callback_?(p)
        
    }
}
extension SignInWithAppleTool: ASAuthorizationControllerPresentationContextProviding {
    @available(iOS 13.0, *)
    public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        viewController.view.window ?? UIWindow()
    }
}

  • 授权后就可以拿到 User ID / Identity token , app端不一定可以拿到用户的email , 假如用户在授权时选择隐藏就会没有, 但没事, 后端验证后可以拿到email, 苹果登陆app端还是比较简单的, 这里将获取到的数据丢给后端就可以了
  • authorizationCode:授权code, 给授权码的验证使用的

后端Node 验证

  • 苹果提供两种验证方式: 一种是基于JWT的算法验证,另外一种是基于授权码的验证
  • 我们这里只要讲解 JWT
  • 想了解JWT 的朋友请自行查阅
  • 苹果公钥网站
const NodeRSA = require('node-rsa');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const express = require('express')
const app = express()


/*
alg:string
The encryption algorithm used to encrypt the token.
e: string
The exponent value for the RSA public key.
kid: string
A 10-character identifier key, obtained from your developer account.
kty: string
The key type parameter setting. This must be set to "RSA".
n: string
The modulus value for the RSA public key.
use: string
* kid,为密钥id标识,签名算法采用的是RS256(RSA 256 + SHA 256),kty常量标识使用RSA签名算法,其公钥参数
*/
// 获取苹果的公钥
async function getApplePublicKey() {
    let res = await axios.request({
        method: "GET",
        url: "https://appleid.apple.com/auth/keys",
        headers: {
            'Content-Type': 'application/json'
        }
    })
    let key = res.data.keys[0]
    const pubKey = new NodeRSA();
    pubKey.importKey({ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') }, 'components-public');
    return pubKey.exportKey(['public']);
};

// 验证id_token
// id_token:  Identity token
// audience : app bundle id  , 可以不用
// subject : userId , 可以不用
function verifyIdToken(id_token, audience, subject, callback) {
    const applePublicKey = await getApplePublicKey();
    // const jwtClaims = jwt.verify(id_token, applePublicKey, { algorithms: 'RS256', issuer: "https://appleid.apple.com", audience, subject });
    jwt.verify(id_token, applePublicKey, { algorithms: 'RS256' }, (err, decode) => {

        if (err) {
            //message: invalid signature  / jwt expired
            console.log("JJ: verifyIdToken -> error", err.name, err.message, err.date);
            callback && callback(err);
        } else if (decode) {

            // let decode = {
            //     iss: 'https://appleid.apple.com',
            //     aud: 'xxxxxxxx',   
            //     exp: 1579171507,
            //     iat: 1579170907,
            //     sub: 'xxxxxxxx.xxxx',
            //     c_hash: 'xxxxxxxxxxxx',
            //     email: '[email protected]',
            //     email_verified: 'true',
            //     auth_time: 1579170907
            // }
            console.log("JJ: verifyIdToken -> decode", decode)
            callback && callback(decode);
          // sub 就是用户的唯一标识, 服务器可以保存它用来检查用户有没用过apple pay login , 至于用户第一次Login时,服务器就默认开一个member 给用户, 还是见到没login 过就自己再通过app 返回到注册页面再接着注册流程, 最后再pass userId 到server 保存. 这个看公司需求.
        }
    });
};

app.get('/', function (req, res) {
 verifyIdToken('eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmlwcHVkby5haWdlbnMuaW9zIiwiZXhwIjoxNTc5MTcxNTA3LCJpYXQiOjE1NzkxNzA5MDcsInN1YiI6IjAwMTUzMi5iMTZlYWI3NGE5Y2M0ODYyYTQ0ODQ4MDk1MGQzNmVjMC4wOTAyIiwiY19oYXNoIjoidGZDVGFVRGlPVTVxaE9LNWRKYXFnUSIsImVtYWlsIjoiNjAxMzE1NTM4QHFxLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU3OTE3MDkwN30.ROqWg35ENqzonXyiBGTYVWCsBFbPD8o96vEQy_pzeXrem2Bsry9eGPl47Kst2aaqrKRPb9KhxXpOR02dHMQubRC_6Dj1zthapyKyAYjusqLGl9S2bl03hYKJrh_JB4Cnar71gksA8nMvsDukFfxYITDtmX51iAQVzKhdqrk1mwc4XjnjQUk0opk6uiarRi2quJZ8CS9vHxOHAoOTeWZvWMdiesaLf4zItdPCvBckmsFyq2YLYiv9sFXGVhE1IUc6jrKA7KiuWY52OlLCBLlQb2sQ2mpePqhkb7SpIPuRdsAUVNMz4nFxS2f863TLLdY-f2NDUQOJdXwYJe-piAPfVw');
 res.send('Hello World');
})

app.listen(3000)

后端Java 验证 参考于此

// 1. 还是要获取苹果的公钥, 再将 n / e  , 丢到这个方法
// 
#获取验证所需的PublicKey
public PublicKey getPublicKey(String n,String e)throws NoSuchAlgorithmException, InvalidKeySpecException {
         BigInteger bigIntModulus = new BigInteger(1,Base64.decodeBase64(n));
         BigInteger bigIntPrivateExponent = new BigInteger(1,Base64.decodeBase64(e));
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        return publicKey;
 }

/*
key: 通过上面的方法获取
jwt: identityToken
audience: app bundle id
subject: userId
*/
public int verify(PublicKey key, String jwt, String audience, String subject) {                      
    JwtParser jwtParser = Jwts.parser().setSigningKey(key);              
    jwtParser.requireIssuer("https://appleid.apple.com");        
    jwtParser.requireAudience(audience);
    jwtParser.requireSubject(subject); 
    try {
       Jws claim = jwtParser.parseClaimsJws(jwt);
       if (claim != null && claim.getBody().containsKey("auth_time")) {  
          return GlobalCode.SUCCESS;            
       }           
       return GlobalCode.THIRD_AUTH_CODE_INVALID;
    } catch (ExpiredJwtException e) { 
       log.error("apple identityToken expired", e);
       return GlobalCode.THIRD_AUTH_CODE_INVALID;
    } catch (Exception e) {
       log.error("apple identityToken illegal", e);
    }
}


JWT工具库为:

    io.jsonwebtoken
    jjwt
    0.9.1

你可能感兴趣的:(苹果登陆及后端Node 验证)