第一部分:Spring Boot 2——简化开发的利器

第1章 问题锚定:为什么我们要学Spring Boot?

1.1 场景化抛出问题:传统SSM开发的「至暗时刻」

想象一下,你现在是一家初创电商公司的Java后端开发。老板让你从零搭建一个订单管理系统。如果使用传统的 Spring + Spring MVC + MyBatis(SSM) 框架,你会经历什么?

  1. 复杂的依赖管理:打开Maven的pom.xml,你需要手动引入Spring Core、Spring Context、Spring Web MVC、MyBatis、数据库驱动、连接池(如C3P0)、JSON解析库(Jackson)... 光是找这些依赖的坐标(groupId和artifactId)和兼容版本,就能让你浪费半天时间。更别提稍不注意,引入的Spring Core是4.3,而Spring MVC是5.0,版本冲突导致的NoSuchMethodError会直接让你怀疑人生 。

  2. 地狱般的配置:接下来,你需要创建一堆XML配置文件:

    • applicationContext.xml:配置Spring的注解扫描、数据源DataSource、事务管理器。

    • spring-mvc.xml:配置Spring MVC的注解驱动、视图解析器、静态资源过滤。

    • mybatis-config.xml:配置MyBatis的别名、插件。

    • jdbc.properties:配置数据库连接参数。
      这些配置文件动辄上百行,而且大部分是重复的模板代码。仅仅是配置组件扫描的包路径、数据库连接信息,你就要在多个文件之间来回切换,稍有不慎拼写错误,项目就启动失败。

  3. 漫长的启动与部署:代码写好了,要测试一下?你需要先把项目打成War包,然后复制到Tomcat的webapps目录下,再启动Tomcat。整个过程耗时以分钟计,严重影响开发效率。

  4. 依赖外部容器:你的应用严重依赖于本地安装的Tomcat,如果团队新同学电脑上Tomcat版本不对或者配置有问题,项目又跑不起来了。

这就是传统的Java Web开发之痛:配置繁琐、依赖冲突、部署笨重。我们的大部分精力被消耗在“配置”和“整合”上,而不是真正的“业务逻辑”上。

1.2 技术定位:Spring Boot就是来「收拾残局」的

Spring Boot的核心价值,用一句话就能概括:它通过“习惯优于配置”的理念,让你能够“开箱即用”地快速启动和运行Spring应用。

它就像一个贴心的“自动化工程师”,把那些繁琐的配置工作全部接手了:

  • 启动器(Starter)依赖:你想做Web项目?引入spring-boot-starter-web,它会自动把Spring MVC、内嵌Tomcat、Jackson等所有相关依赖都拉下来,而且版本都经过严格测试,保证兼容。

  • 自动配置(Auto-Configuration):你引入了数据库驱动和Spring Data JPA的Starter?Spring Boot会自动帮你创建DataSourceEntityManagerFactory等核心Bean。你只需要在application.properties里写上数据库连接串即可。

  • 内嵌服务器:你的应用自带Tomcat、Jetty或Undertow服务器。运行一个main方法,就能启动一个Web应用,无需单独安装部署容器。

  • 生产就绪:提供了健康检查、指标度量、外部化配置等生产级功能。

所以,学Spring Boot不是为了学一门新“技术”,而是为了学一种 “简化开发”的解决方案。它能让你告别“配置地狱”,专注于编写业务代码。

1.3 技术边界:它能做什么,不能做什么?
  • 它能做:快速构建独立的、生产级的Spring应用;简化Maven/Gradle配置;实现自动配置;提供非功能性的生产级特性(指标、健康检查)。

  • 它不能做:Spring Boot本身不生成业务代码,不解决分布式事务,不替代数据库。它只是一个脚手架,帮你把房子(Spring生态)的框架搭好,装修(业务逻辑)还得靠你自己。


第2章 基础认知:Spring Boot 2的核心概念与Hello World

2.1 核心概念拆解(大白话版)

在动手写代码前,我们先来认识几个Spring Boot 2.x中最重要的核心概念,这次我们用最通俗的语言来解释。

  • 启动器(Starter):这就像宜家的“套餐包”。你想组装一个沙发(开发Web应用),不用单独去买木板、螺丝、海绵、布料(各种依赖Jar包)。你直接买一个“沙发套餐包”(spring-boot-starter-web),里面所有材料都给你配齐了,而且保证能拼到一起(版本兼容)。

  • 自动配置(Auto-Configuration):这就像一辆“自动驾驶汽车”。你只要坐进驾驶室(引入了相关Starter),汽车会自动帮你调节座椅(创建DataSource)、系好安全带(配置事务管理器)、发动引擎(启动内嵌服务器)。你只需要告诉它目的地(数据库URL、用户名密码)。

  • 内嵌容器:你的应用现在是“自带桌椅”的移动摊位,而不是需要去租用固定的菜市场摊位(外部Tomcat)。你走到哪(任何有JRE的机器),摊子就能在哪开张(java -jar运行)。

  • SpringApplication:这是Spring Boot应用程序的“总导演”。它负责创建和启动Spring容器,管理内嵌服务器,处理命令行参数等一切幕后工作。你只需要在main方法里调用SpringApplication.run(),把指挥权交给它就行。

2.2 最小可运行示例:我们的第一个Spring Boot 2应用

光说不练假把式,我们来创建一个最简版的Spring Boot Web应用。

  1. 创建项目:访问 start.spring.io(Spring官方项目初始化器)。

    • Project:选择 Maven Project

    • Language:Java

    • Spring Boot:选择 2.7.x(这是Spring Boot 2.x的最新稳定版本线)

    • Project Metadata:填上你的组名和项目名,比如 com.example 和 demo

    • Packaging:选择 Jar

    • Java:选择 8 或 11(Spring Boot 2.x完美支持Java 8)

    • Dependencies:点击"ADD DEPENDENCIES",搜索并添加 Spring Web

    • 最后点击 GENERATE,下载一个项目压缩包。

  2. 项目结构一览:解压后用IDEA打开,你会发现项目结构异常清爽。

    text

    demo/
    ├── src/
    │   ├── main/
    │   │   ├── java/
    │   │   │   └── com/example/demo/
    │   │   │       └── DemoApplication.java      // 启动类
    │   │   └── resources/
    │   │       ├── static/                        // 存放静态资源 (css, js, 图片)
    │   │       ├── templates/                     // 存放模板文件 (Thymeleaf等)
    │   │       └── application.properties        // 核心配置文件
    │   └── test/
    │       └── java/
    └── pom.xml                                     // Maven构建文件
  3. 核心代码解析

    • pom.xml:打开它,你会发现核心依赖只有一行:

      xml

      <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.7.0</version>
      </parent>
      
      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
      </dependencies>

      spring-boot-starter-parent 定义了常用依赖的版本号,帮我们做了版本锁定。而 spring-boot-starter-web 就是我们说的“套餐包”,它会传递依赖Spring MVC、内嵌Tomcat、Jackson等所有Web开发所需Jar包。

    • 启动类 DemoApplication.java

      java

      package com.example.demo;
      
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @SpringBootApplication  // 这是一个复合注解,稍后详解
      @RestController          // 表明这是一个控制器,并且所有方法返回JSON
      public class DemoApplication {
      
          public static void main(String[] args) {
              // 启动Spring Boot应用,这行代码启动了整个Spring世界
              SpringApplication.run(DemoApplication.class, args);
          }
      
          @GetMapping("/hello")
          public String hello() {
              return "Hello, Spring Boot 2!";
          }
      }
    • 运行:直接右键运行DemoApplicationmain方法。看到类似如下的日志,就说明启动成功了:

      text

      .   ____          _            __ _ _
      /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
      \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
      =========|_|==============|___/=/_/_/_/
      :: Spring Boot ::                (v2.7.0)
      
      2024-01-01 12:00:00.001  INFO 12345 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http)
      2024-01-01 12:00:00.005  INFO 12345 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.5 seconds (JVM running for 3.1)
    • 测试:打开浏览器,访问 http://localhost:8080/hello,你会看到亲切的 "Hello, Spring Boot 2!"。

