Spring Security 在登录时如何添加图形验证码

news2025/7/11 18:26:07

前言

在前面的几篇文章中,登录时都是使用用户名 + 密码进行登录的,但是在实际项目当中,登录时,还需要输入图形验证码。那如何在 Spring Security 现有的认证体系中,加入自己的认证逻辑呢?这就是本文的内容,本文会介绍两种实现方案,一是基于过滤器实现;二是基于认证器实现。

验证码生成

既然需要输入图形验证码,那先来生成验证码吧。

加入验证码依赖

<!--验证码生成器-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
复制代码

Kaptcha 依赖是谷歌的验证码工具。

验证码配置

@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha captchaProducer() {
        Properties properties = new Properties();
        // 是否显示边框
        properties.setProperty("kaptcha.border","yes");
        // 边框颜色
        properties.setProperty("kaptcha.border.color","105,179,90");
        // 字体颜色
        properties.setProperty("kaptcha.textproducer.font.color","blue");
        // 字体大小
        properties.setProperty("kaptcha.textproducer.font.size","35");
        // 图片宽度
        properties.setProperty("kaptcha.image.width","300");
        // 图片高度
        properties.setProperty("kaptcha.image.height","100");
        // 文字个数
        properties.setProperty("kaptcha.textproducer.char.length","4");
        //文字大小
        properties.setProperty("kaptcha.textproducer.font.size","100");
        //文字随机字体
        properties.setProperty("kaptcha.textproducer.font.names", "宋体");
        //文字距离
        properties.setProperty("kaptcha.textproducer.char.space","16");
        //干扰线颜色
        properties.setProperty("kaptcha.noise.color","blue");
        // 文本内容 从设置字符中随机抽取
        properties.setProperty("kaptcha.textproducer.char.string","0123456789");
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        kaptcha.setConfig(new Config(properties));
        return kaptcha;
    }
}
复制代码

验证码接口

/**
 * 生成验证码
 */
@GetMapping("/verify-code")
public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException {
    resp.setContentType("image/jpeg");
    // 生成图形校验码内容
    String text = producer.createText();
    // 将验证码内容存入HttpSession
    session.setAttribute("verify_code", text);
    // 生成图形校验码图片
    BufferedImage image = producer.createImage(text);
    // 使用try-with-resources 方式,可以自动关闭流
    try(ServletOutputStream out = resp.getOutputStream()) {
        // 将校验码图片信息输出到浏览器
        ImageIO.write(image, "jpeg", out);
    }
}
复制代码

代码注释写的很清楚,就不过多的介绍。属于固定的配置,既然配置完了,那就看看生成的效果吧!

接下来就看看如何集成到 Spring Security 中的认证逻辑吧!

加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码

基于过滤器

编写自定义认证逻辑

这里继承的过滤器为 UsernamePasswordAuthenticationFilter,并重写attemptAuthentication方法。用户登录的用户名/密码是在 UsernamePasswordAuthenticationFilter 类中处理,那我们就继承这个类,增加对验证码的处理。当然也可以实现其他类型的过滤器,比如:GenericFilterBeanOncePerRequestFilter,不过处理起来会比继承UsernamePasswordAuthenticationFilter麻烦一点。

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 需要是 POST 请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 获得请求验证码值
        String code = request.getParameter("code");
        HttpSession session = request.getSession();
        // 获得 session 中的 验证码值
        String sessionVerifyCode = (String) session.getAttribute("verify_code");
        if (StringUtils.isEmpty(code)){
            throw new AuthenticationServiceException("验证码不能为空!");
        }
        if(StringUtils.isEmpty(sessionVerifyCode)){
            throw new AuthenticationServiceException("请重新申请验证码!");
        }
        if (!sessionVerifyCode.equalsIgnoreCase(code)) {
            throw new AuthenticationServiceException("验证码错误!");
        }

        // 验证码验证成功,清除 session 中的验证码
        session.removeAttribute("verify_code");
        // 验证码验证成功,走原本父类认证逻辑
        return super.attemptAuthentication(request, response);
    }

}
复制代码

