Electron Apple SignIn 登录

发布时间:2024年01月18日

本人写博客,向来主张:代码要完整,代码可运行,文中不留下任何疑惑。

最讨厌写博客,代码只留下片段,文中关键的东西没写清楚。之前看了那么多文章,就是不告诉我clientId从哪来的。

官方资料地址:

Sign in with Apple JS | Apple Developer Documentation

一、网页客户端代码

clientId:这个会在下文中告诉你怎么来的

usePopup:如果设置为true,就会以弹框的方式打开苹果登录窗口。设置 为false,你自己试试吧

redirectURI:这在usePopup=true时,没啥用

state:在各种页面跳转时会原样传递,你自己看着办

nonce:一个随机数,至于作用么,你自己猜,照着做就好

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
    <title></title>
    <script type="text/javascript">
        var hostBase = "https://myserver.cn";
        function getQuery(i) { var j = location.search.match(new RegExp("[?&]" + i + "=([^&]*)(&?)", "i")); return j ? j[1] : j; }
        function getQueryIn(i, params) { var j = ("?&" + params).match(new RegExp("[?&]" + i + "=([^&]*)(&?)", "i")); return j ? j[1] : j; }
    </script>
</head>

<body>
    <div id="app">
        <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
    </div>
</body>

<style></style>

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script type="text/javascript"
    src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>

<script>

    window.app = new Vue({
        el: '#app',
        data: function () {
            return {
            }
        },
        watch: {
        },
        created: function () {
            setTimeout(function () {
                AppleID.auth.init({
                    clientId: '填上你的clientId',
                    scope: 'name email',
                    redirectURI: 'https://myclient.cn/login_apple_redirect.html',
                    state: "这个参数在各种跳转中会一直带上,你可以用来标记这次登录过程",
                    nonce: '' + new Date().getTime(),
                    usePopup: true
                });
            }, 10);
        },
        methods: {
            OnAppleSignIn: function (authorization) {
                var that = this;

                var req = {
                    state: authorization.state,
                    code: authorization.code,
                    idToken: authorization.id_token
                };
                $.ajax({
                    url: hostBase + "/fawork/AppleLoginGetResult", //请求的url地址
                    dataType: "json", //返回格式为json
                    contentType: "application/json; charset=utf-8",
                    async: true, //请求是否异步,默认为异步,这也是ajax重要特性
                    data: JSON.stringify(req),
                    async: true, //请求是否异步,默认为异步,这也是ajax重要特性
                    type: "POST", //请求方式
                    beforeSend: function () {
                        //请求前的处理
                    },
                    success: function (rsp) {
                        console.log(rsp);
                        // 返回 uid和token
                    },
                    complete: function () {
                        //请求完成的处理
                    },
                    error: function () {
                        //请求出错处理
                    }
                });
            }
        }
    });

    // Listen for authorization success.
    document.addEventListener('AppleIDSignInOnSuccess', function (event) {
        // Handle successful response.
        console.log(event);
        window.app.OnAppleSignIn(event.detail.authorization);
    });

    // Listen for authorization failures.
    document.addEventListener('AppleIDSignInOnFailure', function (event) {
        // Handle error.
        console.log(event);
    });
</script>

</html>

登录成功时,前端 AppleIDSignInOnSuccess事件中,打印的event参数值

二、服务器端代码

前端传递过来code和id_token,这是两个不同的校验方法

code是一种,不过在苹果登录中我不知道怎么用,我用的是id_token校验。

对于id_token,这就是JWT校验技术。

JWT的介绍,请看我用ChatGpt问的结果。

在JWT(JSON Web Token)中,id token是一种用于身份验证和认证的令牌。要验证id token的有效性,您可以遵循以下步骤:

  1. 解码id token:JWT由三部分组成,即头部、载荷和签名。使用Base64解码id token,您可以获取其中的头部和载荷信息。

  2. 验证签名:使用头部中提供的算法(通常是HMAC、RSA或ECDSA)和密钥,验证签名的正确性。您需要获取与签名算法相对应的密钥,并将其与头部和载荷一起使用相同的算法进行签名验证。如果签名验证失败,则表示id token被篡改过或者是伪造的。

  3. 验证令牌的有效期:在载荷中,id token包含了发行时间(issued time)和过期时间(expiration time)。您需要检查当前时间是否在有效期范围内。如果当前时间在过期时间之后,说明id token已过期,不能再继续使用。

  4. 校验接收者:在载荷中,id token还可以包含一个接收者(audience)字段,用于指定该令牌的预期接收者。您可以检查接收者字段是否与您的应用程序的标识符匹配,以确保id token只能被合法的接收者使用。

  5. 可选的附加校验:根据您的需求,您还可以进行其他的校验,例如验证签发者(issuer)字段、检查令牌是否被撤销等。

