【密码学】MD5、UUID,加盐,JWT的理解与使用范例

news2024/9/29 21:18:52

文章目录

    • MD5加密:
    • 1、MD5加密
      • 安全访问认证
      • 示例代码:
    • 2、UUID
      • 简介:
      • 使用:
    • 3、加盐
      • 原理:
      • 示例代码:
    • 4、jwt
      • 认知:
      • JWT 结构:
      • 范例代码:

在这里插入图片描述

MD5加密:

1、MD5加密

  • Message Digest Algorithm MD5(中文名为消息摘要算法第五版)
  • 应用程序的密码通常不会明文保存,会使用各种各样的加密算法对密码进行加密
  • MD5算法相对来说较为安全。
  • 初始的MD5算法是由C语言实现
  • Java版本的MD5算法是根据C语言的MD5算法演变而来的
  • MD5加解密在线工具:http://www.cmd5.com/

安全访问认证

当用户登录的时候,系统把用户输入的密码进行MD5 Hash运算,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。这可以避免用户的密码被具有系统管理员权限的用户知道。

MD5将任意长度的“字节串”映射为一个128bit的大整数,并且是通过该128bit反推原始字符串是困难的,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。

正是因为这个原因,现在被黑客使用最多的一种破译密码的方法就是一种被称为"跑字典"的方法。有两种方法得到字典,一种是日常搜集的用做密码的字符串表,另一种是用排列组合方法生成的,先用MD5程序计算出这些字典项的MD5值,然后再用目标的MD5值在这个字典中检索。我们假设密码的最大长度为8位字节(8 Bytes),同时密码只能是字母和数字,共26+26+10=62个字节,排列组合出的字典的项数则是P(62,1)+P(62,2)….+P(62,8),那也已经是一个很天文的数字了,存储这个字典就需要TB级的磁盘阵列,而且这种方法还有一个前提,就是能获得目标账户的密码MD5值的情况下才可以。

示例代码:

md5加密工具类:

  • 对字符串md5加密(小写+数字)getMD5()
  • 对字符串md5加密(大写+数字)toMD5()