就这么简单!没有一行XML配置,没有部署War包,一个Web应用就跑起来了。

2.3 核心原理极简讲解:启动类到底做了什么?

为什么一个main方法加上一个@SpringBootApplication注解就能启动一切?我们不需要深挖底层源码,但必须理解它的核心工作流。

  1. @SpringBootApplication 是个“三合一”复合注解 :

    • @SpringBootConfiguration:本质上是@Configuration,表明这是一个Spring的配置类。

    • @EnableAutoConfiguration:这是自动配置的核心。它负责开启Spring Boot的自动配置功能。它会根据你添加的Starter依赖(比如spring-boot-starter-web),去尝试配置你需要的Bean。它会去读取spring-boot-autoconfigure包下的META-INF/spring.factories文件,里面列出了所有可能的自动配置类(如WebMvcAutoConfigurationDataSourceAutoConfiguration)。然后通过一系列@Conditional条件注解(例如@ConditionalOnClass:判断classpath下是否存在某个类;@ConditionalOnMissingBean:判断当前容器中是否已有某个Bean),来决定是否执行这个自动配置。这就是“智能”的来源 。

    • @ComponentScan:启用组件扫描,默认扫描@SpringBootApplication注解所在类的同级包以及子包。这就是为什么你的Controller放在com.example.demo包下能被扫描到。

  2. SpringApplication.run() 做了两件大事

    • 初始化Spring容器:创建一个AnnotationConfigApplicationContext(或AnnotationConfigServletWebServerApplicationContext)实例,并将启动类(DemoApplication)作为配置类注册进去。

    • 启动内嵌Web服务器:如果检测到Web环境,它会自动启动内嵌的Tomcat服务器,并将Spring容器生成的DispatcherServlet注册到Tomcat中,完成所有配置。


第3章 核心用法拆解:Spring Boot 2的常用注解与资源整合

3.1 常用注解大观园(按场景分类)

Spring Boot虽然简化了配置,但注解依然是我们的主要工具。我们按企业高频场景来拆解这些注解。

场景一:定义和注册Bean(IoC容器)
这是最基础的场景,我们需要把对象交给Spring管理。

注解 使用场景 大白话解释 代码示例
@Component 通用的Spring组件。当你写的类不属于Controller、Service、Repository任何一层时使用。 告诉Spring:“嘿,这个类你帮我管起来,以后我要用就从你这拿。”  @Component public class RedisUtils { ... }
@Service 业务逻辑层(Service层)的注解。 @Component功能一样,只是语义更明确,表示这是“干活的”业务类。  @Service public class UserService { ... }
@Repository 数据访问层(DAO层)的注解。 除了注册Bean,它还额外提供了数据库异常转换的功能(将SQLException转为Spring的DataAccessException)。  @Repository public interface UserMapper { ... }
@Controller MVC控制层(Controller层)的注解。 标记这个类是Web层的控制器,负责接收和响应请求。  @Controller public class UserController { ... }
@Bean 通常用在@Configuration注解的类中的方法上。 告诉Spring,这个方法的返回值是一个Bean,请帮我管理起来。适用于实例化那些无法用@Component注解的第三方类。  @Configuration public class AppConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }

场景二:依赖注入(DI)
对象定义好了,怎么把它们自动装配到我们需要的地方?

注解 使用场景 大白话解释 代码示例
@Autowired 类型自动注入。这是最常用的注入方式。 Spring说:“我看你这个属性需要UserService类型的对象,我容器里正好有一个,给你塞进去。”  @Service public class OrderService { @Autowired private UserService userService; }
@Resource 名称自动注入(JSR-250规范)。 如果Spring容器里有多个同类型的Bean(比如两个DataSource),用@Autowired就不知道该注入谁了。这时可以用@Resource(name = "oracleDataSource")来指定名字。  @Resource(name = "userMapper") private UserMapper userMapper;
@Qualifier 配合@Autowired使用,按名称注入。 当你坚持要用@Autowired,但又想按名字注入时,就拉上@Qualifier这个帮手。  @Autowired @Qualifier("primaryDataSource") private DataSource dataSource;
@Value 注入配置文件中的属性值。 application.properties里的配置项,直接喂给你的变量。  @Value("${server.port}") private Integer serverPort;

场景三:Web开发(Spring MVC)
虽然Spring Boot帮你配好了,但写接口还得靠这些。

注解 使用场景 大白话解释 代码示例
@RestController 组合注解 = @Controller + @ResponseBody 让你的类里所有方法返回的数据,都直接写进HTTP响应体里(通常是JSON),而不是返回一个视图页面。现在前后端分离开发的首选。 @RestController public class ApiController { ... }
@RequestMapping 映射Web请求到某个类或方法上。 告诉浏览器,访问“/user/list”这个地址,就来找我这个方法处理。  @RequestMapping("/user") public class UserController { @RequestMapping(value = "/list", method = RequestMethod.GET) public List<User> list() { ... } }
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping 组合注解,分别是@RequestMapping(method = RequestMethod.GET)等的简写。 遵循RESTful风格,明确告诉别人我这个接口是干什么的(查询、新增、修改、删除)。 @GetMapping("/user/{id}") public User getById(@PathVariable Long id) { ... }
@PathVariable 获取URL路径中的变量。 把URL中{ }里的值,绑定到方法的参数上。 @GetMapping("/user/{id}") public User getById(@PathVariable("id") Long userId) { ... }
@RequestParam 获取请求参数(URL?后面的参数,或者表单数据)。 把请求参数的值,绑定到方法的参数上。 @GetMapping("/user") public User getByName(@RequestParam("name") String userName) { ... }
@RequestBody 获取请求体中的内容,并将其转换为Java对象。 处理前端传来的JSON数据时最常用。 @PostMapping("/user") public User add(@RequestBody User user) { ... }
3.2 资源整合:如何连接数据库和MyBatis?