需要注意的是,为了保证安全性,您应该将密钥存储在安全的位置,并定期更换密钥以防止泄露和滥用。此外,使用受信任的JWT库来处理JWT的解码和验证操作,而不是自行编写代码,以确保正确性和安全性。

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        
<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.22</version>
        </dependency>

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>
package cn.huali.fawork.constant;

import cn.huali.fawork.exception.SelfException;
import cn.huali.fawork.utils.Base64Util;
import cn.huali.fawork.utils.HttpsUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import org.apache.commons.codec.binary.Base64;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.Map;

public class AppleAuthorizationConfig {

    private static final String APPLE_HOST_URL = "https://appleid.apple.com";
    private static final String APPLE_PUB_KEY_ENDPOINT = "https://appleid.apple.com/auth/keys";
    private static final String APPLE_AUTH_TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
    private static final String  Apple_Client_Id = "填上你的clientId";

    public static AppleAuthCheckResult checkAuth(String idToken, long unixtimeAt) throws UnsupportedEncodingException {
        AppleAuthCheckResult result = new AppleAuthCheckResult();
        result.isOK = false;
        try {
            String[] identityTokens = idToken.split("\\.");

            String headerStr = new String(Base64Util.decodeWithUTF8(identityTokens[0]));
            JSONObject jsonObjectHeader = JSON.parseObject(headerStr);

            String contentStr = new String(Base64Util.decodeWithUTF8(identityTokens[1]));
            JSONObject jsonObjectContent = JSON.parseObject(contentStr);

            System.out.println(headerStr);
            System.out.println(contentStr);

            String kid = jsonObjectHeader.getString("kid");
            String alg = jsonObjectHeader.getString("alg");


            String iss = jsonObjectContent.getString("iss");
            String aud = jsonObjectContent.getString("aud");
            String exp = jsonObjectContent.getString("exp");
            String iat = jsonObjectContent.getString("iat");
            String sub = jsonObjectContent.getString("sub");

            String nonce = jsonObjectContent.getString("nonce");
            String c_hash = jsonObjectContent.getString("c_hash");
            String email = jsonObjectContent.getString("email");
            String email_verified = jsonObjectContent.getString("email_verified");
            boolean is_private_email = jsonObjectContent.getBooleanValue("is_private_email");
            String auth_time = jsonObjectContent.getString("auth_time");
            String nonce_supported = jsonObjectContent.getString("nonce_supported");


            result.email = email;
            result.sub = sub;
            result.is_private_email = is_private_email;
            result.tokenPayload = contentStr;

            JSONObject publicKey = getPublicKey(APPLE_PUB_KEY_ENDPOINT, kid);
            PublicKey rsaPublicKey = getRSAPublicKey(publicKey.getString("n"), publicKey.getString("e"));


// require部分,是jwt自动帮你校验,如果校验不通过,会报异常
// 切记不要在require部分校验auth_time和iat两部分,Jwt有bug,会报异常的,所以还是手动校验比较好
            JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(rsaPublicKey)
                    .requireAudience(Apple_Client_Id) //一般是项目包名称
                    .requireIssuer(APPLE_HOST_URL) //固定值
                    //.require("auth_time", auth_time) //这里做了个简单的验证,如果auth_time == iat则是有效的。
                    .require("email", email)
                    .require("sub", sub)
                    .build();
            Jws<Claims> claimsJws = jwtParser.parseClaimsJws(idToken);
            Claims claims = claimsJws.getBody();

            if (!claims.get("auth_time").toString().equalsIgnoreCase(auth_time)) {
                result.isOK = false;
                result.msg = "auth_time不一致";
            } else if (!claims.get("iat").toString().equalsIgnoreCase(iat)) {
                result.isOK = false;
                result.msg = "iat不一致";
            } else if (!auth_time.equalsIgnoreCase(iat)) {
                result.isOK = false;
                result.msg = "iat和auth_time不一致";
            } else if (!claims.get("exp").toString().equalsIgnoreCase(exp)) {
                result.isOK = false;
                result.msg = "exp不一致";
            } else if (Long.parseLong(exp) < unixtimeAt) {
                result.isOK = false;
                result.msg = "exp已过期";
            }

            result.isOK = true;
            result.msg = "";
        }
        catch (SelfException e) {
            result.isOK = false;
            result.msg = e.msg;
        }
        catch (Exception e) {
            result.isOK = false;
            result.msg = e.getMessage();
        }

        return result;
    }

