Electron Apple SignIn 登录

news2024/11/15 19:56:06

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

最讨厌写博客,代码只留下片段,文中关键的东西没写清楚。之前看了那么多文章,就是不告诉我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按钮后,再进行配置

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1397391.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5. UE5 RPG使用GAS技能系统

之前也介绍过GAS的使用&#xff1a; UE 5 GAS Gameplay Ability System UE 5 GAS 在项目中处理AttributeSet相关 UE 5 GAS 在项目中通过数据初始化 基础的讲解这里不再诉说&#xff0c;有兴趣的可以翻我之前的博客。 接下来&#xff0c;在RPG游戏中实现GAS系统的使用。 GAS系统…

一次缓存失效引发的惨案!

分享是最有效的学习方式。 故事 对于小猫来讲&#xff0c;最近的一段日子是不好过的&#xff0c;纵使听着再有节拍的音乐&#xff0c;也换不起他对生活的热情。由于上一次“幂等事件”躺枪&#xff0c;他已经有几天没有休息好了。他感觉人生到了低谷。 当接手这个商城项目之后…

Linux 为何不把图形用户界面写入内核?

Linux 为何不把图形用户界面写入内核&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「Linux的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#…

禅道:从安装到使用,一篇文章带你全面了解

博客前言&#xff1a; 在这个充满竞争和快节奏的世界里&#xff0c;项目管理已经成为了许多行业的关键环节。禅道作为一种功能强大、易用的项目管理工具&#xff0c;正在被越来越多的企业和团队所采用。它不仅能帮助我们高效地管理项目&#xff0c;还能提升团队协作和沟通的效…

DC-1靶机刷题记录

靶机下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1GX7qOamdNx01622EYUBSow?pwd9nyo 提取码&#xff1a;9nyo 参考答案&#xff1a; https://c3ting.com/archives/kai-qi-vulnhnbshua-tiDC-1.pdf【【基础向】超详解vulnhub靶场DC-1】 https://www.bilibi…

【数据结构二】链表和LinkedList详解

目录 链表和LinkedList 1.链表的实现 2.LinkedList的使用 3.ArrayList和LinkedList的区别 4.链表OJ题训练 链表和LinkedList 当 在 ArrayList 任意位置插入或者删除元素时&#xff0c;就需要将后序元素整体往前或者往后 搬移&#xff0c;时间复杂度为 O(n) &#xff0c;效率…

ChatGLM-6B部署和微调实例

文章目录 前言一、ChatGLM-6B安装1.1 下载1.2 环境安装 二、ChatGLM-6B推理三、P-tuning 微调3.1微调数据集3.2微调训练3.3微调评估3.4 调用新的模型进行推理 总结 前言 ChatGLM-6B ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;基于 General Language Mo…

unity-shader笔记OLD

shader shader在面板中的位置相关代码代码切换shader shader在面板中的位置 选中物体属性面板中 相关代码 代码切换shader 挂载到怪物上的shader名字统一叫body&#xff0c;然后获取上面的SkinnedMeshRender SkinnedMeshRender smr&#xff1b; //恢复到原来的shader …

JavaScript DOM可以做什么?

1、通过id获取标签元素 DOM是文档对象模型&#xff0c;它提供了一些属性和方法来方便我们操作document对象&#xff0c;比如getElementById()方法可以通过某个标签元素的id来获取这个标签元素 // 用法 window.document.getElementById(id); // 例子 <!DOCTYPE html> &l…

LeetCode、374. 猜数字大小【简单,二分】

文章目录 前言LeetCode、374. 猜数字大小【简单&#xff0c;二分】题目及类型思路及代码实现 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技…

移动云助力智慧交通数智化升级

智慧交通是在整个交通运输领域充分利用物联网、空间感知、云计算、移动互联网等新一代信息技术&#xff0c;综合运用交通科学、系统方法、人工智能、知识挖掘等理论与工具&#xff0c;以全面感知、深度融合、主动服务、科学决策为目标&#xff0c;推动交通运输更安全、更高效、…

联想拯救者冠名2024第二届OPENAIGC开发者大赛,开启AI落地新纪元

2024年1月17日&#xff0c;在联想拯救者及消费生态新品发布会上&#xff0c;AIGC开放社区携手联想拯救者&#xff0c;宣布将共同举办“AI生成未来第二届拯救者杯OPENAIGC开发者大赛”。此次大赛旨在集结所有开发者的智慧和创造力&#xff0c;推动人工智能技术的创新和应用实践。…

Promise的几道基础题

event loop它的执行顺序&#xff1a; 一开始整个脚本作为一个宏任务执行执行过程中同步代码直接执行&#xff0c;宏任务进入宏任务队列&#xff0c;微任务进入微任务队列当前宏任务执行完出队&#xff0c;检查微任务列表&#xff0c;有则依次执行&#xff0c;直到全部执行完执…

服务器自动拉取git代码运行脚本

# 1.场景分析 工作中常常会遇到本地编辑shell脚本或者python脚本完成后需要在服务器上运行的情况&#xff0c;每次进行拷贝费时费力。下面介绍下通过git管理器&#xff0c;实现本地与服务器代码同步的方式。选择公司搭建的gitlab为例&#xff1a; 2.gitlab配置服务器ssh密钥 …

免费的爬虫软件【2024最新】

在国际市场竞争日益激烈的背景下&#xff0c;国外网站的SEO排名直接关系到网站在搜索引擎中的曝光度和用户点击量。良好的SEO排名能够带来更多的有针对性的流量&#xff0c;提升网站的知名度和竞争力。 二、国外网站SEO排名的三种方法 关键词优化&#xff1a; 关键词优化是SEO…

【IAP】核心开发流程

最近做了IAP U盘升级模块开发&#xff0c;总结下IAP基本开发流程&#xff0c;不深入讨论原理。 详细原理参考 首先需要知道我们需要把之前的APP区域拆一块出来做BOOT升级程序区域。 以STM32F103为例&#xff0c;0x08000000到0x0807FFFF为FLASH空间&#xff0c;即上图代码区域…

Java基础面试题-2day

面向对象 创建一个对象用什么运算符&#xff0c;对象实体和对象引用有什么不同&#xff1f; 创建对象使用new String A new String(); A即为对象引用&#xff0c;通过new运算符&#xff0c;创建String()类型的对象实体。 对象引用的存储位置在栈内存 对象实体的存储位置在堆…

2024玩儿转TikTok之环境介绍及搭建

郑重申明&#xff1a;本文章只对合法合理做tiktok视频运营的用户做学习交流使用&#xff0c;有其他使用不当的违规违法行为后果自负&#xff01; 一、网络环境图介绍&#xff1a;我们只需要保证红色的环境通畅即可(手机阿里tiktok运营专用服务器) 二、服务器部分环境搭建 1、…

STM32F103标准外设库——SysTick系统定时器(八)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…

软件测试|sqlalchemy一对一关系详解

简介 SQLAlchemy 是一个强大的 Python ORM&#xff08;对象关系映射&#xff09;库&#xff0c;它允许我们将数据库表映射到 Python 对象&#xff0c;并提供了丰富的关系模型来处理不同类型的关系&#xff0c;包括一对一关系。在本文中&#xff0c;我们将深入探讨 SQLAlchemy …