SpringBoot整合Spring-Security 认证篇(保姆级教程)

本文项目基于以下教程的代码版本: https://javaxbfs.blog.csdn.net/article/details/135195636

代码仓库: https://gitee.com/skyblue0678/springboot-demo

为了跟shiro区别开,新建了一个分支:

目录

🌹1、友善问候一下 Spring Security

⭐2、 POM依赖

🌹3、登录

⭐4、根据账号从DB中获取用户实体

🌹5、校验密码是否正确

🌹6、全局异常返回

⭐7、测试

🌹8、细说spring security


🌹1、友善问候一下 Spring Security

Spring Security是Spring家族中的安全框架,可以用来做用户验证和权限管理等。Spring Security是一款重型框架,不过功能十分强大。

一般来说,如果项目中需要进行权限管理,具有多个角色和多种权限,我们可以使用Spring Security。如果是较为简单的项目,只需要控制一下某些接口只有登录后才能访问,则可以使用Shiro框架,Shiro也是一款安全框架,它是一款轻量级框架,功能没有Spring Security多,但使用起来要简单不少。

也就是说,Spring Security的颗粒度更细。

SpringSecurity 采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。

不过我们不需要去仔细了解每一个过滤器的含义和用法,只需要搞定以下几个问题即可:怎么登录、怎么校验账户、认证失败处理。

2、 POM依赖

没啥好说的,maven导入即可。

<!--springSecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

不写版本号,默认就会下载最新的版本。

3、登录

不管你用哪种权限框架,第一个要解决的问题就是登录。就是在我们的登录接口中,将账户密码委托给权限框架接管,让权限框架帮我们做校验和权限认证。

新建一个登录方法

@GetMapping("/security/login")
public String securityLogin(String username,String password){
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username,password);
    // 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,
    Authentication authentication = authenticationManager.authenticate(authToken);
    if(authentication == null) {
        throw new RuntimeException("Login false");
    }

    //获取符合security规范的User
    SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();

    String token = jwtUtil.createToken(securityUser.getUser());
    return token;
}

这个方法的目的是验证用户的用户名和密码,并在验证成功后为该用户生成一个JWT。 UsernamePasswordAuthenticationToken就是我们委托框架帮我们托管的登录凭证,shiro框架也有类似的东西。 然后是:

Authentication authentication = authenticationManager.authenticate(authToken);

这就是spring security帮我们执行认证和授权的方法,最终返回一个认证结果。大家思考一下,我们正常登录的逻辑无非是四步走:

  1. 输入账号密码

  2. 根据账号从DB中获取用户实体

  3. 校验密码是否正确

  4. 校验成功,将用户生成token后返回

我们再回过来看这段代码,第2步和第3步没见到,只见到spring security帮我们做了。但是,这并不代表我们可以省略这两步,只是需要我们写在别的地方,仅此而已。

4、根据账号从DB中获取用户实体

这个步骤是不可能不写的,只是写到了别处。 先说个事儿哈,spring security中的用户概念,有自己的一套规则,不能直接用我们系统里面的User类。

如果我们要用spring security,就得实现他的用户接口:UserDetails。 所以,我们新建一个SecurityUser类:

// 使用Lombok库的@Data注解,自动生成getter、setter、equals、hashCode和toString方法
// 同样使用@NoArgsConstructor注解,自动生成无参构造函数
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {

    // 使用聚合模式,将我们自己的User对象聚合到SecurityUser中
    // user字段存储了用户的一些信息,例如用户名和密码等
    private User user;

    // 覆盖UserDetails接口中的getAuthorities方法,返回用户的权限集合
    // 这里返回null,表示未实现获取权限的逻辑
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    // 覆盖UserDetails接口中的getPassword方法,返回用户的密码
    // 这里通过调用user对象的getPwd方法获取密码
    @Override
    public String getPassword() {
        return user.getPwd();
    }

    // 覆盖UserDetails接口中的getUsername方法,返回用户的用户名
    // 这里通过调用user对象的getUserName方法获取用户名,并注释说明有的地方可能会用email作为用户名,这里还是使用userName
    @Override
    public String getUsername() {
        // 用户名:有的地方可能会用email作为用户名,我们这还是userName
        return user.getUserName();
    }

    // 覆盖UserDetails接口中的isAccountNonExpired方法,判断账户是否过期
    // 这里直接返回true,表示账户不过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 覆盖UserDetails接口中的isAccountNonLocked方法,判断账户是否被锁定
    // 这里直接返回true,表示账户未被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 覆盖UserDetails接口中的isCredentialsNonExpired方法,判断凭证是否过期
    // 这里直接返回true,表示凭证不过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 覆盖UserDetails接口中的isEnabled方法,判断用户是否启用
    // 这里直接返回true,表示用户启用状态为true
    @Override
    public boolean isEnabled() {
        return true;
    }
}