代码逻辑很简单,验证验证码是否正确,正确则走父类原本逻辑,去验证用户名密码是否正确。 过滤器定义完成后,接下来就是用我们自定义的过滤器代替默认的 UsernamePasswordAuthenticationFilter

  • SecurityConfig
import cn.cxyxj.study04.Authentication.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study04.Authentication.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }


    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 用自定义的 VerifyCodeFilter 实例代替 UsernamePasswordAuthenticationFilter
        http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);

        http.authorizeRequests()  //开启配置
                // 验证码、登录接口放行
                .antMatchers("/verify-code","/auth/login").permitAll()
                .anyRequest() //其他请求
                .authenticated().and()//验证   表示其他请求需要登录才能访问
                .csrf().disable();  // 禁用 csrf 保护
    }

    @Bean
    VerifyCodeFilter loginFilter() throws Exception {
        VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
        verifyCodeFilter.setFilterProcessesUrl("/auth/login");
        verifyCodeFilter.setUsernameParameter("account");
        verifyCodeFilter.setPasswordParameter("pwd");
        verifyCodeFilter.setAuthenticationManager(authenticationManagerBean());
        verifyCodeFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        verifyCodeFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return verifyCodeFilter;
    }

}
复制代码

当我们替换了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,那些失效的属性,都可以在配置 VerifyCodeFilter 实例的时候配置;还需要记得配置AuthenticationManager,否则启动时会报错。

  • MyAuthenticationFailureHandler
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 登录失败回调
 */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        String msg = "";
        if (e instanceof LockedException) {
            msg = "账户被锁定,请联系管理员!";
        }
       else if (e instanceof BadCredentialsException) {
            msg = "用户名或者密码输入错误,请重新输入!";
        }
        out.write(e.getMessage());
        out.flush();
        out.close();
    }
}
复制代码
  • MyAuthenticationSuccessHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 登录成功回调
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    }

}
复制代码

测试

  • 不传入验证码发起请求。

  • 请求获取验证码接口

  • 输入错误的验证码

  • 输入正确的验证码

  • 输入已经使用过的验证码

    各位读者是不是会觉得既然继承了 Filter,那是不是每个接口都会进入到我们的自定义方法中呀!如果是继承了 GenericFilterBean、OncePerRequestFilter 那是肯定会的,需要手动处理。 但我们继承的是 UsernamePasswordAuthenticationFilter,security 已经帮忙处理了。处理逻辑在其父类 AbstractAuthenticationProcessingFilter#doFilter 中。

基于认证器

编写自定义认证逻辑

验证码逻辑编写完成,那接下来就自定义一个 VerifyCodeAuthenticationProvider 继承自 DaoAuthenticationProvider,并重写 authenticate 方法。

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 验证码验证器
 */
public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获得请求验证码值
        String code = req.getParameter("code");
        // 获得 session 中的 验证码值
        HttpSession session = req.getSession();
        String sessionVerifyCode = (String) session.getAttribute("verify_code");
        if (StringUtils.isEmpty(code)){
            throw new AuthenticationServiceException("验证码不能为空!");
        }
        if(StringUtils.isEmpty(sessionVerifyCode)){
            throw new AuthenticationServiceException("请重新申请验证码!");
        }
        if (!code.toLowerCase().equals(sessionVerifyCode.toLowerCase())) {
            throw new AuthenticationServiceException("验证码错误!");
        }
        // 验证码验证成功,清除 session 中的验证码
        session.removeAttribute("verify_code");
        // 验证码验证成功,走原本父类认证逻辑
        return super.authenticate(authentication);
    }
}
复制代码

自定义的认证逻辑完成了,剩下的问题就是如何让 security 走我们的认证逻辑了。

在 security 中,所有的 AuthenticationProvider 都是放在 ProviderManager 中统一管理的,所以接下来我们就要自己提供 ProviderManager,然后注入自定义的 VerifyCodeAuthenticationProvider。

  • SecurityConfig
