Jwt过期时间

news2025/2/13 10:18:11

最近在复习Spring Security,发现测试jwt解密的时候会报错,之前没有问题,但是最近几次都出现了问题,我决定究其原因。

debug看一下,

进入真正的解析 

看一下这里的源码

@Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {

    Assert.hasText(jwt, "JWT String argument cannot be null or empty.");

    String base64UrlEncodedHeader = null;
    String base64UrlEncodedPayload = null;
    String base64UrlEncodedDigest = null;

    int delimiterCount = 0;

    StringBuilder sb = new StringBuilder(128);

    for (char c : jwt.toCharArray()) {

        if (c == SEPARATOR_CHAR) {

            CharSequence tokenSeq = Strings.clean(sb);
            String token = tokenSeq!=null?tokenSeq.toString():null;

            if (delimiterCount == 0) {
                base64UrlEncodedHeader = token;
            } else if (delimiterCount == 1) {
                base64UrlEncodedPayload = token;
            }

            delimiterCount++;
            sb.setLength(0);
        } else {
            sb.append(c);
        }
    }

    if (delimiterCount != 2) {
        String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
        throw new MalformedJwtException(msg);
    }
    if (sb.length() > 0) {
        base64UrlEncodedDigest = sb.toString();
    }

    if (base64UrlEncodedPayload == null) {
        throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
    }

    // =============== Header =================
    Header header = null;

    CompressionCodec compressionCodec = null;

    if (base64UrlEncodedHeader != null) {
        String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
        Map<String, Object> m = readValue(origValue);

        if (base64UrlEncodedDigest != null) {
            header = new DefaultJwsHeader(m);
        } else {
            header = new DefaultHeader(m);
        }

        compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
    }

    // =============== Body =================
    String payload;
    if (compressionCodec != null) {
        byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
        payload = new String(decompressed, Strings.UTF_8);
    } else {
        payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
    }

    Claims claims = null;

    if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
        Map<String, Object> claimsMap = readValue(payload);
        claims = new DefaultClaims(claimsMap);
    }

    // =============== Signature =================
    if (base64UrlEncodedDigest != null) { //it is signed - validate the signature

        JwsHeader jwsHeader = (JwsHeader) header;

        SignatureAlgorithm algorithm = null;

        if (header != null) {
            String alg = jwsHeader.getAlgorithm();
            if (Strings.hasText(alg)) {
                algorithm = SignatureAlgorithm.forName(alg);
            }
        }

        if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
            //it is plaintext, but it has a signature.  This is invalid:
            String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
                         "algorithm.";
            throw new MalformedJwtException(msg);
        }

        if (key != null && keyBytes != null) {
            throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
        } else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
            String object = key != null ? "a key object" : "key bytes";
            throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
        }

        //digitally signed, let's assert the signature:
        Key key = this.key;

        if (key == null) { //fall back to keyBytes

            byte[] keyBytes = this.keyBytes;

            if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
                if (claims != null) {
                    key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
                } else {
                    key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
                }
            }

            if (!Objects.isEmpty(keyBytes)) {

                Assert.isTrue(algorithm.isHmac(),
                              "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");

                key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
            }
        }

        Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");

        //re-create the jwt part without the signature.  This is what needs to be signed for verification:
        String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;

        JwtSignatureValidator validator;
        try {
            validator = createSignatureValidator(algorithm, key);
        } catch (IllegalArgumentException e) {
            String algName = algorithm.getValue();
            String msg = "The parsed JWT indicates it was signed with the " +  algName + " signature " +
                         "algorithm, but the specified signing key of type " + key.getClass().getName() +
                         " may not be used to validate " + algName + " signatures.  Because the specified " +
                         "signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
                         "this algorithm, it is likely that the JWT was not expected and therefore should not be " +
                         "trusted.  Another possibility is that the parser was configured with the incorrect " +
                         "signing key, but this cannot be assumed for security reasons.";
            throw new UnsupportedJwtException(msg, e);
        }

        if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
            String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
                         "asserted and should not be trusted.";
            throw new SignatureException(msg);
        }
    }

    final boolean allowSkew = this.allowedClockSkewMillis > 0;

    //since 0.3:
    if (claims != null) {

        SimpleDateFormat sdf;

        final Date now = this.clock.now();
        long nowTime = now.getTime();

        //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4
        //token MUST NOT be accepted on or after any specified exp time:
        Date exp = claims.getExpiration();
        if (exp != null) {

            long maxTime = nowTime - this.allowedClockSkewMillis;
            Date max = allowSkew ? new Date(maxTime) : now;
            if (max.after(exp)) {
                sdf = new SimpleDateFormat(ISO_8601_FORMAT);
                String expVal = sdf.format(exp);
                String nowVal = sdf.format(now);

                long differenceMillis = maxTime - exp.getTime();

                String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
                    differenceMillis + " milliseconds.  Allowed clock skew: " +
                    this.allowedClockSkewMillis + " milliseconds.";
                throw new ExpiredJwtException(header, claims, msg);
            }
        }

        //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5
        //token MUST NOT be accepted before any specified nbf time:
        Date nbf = claims.getNotBefore();
        if (nbf != null) {

            long minTime = nowTime + this.allowedClockSkewMillis;
            Date min = allowSkew ? new Date(minTime) : now;
            if (min.before(nbf)) {
                sdf = new SimpleDateFormat(ISO_8601_FORMAT);
                String nbfVal = sdf.format(nbf);
                String nowVal = sdf.format(now);

                long differenceMillis = nbf.getTime() - minTime;

                String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
                    ", a difference of " +
                    differenceMillis + " milliseconds.  Allowed clock skew: " +
                    this.allowedClockSkewMillis + " milliseconds.";
                throw new PrematureJwtException(header, claims, msg);
            }
        }

        validateExpectedClaims(header, claims);
    }

    Object body = claims != null ? claims : payload;

    if (base64UrlEncodedDigest != null) {
        return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
    } else {
        return new DefaultJwt<Object>(header, body);
    }
}