public class MD5Util {
  /**
    * 对字符串md5加密(小写+数字)
    *
    * @param str传入要加密的字符串
    * @return MD5加密后的字符串
    */
  public static String getMD5(String source) {
    try {
      // 生成一个MD5加密计算摘要
      MessageDigest md = MessageDigest.getInstance("MD5");
      // 计算md5函数
      md.update(source.getBytes());
      // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
      // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
      return new BigInteger(1, md.digest()).toString(16);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  /**
    * 对字符串md5加密(大写+数字)
    *
    * @param str传入要加密的字符串
    * @return MD5加密后的字符串
    */
  public static String toMD5(String source) {
    char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    try {
      byte[] btInput = source.getBytes();
      // 获得MD5摘要算法的 MessageDigest 对象
      MessageDigest mdInst = MessageDigest.getInstance("MD5");
      // 使用指定的字节更新摘要
      mdInst.update(btInput);
      // 获得密文
      byte[] md = mdInst.digest();
      // 把密文转换成十六进制的字符串形式
      int j = md.length;
      char str[] = new char[j * 2];
      int k = 0;
      for (int i = 0; i < j; i++) {
        byte byte0 = md[i];
        str[k++] = hexDigits[byte0 >>> 4 & 0xf];
        str[k++] = hexDigits[byte0 & 0xf];
      }
      return new String(str);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
}

自定义类-测试方法:模拟前端用户输入和后端数据库MD5加密比对

public static void main(String[] args) {
		//设置一个密码
		String password = "123456";
		System.out.println("MD5加密格式(大写+数字):" + MD5Util.toMD5(password);
		System.out.println("MD5加密格式(小写+数字):" + MD5Util.getMD5(password));
		
		//模拟后端数据库密码经过MD5加密
		String pwd_database = MD5Util.toMD5("123456");
		//模拟前端用户输入密码经过MD5加密
		String pwd_input = "123456";
		//打印
		System.out.println("前端用户输入和后端数据库MD5加密比对:" + MD5Util.toMD5(pwd_input).equals(pwd_database));
	}
}

在这里插入图片描述

可以看到密码明文一致的时候,加密的信息也是一致的,所以可以后端保存加密信息,然后将用户输入的密码明文进行MD5加密处理,来与后端数据库进行比对,作为一个简单的密码保护。

2、UUID

简介:

UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。最广泛应用的UUID,是微软公司的全局唯一标识符(GUID),而其他重要的应用,则有Linux ext2/ext3文件系统、LUKS加密分区、GNOME、KDE、Mac OS X等等。另外我们也可以在e2fsprogs包中的UUID库找到实现。
对UUID的详细认识:https://baike.baidu.com/item/UUID/5921266?fr=aladdin

使用:

在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。
而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)

public static void main(String[] args) {
    System.out.println(UUID.randomUUID());   //每次生成的编码不一样
}

在这里插入图片描述

结果串是用“-”来连接的,可以将它替换:

UUID uuid = UUID.randomUUID();   //生成结果串
String str = uuid.toString();   //结果串转换为String类型
String temp = str.replace("-","");   //将连接符“-”去掉
System.out.println(temp);

在这里插入图片描述

3、加盐

原理:

我们通常会将用户的密码进行 Hash 加密,如果不加盐,即使是两层的 md5 都有可能通过彩虹表的方式进行破译。彩虹表就是在网上搜集的各种字符组合的 Hash 加密结果。而加盐,就是人为的通过一组随机字符与用户原密码的组合形成一个新的字符,从而增加破译的难度。

原理:
简单来说:由原来的H( p) 变成了H(p+salt),相当于哈希函数H发生了变化,每次哈希计算使用的salt是随机的。
H如果发生了改变,则已有的彩虹表数据就完全无法使用,必须针对特定的H重新生成,这样就提高了破解的难度。

示例代码:

里面的静态方法包括:

  • 不加盐MD5:MD5WithoutSalt()
  • 生成盐:salt()
  • MD5加盐:MD5WithSalt()

运行文件的main方法:
在这里插入图片描述

package com.hz.springboot_crontab;

import java.security.MessageDigest;
import java.util.Random;

/**
     * 散列加密之32位哈希值的MD5算法,调用JDK里的API
     *ps:准确来说散列加密不是加密算法,因为它是不可逆的(只能加密,不能解密)
     */
    public class MyMD5 {

        private static char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

        public static void main(String[] args) throws Exception {
            String input = "123456789";
            System.out.println("MD5加密" + "\n"
                    + "明文:" + input + "\n"
                    + "无盐密文:" + MD5WithoutSalt(input));
            System.out.println("带盐密文:" + MD5WithSalt(input,0));
        }

        /**
         *@params: [inputStr] 输入明文
         *@Descrption: 不加盐MD5
         */
        public static String MD5WithoutSalt(String inputStr) {
            try {
            	//声明使用MD5算法,更改参数为"SHA"就是SHA算法了
                MessageDigest md = MessageDigest.getInstance("MD5");
                return byte2HexStr(md.digest(inputStr.getBytes()));//哈希计算,转换输出
            } catch (Exception e) {
                e.printStackTrace();
                return e.toString();
            }
        }

        /**
         *@params: [inputStr, type] inputStr是输入的明文;type是处理类型,0表示注册存hash值到库时,1表示登录验证时
         *@Descrption:  MD5加盐,盐的获取分两种情况;输入明文加盐;输出密文带盐(将salt存储到hash值中)
         */
        public static String MD5WithSalt(String inputStr, int type) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                
                String salt = null;
                if (type == 0) {//注册存hash值到库时,new salt
                    salt = salt();   
                } else if (type == 1) {//登录验证时,使用从库中查找到的hash值提取出的salt
                    String queriedHash=null;//从库中查找到的hash值
                    salt=getSaltFromHash(queriedHash);
                }

                String inputWithSalt = inputStr + salt;//加盐,输入加盐
                String hashResult = byte2HexStr(md.digest(inputWithSalt.getBytes()));//哈希计算,转换输出
                System.out.println("加盐密文:"+hashResult);

                /*将salt存储到hash值中,用于登陆验证密码时使用相同的盐*/
                char[] cs = new char[48];
                for (int i = 0; i < 48; i += 3) {
                    cs[i] = hashResult.charAt(i / 3 * 2);
                    cs[i + 1] = salt.charAt(i / 3);//输出带盐,存储盐到hash值中;每两个hash字符中间插入一个盐字符
                    cs[i + 2] = hashResult.charAt(i / 3 * 2 + 1);
                }
                hashResult = new String(cs);
                return hashResult;
            } catch (Exception e) {
                e.printStackTrace();
                return e.toString();
            }
        }


        /**
         * @return: salt
         * @params:
         * @Descrption: 自定义简单生成盐,是一个随机生成的长度为16的字符串,每一个字符是随机的十六进制字符
         */
        public static String salt() {
            Random random = new Random();
            StringBuilder sb = new StringBuilder(16);
            for (int i = 0; i < sb.capacity(); i++) {
                sb.append(hex[random.nextInt(16)]);
            }
            return sb.toString();
        }

        /**
         * @return: 十六进制字符串
         * @params: [bytes]
         * @Descrption: 将字节数组转换成十六进制字符串
         */
        private static String byte2HexStr(byte[] bytes) {
            /**
             *@Author: DavidHuang
             *@Time: 19:41 2018/5/10
             *@return: java.lang.String
             *@params:  * @param bytes
             *@Descrption:
             */
            int len = bytes.length;
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < len; i++) {
                byte byte0 = bytes[i];
                result.append(hex[byte0 >>> 4 & 0xf]);
                result.append(hex[byte0 & 0xf]);
            }
            return result.toString();
        }