import cn.cxyxj.study02.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study02.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }

    @Bean
    VerifyCodeAuthenticationProvider verifyCodeAuthenticationProvider() {
        VerifyCodeAuthenticationProvider provider = new VerifyCodeAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService());
        return provider;
    }


    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        ProviderManager manager = new ProviderManager(verifyCodeAuthenticationProvider());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()  //开启配置
                // 验证码接口放行
                .antMatchers("/verify-code").permitAll()
                .anyRequest() //其他请求
                .authenticated()//验证   表示其他请求需要登录才能访问
                .and()
                .formLogin()
                .loginPage("/login.html") //登录页面
                .loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
                .usernameParameter("account") //用户名字段
                .passwordParameter("pwd") //密码字段
                .successHandler(new MyAuthenticationSuccessHandler())
                .failureHandler(new MyAuthenticationFailureHandler())
                .permitAll() // 上述 login.html 页面、/auth/login接口放行
                .and()
                .csrf().disable();  // 禁用 csrf 保护
        ;
    }
}
复制代码

测试

  • 不传入验证码发起请求。

  • 请求获取验证码接口

  • 输入错误的验证码

  • 输入正确的验证码

  • 输入已经使用过的验证码

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。

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

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

相关文章

matplotlib简介

matplotlib是一款用于画图的软件&#xff0c;以下步骤建议在.ipynb中完成。 导包 你需要导入以下包&#xff1a; import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np一个简单案例 matplotlib 在 Figure上绘制图形&#xff0c;每一个Figure会包含…

【附源码】Python计算机毕业设计手游账号交易系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Java注解式开发

目录 1. ssm框架的搭建 1.1 maven项目的创建 1.2 导入所需的包到pom.xml文件中 1.3 导入数据库连接文件、日志文件、redis连接文件 1.3.1 数据库连接文件 1.3.2 日志文件 1.3.3 redis连接文件 1.4 导入spring-mybatis、spring-mvc、spring-base、spring-redis四种集成文…

改变自己 只需要两年

改变自己 只需要两年 https://v.douyin.com/rLDmdQK/ 可以快速浏览上面视频 今天分享的这篇文章是TED上的一篇演讲 希望对下定决心想改变的你一些帮助。 用两年时间证明你可以 两年时间不算多长&#xff0c;但与此同时&#xff0c;很多事情都能在两年内完成&#xff0c;你…

WinHex(三)

目录 一、新建简单卷 二、MBR作用与结构 一、新建简单卷 1.右键点击刚刚新建的虚拟磁盘&#xff0c;选择新建简单卷。我新建了两个一个是NTFS&#xff0c;一个是FAT32 2.我们在刚刚新建的虚拟磁盘中放入一张图片&#xff0c;打开WinHex,点击“打开磁盘”选项&#xff0c;打…

[野火]STM32 F103 HAL库开发实战指南笔记之简单外设总结

1、GPIO编程总结 使能 GPIO 端口时钟&#xff1b;初始化 GPIO 目标引脚为推挽输出模式&#xff1b;编写简单测试程序&#xff0c;控制 GPIO 引脚输出高、低电平。 这部分宏控制 LED 亮灭的操作是直接向 BSRR 寄存器写入控制指令来实现的&#xff0c;对 BSRR 低 16 位 写 1 输出…

大学生静态HTML网页源码 我的校园网页设计成品 学校班级网页制作模板 web课程设计 dreamweaver网页作业

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

从0实现线性回归

编码题&#xff1a; 按要求完成下面的内容 1请用python完成从0实现线性回归&#xff0c;尝试使用不同的训练参数&#xff08;学习率&#xff0c;迭代次数&#xff09;&#xff0c; 以及不同的评价方法&#xff08;MSE,MAE,RMSE,R2&#xff09;等。 2比较说明sklearn的线性模…

断言(assert)的用法

参考&#xff1a;https://www.runoob.com/w3cnote/c-assert.html 目录作用总结与注意事项Demo作用 assert 是个宏&#xff0c;并且作用并非"报错"。 assert() 的用法像是一种"契约式编程"&#xff0c;程序满足我的假设条件&#xff0c;才能正常良好的运作…

做视频素材资源(free视频,音频,图片)

素材资源 一、视频 Videezy &#xff1a;https://www.videezy.com/ Videovo&#xff1a;https://www.videvo.net/ mixkit&#xff1a;https://mixkit.co/&#xff0c;可以 distill&#xff1a;https://wedistill.io/ splitshire&#xff1a;https://www.splitshire.com/ pixa…