    public static class AppleAuthCheckResult {
        public boolean isOK;
        public String msg;

        /**
         * 用户唯一账号
         */
        public String sub;
        public String email;
        public boolean is_private_email;
        public String tokenPayload;
    }

    private static volatile Map<String, JSONObject> pubKeyMap = new HashMap<>();

    private static PublicKey getRSAPublicKey(String modulus, String exponent) {
        try {
            BigInteger bigModule = new BigInteger(1, Base64.decodeBase64(modulus));
            BigInteger bigExponent = new BigInteger(1, Base64.decodeBase64(exponent));
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigModule, bigExponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            return publicKey;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * {
     * "keys": [
     * {
     * "kty": "RSA",
     * "kid": "W6WcOKB",
     * "use": "sig",
     * "alg": "RS256",
     * "n": "2Zc5d0-zkZ5AKmtYTvxHc3vRc41YfbklflxG9SWsg5qXUxvfgpktGAcxXLFAd9Uglzow9ezvmTGce5d3DhAYKwHAEPT9hbaMDj7DfmEwuNO8UahfnBkBXsCoUaL3QITF5_DAPsZroTqs7tkQQZ7qPkQXCSu2aosgOJmaoKQgwcOdjD0D49ne2B_dkxBcNCcJT9pTSWJ8NfGycjWAQsvC8CGstH8oKwhC5raDcc2IGXMOQC7Qr75d6J5Q24CePHj_JD7zjbwYy9KNH8wyr829eO_G4OEUW50FAN6HKtvjhJIguMl_1BLZ93z2KJyxExiNTZBUBQbbgCNBfzTv7JrxMw",
     * "e": "AQAB"
     * },
     * {
     * "kty": "RSA",
     * "kid": "fh6Bs8C",
     * "use": "sig",
     * "alg": "RS256",
     * "n": "u704gotMSZc6CSSVNCZ1d0S9dZKwO2BVzfdTKYz8wSNm7R_KIufOQf3ru7Pph1FjW6gQ8zgvhnv4IebkGWsZJlodduTC7c0sRb5PZpEyM6PtO8FPHowaracJJsK1f6_rSLstLdWbSDXeSq7vBvDu3Q31RaoV_0YlEzQwPsbCvD45oVy5Vo5oBePUm4cqi6T3cZ-10gr9QJCVwvx7KiQsttp0kUkHM94PlxbG_HAWlEZjvAlxfEDc-_xZQwC6fVjfazs3j1b2DZWsGmBRdx1snO75nM7hpyRRQB4jVejW9TuZDtPtsNadXTr9I5NjxPdIYMORj9XKEh44Z73yfv0gtw",
     * "e": "AQAB"
     * },
     * {
     * "kty": "RSA",
     * "kid": "lVHdOx8ltR",
     * "use": "sig",
     * "alg": "RS256",
     * "n": "nXDu9MPf6dmVtFbDdAaal_0cO9ur2tqrrmCZaAe8TUWHU8AprhJG4DaQoCIa4UsOSCbCYOjPpPGGdE_p0XeP1ew55pBIquNhNtNNEMX0jNYAKcA9WAP1zGSkvH5m39GMFc4SsGiQ_8Szht9cayJX1SJALEgSyDOFLs-ekHnexqsr-KPtlYciwer5jaNcW3B7f9VNp1XCypQloQwSGVismPHwDJowPQ1xOWmhBLCK50NV38ZjobUDSBbCeLYecMtsdL5ZGv-iufddBh3RHszQiD2G-VXoGOs1yE33K4uAto2F2bHVcKOUy0__9qEsXZGf-B5ZOFucUkoN7T2iqu2E2Q",
     * "e": "AQAB"
     * }
     * ]
     * }
     *
     * @param url
     * @param kid
     * @return
     */
    private static JSONObject getPublicKey(String url, String kid) {
        if (!pubKeyMap.containsKey(kid)) {
            String allPubKeyJsonStr = getPublicKeyFromServer(url);
            JSONObject jsonObjectAllPubKey = JSON.parseObject(allPubKeyJsonStr);
            JSONArray keysArray = jsonObjectAllPubKey.getJSONArray("keys");

            if (keysArray.size() > 0) {
                pubKeyMap.clear();

                for (int i = 0; i < keysArray.size(); i++) {
                    JSONObject key = keysArray.getJSONObject(i);
                    String tmpKid = key.getString("kid");

                    pubKeyMap.put(tmpKid, key);
                }
            }
        }

        JSONObject keyJsonObject = pubKeyMap.getOrDefault(kid, null);

        if (keyJsonObject == null) {
            throw new SelfException("没有找到PublicKey:kid=" + kid);
        }

        return keyJsonObject;
    }

    private static String getPublicKeyFromServer(String url) {
        HttpsUtils.HttpRsp httpRsp = HttpsUtils.get(url);
        if (httpRsp.statusCode != 200) {
            throw new SelfException("获取PublicKey出错:" + httpRsp.statusCode + "," + httpRsp.statusDesc);
        }

        return httpRsp.content;
    }
}
package cn.huali.fawork.utils;

import java.io.UnsupportedEncodingException;
import java.util.Base64;

public class Base64Util {
    public static String encode(byte[] bytes) {
        byte[] newBytes = Base64.getEncoder().encode(bytes);
        String content = new String(newBytes);
        return content;
    }

    public static byte[] decode(String content) {
        byte[] newBytes = Base64.getDecoder().decode(content.getBytes());
        return newBytes;
    }

    public static String encodeWithUTF8(byte[] bytes) throws UnsupportedEncodingException {
        byte[] newBytes = Base64.getEncoder().encode(bytes);
        String content = new String(newBytes, "UTF-8");
        return content;
    }

    public static byte[] decodeWithUTF8(String content) throws UnsupportedEncodingException {
        byte[] newBytes = Base64.getDecoder().decode(content.getBytes("UTF-8"));
        return newBytes;
    }
}
/**
 *
 */
package cn.huali.fawork.utils;

import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * @author Administrator
 *
 */
public class HttpsUtils {
    public static HttpRsp get(String url) {
        return doGet(url, null, null);
    }

    public static HttpRsp get(String url, String param) {
        return doGet(url, param, null);
    }

    public static HttpRsp get(String url, Map<String, String> param) {
        return doGet(url, makeParam(param), null);
    }

    public static HttpRsp get(String url, Map<String, String> param, Map<String, String> headMap) {
        return doGet(url, makeParam(param), headMap);
    }

    public static HttpRsp get(String url, String param, Map<String, String> headMap) {
        return doGet(url, param, headMap);
    }

    private static HttpRsp doGet(String url, String param, Map<String, String> headMap) {
        HttpRsp rsp = new HttpRsp();

        BufferedReader in = null;
        try {
            if (param != null && param.isEmpty() == false) {
                if (url.endsWith("&") || url.endsWith("?")) {
                    url += param;
                } else if (url.contains("?")) {
                    url += "&" + param;
                } else {
                    url += "?" + param;
                }
            }

            HttpURLConnection connection = (HttpURLConnection) (new URL(url)).openConnection();
            connection.setRequestMethod("GET");
            connection.setDoInput(true);

            if (headMap != null && headMap.isEmpty() == false) {
                // 设置包头
                Iterator<Entry<String, String>> it = headMap.entrySet().iterator();
                Entry<String, String> entry = null;
                while (it.hasNext()) {
                    entry = it.next();
                    System.out.println(entry.getKey() + ":" + entry.getValue());
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));

            StringBuffer sb = new StringBuffer();
            String line;
            while ((line = in.readLine()) != null) {
                sb.append(line + System.lineSeparator());
            }
            rsp.content = sb.toString();

            rsp.headerFieldsMap = connection.getHeaderFields();
            rsp.statusCode = connection.getResponseCode();
            rsp.statusDesc = connection.getResponseMessage();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return rsp;
    }

    public static HttpRsp post(String url) {
        return doPost(url, null, null);
    }

    public static HttpRsp post(String url, String param) {
        return doPost(url, param, null);
    }

    public static HttpRsp post(String url, Map<String, String> param) {
        return doPost(url, makeParam(param), null);
    }

    public static HttpRsp post(String url, Map<String, String> param, Map<String, String> headMap) {
        return doPost(url, makeParam(param), headMap);
    }

    public static HttpRsp post(String url, String param, Map<String, String> headMap) {
        return doPost(url, param, headMap);
    }

    private static HttpRsp doPost(String url, String param, Map<String, String> headMap) {
        HttpRsp rsp = new HttpRsp();

        PrintWriter out = null;
        BufferedReader in = null;
        try {
            HttpURLConnection connection = (HttpURLConnection) (new URL(url)).openConnection();
            connection.setRequestMethod("POST");

            connection.setDoInput(true);
            connection.setDoOutput(true);

            if (headMap != null && headMap.isEmpty() == false) {
                // 设置包头
                Iterator<Entry<String, String>> it = headMap.entrySet().iterator();
                Entry<String, String> entry = null;
                while (it.hasNext()) {
                    entry = it.next();
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            if (param != null && param.isEmpty() == false) {

                out = new PrintWriter(connection.getOutputStream());
                out.print(param);
                out.flush();
            }

            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));

            StringBuffer sb = new StringBuffer();
            String line;
            while ((line = in.readLine()) != null) {
                sb.append(line + System.lineSeparator());
            }
            rsp.content = sb.toString();

            rsp.headerFieldsMap = connection.getHeaderFields();
            rsp.statusCode = connection.getResponseCode();
            rsp.statusDesc = connection.getResponseMessage();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return rsp;
    }

    /**
     * 将参数组织在一块
     *
     * @param map
     * @return
     */
    public static String makeParam(Map<String, String> map) {
        StringBuffer sb = new StringBuffer();
        Iterator<Entry<String, String>> it = map.entrySet().iterator();
        Entry<String, String> entry = null;

        if (it.hasNext()) {
            entry = it.next();
            sb.append(entry.getKey() + "=" + entry.getValue());
        }

        while (it.hasNext()) {
            entry = it.next();
            sb.append("&" + entry.getKey() + "=" + entry.getValue());
        }

        return sb.toString();
    }

    public static class HttpRsp {
        public int statusCode;
        public String statusDesc;
        public String content;
        public Map<String, List<String>> headerFieldsMap = new HashMap<String, List<String>>();

        @Override
        public String toString() {
            return "statusCode=" + statusCode + ", statusDesc=" + statusDesc + ", content=" + content;
        }

        public List<String> getCookie() {
            if (headerFieldsMap != null) {
                return headerFieldsMap.get("Set-Cookie");
            } else {
                return null;
            }
        }
    }
}
package cn.huali.fawork.exception;

/**
 * 此处的异常一定要集成于RuntimeException,是为了数据库事务回滚,请不要改动
 */
public class SelfException extends RuntimeException {
    public Integer code;
    public String msg;
    public Object data;

    public SelfException(Exception e) {
        super(e);
        this.code = 1;
        this.msg = e.getLocalizedMessage();
    }

    public SelfException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public SelfException(String msg) {
        super(msg);
        this.code = 1;
        this.msg = msg;
    }

    public SelfException(int code, String msg, Object data) {
        super(msg);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public String toString() {
        return "code=" + code + ",msg=" + msg;
    }
}

这段代码是关键,即验证id_token是否有效;也验证require部分的字段是否存在,是否一致。注意,如果验证不通过,这段代码会报Exception的,如果报了Exception说明id_token是无效的。

这一部分,你主要验证一下 auth_time 和 iat 两个时间是否过了期限,其它无所谓。我写的可代码可能有点多余,你自己看着办。

三、创建clientId

Sign In - Apple

1、创建一个Services IDs

看到没,这个Identifier就是clientId

后面我就不截图了,反正会关联一个 appId;也会填一个域名,域名就是前段登录的网页的域名;redirectURI或者returnURI,就是登录后跳转的页面,与客户端网页代码中的那个字段保持一致即可,一般对于usePopup=true时,这个字段用不上。

2、在AppId中,启用登录功能

点上图中的那个Edit按钮后,再进行配置

文章来源:https://blog.csdn.net/wuyeyixi/article/details/135633087
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。