看一下和这次报错相关的代码

final boolean allowSkew = this.allowedClockSkewMillis > 0;

//since 0.3:
if (claims != null) {

    SimpleDateFormat sdf;

    final Date now = this.clock.now();
    long nowTime = now.getTime();

    //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4
    //token MUST NOT be accepted on or after any specified exp time:
    Date exp = claims.getExpiration();
    if (exp != null) {

        long maxTime = nowTime - this.allowedClockSkewMillis;
        Date max = allowSkew ? new Date(maxTime) : now;
        if (max.after(exp)) {
            sdf = new SimpleDateFormat(ISO_8601_FORMAT);
            String expVal = sdf.format(exp);
            String nowVal = sdf.format(now);

            long differenceMillis = maxTime - exp.getTime();

            String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
                differenceMillis + " milliseconds.  Allowed clock skew: " +
                this.allowedClockSkewMillis + " milliseconds.";
            throw new ExpiredJwtException(header, claims, msg);
        }
    }

    //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5
    //token MUST NOT be accepted before any specified nbf time:
    Date nbf = claims.getNotBefore();
    if (nbf != null) {

        long minTime = nowTime + this.allowedClockSkewMillis;
        Date min = allowSkew ? new Date(minTime) : now;
        if (min.before(nbf)) {
            sdf = new SimpleDateFormat(ISO_8601_FORMAT);
            String nbfVal = sdf.format(nbf);
            String nowVal = sdf.format(now);

            long differenceMillis = nbf.getTime() - minTime;

            String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
                ", a difference of " +
                differenceMillis + " milliseconds.  Allowed clock skew: " +
                this.allowedClockSkewMillis + " milliseconds.";
            throw new PrematureJwtException(header, claims, msg);
        }
    }

    validateExpectedClaims(header, claims);
}

final boolean allowSkew = this.allowedClockSkewMillis > 0;

