*Java 沉淀重走长征路*之——《Spring Boot 2 + Spring Security 企业级实战指南!》
第一部分:Spring Boot 2——简化开发的利器
第1章 问题锚定:为什么我们要学Spring Boot?
1.1 场景化抛出问题:传统SSM开发的「至暗时刻」
想象一下,你现在是一家初创电商公司的Java后端开发。老板让你从零搭建一个订单管理系统。如果使用传统的 Spring + Spring MVC + MyBatis(SSM) 框架,你会经历什么?
-
复杂的依赖管理:打开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会直接让你怀疑人生 。 -
地狱般的配置:接下来,你需要创建一堆XML配置文件:
-
applicationContext.xml:配置Spring的注解扫描、数据源DataSource、事务管理器。 -
spring-mvc.xml:配置Spring MVC的注解驱动、视图解析器、静态资源过滤。 -
mybatis-config.xml:配置MyBatis的别名、插件。 -
jdbc.properties:配置数据库连接参数。
这些配置文件动辄上百行,而且大部分是重复的模板代码。仅仅是配置组件扫描的包路径、数据库连接信息,你就要在多个文件之间来回切换,稍有不慎拼写错误,项目就启动失败。
-
-
漫长的启动与部署:代码写好了,要测试一下?你需要先把项目打成War包,然后复制到Tomcat的
webapps目录下,再启动Tomcat。整个过程耗时以分钟计,严重影响开发效率。 -
依赖外部容器:你的应用严重依赖于本地安装的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会自动帮你创建
DataSource、EntityManagerFactory等核心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应用。
-
创建项目:访问 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,下载一个项目压缩包。
-
-
项目结构一览:解压后用IDEA打开,你会发现项目结构异常清爽。
text
demo/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/demo/ │ │ │ └── DemoApplication.java // 启动类 │ │ └── resources/ │ │ ├── static/ // 存放静态资源 (css, js, 图片) │ │ ├── templates/ // 存放模板文件 (Thymeleaf等) │ │ └── application.properties // 核心配置文件 │ └── test/ │ └── java/ └── pom.xml // Maven构建文件
-
核心代码解析
-
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!"; } } -
运行:直接右键运行
DemoApplication的main方法。看到类似如下的日志,就说明启动成功了: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注解就能启动一切?我们不需要深挖底层源码,但必须理解它的核心工作流。
-
@SpringBootApplication是个“三合一”复合注解 :-
@SpringBootConfiguration:本质上是@Configuration,表明这是一个Spring的配置类。 -
@EnableAutoConfiguration:这是自动配置的核心。它负责开启Spring Boot的自动配置功能。它会根据你添加的Starter依赖(比如spring-boot-starter-web),去尝试配置你需要的Bean。它会去读取spring-boot-autoconfigure包下的META-INF/spring.factories文件,里面列出了所有可能的自动配置类(如WebMvcAutoConfiguration、DataSourceAutoConfiguration)。然后通过一系列@Conditional条件注解(例如@ConditionalOnClass:判断classpath下是否存在某个类;@ConditionalOnMissingBean:判断当前容器中是否已有某个Bean),来决定是否执行这个自动配置。这就是“智能”的来源 。 -
@ComponentScan:启用组件扫描,默认扫描@SpringBootApplication注解所在类的同级包以及子包。这就是为什么你的Controller放在com.example.demo包下能被扫描到。
-
-
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,它会自动帮你配置SqlSessionFactory和SqlSessionTemplate。
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:编写代码
-
实体类 (Model):在
com.example.demo.model包下创建User.java。 -
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(); } -
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> -
Service层:注入Mapper,编写业务逻辑。
-
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响应给前端
代码协作实现:
-
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("注册成功"); } } -
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); } } -
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,自带insert、updateById、selectList等方法,无需写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 代码结构与规范落地
-
项目分层
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 // 日志配置 -
统一返回结果 (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; } } -
自定义业务异常 (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; } } -
全局异常处理器 (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("系统繁忙,请稍后重试"); } } -
商品实体类 (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; } -
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方法,非常方便 } -
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()); } } -
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 测试与部署
-
单元测试:可以使用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)); } } -
打包部署:
-
在项目根目录执行
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 最佳实践总结
-
理解
@SpringBootApplication的包扫描范围:启动类应放在根包下(如com.example.demo),这样它的子包都能被默认扫描到。如果其他模块不在子包下,需要显式添加@ComponentScan。 -
合理使用YAML配置文件:
application.yml比properties文件结构更清晰,支持多环境配置(---分隔)。yaml
spring: profiles: dev server: port: 8080 --- spring: profiles: prod server: port: 80
-
日志规范:永远使用
private static final Logger log = LoggerFactory.getLogger(Xxx.class);或 Lombok的@Slf4j,不要用System.out.println。配置logback-spring.xml实现日志滚动、分级存储。 -
配置分离:敏感信息(如数据库密码、API密钥)不要硬编码在配置文件里,通过环境变量或外部配置中心传入。例如:
spring.datasource.password=${DB_PASSWORD:root},其中DB_PASSWORD为环境变量,:root为默认值。 -
自定义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,可以利用
record、text block等新特性简化代码 。 -
Jakarta EE 9+:包名从
javax.*迁移到了jakarta.*。如果你的代码里直接使用了javax.servlet、javax.persistence等,升级时需要全局替换 。 -
AOT与原生镜像:Spring Boot 3 支持通过GraalVM将应用编译成原生可执行文件,实现“瞬时启动”和更低的内存占用,非常适合云原生和Serverless场景。
6.3 面试/工作高频问题
-
问:Spring Boot自动配置的原理是什么?
答:启动类上的@EnableAutoConfiguration注解会导入AutoConfigurationImportSelector类,该类会扫描所有jar包下的META-INF/spring.factories文件,获取所有自动配置类的全限定名。然后根据一系列@Conditional条件注解(如@ConditionalOnClass、@ConditionalOnBean)来决定这些配置类是否生效,从而自动配置相应的Bean。 -
问:Spring Boot的Starter是什么?如何自定义一个Starter?
答:Starter是一组方便的依赖描述符,它聚合了某个功能所需的所有依赖,并提供了自动配置的入口。自定义Starter需要创建一个自动配置模块,通常包括:一个自动配置类(标注@Configuration和@Conditional)、一个用于配置属性的@ConfigurationProperties类,并在META-INF/spring.factories中注册自动配置类。 -
问: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 场景化抛出问题:手写权限控制的噩梦
想象一下,你的电商系统上线了,一切运转良好。但老板突然提出几个需求:
-
用户认证:“只有登录过的用户才能下单,没登录的跳转到登录页。”
-
权限管理:“运营人员可以查看订单,但不能修改价格;管理员什么都能干。”
你会怎么做?可能第一反应是:在每个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过滤器链顺序(简化版)如下:
-
SecurityContextPersistenceFilter:在请求开始时,从Session(或别的存储)中获取用户信息,放到SecurityContextHolder里;请求结束后,清理SecurityContextHolder。 -
UsernamePasswordAuthenticationFilter:拦截POST /login请求,尝试从请求中提取用户名密码,并进行认证。 -
BasicAuthenticationFilter:处理HTTP Basic认证。 -
ExceptionTranslationFilter:捕获过滤器链中抛出的任何认证/授权异常,并决定是重定向到登录页,还是返回401/403错误 。 -
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 代码实现
-
引入依赖(假设我们依然用MyBatis-Plus操作数据库)。
-
定义实体类
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; } } -
实现
UserDetailsServicejava
@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); } } -
配置密码加密器
PasswordEncoder
绝对不能明文存储密码!Spring Security 强制要求使用PasswordEncoder。java
@Configuration @EnableWebSecurity // 开启Web安全支持 public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { // BCrypt 是一种强哈希算法,每次加密结果都不同,但可以验证。 // 这是目前最主流的密码加密方式。 return new BCryptPasswordEncoder(); } // ... 其他配置 } -
修改数据库存储:在用户注册时,使用
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 常见坑点演示
-
坑点:
UserDetailsService返回的User密码为空或未加密,导致一直提示“Bad credentials”。
解决:确保从数据库加载的密码是已经通过PasswordEncoder加密过的密文,并且UserDetails实现类正确返回了密码。 -
坑点:自定义登录页面提交后,一直403。
解决:通常是CSRF防护没关导致的。如果决定前后端分离开发,应在配置中显式禁用CSRF:http.csrf().disable()。 -
坑点:使用
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) 成为了更流行的无状态认证方案。
业务流程:
-
用户提交用户名密码到
/authenticate。 -
服务器验证成功,生成一个包含用户信息(如用户名、角色)的JWT令牌,返回给前端。
-
前端将JWT存储在LocalStorage或Cookie中。
-
前端每次请求API时,在HTTP Header的
Authorization: Bearer <token>中带上JWT。 -
服务器端有一个JWT认证过滤器,它会拦截请求,解析并验证JWT。如果合法,就根据JWT里的信息创建
Authentication对象,存入SecurityContextHolder,代表用户已登录。 -
后续的过滤器(如
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认证过滤器
代码如前所述,注入JwtTokenProvider和CustomUserDetailsService,从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 测试
-
获取令牌:POST
/auth/login,带上用户名密码。 -
访问受保护资源:GET
/product/list,在请求头中加入Authorization: Bearer <your_token>。 -
无令牌访问:返回401 Unauthorized。
第6章 复盘升华:如何用好Spring Security?
6.1 最佳实践总结
-
密码永远加密:永远、永远不要明文存储密码。坚持使用
BCryptPasswordEncoder或更高级的Argon2PasswordEncoder。 -
权限命名规范:角色名统一用
ROLE_前缀(如ROLE_ADMIN),权限名统一大写(如USER_READ,ORDER_DELETE)。 -
合理使用方法级安全:对于复杂的业务权限(如“只有文章的创建者才能修改”),在Service层用
@PreAuthorize配合SpEL表达式,比在Controller层写一堆代码要优雅得多。 -
前后端分离必用JWT:在这种模式下,务必设置无状态会话(
SessionCreationPolicy.STATELESS)并禁用CSRF。 -
异常处理:不要忘了配置
AuthenticationEntryPoint(处理未认证)和AccessDeniedHandler(处理未授权),让它们返回统一的JSON错误信息,而不是重定向到登录页。
6.2 技术演进
-
Spring Security 5.x vs 6.x:Spring Security 6.x(与Spring Boot 3.x配套)废弃了过时的
WebSecurityConfigurerAdapter,强制要求基于组件的配置方式。在授权表达式上也有所调整,比如antMatchers被requestMatchers取代,permitAll()等依然保留 。 -
OAuth 2.0 / OIDC:Spring Security提供了强大的OAuth 2.0客户端和资源服务器支持,可以轻松实现“第三方登录”(如使用GitHub登录)和单点登录SSO 。
-
云原生安全:随着服务网格(Service Mesh)的兴起,部分安全责任(如传输层安全、身份验证)可能下沉到基础设施层。但业务层面的授权(RBAC)依然是应用层的职责,Spring Security依然不可或缺。
6.3 面试/工作高频问题
-
问:Spring Security的过滤器链是如何工作的?请列举几个核心过滤器。
答:Spring Security通过一组过滤器链来实现安全控制。核心过滤器包括:SecurityContextPersistenceFilter(维护SecurityContext)、UsernamePasswordAuthenticationFilter(处理表单登录)、BasicAuthenticationFilter(处理HTTP Basic)、ExceptionTranslationFilter(处理安全异常)、FilterSecurityInterceptor(执行授权决策)。请求依次经过这些过滤器,只有全部通过才能访问资源 。 -
问:如何实现动态权限管理(即权限规则可以动态修改而不重启)?
答:需要自定义SecurityMetadataSource。例如,实现FilterInvocationSecurityMetadataSource接口,在getAttributes方法中,根据当前请求的URL,从数据库(或缓存)中动态查询该URL需要哪些权限。然后结合自定义的AccessDecisionManager(访问决策管理器)来进行动态授权判断。 -
问: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,而是理解其背后的设计思想和要解决的痛点。当你掌握了这个“六阶段”思考框架,以后面对任何新技术,你都能快速抓住其核心,不仅知其然,更知其所以然,最终在工作中游刃有余。
希望这篇超长博客能对你有所帮助。如果你在学习过程中有任何疑问,欢迎在评论区留言讨论。如果你觉得内容还不错,请点赞、收藏、分享,支持一下!
更多推荐




所有评论(0)