        /**
         *@return: 提取的salt
         *@params: [hash] 3i byte带盐的hash值,带盐方法与MD5WithSalt中相同
         *@Descrption:  从库中查找到的hash值提取出的salt
         */
        public static String getSaltFromHash(String hash){
            StringBuilder sb=new StringBuilder();
            char [] h=hash.toCharArray();
            for(int i=0;i<hash.length();i+=3){
                sb.append(h[i+1]);
            }
            return sb.toString();
        }

}

4、jwt

【好文章要分享:https://zhuanlan.zhihu.com/p/433674847,有图片理解内部原理】

认知:

什么是jwt?

  • JWT只是缩写,全拼则是 JSON Web Tokens ,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。
    在这里插入图片描述

jwt原理:

  • jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。

JWT 使用:
1、服务端根据用户登录状态,将用户信息加密到token中,返给客户端
2、客户端收到服务端返回的token,存储在cookie中
3、客户端和服务端每次通信都带上token,可以放在http请求头信息中,如:Authorization字段里面
4、服务端解密token,验证内容,完成相应逻辑

JWT 特点:

  • JWT更加简洁,更适合在HTML和HTTP环境中传递
  • JWT适合一次性验证,如:激活邮件
  • JWT适合无状态认证
  • JWT适合服务端CDN分发内容
  • 相对于数据库Session查询更加省时
  • JWT默认不加密
  • 使用期间不可取消令牌或更改令牌的权限
  • JWT建议使用HTTPS协议来传输代码

JWT 结构:

  • 一个token分为3部分:头部(header)、载荷(payload)、签名(signature)
  • 三部分用点.隔开
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • header里面包含两部分信息,token的类型,算法的名称
    在这里插入图片描述
  • Payload
    在这里插入图片描述
  • Signature签名:将加密之后的上面两部分用点拼接的再次加密
    在这里插入图片描述

范例代码:

jar:jdk大于1.8的版本会报错,要导入其他依赖

<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
        <!--因为jdk的版本过高要加入的依赖,避免报错;  jdk1.8可以不加-->
<!--        <dependency>-->
<!--            <groupId>javax.xml.bind</groupId>-->
<!--            <artifactId>jaxb-api</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>com.sun.xml.bind</groupId>-->
<!--            <artifactId>jaxb-impl</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>com.sun.xml.bind</groupId>-->
<!--            <artifactId>jaxb-core</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>javax.activation</groupId>-->
<!--            <artifactId>activation</artifactId>-->
<!--            <version>1.1.1</version>-->
<!--        </dependency>-->

JWTTest类:加密解密的方法

/**
 * jwt加密解密测试代码
 */
public class JWTTest {

    private long time = 1000*60*60*24;   //设置24小时之后失效
    private String signature = "admin";  //设置一个签名信息,加密解密都要通过他

    /**
     * 加密
     */
    @Test
    public void jwt() {
        //创建JwtBuilder对象
        JwtBuilder jwtBuilder = Jwts.builder();
        String jwtToken = jwtBuilder
                //header
                .setHeaderParam("typ","JWT")
                .setHeaderParam("alg","HS256")
                //payload
                .claim("username","Jules")
                .claim("role","admin")
                .setSubject("admin-test")   //主题
                .setExpiration(new Date(System.currentTimeMillis()+time))   //有效时间
                .setId(UUID.randomUUID().toString())
                //signature
                .signWith(SignatureAlgorithm.HS256,signature)
                //将上面三部分拼起来
                .compact();
        System.out.println(jwtToken);
    }