这段代码是 Spring Security 中 JWT 验证过滤器的一部分,用于检查 JWT 的时间戳是否有效。它首先检查 allowedClockSkewMillis 是否大于 0,如果是,则表示允许 JWT 的时间戳与当前时间存在一定的偏差。然后,它通过比较 JWT 中的时间戳(即 iat 和 exp 声明)和当前时间来判断 JWT 是否过期或者尚未生效。如果 JWT 的时间戳在允许的偏差范围内,那么该 JWT 就被认为是有效的。否则,将抛出异常,表示 JWT 已经过期或者尚未生效。

SimpleDateFormat sdf;

final Date now = this.clock.now();
long nowTime = now.getTime();

//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4
//token MUST NOT be accepted on or after any specified exp time:
Date exp = claims.getExpiration();
if (exp != null) {

    long maxTime = nowTime - this.allowedClockSkewMillis;
    Date max = allowSkew ? new Date(maxTime) : now;
    if (max.after(exp)) {
        sdf = new SimpleDateFormat(ISO_8601_FORMAT);
        String expVal = sdf.format(exp);
        String nowVal = sdf.format(now);

        long differenceMillis = maxTime - exp.getTime();

        String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
            differenceMillis + " milliseconds.  Allowed clock skew: " +
            this.allowedClockSkewMillis + " milliseconds.";
        throw new ExpiredJwtException(header, claims, msg);
    }
}

这段代码是用于检查 JWT 是否已过期的逻辑。首先,它获取当前时间和 JWT 的过期时间,并将它们转换为毫秒数。然后,它比较当前时间与允许的时钟偏差之后的最大时间是否在 JWT 过期时间之前。如果是,则说明 JWT 仍然有效;否则,就抛出一个 ExpiredJwtException 异常,表示 JWT 已过期。

在比较时间之前,代码还考虑了允许的时钟偏差。这是因为即使是在同一台计算机上,不同的程序可能会使用不同的时钟,导致它们之间存在一定的时间差。为了解决这个问题,代码允许在当前时间基础上加上一定的时钟偏差,从而容忍这种时间差。

最后,如果 JWT 已过期,代码会生成一条错误消息,其中包括 JWT 的过期时间、当前时间以及它们之间的时间差。这条错误消息将被传递给 ExpiredJwtException 异常,以便调用方可以捕获并处理它。

Date exp = claims.getExpiration();

这段代码是用来获取 JWT 的过期时间的。在 JWT 中,通常会包含一个 exp 字段,用来表示 JWT 的过期时间。在使用 JWT 进行认证时,需要检查当前时间是否在 JWT 的有效期内,如果超过了有效期,则认证失败。

首先,通过 claims.getExpiration() 方法获取 JWT 中的过期时间。claims 是一个包含 JWT 所有声明信息的对象,可以通过它来获取 JWT 中的各种信息。getExpiration() 方法就是用来获取 JWT 的过期时间的。

如果 JWT 中没有设置过期时间,或者过期时间无效(比如格式不正确),则 getExpiration() 方法会返回 null。因此,在使用 JWT 进行认证时,需要先判断 exp 是否为 null,以及当前时间是否在有效期内,才能确定 JWT 是否有效。

long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now;

这段代码是用来计算 JWT 的最大有效期的。JWT 中包含了一个时间戳,以便在验证时检查 JWT 是否已经过期。但是由于客户端和服务器之间的时间可能存在差异,因此需要考虑一些时钟偏差。这里的 allowedClockSkewMillis 变量就是用来设置时钟偏差的毫秒数。

首先,获取当前时间戳 nowTime,然后减去允许的时钟偏差 allowedClockSkewMillis 得到最大有效期的时间戳 maxTime。接着,根据是否允许时钟偏差来创建一个 Date 对象 max,如果允许时钟偏差,则使用 maxTime 创建 Date 对象;否则,使用当前时间 now 创建 Date 对象。

最终,max 就是 JWT 的最大有效期,用于在验证 JWT 时检查时间戳是否在有效期内。

显然,不允许允许时钟偏差

 判断最大有效期已经超过过期时间,返回true,说明Jwt已经过期

直接放行

public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000  一个小时

我设置了过期时间,之前只是使用,但是没有仔细看(只怪我学的粗心,只是用了,没有仔细看)

