SpringSecurity-用户认证

news2024/12/23 15:38:04

1、用户认证

1.1 用户认证核心组件

我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的,这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。

我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取AuthenticationSecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象,什么是ThreadLocal呢?ThreadLocal也叫线程变量,提供了线程本地的实例

1.2 Spring Security中三个核心组件

1、Authentication:存储了认证信息,代表当前登录用户

2、SeucirtyContext:上下文对象,用来获取Authentication

3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

Authentication中是什么信息呢:

1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

2、Credentials:用户凭证,一般是密码

3、Authorities:用户权限

2. 进行用户认证的流程

AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的,该过滤器负责认证逻辑。

Spring Security用户认证关键代码如下:

// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

2.1 认证接口分析

AuthenticationManager的校验逻辑比较简单:

根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。

重点是这里每一个步骤Spring Security都提供了相关的组件:

1、是谁执行 根据用户名查询出用户对象 逻辑的呢?用户对象数据可以存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由**UserDetialsService** 处理,该接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。

2、那查询出来的 用户对象 又是什么呢?每个系统中的用户对象数据都不尽相同,咱们需要确认我们的用户数据是啥样的才行。Spring Security中的用户数据则是由**UserDetails** 来体现,该接口中提供了账号、密码等通用属性。

3、对密码进行校验:Spring Security框架除了if、else外还解决了密码加密的问题,这个组件就是**PasswordEncoder**,负责密码加密与校验。

PasswordEncoder接口如下:

package org.springframework.security.crypto.password;

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

2.2 分析结果:

UserDetialsServiceUserDetailsPasswordEncoder,这三个组件Spring Security都有默认实现,这一般是满足不了我们的实际需求的,所以这里需要我们自己来实现这些组件,对组件自定义实现。

3.在项目实现用户认证

这里我们采用MD5加密方式

在项目中建立utils包,导入MD5工具类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println(MD5.encrypt("111111"));
    }
}

3.1 自定义加密处理组件:CustomMd5PasswordEncoder

提示:@component:  标注一个类为Spring容器的Bean

import com.atguigu.common.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}

3.2 用户对象UserDetails

该接口就是我们所说的用户对象,它提供了用户的一些通用属性,源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:

添加自定义对象

3.3 添加UserDetailsServiceImpl类,实现UserDetailsService接口

到此AuthenticationManager校验所调用的三个组件已经进行自定义实现,接下来编写自定义用户认证接口,在filter包下创建TokenLoginFilter类

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    public TokenLoginFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        if(e.getCause() instanceof RuntimeException) {
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}

3.4 认证解析token

分析:由于用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体。

filter包中创建TokenAuthenticationFilter类,实现认证解析过程

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private RedisTemplate redisTemplate;
    public TokenAuthenticationFilter(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,//请求
                                    HttpServletResponse response,//响应
                                    FilterChain chain) throws ServletException, IOException {//获取请求头中的token
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }
        //如果不是登录接口,则需要验证token
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
            //如果token不为空,则设置到SecurityContextHolder中
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //放行
            chain.doFilter(request, response);
        } else {
            //如果token为空,则返回错误信息
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
    //获取token
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从请求头中获取token
        String token = request.getHeader("token");
        if(!StringUtils.isEmpty(token)){
            //从token中获取用户名称
            String username = JwtHelper.getUsername(token);
            if(!StringUtils.isEmpty(username)){
                //从redis中获取用户权限信息
                String authString = (String) redisTemplate.opsForValue().get(username);
                if(!StringUtils.isEmpty(authString))
                {
                    //将权限信息转换为SimpleGrantedAuthority对象
                    List<Map> mapList = JSON.parseArray(authString,Map.class);
                    System.out.println(mapList);
                    //遍历权限信息,将每个权限转换为SimpleGrantedAuthority对象
                    List<SimpleGrantedAuthority> authList = new ArrayList<>();
                    for (Map map:mapList) {
                        //获取权限字符串
                        String authority = (String) map.get("authority");
                        authList.add(new SimpleGrantedAuthority(authority));
                    }
                    //返回UsernamePasswordAuthenticationToken对象
                    return new UsernamePasswordAuthenticationToken(username,null, authList);
                }else {
                    //如果redis中没有权限信息,则返回空的SimpleGrantedAuthority对象
                    return new UsernamePasswordAuthenticationToken(username,null, new ArrayList<>());
                }

            }
            }
        return null;
        }
    }