光有Web层还不够,企业应用必然要操作数据库。我们来看看如何将Spring Boot 2与最主流的持久层框架MyBatis整合。

Step 1:引入Starter依赖
pom.xml中添加:

xml

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

注意:MyBatis官方也提供了自己的Starter,它会自动帮你配置SqlSessionFactorySqlSessionTemplate

Step 2:配置数据源和MyBatis
application.properties(或application.yml)中添加配置:

properties

# 数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# MyBatis配置
# 指定实体类包路径,为SQL映射文件中的resultType提供别名
mybatis.type-aliases-package=com.example.demo.model
# 指定MyBatis的XML映射文件位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 开启驼峰命名自动映射 (比如数据库字段user_name 映射为 userName)
mybatis.configuration.map-underscore-to-camel-case=true

Step 3:编写代码

  1. 实体类 (Model):在com.example.demo.model包下创建User.java

  2. Mapper接口:在com.example.demo.mapper包下创建UserMapper.java

    java

    package com.example.demo.mapper;
    
    import com.example.demo.model.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    @Mapper // 告诉MyBatis这是一个Mapper接口
    public interface UserMapper {
    
        @Select("SELECT * FROM user WHERE id = #{id}")
        User findById(Long id);
    
        // 复杂的SQL可以写在XML映射文件里
        List<User> findAll();
    }
  3. Mapper XML文件:在resources/mapper/目录下创建UserMapper.xml

    xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.example.demo.mapper.UserMapper">
        <select id="findAll" resultType="com.example.demo.model.User">
            SELECT * FROM user
        </select>
    </mapper>
  4. Service层:注入Mapper,编写业务逻辑。

  5. Controller层:注入Service,提供REST API。

Step 4:关键一步 - 扫描Mapper
你需要在启动类上加上@MapperScan注解,告诉Spring去哪里找MyBatis的Mapper接口。

java

@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 指定Mapper接口所在的包
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

或者你也可以在每个Mapper接口上单独加@Mapper注解(如上面的例子),但在实际项目中,使用@MapperScan更简洁。

至此,一个完整的、可运行的Spring Boot 2 + MyBatis + MySQL的Web应用就搭建完成了。

3.3 高级特性:本地热部署,开发效率翻倍

在开发过程中,每次修改代码都要手动重启应用,非常浪费时间。Spring Boot提供了本地热部署能力,让你修改代码后,应用自动重启(或自动加载修改)。

实现方式:引入一个开发工具依赖即可。

xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

工作原理devtools 嵌在应用里。当它检测到classpath下的文件发生变化时,会自动重启应用。但与手动冷启动不同,它利用了两个类加载器:那些不会频繁变动的第三方Jar(如Spring框架本身)由基础类加载器(Base ClassLoader)加载,而我们自己编写的代码由重启类加载器(Restart ClassLoader)加载。当代码发生变化时,只抛弃并重新创建重启类加载器,这比重启整个JVM要快得多。

常见坑点

  • 生效范围:修改Java类文件或配置文件,触发重启。修改静态资源(如HTML、JS)一般不会触发重启,因为不需要重启服务器。

  • 与IDE配合:IDE需要开启自动编译功能(如IDEA中:Build -> Build Project 或 Ctrl + F9 手动触发编译,devtools检测到变化后就会重启)。

  • 生产环境禁用devtools 会通过spring.devtools.restart.enabled参数在生产环境自动禁用,或者你可以在启动时显式设置-Dspring.devtools.restart.enabled=false,确保生产环境稳定性。


第4章 场景融合:在业务流程中看Spring Boot

Spring Boot不是一个孤立的技术,它是整个Spring生态的底座。我们来串联一个最简单的“用户注册”流程,看看注解们是如何协作的。

业务流程图

text

用户请求(POST /user/register) 
    → [Controller层] 
        → 调用 [Service层] 
            → 调用 [DAO/Mapper层] 
                → 数据库插入成功 
            ← 返回结果
        ← 封装注册结果
    ← 返回JSON响应给前端

代码协作实现

  1. Controller层:接收请求,不处理任何业务逻辑,只是参数校验和响应封装。

    java

    @RestController // 1. 标记为控制器,返回JSON
    @RequestMapping("/user") // 2. 类级别请求映射
    public class UserController {
    
        @Autowired // 3. 注入Service
        private UserService userService;
    
        @PostMapping("/register") // 4. 方法级别POST映射
        public Result<String> register(@RequestBody UserRegisterDto dto) { // 5. 接收JSON参数
            // 可能做简单的参数校验
            if (dto.getUsername() == null || dto.getUsername().length() < 6) {
                return Result.error("用户名至少6位");
            }
            // 调用Service执行业务
            userService.register(dto);
            return Result.success("注册成功");
        }
    }
  2. Service层:处理核心业务逻辑(如密码加密、检查用户是否存在)。

    java

    @Service // 6. 标记为业务组件
    public class UserService {
    
        @Autowired // 7. 注入Mapper
        private UserMapper userMapper;
    
        @Autowired
        private PasswordEncoder passwordEncoder; // 用于加密密码
    
        @Transactional // 8. 事务注解,保证数据库操作原子性
        public void register(UserRegisterDto dto) {
            // 检查用户是否存在
            User existUser = userMapper.findByUsername(dto.getUsername());
            if (existUser != null) {
                throw new BusinessException("用户名已存在");
            }
            // 创建新用户对象
            User newUser = new User();
            newUser.setUsername(dto.getUsername());
            // 密码加密后存储
            newUser.setPassword(passwordEncoder.encode(dto.getPassword()));
            newUser.setEmail(dto.getEmail());
            // 调用Mapper插入数据库
            userMapper.insert(newUser);
        }
    }
  3. Mapper层:与数据库交互。

    java

    @Mapper // 9. 标记为Mapper接口
    public interface UserMapper {
        User findByUsername(@Param("username") String username);
        int insert(User user);
    }

在这个流程中,Spring Boot的自动配置确保了@Transactional生效、PasswordEncoder有实例、UserMapper能被注入。所有的注解各司其职,通过Spring IoC容器这个“大脑”串联起来,完成了一个完整的企业级请求处理。

4.1 技术选型对比:为什么选MyBatis-Plus而不是MyBatis?

在实际项目中,原生MyBatis还需要写很多简单的CRUD SQL,比较繁琐。现在越来越多的公司选择 MyBatis-Plus(简称MP),它是在MyBatis基础上的一个增强工具。

  • 选型场景:当你的项目中有大量的单表CRUD操作,不想写繁琐的XML和SQL时。

  • 优势

    • 启动即用:引入mybatis-plus-boot-starter,自动集成。

    • 万能CRUD:提供BaseMapper,自带insertupdateByIdselectList等方法,无需写SQL。

    • 条件构造器:通过LambdaQueryWrapper可以链式构建复杂查询条件,避免在代码里拼接SQL。

    • 分页插件:内置分页插件,简单配置即可用。

  • 劣势:对于极其复杂的多表联合查询,最终还是需要手写SQL,但MP也支持。