    /**
     * 解密
     */
   ……
}

加密后得到username和role的token
在这里插入图片描述

/**
 * 解密
 */
 @Test
    public void parse() {
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9………………";  //我省略了,复制自己的
        JwtParser jwtParser = Jwts.parser();
        Jws<Claims> claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();   //加密封装的东西都放到了claims里面
        //直接取:
        System.out.println(claims.get("username"));
        System.out.println(claims.get("role"));
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println(claims.getExpiration());

    }

将原来的数据解密:

在这里插入图片描述

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

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

相关文章

计算机毕业设计springboot+vue基本微信小程序的疫情防控平台系统

项目介绍 当今社会疫情防控平台是必不可少的,大家都在听从政府的号召在居家隔离,不管是在城市还是在乡镇、农村,这引起我的注目,设计一套社区疫情防控系统,疫情防控需要大家共同努力、团结对社区居民进行了新型冠状病毒肺炎防控知识普及和宣传教育。针对这一需求,本文设计并实现…

LVS详细介绍,这么讲不信你不明白负载均衡

Linux virtual server&#xff0c;是一个基于集群技术和Linux操作系统&#xff0c;目的是实现一个高性能、高可用的服务器&#xff0c;主要工作在网络层。 他采取IP负载均衡&#xff0c;也叫三层负载均衡&#xff08;因为工作在OSI模型的第三层--网络层&#xff09;&#xff0…

java计算机毕业设计springboot+vue旅游攻略平台

项目介绍 目前,我国旅游业正处于高度发展的状态。越来越多的人在假日里选择出游作为放松自己身心的手段。随着网络的普及和发展,人们开始习惯性地在做某事之前先在网络上浏览相关的内容。旅游网站能够帮助游客了解景点相关信息,推出相应的线路信息供游客们选择,并查看相应的旅…

计算机网络实验——路由器的配置静态与RIP配置

前言 由于几个资料和老师给的ppt说的都不清楚&#xff0c;自己也没想到什么很巧妙的归纳方法&#xff0c;写个总结记录一下。 想必静态路由配置的过程中唯一的难点就是ip route这个指令&#xff0c;其他的什么添加接口&#xff0c;设置ip啥的都是基础路由配置中的内容&#xf…

2023春招面试专题:JAVA基础高频面试

ArrayList和LinkedList有哪些区别 ArrayList扩容机制: ArrayList() 会使用长度为零的数组ArrayList(int initialCapacity) 会使用指定容量的数组public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量add(Object o) 首次扩容为 10&#xff0c;再次…

3D激光里程计其一:ICP

这里写目录标题1. ICP 整体流程2. ICP 的数学表示3. 基于 SVD 的 ICP3.1 旋转部分求解3.2 平移部分求解4. 基于优化的 ICP5. ICP 系列汇总Reference: 深蓝学院-多传感器融合 1. ICP 整体流程 目的&#xff1a;有两个相似的刚体点云&#xff0c;它们之间没有做好配准&#xff…

Golang原理分析:闭包及for range延迟绑定问题原理及解决

1.Golang中的闭包 1.1.什么是闭包 当一个函数引用了环境的变量&#xff0c;被引用的变量与该函数同时存在组成的系统&#xff0c;被称为闭包。 闭包 环境的变量 函数。 以下JavaScript代码展示了一个基础的闭包&#xff1a; name是init函数中的内部变量displayName()是i…

机器学习 鸢尾花(Iris Flower)数据集分析

目录 一&#xff1a;加载数据 二&#xff1a;提取特征数据 三&#xff1a;提取标签数据 四&#xff1a;数据划分 一&#xff1a;加载数据 加载数据&#xff0c;查看数据特征 from sklearn.datasets import load_iris# 1 加载数据 鸢尾花load_iris iris_datasets load_iri…

数据校验及在数据校验的情况下增加headers拿回数据

什么是数据校验&#xff1a; 当你向一个数据端口请求数据时&#xff0c;如果这个数据端口没有设置&#xff1a;Access-Control-Allow-Origin&#xff1a;*&#xff0c;那就是存在跨域限制了&#xff0c;你是拿不回来数据的。图示&#xff1a; 但是有些数据端口是设置了 Access…