Mysql常见指令以及用法(保姆级)

文章目录基础篇通用语法及分类DDL&#xff08;数据定义语言&#xff09;数据库操作注意事项表操作DML&#xff08;数据操作语言&#xff09;添加数据注意事项更新和删除数据DQL&#xff08;数据查询语言&#xff09;基础查询条件查询聚合查询&#xff08;聚合函数&#xff09;分…

前端性能-首次加载优化70%

前言 本篇文章&#xff0c;我们来总结归纳下万恶的this以及衍生出来的call/apply/bind对this进行绑定&#xff0c;想了很久&#xff0c;决定用实例演示的方式来讲解this&#xff0c;这样才能够理解this&#xff0c;因为this确实变化莫测&#xff0c;只靠概念&#xff0c;是不能…

【JS 构造|原型|原型链|继承(圣杯模式)|ES6类语法】下篇

⌚️⌚️⌚️个人格言&#xff1a;时间是亳不留情的&#xff0c;它真使人在自己制造的镜子里照见自己的真相! &#x1f4d6;Git专栏&#xff1a;&#x1f4d1;Git篇&#x1f525;&#x1f525;&#x1f525; &#x1f4d6;JavaScript专栏&#xff1a;&#x1f4d1;js实用技巧篇…

【数据结构】带头双向循环链表基本操作的实现(C语言)

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f40c; 个人主页&#xff1a;蜗牛牛啊 &#x1f525; 系列专栏&#xff1a;&#x1f6f9;初出茅庐C语言、&#x1f6f4;数据结构 &#x1f4d5; 学习格言&#xff1a;博…

峰会实录 | StarRocks PMC Chair 赵纯:数据分析的极速统一3.0 时代

作者&#xff1a;StarRocks PMC Chair 赵纯&#xff08;本文为作者在 StarRocks Summit Asia 2022 上的分享&#xff09; 一年前&#xff0c;StarRocks 源码开放&#xff0c;StarRocks 社区也正式成立。经过一年发展&#xff0c;社区已经获得了 3400 个 Star&#xff0c;7500 …

如何用windows上架ios到苹果商城

1.苹果账号 1.你需要申请苹果账号 官网有提示&#xff1a;Sign In - Apple 2.登录 登录后店家 account&#xff0c;进入account。 点击证书&#xff0c;进入。 2.开始上架步骤 1.注册标识符&#xff08;Bundle ID&#xff09; 进入这个界面后&#xff0c;点击 Identifiers …

Elasticsearch快照备份

目录 1、Repositories 1、配置路径 2、注册快照存储库 2、查看注册的库 3、创建快照 1、为全部索引创建快照 2、为指定索引创建快照 4、查看备份完成的列表 5、删除快照 6、从快照恢复 1、恢复指定索引 2、恢复所有索引&#xff08;除.开头的系统索引&#xff09; …

【Redis】 数据结构:Redis对象与编码(底层结构)对应关系详解

【Redis】 数据结构&#xff1a;Redis对象与编码(底层结构)对应关系详解 文章目录【Redis】 数据结构&#xff1a;Redis对象与编码(底层结构)对应关系详解Redis对象与编码(底层结构)对应关系引入Redis数据结构-RedisObjectredisObject数据结构Redis的编码方式五种数据结构Redis…

2022年深信服杯四川省大学生信息安全技术大赛-CTF-Reverse复现(部分)

Rush B 开始先设置一下数字以16进制格式显示 看主函数 __int64 __fastcall main(int a1, char **a2, char **a3) {int v3; // eaxsize_t v4; // raxint v5; // ecxchar v6; // alint v7; // ecxint v9; // [rsp3Ch] [rbp-404h]char s[1000]; // [rsp40h] [rbp-400h] BYREFchar …

免杀技术(详细)

恶意软件 ● 病毒、木马、蠕虫、键盘记录、僵尸程序、流氓软件、勒索软件、广告程序 ● 在用户非资源的情况下执行安装 ● 出于某种恶意的目的&#xff1a;控制、窃取、勒索、偷窥、推送、攻击。。。。。 恶意程序最重要的防护手段 ● 杀毒软件 / 防病毒软件 ● 客户端 / 服…