第5章 企业级实战:构建一个极简的商品管理系统

理论说再多,不如动手写一个。我们以一个极简商品管理系统的后端API为例,实践Spring Boot 2的企业开发规范。

5.1 项目准备
  • 需求:实现对商品的CRUD操作,商品信息包含ID、名称、价格、库存、创建时间。

  • 技术栈:Spring Boot 2.7.x + MyBatis-Plus + MySQL + Maven

  • 开发规范:严格分层、自定义异常、全局统一返回、日志打印。

5.2 代码结构与规范落地
  1. 项目分层

    text

    src/main/java/com/example/demo
    ├── DemoApplication.java                 // 启动类
    ├── common/                               // 通用模块
    │   ├── config/                            // 配置类
    │   ├── exception/                          // 异常处理
    │   ├── result/                              // 统一返回结果
    │   └── constant/                           // 常量
    ├── modules/                               // 功能模块
    │   └── product/                            // 商品模块
    │       ├── controller/                        // Controller层
    │       ├── service/                           // Service层
    │       │   ├── impl/                             // Service实现类
    │       │   └── ProductService.java
    │       ├── mapper/                           // Mapper层
    │       │   └── ProductMapper.java
    │       ├── model/                            // 模型
    │       │   ├── entity/                          // 实体类
    │       │   ├── dto/                             // 数据传输对象 (Request/Response)
    │       │   └── vo/                              // 视图对象 (可选)
    │       └── controller/                        // Controller
    └── resources/
        ├── application.yml                     // YAML格式配置文件
        ├── mapper/                              // MyBatis XML文件存放处
        └── logback-spring.xml                  // 日志配置
  2. 统一返回结果 (Result.java)

    java

    package com.example.demo.common.result;
    
    import lombok.Data;
    
    @Data
    public class Result<T> {
        private Integer code; // 编码:200成功,其他为失败
        private String msg;  // 消息
        private T data;      // 数据
    
        public static <T> Result<T> success(T data) {
            Result<T> result = new Result<>();
            result.setCode(200);
            result.setMsg("成功");
            result.setData(data);
            return result;
        }
    
        public static <T> Result<T> error(String msg) {
            Result<T> result = new Result<>();
            result.setCode(500);
            result.setMsg(msg);
            return result;
        }
    
        public static <T> Result<T> error(Integer code, String msg) {
            Result<T> result = new Result<>();
            result.setCode(code);
            result.setMsg(msg);
            return result;
        }
    }
  3. 自定义业务异常 (BusinessException.java)

    java

    package com.example.demo.common.exception;
    
    public class BusinessException extends RuntimeException {
        private Integer code;
    
        public BusinessException(String message) {
            super(message);
            this.code = 500;
        }
    
        public BusinessException(Integer code, String message) {
            super(message);
            this.code = code;
        }
    
        public Integer getCode() {
            return code;
        }
    }
  4. 全局异常处理器 (GlobalExceptionHandler.java)

    java

    package com.example.demo.common.exception;
    
    import com.example.demo.common.result.Result;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @Slf4j
    @RestControllerAdvice // 拦截所有Controller的异常
    public class GlobalExceptionHandler {
    
        // 处理自定义的业务异常
        @ExceptionHandler(BusinessException.class)
        public Result<String> handleBusinessException(BusinessException e) {
            log.error("业务异常:{}", e.getMessage(), e);
            return Result.error(e.getCode(), e.getMessage());
        }
    
        // 处理其他所有未捕获的异常
        @ExceptionHandler(Exception.class)
        public Result<String> handleException(Exception e) {
            log.error("系统异常:{}", e.getMessage(), e);
            return Result.error("系统繁忙,请稍后重试");
        }
    }
  5. 商品实体类 (Product.java)

    java

    package com.example.demo.modules.product.model.entity;
    
    import com.baomidou.mybatisplus.annotation.*;
    import lombok.Data;
    import java.math.BigDecimal;
    import java.util.Date;
    
    @Data
    @TableName("product") // 指定数据库表名
    public class Product {
        @TableId(type = IdType.AUTO) // 主键自增
        private Long id;
        private String name;
        private BigDecimal price;
        private Integer stock;
        @TableField(fill = FieldFill.INSERT) // 插入时自动填充
        private Date createTime;
        @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
        private Date updateTime;
    }
  6. Mapper层 (ProductMapper.java)

    java

    package com.example.demo.modules.product.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.example.demo.modules.product.model.entity.Product;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface ProductMapper extends BaseMapper<Product> {
        // 继承BaseMapper,就拥有了基础的CRUD方法,非常方便
    }
  7. Service层 (ProductService.java 及其实现)

    java

    // --- ProductService.java (接口) ---
    package com.example.demo.modules.product.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.example.demo.modules.product.model.entity.Product;
    import com.example.demo.modules.product.model.dto.ProductSaveDto;
    import com.example.demo.modules.product.model.dto.ProductUpdateDto;
    
    public interface ProductService extends IService<Product> {
        // 可以在这里定义特殊的业务方法
        Long addProduct(ProductSaveDto dto);
        void updateProduct(ProductUpdateDto dto);
    }
    
    // --- ProductServiceImpl.java (实现类) ---
    package com.example.demo.modules.product.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.example.demo.common.exception.BusinessException;
    import com.example.demo.modules.product.mapper.ProductMapper;
    import com.example.demo.modules.product.model.entity.Product;
    import com.example.demo.modules.product.model.dto.ProductSaveDto;
    import com.example.demo.modules.product.model.dto.ProductUpdateDto;
    import com.example.demo.modules.product.service.ProductService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.BeanUtils;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Slf4j
    @Service
    public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
    
        @Override
        @Transactional // 事务,如果保存失败则回滚
        public Long addProduct(ProductSaveDto dto) {
            log.info("开始新增商品: {}", dto.getName());
            // 参数校验:名称不能重复
            Product existProduct = lambdaQuery().eq(Product::getName, dto.getName()).one();
            if (existProduct != null) {
                throw new BusinessException("商品名称已存在");
            }
            Product product = new Product();
            BeanUtils.copyProperties(dto, product);
            // 保存到数据库
            this.save(product);
            log.info("商品新增成功, ID: {}", product.getId());
            return product.getId();
        }
    
        @Override
        @Transactional
        public void updateProduct(ProductUpdateDto dto) {
            log.info("开始更新商品, ID: {}", dto.getId());
            Product product = this.getById(dto.getId());
            if (product == null) {
                throw new BusinessException("商品不存在");
            }
            // 如果更新了名称,需要检查是否与其他商品冲突
            if (dto.getName() != null && !dto.getName().equals(product.getName())) {
                Product existProduct = lambdaQuery().eq(Product::getName, dto.getName()).one();
                if (existProduct != null) {
                    throw new BusinessException("商品名称已存在");
                }
            }
            BeanUtils.copyProperties(dto, product);
            this.updateById(product);
            log.info("商品更新成功, ID: {}", dto.getId());
        }
    }
  8. Controller层 (ProductController.java)

    java

    package com.example.demo.modules.product.controller;
    
    import com.example.demo.common.result.Result;
    import com.example.demo.modules.product.model.entity.Product;
    import com.example.demo.modules.product.model.dto.ProductSaveDto;
    import com.example.demo.modules.product.model.dto.ProductUpdateDto;
    import com.example.demo.modules.product.service.ProductService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import javax.validation.Valid; // 引入JSR-303校验
    import java.util.List;
    
    @RestController
    @RequestMapping("/product")
    public class ProductController {
    
        @Autowired
        private ProductService productService;
    
        @PostMapping
        public Result<Long> add(@Valid @RequestBody ProductSaveDto dto) {
            Long id = productService.addProduct(dto);
            return Result.success(id);
        }
    
        @PutMapping
        public Result<String> update(@Valid @RequestBody ProductUpdateDto dto) {
            productService.updateProduct(dto);
            return Result.success("更新成功");
        }
    
        @DeleteMapping("/{id}")
        public Result<String> delete(@PathVariable Long id) {
            productService.removeById(id);
            return Result.success("删除成功");
        }
    
        @GetMapping("/{id}")
        public Result<Product> getById(@PathVariable Long id) {
            Product product = productService.getById(id);
            return Result.success(product);
        }
    
        @GetMapping("/list")
        public Result<List<Product>> list() {
            List<Product> list = productService.list();
            return Result.success(list);
        }
    }