这是jwt工具类

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "sPowerveil";

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("pv")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        System.out.println(JWT_KEY.length());
        System.out.println("==============================================");
        String jwt = createJWT("123456");
        System.out.println(jwt);

    }

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

最后感谢OpenAI提供帮助,辅助我解决问题。

 

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

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

相关文章

Ansible 部署

ansible 自动化运维工具&#xff0c;可以实现批量管理多台&#xff08;成百上千&#xff09;主机&#xff0c;应用级别的跨主机编排工具 特性&#xff1a; 无agent的存在&#xff0c;不要在被控制节点上安装客户端应用 通过ssh协议与被控制节点通信 基于模块工作的&#xff0c…

面试华为,足足花了2个月才上岸,真的难呀····

花2个月时间面试一家公司&#xff0c;你们觉得值吗&#xff1f; 背景介绍 二本计算机专业&#xff0c;代码能力一般&#xff0c;之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发&#xff0c;第二份实习由于大三暑假回国的时间比…

chatgpt赋能python:Python如何打包APK

Python如何打包APK Python是现代编程语言中最流行的之一&#xff0c;它是一种易于学习和使用的语言&#xff0c;因为它拥有直观的语法并且具有许多强大的工具和库。其互动性和可移植性使得Python适合用于开发各种类型的应用程序&#xff0c;包括移动应用程序。 在本文中&…

Android Retrofit 给你的接口加上缓存

转载请注明出处&#xff1a;https://blog.csdn.net/kong_gu_you_lan/article/details/131200501?spm1001.2014.3001.5501 本文出自 容华谢后的博客 往期回顾&#xff1a; Android Retrofit RxJava使用详解 Android 探讨一下Retrofit封装的最佳姿势 Android 谈谈我所理解的…

数据分析规范总结-V2.0

结构规范及写作 报告常用结构&#xff1a; 1. 架构清晰、主次分明 数据分析报告要有一个清晰的架构&#xff0c;层次分明能降低阅读成本&#xff0c;有助于信息的传达。虽然不同类型的分析报告有其适用的呈现方式&#xff0c;但总的来说作为议论文的一种&#xff0c;大部分的分…

C语言之函数栈帧的创建与销毁讲解(2)

上一篇博客我们讲到了函数栈帧的创建与销毁&#xff08;1&#xff09;今天我们来讲解Add函数的函数栈帧相关知识 在开始本章博客之前&#xff0c;大家可以把上一篇博客的主要内容仔细复习一下 看图 第一个mov&#xff1a;把b的值放到eax里面去 第二个mov&#xff1a;把a的…

wangEditor富文本编辑器的调用开发实录(v5版本、获取HTML内容、上传图片、隐藏上传视频)

wangEditor 是一款基于原生 JavaScript 封装&#xff0c;开源免费的富文本编辑器&#xff0c;支持常规的文字排版操作、插入图片、插入视频、插入代码等功能&#xff0c;同时提供多样化的扩展功能&#xff08;如字体、颜色、表情、代码、地图等插件&#xff09;&#xff0c;支持…

SpringBoot(运维篇)

SpringBoot运维篇 SpringBoot程序的打包和运行 程序打包 SpringBoot程序是基于Maven创建的&#xff0c;在Maven中提供有打包的指令&#xff0c;叫做package。本操作可以在Idea环境下执行 mvn package打包后会产生一个与工程名类似的jar文件&#xff0c;其名称由模块名版本号…

小程序开发:如何从零开始建立你的第一个小程序

你可能有一个小程序的想法&#xff0c;但它仍然是一个想法。对于开发人员来说&#xff0c;这是一项艰巨的任务&#xff0c;因为你必须确保你有足够的时间来开发你的第一个小程序。如果你决定使用小程序&#xff0c;那就有很多事情要做。创建一个小程序可能是一件非常耗时的事情…

无线蓝牙耳机推荐有哪些?八大无线蓝牙耳机排行