【JS ES6】了解学习set类型和weakset类型

✍️ 作者简介: 前端新手学习中。 &#x1f482; 作者主页: 作者主页查看更多前端教学 &#x1f393; 专栏分享&#xff1a;css重难点教学 Node.js教学 从头开始学习 ajax学习 目录set类型与array和object的区别set元素检测与管理类型转换的重要性遍历set类型的方式使用set…

Spring - 事件监听机制 源码解析

文章目录Pre概述ApplicationEvent ------ 事件ApplicationListener ------ 事件监听器ApplicationEventPublisher ------ 事件发布者ApplicationEventMulticaster ------ 事件广播器spring主要的内置事件ContextRefreshedEventContextStartedEventContextStoppedEventContextCl…

设计模式概述之单例模式(四)

很多小伙伴&#xff0c;不知道设计模式是什么&#xff1f; 通常我们所说的设计模式是一种设计方案&#xff0c;是前人留下的经验及最佳实践。 想要学习设计模式&#xff0c;至少要把面向对象的基本结构全部了解。 设计模式&#xff0c;是建立在一定基础上的思维训练。 学习…

喜报 | 中关村发来贺电

2022年12月14日&#xff0c;由中关村金融科技产业发展联盟、中关村互联网金融研究院举办的“中关村金融科技系列活动——2023第十届中关村金融科技论坛年会暨2022“光大杯”中关村番钛客金融科技国际创新大赛颁奖典礼”已圆满落幕。本次会议为建设金融科技中心&#xff0c;共建…

【Pintos】实现自定义 UserProg 系统调用

&#x1f4ad; 写在前面&#xff1a;本文讲解的内容不属于 Pintos 的 Project 项目&#xff0c;而是关于 userprog 如何添加系统调用的&#xff0c;学习如何额外实现一些功能到系统调用中以供用户使用。因为涉及到 src/example 下的Makefile 的修改、lib 目录下 syscall-nr 系统…

搜索二叉树详解

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、搜索二叉树框架二、搜索二叉树概念三、搜索二叉树操作①Erase②Find递归③Insert递归④Erase递归&#xff0c;比Erase更简洁⑤析构函数⑥…

unity学习笔记--day01

今天学习制作了一个简单的抽卡功能&#xff0c;学习结束后目录结构如下&#xff1a; .mate文件是unity生成的配置文件&#xff0c;不用管 制作第一张卡片 创建一个空物体&#xff0c;改名为Card。 在Card下挂载以下UI组件&#xff1a; 编写数据脚本并挂载&#xff0c;unity采用…

Nginx教程(4)—Keepalived

文章目录4.1 高可用集群架构Keepalived双机主备原理4.2 安装Keepalived4.3 Keepalived核心配置文件4.4 Keepalived实现双主机主备高可用测试4.5 高可用集群架构Keepalived双主热备原理Nginx教程一 Nginx教程二 Nginx教程三 4.1 高可用集群架构Keepalived双机主备原理 我们知道…

【计算机毕业设计】78.汽车租赁系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 目 录 摘 要 前 言 第1章 概述 1.1 研究背景 1.2 研究目的 1.3 研究内容 第二章 开发技术介绍 2.1 Java技术 2.2 Mysql数据库 2.3 B/S结构 2.4 SSM框架 第三章 系统分析 3.1 可行性分析 3.1.1 技术…

UnrealUBlueprintAsyncActionBase的使用

实现异步调用&#xff0c;之前我们介绍过一种FLatentActionInfo的方法&#xff0c;还有另外一种UBlueprintAsyncActionBase方法&#xff0c;可以实现异步节点&#xff0c;用于异步监听然后进行回调。按照如下步骤进行使用&#xff0c;我们同样以Delay一定帧数&#xff0c;并每帧…

面对新技术,必须找到与其发展相辅相成的长期主义的方法

从Meta股价的一路走低到扎克伯格发布的头显并不被用户买账&#xff0c;Facebook全力拥抱Meta正在经历一场过山车。   扎克伯格和他所带领下的Meta遭遇到的如此多的困境和难题&#xff0c;越来越多地让我们开始相信&#xff1a;所谓的元宇宙并非是一蹴而就的&#xff0c;它是一…