5.3 测试与部署
  1. 单元测试:可以使用JUnit 5和MockMvc来测试Controller层。

    java

    @SpringBootTest
    @AutoConfigureMockMvc
    public class ProductControllerTest {
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        public void testAddProduct() throws Exception {
            String json = "{\"name\":\"测试商品\",\"price\":99.9,\"stock\":100}";
            mockMvc.perform(post("/product")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(json))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200));
        }
    }
  2. 打包部署

    • 在项目根目录执行 mvn clean package,会在target目录下生成一个可执行的Jar文件(如 demo-0.0.1-SNAPSHOT.jar)。

    • 部署到服务器:java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod (通过--可以覆盖配置文件中的参数)。


第6章 复盘升华:如何用好Spring Boot 2?

6.1 最佳实践总结
  1. 理解@SpringBootApplication的包扫描范围:启动类应放在根包下(如com.example.demo),这样它的子包都能被默认扫描到。如果其他模块不在子包下,需要显式添加@ComponentScan

  2. 合理使用YAML配置文件application.yml 比 properties 文件结构更清晰,支持多环境配置(---分隔)。

    yaml

    spring:
      profiles: dev
    server:
      port: 8080
    ---
    spring:
      profiles: prod
    server:
      port: 80
  3. 日志规范:永远使用private static final Logger log = LoggerFactory.getLogger(Xxx.class); 或 Lombok的@Slf4j,不要用System.out.println。配置logback-spring.xml实现日志滚动、分级存储。

  4. 配置分离:敏感信息(如数据库密码、API密钥)不要硬编码在配置文件里,通过环境变量或外部配置中心传入。例如:spring.datasource.password=${DB_PASSWORD:root},其中DB_PASSWORD为环境变量,:root为默认值。

  5. 自定义Starter:如果你公司有公共的中间件(如统一的Redis客户端、权限校验组件),可以封装成一个自定义Starter,内部包含自动配置,其他项目直接引入依赖即可使用,这是Spring Boot“简化开发”思想的高级体现。

6.2 技术演进:从Spring Boot 2到Spring Boot 3

截至2024年,Spring Boot 2.x官方已停止部分版本的维护,Spring Boot 3.x已成为主流。如果你是新项目,建议直接上Spring Boot 3 。

  • Java 17 基础:Spring Boot 3 强制要求 Java 17,可以利用recordtext block等新特性简化代码 。

  • Jakarta EE 9+:包名从 javax.* 迁移到了 jakarta.*。如果你的代码里直接使用了javax.servletjavax.persistence等,升级时需要全局替换 。

  • AOT与原生镜像:Spring Boot 3 支持通过GraalVM将应用编译成原生可执行文件,实现“瞬时启动”和更低的内存占用,非常适合云原生和Serverless场景。

6.3 面试/工作高频问题
  1. 问:Spring Boot自动配置的原理是什么?
    :启动类上的@EnableAutoConfiguration注解会导入AutoConfigurationImportSelector类,该类会扫描所有jar包下的META-INF/spring.factories文件,获取所有自动配置类的全限定名。然后根据一系列@Conditional条件注解(如@ConditionalOnClass@ConditionalOnBean)来决定这些配置类是否生效,从而自动配置相应的Bean。

  2. 问:Spring Boot的Starter是什么?如何自定义一个Starter?
    :Starter是一组方便的依赖描述符,它聚合了某个功能所需的所有依赖,并提供了自动配置的入口。自定义Starter需要创建一个自动配置模块,通常包括:一个自动配置类(标注@Configuration@Conditional)、一个用于配置属性的@ConfigurationProperties类,并在META-INF/spring.factories中注册自动配置类。

  3. 问:Spring Boot 2和Spring Boot 3的主要区别有哪些?
    :主要区别在于:Java基线提升至17;Java EE迁移到Jakarta EE 9+,导致包名变更;对原生镜像的支持(Spring Native演变为Spring Boot 3原生支持);部分API的弃用和移除(如WebSecurityConfigurerAdapter)。


第二部分:Spring Security——企业级安全与权限管理

第1章 问题锚定:为什么要引入专门的安全框架?

1.1 场景化抛出问题:手写权限控制的噩梦

想象一下,你的电商系统上线了,一切运转良好。但老板突然提出几个需求:

  1. 用户认证:“只有登录过的用户才能下单,没登录的跳转到登录页。”

  2. 权限管理:“运营人员可以查看订单,但不能修改价格;管理员什么都能干。”

你会怎么做?可能第一反应是:在每个Controller方法里加if判断?

java

@PostMapping("/order")
public Result<String> createOrder(@RequestBody OrderDto dto, HttpSession session) {
    // 1. 检查是否登录
    User user = (User) session.getAttribute("user");
    if (user == null) {
        return Result.error("请先登录");
    }

    // 2. 如果是修改价格的请求,检查是不是管理员
    if (dto.getPrice() != null && !"ADMIN".equals(user.getRole())) {
        return Result.error("无权修改价格");
    }

    // 3. 业务逻辑...
    return Result.success("下单成功");
}

如果你有100个接口需要做权限控制,难道要写100遍这种重复的if-else吗?这简直就是代码灾难

更可怕的是,你还需要考虑:

  • 密码安全:密码能明文存数据库吗?不能!必须加密。

  • 会话安全:如何防止会话固定攻击?如何防止CSRF攻击?

  • 第三方登录:如何接入微信/QQ登录?如何实现OAuth2?

  • 前后端分离:如果是JWT无状态登录,又该怎么玩?