3.5 进行用户认证配置



import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.filter.TokenAuthenticationFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserDetailsService userDetailsService;//自定义的UserDetailsService

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder; //自定义的密码加密器


    @Bean // 将AuthenticationManager注册为Bean,方便在其他地方使用
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {// 重写authenticationManager方法,返回AuthenticationManagerBuilder
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf跨站请求伪造
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                .antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(),redisTemplate));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

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

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

相关文章

百度在线翻译神器?这3款工具让你秒变语言达人!

在数字化的今天&#xff0c;我们早已离不开在线翻译工具了&#xff01;从日常的简单翻译到专业级的文献翻译&#xff0c;这些翻译工具就像是我们的“翻译官”&#xff0c;为我们的生活带来了便利&#xff1b;在这里&#xff0c;我给大家分享一下我的百度在线翻译使用感受&#…

Elasticsearch 分片迁移与移除集群节点操作

Elasticsearch 分片迁移与移除集群节点操作 问题背景 在单台服务器上部署了 7 个 Elasticsearch 节点&#xff0c;分别为 es-node1 到 es-node7&#xff0c;端口从 9201 到 9207。每个节点都承载大量数据&#xff0c;但没有设置副本分片。由于多个节点共享同一台服务器的硬件…

自动化测试常用函数:弹窗、等待、导航、上传与参数设置

目录 一、弹窗 1. 警告弹窗确认弹窗 2. 提示弹窗 二、等待 1. 强制等待 2. 隐式等待 3. 显示等待 三、浏览器导航 1. 打开网站 2. 浏览器的前进、后退、刷新 四、文件上传 五、浏览器参数设置 1. 设置无头模式 2. 页面加载策略 一、弹窗 弹窗是在页面是找不到任何…

震撼!最强开源模型通义千问2.5 72B竟在4GB老显卡上成功运行!

炸裂&#xff01;最强开源模型一夜之间易主。阿里发布千问2.5模型&#xff0c;72B版本在MMLU、MATH、MBPP等大部分评测指标上都超过了Llama3 405B&#xff0c;甚至一些指标也超过了GPT4o。正式加冕最强开源模型新王&#xff01; 今天要挑战用我的4GB老显卡不做量化、不做压缩&…

光伏仿真:排布设计如何优化用户体验?

1、屋顶绘制精准 光伏系统的性能直接受到屋顶结构的影响&#xff0c;因此&#xff0c;屋顶绘制的精准性是光伏仿真设计的首要任务。现代光伏仿真软件通过直观的界面和强大的图形编辑功能&#xff0c;使得用户能够轻松导入或绘制出待安装光伏系统的屋顶形状。无论是平面屋顶、斜…

LLM - 使用 XTuner 指令微调 多模态大语言模型(InternVL2) 教程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142528967 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 XTuner…

国庆节到了,扣子智能体coze画板功能实现贺卡编辑智能体自动添加logo和二维码,让海报品牌化

大家好,我是Shelly,一个专注于输出AI工具和科技前沿内容的AI应用教练,体验过300+款以上的AI应用工具。关注科技及大模型领域对社会的影响10年+。关注我一起驾驭AI工具,拥抱AI时代的到来。 自媒体时代,不管是一个人、一个团队还是一家公司,都是一个IP。那么添加品牌的标志…

JavaWeb校园二手交易平台

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 spring-mybatis.xml3.5 spring-mvc.xml3.5 login.jsp 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优…

AI大模型助力数据消费,构建数据飞轮科学、高效的体系

随着互联网的技术高速发展&#xff0c;越来越多的应用层出不穷&#xff0c;伴随着数据应用的需求变多&#xff0c;为快速响应业务需求&#xff0c;很多企业在初期没有很好的规划的情况下&#xff0c;存在不同程度的烟囱式的开发模式&#xff0c;这样会导致企业不同业务线的数据…

Java Map类

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;Java 目录 &#x1f449;&#x1f3fb;map1. 常见的实现2. 主要方法2.1. put(K key, V value)2.2. get(Object key)2.3. remove(Object key)2.4. containsKe…

西部移动硬盘怎么恢复数据?4种详细且实用的方法