在近几年蓝牙耳机的飞速发展&#xff0c;我们对于音乐和通讯的需求也越来越高。传统的耳机和听筒虽然能够满足我们基本的听觉需求&#xff0c;但是它们也带来了一些问题&#xff0c;比如&#xff1a;长时间佩戴会导致耳朵疲劳、引起耳道炎等。针对这些问题&#xff0c;蓝牙耳机…

解决Dbeaver连接一段时间不操作后断开的问题

1、首先右键数据库连接点击【编辑连接】 2、点击【初始化】将【连接保持】改成60s&#xff0c;这样数据库就不会自己断开了

动态组件和异步组件

动态组件 相关api <!-- 失活的组件将会被缓存&#xff01;--> <keep-alive include"Tab1,Tab2"><component :is"currentTabComponent"></component> </keep-alive>component属性 is“全局注册或局部注册的组件名” keep…

Windows安装Pytorch3d

Windows安装Pytorch3d 1.前提&#xff1a; 安装Visual Studio 2019 【我记得必须是2017-2019之间的版本&#xff0c;我一开始用的是2022的版本就安装不了】网址pytorch和pytorch3d、cuda和NVIDIA CUB版本需要相互对应 pytorch和pytorch3d版本对应关系如下&#xff1a;https:…

springcloud-Nacos处理高并发的注册

添加服务 第一 次判断 提供性能&#xff1a;避免多个线程同时在等 synchronzied 释放 第二次 判断 &#xff1a; 别的线程可能已经将实例加入了 serviceMap() 注意这里还有个ConcurrentSkipListMap 有利于提高读写性能。 所以内层的Map 是个ConcurrentSkipLlistMap&#xff…

Binder对象的流转(系统服务的调用过程、AIDL的使用过程)

零、Binder的传递 Android系统中&#xff0c;存在大量的 IPC 交互&#xff0c;同时也使用了大量的 Binder&#xff0c;那么Binder是怎么在各进程中进行对象的传递&#xff1f; 一、调用系统服务时&#xff0c;Binder的传递 回忆一下&#xff0c;Android系统的启动流程&#x…

看一图而思全云

>> 前言 << 我在看财经十一人吴俊宇老师撰写的《阿里云计划在12月内独立上市》时&#xff0c;看到了一张全球及中国IT支出结构图。就是下图这张图&#xff0c;盯着这张图&#xff0c;我看到了星辰大海&#xff0c;也想到了广阔天地大有可为。 但只看这个图不够过瘾…

1.7C++流插入运算符重载

C流插入运算符重载 在 C 中&#xff0c;流插入运算符&#xff08;<<&#xff09;用于输出数据到流中的运算符&#xff0c;流插入运算符可以被重载&#xff0c;使得程序员可以自定义输出对象的方式。 重载流插入运算符的一般形式如下&#xff1a; 其中&#xff0c;T 是…

Vue中如何进行瀑布流布局与图片加载优化

Vue中如何进行瀑布流布局与图片加载优化 瀑布流布局是一种常用的网页布局方式&#xff0c;它可以让页面看起来更加有趣和美观。在Vue.js中&#xff0c;我们可以使用第三方插件或者自己编写组件来实现瀑布流布局。同时&#xff0c;为了优化图片加载的性能&#xff0c;我们还可以…

部署minio分布式测试环境

准备了4台虚拟机作为minio分布式节点服务器。 操作系统为TencentOS3.1(相当于CentOS8) 选择从官网下载minio安装包,minio-20230602231726.0.0.x86_64.rpm 安装命令如下: rpm -ivh minio-20230602231726.0.0.x86_64.rpm 安装完毕,minio命令将会放在/usr/local/bin下。…

推动开源行业高质量发展|2023开放原子全球开源峰会圆满落幕

6 月 13 日&#xff0c;由 2023 全球数字经济大会组委会主办&#xff0c;开放原子开源基金会、北京市经济和信息化局、北京经济技术开发区管理委员会承办的 2023 开放原子全球开源峰会在北京顺利落下帷幕。本次峰会以“开源赋能&#xff0c;普惠未来”为主题&#xff0c;设置了…