如果你自己手写一套,不仅要处理业务,还要考虑这些复杂的安全细节,项目可能半年都上不了线。

1.2 技术定位:Spring Security就是你的「安全保镖」

Spring Security是一个功能强大且高度可定制的身份认证和访问控制框架。它的核心使命就是解决应用安全问题,让你专注于业务逻辑,把安全相关的“脏活累活”都交给它 。

  • 认证(Authentication):帮你解决“你是谁?”的问题。它支持表单登录、HTTP Basic、OAuth 2.0、JWT、LDAP等几乎所有你能想到的认证方式。

  • 授权(Authorization):帮你解决“你能干什么?”的问题。它支持基于URL的授权、基于方法的授权,甚至可以精细到某个对象实例的权限控制(ACL)。

  • 安全防护:内置防御常见网络攻击,如CSRF(跨站请求伪造)、会话固定、点击劫持等 。

1.3 技术边界:Spring Security vs. Shiro

提到安全框架,不得不提另一个轻量级选手——Apache Shiro 。

对比维度 Spring Security Apache Shiro
优点 与Spring生态无缝集成,功能极其强大,社区活跃,文档丰富,是事实上的企业级标准。 轻量级,简单易用,学习曲线平缓,不依赖Spring,可在任何Java应用中使用。
缺点 学习曲线陡峭,配置相对复杂(Spring Boot后已简化很多)。 功能相对较少,微服务/云原生支持较弱,安全维护更新不如Spring Security频繁。
选型建议 首选。尤其如果你的项目是Spring Boot/Cloud,无需犹豫,Spring Security是集成度最高、未来演进最顺畅的选择。 适用于非Spring体系的小型项目,或对框架大小有极致要求的老项目维护。

第2章 基础认知:Spring Security的核心概念与Hello World

2.1 核心概念拆解(大白话版)

Spring Security的架构有些复杂,但我们可以把它的核心组件想象成一个“机场安检系统”。

  • 你(Subject):拿着机票,准备登机的旅客。

  • 护照/身份证(Authentication Token):你的身份凭证,可能是用户名密码,也可能是JWT令牌。

  • 安检口(AuthenticationManager):负责核验你身份的机构。它看你的证件(调用认证逻辑),判断你是不是本人。

  • 安检员(AuthenticationProvider):真正干活的人。一个安检口可以有多个安检员,有的查人脸,有的查指纹。

  • 登机牌(GrantedAuthority):认证通过后,发给你的权限凭证,上面写着你的“角色”或“权限”,比如“头等舱旅客”或“VIP休息室权限”。

  • 闸机口(FilterSecurityInterceptor):通往各个登机口的关卡。闸机会扫描你的登机牌,判断你是否有资格进入这个登机口(访问某个URL)。

  • 安检流程(Filter Chain):整个安检过程由一系列安检环节组成:先查身份证,再查行李,最后搜身。Spring Security里就是一系列 Filter 构成的链条 。

2.2 最小可运行示例:Spring Boot 2 + Spring Security的“Hello World”

我们继续在刚才的Spring Boot 2项目基础上,整合Spring Security。

Step 1:引入Starter依赖

xml

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

就这?对,就这一行!

Step 2:重启应用,见证奇迹
什么都不用配置,重启你的DemoApplication。启动日志里会看到这样一行:

text

Using generated security password: 7c6a3f9e-9a7d-4b3a-8f1e-2d5c6a7b8c9d

Spring Security为你自动生成了一个密码,用户名是默认的 user

Step 3:访问测试

  • 打开浏览器,访问 http://localhost:8080/hello(之前我们写的接口)。

  • 神奇的事情发生了!你没有直接看到"Hello"消息,而是被重定向到了一个默认的登录页面http://localhost:8080/login)。

  • 输入用户名 user 和刚才日志里的随机密码,点击登录。成功跳转回 /hello,看到了我们的问候语。

这就是Spring Security最基础的自动防护:它默认拦截了所有请求(除了登录页本身),要求用户必须通过表单登录认证后才能访问。

2.3 核心原理极简讲解:过滤器链

Spring Security为什么能“凭空”拦截所有请求?答案就是 Servlet过滤器(Filter) 。

当你在项目中引入了Spring Security,它会动态配置一个过滤器链,这个链位于你的应用过滤器(如CharacterEncodingFilter)和Spring MVC的DispatcherServlet之间。