我们系统里面有自己的user了,但是为了适配,所以就聚合进来。

然后是如何查询DB呢,是不是得有个Service去查询,我们依据有自己的UserService了,但是很可惜,spring security有自己的规范,我们自己写的user Service,他不认,气死偶了。

没办法,重新写个Service,我们自己写都写了,也不能不管对不对?嗯,那还是聚合进来。

@Service
@Slf4j
public class UserDetailService implements UserDetailsService {

    @Resource
    UserService userService;

    /**
     * 根据用户名直接从DB中查询用户数据,作为登录校验的依据
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getByUsername(username);
        if (user == null) {
            log.info("username not found");
            throw new UsernameNotFoundException("username not found");
        }
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        return securityUser;
    }

}

核心逻辑就是,我们还是用之前的方法拿到User,但为了适配,就塞到SecurityUser里面去。

UserDetailsService是spring security认可的接口,我们得实现这个接口,并且实现loadUserByUsername方法,这个方法在spring security的认证逻辑里面会用到。目的就是拿到DB中真实的User,跟我们登录的账号密码进行比对。

5、校验密码是否正确

为什么会有这一步呢,因为很多时候我们的密码是要进行加密的,但是我们登录肯定传的是明文密码,所以会需要转换后再去比对,否则肯定是校验失败了。

这个校验密码的逻辑,需要写在spring security的配置类中。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Resource
  UserDetailService userService;
  
   /**
     * 新增security账户
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return rawPassword.equals(encodedPassword);
            }
        });
    }

}

因为我们的项目并没有对密码加密,所以就直接比较了。

在LoginController中,我们用到了AuthenticationManager这个对象,需要在配置类中注册。

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    // 身份验证管理器, 直接继承即可.
    return super.authenticationManagerBean();
}

AuthenticationManager是Spring Security框架中的一个核心接口,它负责处理身份验证请求。在认证过程中,用户提交身份验证信息(如用户名和密码),AuthenticationManager会验证这些信息的有效性。

最后是路由的相关配置

@Override  
    protected void configure(HttpSecurity http) throws Exception {  
        http  
                // 禁用跨站请求伪造保护  
                .csrf().disable()   
                // 设置会话管理策略为无会话,因为我们使用token作为信息传递介质  
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)   
                .and()  
                // 进行认证请求的配置  
                .authorizeRequests()   
                // 将所有登入和注册的接口放开,这些都是无需认证就访问的  
                .antMatchers("/security/login").anonymous()   
                // 除了上面的那些,剩下的任何接口请求都需要经过认证  
                .anyRequest().authenticated()   
                .and()  
                // 允许跨域请求  
                .cors()   
                ;  
        // 在UsernamePasswordAuthenticationFilter之前添加JWT认证过滤器  
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);  
    }

这段代码是Spring Security框架中用于配置HTTP安全的部分。它主要涉及到跨站请求伪造(CSRF)的禁用、会话管理策略的设置、认证请求的配置以及跨域请求的允许。同时,还添加了一个JWT(JSON Web Token)认证过滤器。

因为我们项目用到了jwt,所以在进行账号密码验证之前,要先走jwt的过滤器。

jwt过滤器代码如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    JWTUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求
            //请求放行
            filterChain.doFilter(request, response);
            return;
        }
        //token不为空时, 解析token
        User user = null;
        try {
            user = jwtUtil.verify(token);
        } catch (Exception e) {
            // 过滤器中抛出的异常,无法被统一异常捕获,所以在这里直接返回
            e.printStackTrace();
            Result result = new Result();
            result.setCode(403);
            result.setMsg("Token无效:" + e.getMessage());
            WebUtils.response(response,result);
            return;
        }
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        //将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(securityUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

流程可以参考这个图:

如果校验成功了,那么在登录方法中就会往下走,生成token返回,结束。

6、全局异常返回

认证过程中,难免出现各种异常,我们一般会做一个通用的返回,直接上代码,没啥好说的,网上一大堆

Result

Data
public class Result implements Serializable {

    private int code;
    private String msg;
    private Object data;

    public static Result succ(Object data) {
        return success(200, "操作成功", data);
    }

    public static Result error(String msg) {
        return error(400, msg, null);
    }

    public static Result success (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    public static Result error (int code, String msg, Object data) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
}

GlobalExceptionHandler

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 400 错误:运行时异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) {
        log.error("运行时异常:----------------{}", e.getMessage());
        return Result.error(e.getMessage());
    }

    /**
     * 403 错误:权限不足
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = AccessDeniedException.class)
    public Result handler(AccessDeniedException e) {
        log.info("security权限不足:----------------{}", e.getMessage());
        return Result.error("权限不足");
    }

    /**
     * 400 错误:异常请求-方法参数不匹配
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
        log.info("实体校验异常:----------------{}", e.getMessage());
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.error(objectError.getDefaultMessage());
    }

    /**
     * 400 错误:异常请求-非法参数
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) {
        log.error("Assert异常:----------------{}", e.getMessage());
        return Result.error(e.getMessage());
    }


}

7、测试

localhost:8080/security/login?username=jack&password=1

将token放到header,发送 localhost:8080

能正常返回就OK了。

如果token过期,就会报:

{
    "msg": "Token无效:The Token has expired on Tue Dec 26 15:43:42 CST 2023.",
    "code": 403
}

8、细说spring security

Spring Security 并非一个新生的事物,它最早不叫 Spring Security ,叫 Acegi Security,叫 Acegi Security 并不是说它和 Spring 就没有关系了,它依然是为 Spring 框架提供安全支持的。事实上,Java 领域的框架,很少有框架能够脱离 Spring 框架独立存在。

当 Spring Security 还叫 Acegi Security 的时候,虽然功能也还可以,但是实际上这个东西并没有广泛流行开来。最重要的原因就是它的配置太过于繁琐,当时网上流传一句话:“每当有人要使用 Acegi Security,就会有一个精灵死去。” 足见 Acegi Security 的配置是多么可怕。直到今天,当人们谈起 Spring Security 的时候,依然在吐槽它的配置繁琐。

后来 Acegi Security 投入 Spring 的怀抱,改名叫 Spring Security,事情才慢慢开始发生变化。新的开发团队一直在尽力简化 Spring Security 的配置,Spring Security 的配置相比 Acegi Security 确实简化了很多。但是在最初的几年里,Spring Security 依然无法得到广泛的使用。

直到有一天 Spring Boot 像谜一般出现在江湖边缘,彻底颠覆了 JavaEE 的世界。一人得道鸡犬升天,自从 Spring Boot 火了之后,Spring 家族的产品都被带了一把,Spring Security 就是受益者之一,从此飞上枝头变凤凰。

Spring Boot/Spring Cloud 现在作为 Java 开发领域最最主流的技术栈,这一点大家应该都没有什么异议,而在 Spring Boot/Spring Cloud 中做安全管理,Spring Security 无疑是最方便的。

你想保护 Spring Boot 中的接口,添加一个 Spring Security 的依赖即可,事情就搞定了,所有接口就保护起来了,甚至不需要一行配置。

所以说,因为 Spring Boot/Spring Cloud 火爆,让 Spring Security 跟着沾了一把光。

「有的人觉得 Spring Security 配置臃肿。」

如果是 SSM + Spring Security 的话,我觉得这话有一定道理。

但是如果是 Spring Boot 项目的话,其实并不见得臃肿。Spring Boot 中,通过自动化配置 starter 已经极大的简化了 Spring Security 的配置,我们只需要做少量的定制的就可以实现认证和授权了。

「有人觉得 Spring Security 中概念复杂。」这个是这样的,没错。

Spring Security 由于功能比较多,支持 OAuth2 等原因,就显得比较重量级,不像 Shiro 那样轻便。

但是如果换一个角度,你可能会有不一样的感受。

在 Spring Security 中你会学习到许多安全管理相关的概念,以及常见的安全攻击。这些安全攻击,如果你不是 web 安全方面的专家,很多可能存在的 web 攻击和漏洞你可能很难想到,而 Spring Security 则把这些安全问题都给我们罗列出来并且给出了相应的解决方案。

所以我说,我们学习 Spring Security 的过程,也是在学习 web 安全,各种各样的安全攻击、各种各样的登录方式、各种各样你能想到或者想不到的安全问题,Spring Security 都给我们罗列出来了,并且给出了解决方案,从这个角度来看,你会发现 Spring Security 好像也不是那么让人讨厌。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【HBase】——简介

1 HBase 定义 Apache HBase™ 是以 hdfs 为数据存储的&#xff0c;一种分布式、可扩展的 NoSQL 数据库。 2 HBase 数据模型 • HBase 的设计理念依据 Google 的 BigTable 论文&#xff0c;论文中对于数据模型的首句介绍。 Bigtable 是一个稀疏的、分布式的、持久的多维排序 m…

万字长文谈自动驾驶bev感知(一)

文章目录 prologuepaper listcamera bev :1. Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D2. M2BEV: Multi-Camera Joint 3D Detection and Segmentation with Unified Birds-Eye View Representation3. BEVDet: High-Pe…

Oracle开发经验总结

文章目录 1. 加注释2. 增加索引3. nvl(BOARDCODE&#xff0c;100)>004. 去掉distinct可以避免hash比较&#xff0c;提高性能5. like模糊查询优化(转化为instr()函数)6. SQL计算除数为0时&#xff0c;增加nullif判断7. 分页8. 查看执行计划9. <if test"productCode !…

231227-9步在RHEL8.8配置本地yum源仓库

Seciton 1&#xff1a;参考视频 RHEL8配置本地yum源仓库-安徽迪浮_哔哩哔哩_bilibili Seciton 2&#xff1a;具体操作 &#x1f3af; 第1步&#xff1a;查看光驱文件/dev/sr0是否已经挂载&#xff1f;此处已挂在 [lgklocalhost ~]$ df -h &#x1f3af; 第1步&#xff1a;查看…

SQL语句中的函数和高级语句

目录 SQL语句中的函数 数学函数 聚合函数 字符串函数 SQL语句中的高级语句 SELECT ​编辑 DISTINCT ​编辑 WHERE AND OR IN BETWEEN 通配符 LIKE ORDER BY GROUP BY HAVING 别名 子查询 EXISTS 连接查询 UNION 交集值 无交集值 视图 SQL语句中的函数 数学函数 abs…

[Ray Tracing: The Next Week] 笔记

前言 本篇博客参照自《Ray Tracing: The Next Week》教程&#xff0c;地址为&#xff1a;https://raytracing.github.io/books/RayTracingTheNextWeek.html 该教程在ray tracing in one weekend的基础上&#xff0c;增加了运动模糊、BVH树、Texture映射、柏林噪声、光照、体积…

SOA架构介绍与简单代码示例

1.SOA架构介绍 SOA (Service Oriented Architecture)“面向服务的架构":是一种设计方法&#xff0c;其中包含多个服务&#xff0c;服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用。 微服务架构80%…

开集目标检测-标签提示目标检测大模型(吊打YOLO系列-自动化检测标注)

背景 大多数现有的对象检测模型都经过训练来识别一组有限的预先确定的类别。将新类添加到可识别对象列表中需要收集和标记新数据&#xff0c;并从头开始重新训练模型&#xff0c;这是一个耗时且昂贵的过程。该大模型的目标是开发一个强大的系统来检测由人类语言输入指定的任意…

深入了解Python中文件IO的使用技巧,提高代码处理效率!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Python提供了强大而灵活的文件I/O&#xff08;输入/输出&#xff09;工具&#xff0c;能够读取、写入和处理各种文件类型。本文将深入介绍Python文件I/O的技巧和示例代码&#xff0c;帮助大家更好地理解如何在Py…

Swift 周报 第四十二期

文章目录 前言新闻和社区苹果 CEO 库克透露接班计划&#xff0c;希望继任者来自公司内部消息称苹果自研 5G 调制解调器开发再“难产”&#xff0c;将推迟到 2026 年 提案正在审查的提案 Swift论坛推荐博文话题讨论关于我们 前言 本期是 Swift 编辑组整理周报的第四十二期&…

【Tensor张量】AI模型的信息流通货币

官方解释https://www.tensorflow.org/guide/tensor?hl=zh-cn 1.Tensor的概念理解 如果大模型是一个会运行的城市工厂,那么Tensor就是 运输车! 如果大模型是计算机,那么Tensor就是硬盘。 负责深度学习数据的运输和存储!把数据送给AI模型进行训练,AI模型推理后的数据也…

机器学习中的强学习器:AdaBoost算法详解

目录 1. 引言 2. AdaBoost的基本概念 2.1 弱学习器 2.2 错误率与权重更新 3. AdaBoost的工作流程 3.1 初始化权重 3.2 训练弱学习器 3.3 更新样本权重 3.4 构建强学习器 4. AdaBoost的优缺点 4.1 优点 4.2 缺点 5. 应用场景 5.1 图像识别 5.2 语音处理 5.3 生物…

Hive安装笔记——备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

将下发的ds_db01.sql数据库文件放置mysql中 12、编写Scala代码&#xff0c;使用Spark将MySQL的ds_db01库中表user_info的全量数据抽取到Hive的ods库中表user_info。字段名称、类型不变&#xff0c;同时添加静态分区&#xff0c;分区字段为etl_date&#xff0c;类型为String&am…

Python实现张万森下雪了的效果

写在前面 即将步入婚宴殿堂的女主林北星&#xff0c;遭遇了男友展宇的毁约&#xff0c;生活和工作也变得一团糟。与此同时&#xff0c;她被时光老人带回了十八岁的高三时光&#xff0c;重新开启了自己的人生。林北星摆脱了展宇的束缚&#xff0c;认真准备高考&#xff0c;想要…

【深度学习-目标检测】05 - YOLOv1 论文学习与总结

论文地址&#xff1a;You Only Look Once:Unified, Real-Time Object Detection 论文学习 1. 摘要 YOLO的提出&#xff1a;作者提出了YOLO&#xff0c;这是一种新的目标检测方法。与传统的目标检测方法不同&#xff0c;YOLO将目标检测视为一个回归问题&#xff0c;直接从图像…

【数据结构】六、树和二叉树

目录 一、树的基本概念 二、二叉树 2.1二叉树的性质 2.2二叉树的存储结构 2.3遍历二叉树 先序遍历 中序遍历 后序遍历 层次遍历 2.4二叉树的应用 计算叶子数 前序遍历建树 根据序列恢复二叉树 计算树的深度 判断完全二叉树 三、线索二叉树 3.1线索化 四、树和森林…

redis的搭建及应用(三)-Redis主从配置

Redis主从配置 为提升Redis的高可用性&#xff0c;需要搭建多个Redis集群以保证高可用性。常见搭建方式有&#xff1a;主从&#xff0c;哨兵集群等&#xff0c;本节我们搭建一主二从的多Redis架构。 redis主从安装1主2从的方式配置&#xff0c;以端口号为redis的主从文件夹。 主…

前缀和——OJ题(二)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、和为 k 的子数组1、题目讲解2、思路讲解3、代码实现 二、和可被 K 整除的⼦数组1、题目讲…

office bookmarks

Word2007Util.java-CSDN博客

Linux 安装 Tomcat

1、找到安装包 2、解压缩 tar -xzvf xxx.tar.gz Tomcat 很好安装&#xff0c;已经安装好了 3、启动、关闭Tomcat 进入解压后文件夹之后&#xff0c;两个文件分别是启动和关闭 sh startup.sh // 启动 sh shutdown.sh // 关闭 然后就行了 4、主机连接虚拟机Tomcat 注意这里填写…