面对西部移动硬盘数据丢失的问题&#xff0c;用户往往感到焦虑和无助。本文将为您提供一系列详细且实用的数据恢复方法&#xff0c;帮助您轻松应对数据丢失的挑战&#xff0c;重拾宝贵信息。 图片来源于网络&#xff0c;如有侵权请告知 一、西部移动硬盘数据丢失原因 西部移动…

生成式AI在电商场景的应用、前景与挑战,零基础入门到精通,收藏这一篇就够了

编者按 百舸争流的AI时代&#xff0c;“AI”行动在千行百业迅速开展。电商是一个重要场景&#xff0c;**据阿里调研&#xff0c;在电商平台&#xff0c;约30%受访商家已经使用生成式AI&#xff0c;成为生成式AI技术普惠的最佳试验场之一。**目前&#xff0c;已使用生成式AI的商…

828华为云征文|华为云Flexus云服务器X实例之openEuler系统下部署经典扫雷小游戏

828华为云征文&#xff5c;华为云Flexus云服务器X实例之openEuler系统下部署经典扫雷小游戏 前言一、Flexus云服务器X实例介绍1.1 Flexus云服务器X实例简介1.2 Flexus云服务器X实例特点1.3 Flexus云服务器X实例使用场景 二、本次实践介绍2.1 本次实践简介2.2 扫雷小游戏简介2.3…

KPaaS平台用户权限管理系统方案之表单设计统一单据制作与授权

不同的业务系统各自独立运行&#xff0c;需要分别进行授权操作&#xff0c;这不仅繁琐耗时&#xff0c;还容易出现错误和不一致的情况&#xff0c;导致企业在多系统用户权限角色管理中常常陷入困境&#xff0c;那么&#xff0c;有没有一种高效、便捷的解决方案呢&#xff1f; …

关于预处理详解,#define,宏的使用以及命名 函数与宏的区别详细对比

预定义符号 C语⾔设置了⼀些预定义符号&#xff0c;可以直接使⽤&#xff0c;预定义符号也是在预处理期间处理的 __FILE__ //进⾏编译的源⽂件 __LINE__ //⽂件当前的⾏号 __DATE__ //⽂件被编译的⽇期 __TIME__ //⽂件被编译的时间 __STDC__ //如果编译器遵循ANSI C&#xff…

汉诺塔的理解

数学思想——归纳推理&#xff08;不是反证法&#xff09; 为了方便&#xff0c;我把塔叫做牌&#xff0c;最左边的是从大到小&#xff08;底部开始&#xff09;放置的的牌堆。 数字的那一列是递归调用&#xff0c;右边长度不一的箭头是&#xff0c;数字阶段向下调用方法的情况…

稀土抗菌剂在涂料中应用的神奇表现

稀土抗菌剂的抗菌抑菌机理有四个层面:一是稀土化合物与细菌表面静电结合&#xff0c;造成直接的杀灭二是基于稀土的光催化半导体特性&#xff0c;通过光生氧自由基ROS机理杀灭细菌;三是稀土化合物破坏细胞膜通透性&#xff0c;造成破损导致细胞质流出杀灭细菌;四是稀土离子跨膜…

C标准库<string.h>-str、strn开头的函数

char *strcat(char *dest, const char *src) 函数功能 strcat 函数用于将一个字符串追加到另一个字符串的尾部。 参数解释 dest&#xff1a;指向目标字符串的指针&#xff0c;这个字符串的尾部将被追加 src 字符串的内容。src&#xff1a;指向源字符串的指针&#xff0c;其…

最精简的VScode Verilog RTL开发环境搭建教程

【2024-9月更新】最精简的VScode Verilog RTL开发环境搭建教程 文章目录 【2024-9月更新】最精简的VScode Verilog RTL开发环境搭建教程一、官网下载VScode二、登录账号同步三、安装配置拓展插件1.Verilog-HDL/systemVerilog拓展2.安装Universal Ctags● Windows系统安装univer…

(附源码) Springboot 飞速物流管理平台78584

摘要 受疫情的影响&#xff0c;很多城市处于静默的状态&#xff0c;导致店铺很多店铺都处于关闭的状态&#xff0c;给商家带来了极大的损失&#xff0c;很多商家为了减少损失都通过线上进行销售&#xff0c;比如直播、微商等&#xff1b;同时对于消费者来说&#xff0c;网上购买…