一个典型的Spring Security过滤器链顺序(简化版)如下:

  1. SecurityContextPersistenceFilter:在请求开始时,从Session(或别的存储)中获取用户信息,放到SecurityContextHolder里;请求结束后,清理SecurityContextHolder

  2. UsernamePasswordAuthenticationFilter:拦截POST /login请求,尝试从请求中提取用户名密码,并进行认证。

  3. BasicAuthenticationFilter:处理HTTP Basic认证。

  4. ExceptionTranslationFilter:捕获过滤器链中抛出的任何认证/授权异常,并决定是重定向到登录页,还是返回401/403错误 。

  5. FilterSecurityInterceptor:位于过滤器链的最底部,它根据你配置的授权规则(如 /admin/** 需要ROLE_ADMIN),判断当前用户是否有权限访问该资源。如果无权,会抛出AccessDeniedException 。

所有请求都必须通过这个过滤器链的“安检”,才能到达你的Controller。这就是Spring Security实现安全控制的底层架构。


第3章 核心用法拆解:用户认证与权限管理

3.1 用户认证:从内存到数据库

默认的用户名和随机密码显然不能用在实际项目中。我们需要将用户信息存储在数据库中。

3.1.1 核心接口:UserDetailsService
Spring Security提供了一个核心接口 UserDetailsService,它只有一个方法:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException。它的作用就是“根据用户名加载用户信息”。

要实现数据库认证,我们只需要自己实现这个接口,告诉Spring Security如何从数据库里查用户。

3.1.2 代码实现

  1. 引入依赖(假设我们依然用MyBatis-Plus操作数据库)。

  2. 定义实体类 User(需实现 UserDetails 接口,或单独创建 SecurityUser 类)。

    java

    // 简单做法:创建专门的类包装用户认证信息
    public class SecurityUser implements UserDetails {
    
        private Long id;
        private String username;
        private String password;
        private List<SimpleGrantedAuthority> authorities;
        private boolean enabled;
    
        public SecurityUser(User user) {
            this.id = user.getId();
            this.username = user.getUsername();
            this.password = user.getPassword();
            this.enabled = user.getStatus() == 1; // 假设1表示启用
            // 将数据库中的权限字符串(如 "ROLE_ADMIN")转换成GrantedAuthority对象
            this.authorities = user.getRoles().stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    }
  3. 实现 UserDetailsService

    java

    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
        @Autowired
        private UserMapper userMapper; // 操作数据库的Mapper
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1. 从数据库查询用户
            User user = userMapper.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
            // 2. 将查到的用户信息包装成 Spring Security 认识的 UserDetails 对象
            //    注意:这里的密码必须是已经加密过的
            return new SecurityUser(user);
        }
    }
  4. 配置密码加密器 PasswordEncoder
    绝对不能明文存储密码!Spring Security 强制要求使用 PasswordEncoder

    java

    @Configuration
    @EnableWebSecurity // 开启Web安全支持
    public class SecurityConfig {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            // BCrypt 是一种强哈希算法,每次加密结果都不同,但可以验证。
            // 这是目前最主流的密码加密方式。
            return new BCryptPasswordEncoder();
        }
    
        // ... 其他配置
    }
  5. 修改数据库存储:在用户注册时,使用PasswordEncoder对明文密码加密后再存入数据库。

    java

    @Service
    public class UserService {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        public void register(UserRegisterDto dto) {
            User user = new User();
            user.setUsername(dto.getUsername());
            // 加密密码
            user.setPassword(passwordEncoder.encode(dto.getPassword()));
            userMapper.insert(user);
        }
    }

至此,认证逻辑就完全切换到数据库了。当用户登录时,CustomUserDetailsService会从数据库加载用户,BCryptPasswordEncoder会验证登录密码和数据库里的密文是否匹配。

3.2 权限管理:基于角色的URL访问控制

用户能登录了,接下来要控制不同的人访问不同的资源。

3.2.1 配置 SecurityFilterChain(Spring Security 5+ / Spring Boot 2.7+ 新写法)
在较新版本的Spring Boot 2中,官方推荐使用基于组件的安全配置方式,而不是继承过时的 WebSecurityConfigurerAdapter

java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                // 1. 放行静态资源和公开API
                .antMatchers("/css/**", "/js/**", "/images/**", "/webjars/**", "/public/**").permitAll()
                // 2. 需要特定角色的URL
                .antMatchers("/admin/**").hasRole("ADMIN") // 注意:hasRole会自动加上 "ROLE_" 前缀
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 有USER或ADMIN角色都可以
                // 3. 基于权限的细粒度控制
                .antMatchers("/order/**").hasAuthority("ORDER_READ") // 需要有ORDER_READ权限
                // 4. 除了以上规则,所有请求都需要认证
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login") // 自定义登录页
                .loginProcessingUrl("/doLogin") // 登录表单提交的URL
                .defaultSuccessUrl("/") // 登录成功后的默认跳转
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            )
            .csrf(csrf -> csrf.disable()) // 开发REST API时通常禁用CSRF
            .httpBasic(withDefaults()); // 启用HTTP Basic

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 注入我们自定义的UserDetailsService
    @Bean
    public UserDetailsService userDetailsService() {
        return new CustomUserDetailsService();
    }
}

关键点解析

  • .hasRole("ADMIN") 等价于检查权限标识符 ROLE_ADMIN

  • .hasAuthority("ORDER_READ") 直接检查权限字符串 ORDER_READ

  • antMatchers的顺序很重要,精确的规则应放在前面,通用的(.anyRequest())放在最后 。

3.2.2 在 UserDetailsService 中赋予角色/权限
在 CustomUserDetailsService 的 loadUserByUsername 方法中,我们从数据库查出用户对应的角色或权限列表,并封装到 SecurityUser 的 getAuthorities() 方法里。

java

// 假设我们从数据库查出该用户有两个角色和一个权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
authorities.add(new SimpleGrantedAuthority("ORDER_READ"));

return new SecurityUser(user, authorities);
3.3 方法级安全:更精细的控制

有时候,同一个URL下的不同操作,权限可能不一样(例如:GET /order 查看所有订单,POST /order 创建订单)。这时候,基于URL的配置可能不够灵活,我们可以使用方法级安全注解 。

Step 1:开启方法级安全
在任意配置类上添加 @EnableGlobalMethodSecurity 注解。

java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用@PreAuthorize等注解
public class SecurityConfig {
    // ...
}

Step 2:在Service或Controller方法上使用注解

java

@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping
    @PreAuthorize("hasRole('ADMIN') or hasAuthority('ORDER_READ')")
    public List<Order> list() {
        // 管理员或有ORDER_READ权限的用户可以查看所有订单
        return orderService.list();
    }

    @PostMapping
    @PreAuthorize("hasAuthority('ORDER_CREATE')")
    public Order create(@RequestBody Order order) {
        // 只有拥有ORDER_CREATE权限的用户才能创建订单
        return orderService.create(order);
    }

    @GetMapping("/my")
    @PreAuthorize("isAuthenticated()") // 只要登录就能访问
    public List<Order> myOrders() {
        // 获取当前登录用户的信息
        SecurityUser user = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return orderService.findByUserId(user.getId());
    }

    @GetMapping("/{id}")
    @PreAuthorize("@orderSecurity.check(#id)") // 甚至可以使用SpEL调用自定义Bean的方法进行复杂校验
    public Order getById(@PathVariable Long id) {
        return orderService.getById(id);
    }
}

常用表达式 :

  • hasRole(‘ADMIN‘):是否有ADMIN角色。

  • hasAuthority(‘READ‘):是否有READ权限。

  • isAuthenticated():是否已认证(登录)。

  • permitAll():所有人都可访问。

  • #id == principal.id:SpEL表达式,可用于校验当前用户是否是资源的拥有者。

3.4 常见坑点演示
  1. 坑点:UserDetailsService 返回的User密码为空或未加密,导致一直提示“Bad credentials”
    解决:确保从数据库加载的密码是已经通过PasswordEncoder加密过的密文,并且UserDetails实现类正确返回了密码。

  2. 坑点:自定义登录页面提交后,一直403
    解决:通常是CSRF防护没关导致的。如果决定前后端分离开发,应在配置中显式禁用CSRF:http.csrf().disable()

  3. 坑点:使用hasRole(‘ADMIN‘)时,数据库存储的权限必须是ROLE_ADMIN,而不是ADMIN
    解决:Spring Security会自动在hasRole传入的字符串前加上ROLE_前缀。如果你存储的是ADMIN,那么实际期望的权限是ROLE_ADMIN


第4章 场景融合:在业务流程中看Spring Security

4.1 业务流程串联:前后端分离下的JWT认证

现在主流的开发模式是前后端分离,后端提供REST API,前端是Vue/React。在这种模式下,Session机制不再适用,JWT(JSON Web Token) 成为了更流行的无状态认证方案。

业务流程

  1. 用户提交用户名密码到 /authenticate

  2. 服务器验证成功,生成一个包含用户信息(如用户名、角色)的JWT令牌,返回给前端。

  3. 前端将JWT存储在LocalStorage或Cookie中。

  4. 前端每次请求API时,在HTTP Header的 Authorization: Bearer <token> 中带上JWT。

  5. 服务器端有一个JWT认证过滤器,它会拦截请求,解析并验证JWT。如果合法,就根据JWT里的信息创建Authentication对象,存入SecurityContextHolder,代表用户已登录。

  6. 后续的过滤器(如FilterSecurityInterceptor)就会认为该请求已认证,并根据JWT中的角色/权限信息进行授权判断。

关键代码:自定义JWT认证过滤器

java

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider; // 自己封装的JWT生成和解析工具类

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = resolveToken(request);
        if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
            // 解析JWT,获取用户信息
            Authentication auth = tokenProvider.getAuthentication(token);
            // 将认证信息存入SecurityContext
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

修改Security配置:在过滤器链中,把这个自定义过滤器添加到UsernamePasswordAuthenticationFilter之前。

java

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
        .and()
        .authorizeRequests()
            .antMatchers("/authenticate").permitAll()
            .anyRequest().authenticated()
        .and()
        .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

第5章 企业级实战:为商品管理系统加上JWT安全认证

我们在之前商品管理系统的基础上,整合Spring Security + JWT,实现安全的REST API。

5.1 新增依赖

pom.xml中添加JWT相关库(以jjwt为例):

xml

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
5.2 创建JWT工具类 JwtTokenProvider

java

@Component
public class JwtTokenProvider {

    @Value("${app.jwt.secret}")
    private String jwtSecret;

    @Value("${app.jwt.expiration}")
    private int jwtExpiration;

    public String generateToken(Authentication authentication) {
        SecurityUser userPrincipal = (SecurityUser) authentication.getPrincipal();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);

        return Jwts.builder()
                .setSubject(userPrincipal.getUsername())
                .claim("userId", userPrincipal.getId())
                .claim("roles", userPrincipal.getAuthorities())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            log.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            log.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            log.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            log.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            log.error("JWT claims string is empty.");
        }
        return false;
    }
}
5.3 创建认证接口 AuthController

java

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public Result<String> authenticateUser(@RequestBody LoginDto loginDto) {
        // 1. 创建认证Token
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginDto.getUsername(),
                        loginDto.getPassword()
                )
        );

        // 2. 将认证信息存入SecurityContext(可选)
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 3. 生成JWT
        String jwt = tokenProvider.generateToken(authentication);

        return Result.success(jwt);
    }
}
5.4 创建JWT认证过滤器

代码如前所述,注入JwtTokenProviderCustomUserDetailsService,从token中解析用户名,加载用户详情,并构建Authentication存入SecurityContextHolder

5.5 配置SecurityConfig(最终版)

java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .antMatchers("/product/**").authenticated() // 商品接口需要登录
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
5.6 测试
  1. 获取令牌:POST /auth/login,带上用户名密码。

  2. 访问受保护资源:GET /product/list,在请求头中加入 Authorization: Bearer <your_token>

  3. 无令牌访问:返回401 Unauthorized。


第6章 复盘升华:如何用好Spring Security?

6.1 最佳实践总结
  1. 密码永远加密:永远、永远不要明文存储密码。坚持使用BCryptPasswordEncoder或更高级的Argon2PasswordEncoder 。

  2. 权限命名规范:角色名统一用 ROLE_ 前缀(如 ROLE_ADMIN),权限名统一大写(如 USER_READORDER_DELETE)。

  3. 合理使用方法级安全:对于复杂的业务权限(如“只有文章的创建者才能修改”),在Service层用@PreAuthorize配合SpEL表达式,比在Controller层写一堆代码要优雅得多。

  4. 前后端分离必用JWT:在这种模式下,务必设置无状态会话(SessionCreationPolicy.STATELESS)并禁用CSRF。

  5. 异常处理:不要忘了配置AuthenticationEntryPoint(处理未认证)和AccessDeniedHandler(处理未授权),让它们返回统一的JSON错误信息,而不是重定向到登录页。

6.2 技术演进
  • Spring Security 5.x vs 6.x:Spring Security 6.x(与Spring Boot 3.x配套)废弃了过时的WebSecurityConfigurerAdapter,强制要求基于组件的配置方式。在授权表达式上也有所调整,比如antMatchersrequestMatchers取代,permitAll()等依然保留 。

  • OAuth 2.0 / OIDC:Spring Security提供了强大的OAuth 2.0客户端和资源服务器支持,可以轻松实现“第三方登录”(如使用GitHub登录)和单点登录SSO 。

  • 云原生安全:随着服务网格(Service Mesh)的兴起,部分安全责任(如传输层安全、身份验证)可能下沉到基础设施层。但业务层面的授权(RBAC)依然是应用层的职责,Spring Security依然不可或缺。

6.3 面试/工作高频问题
  1. 问:Spring Security的过滤器链是如何工作的?请列举几个核心过滤器。
    :Spring Security通过一组过滤器链来实现安全控制。核心过滤器包括:SecurityContextPersistenceFilter(维护SecurityContext)、UsernamePasswordAuthenticationFilter(处理表单登录)、BasicAuthenticationFilter(处理HTTP Basic)、ExceptionTranslationFilter(处理安全异常)、FilterSecurityInterceptor(执行授权决策)。请求依次经过这些过滤器,只有全部通过才能访问资源 。

  2. 问:如何实现动态权限管理(即权限规则可以动态修改而不重启)?
    :需要自定义SecurityMetadataSource。例如,实现FilterInvocationSecurityMetadataSource接口,在getAttributes方法中,根据当前请求的URL,从数据库(或缓存)中动态查询该URL需要哪些权限。然后结合自定义的AccessDecisionManager(访问决策管理器)来进行动态授权判断。

  3. 问:Spring Security如何防止CSRF攻击?
    :CSRF(跨站请求伪造)利用用户在其他站点已认证的会话,伪造请求。Spring Security通过同步令牌模式来防御:在渲染页面时生成一个随机的CSRF Token存入Session或Cookie;当客户端发起修改状态(POST/PUT/DELETE)的请求时,必须携带这个Token,服务器进行比对。如果Token不匹配或缺失,则拒绝请求。在前后端分离开发中,可以禁用CSRF,因为无状态的JWT本身能防御CSRF(因为请求依赖的是自定义Header,而不是自动携带的Cookie)。


结语:从会用到用好

至此,我们通过六阶段的深度剖析,完整地走过了Spring Boot 2和Spring Security从“解决什么问题”到“企业怎么玩”的全过程。

  • Spring Boot 2 教会我们如何利用“自动配置”和“Starter”来简化开发,将我们从繁琐的配置中解放出来,专注于业务本身。

  • Spring Security 则展示了如何通过成熟的“过滤器链”和“认证授权机制”,为我们的业务系统构建一道坚不可摧的安全屏障。

记住,学习技术的最终目的不是背诵API,而是理解其背后的设计思想要解决的痛点。当你掌握了这个“六阶段”思考框架,以后面对任何新技术,你都能快速抓住其核心,不仅知其然,更知其所以然,最终在工作中游刃有余。

希望这篇超长博客能对你有所帮助。如果你在学习过程中有任何疑问,欢迎在评论区留言讨论。如果你觉得内容还不错,请点赞、收藏、分享,支持一下!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