# springboot3-study **Repository Path**: sparkle3021/springboot3-study ## Basic Information - **Project Name**: springboot3-study - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 4 - **Created**: 2023-12-24 - **Last Updated**: 2025-02-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > 源码地址:https://gitee.com/sparklers/springboot3-study ## 依赖版本 | 依赖 | 版本 | 描述 | | :----------- | :------ | ---------------- | | Java(JDK) | 17 | Java开发环境 | | Spring Boot | 3.2.0 | Spring Boot版本 | | MySQL | 8.x | 数据库 | | Redisson | 3.25.0 | Redis工具库 | | MyBaits-Plus | 3.5.4.1 | Mybatis 增强工具 | | java-jwt | 4.4.0 | JWT 工具库 | | SpringDoc | 2.3.0 | 接口文档 | ## 集成项 - [x] 集成 MyBatis-Plus - [x] 集成 Redis 【Redisson】 - [x] 集成 Spring Security + JWT - [x] 日志框架定制 - [x] 集成SpringDoc 接口文档 ## 集成MyBatis-Plus 1. 导入依赖 ```xml 3.0.3 3.5.4.1 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} org.mybatis mybatis-spring org.mybatis mybatis-spring ${mybatis-spring.version} com.h2database h2 runtime org.projectlombok lombok ``` 2. 编写`application.yml`配置文件 ```yml # ======== 数据源配置 ======== spring: datasource: driver-class-name: org.h2.Driver username: root password: test sql: init: schema-locations: classpath:db/schema-h2.sql data-locations: classpath:db/data-h2.sql # ======== MybatisPlus配置 ======== mybatis-plus: # 搜索指定包别名 type-aliases-package: com.yiyan.study.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapper-locations: classpath:mapper/**/*xml # 配置驼峰 configuration: # 是否开启自动驼峰命名规则(camel case)映射。默认值:true map-underscore-to-camel-case: true # 开启 Mybatis 二级缓存,默认为 true。 cache-enabled: true # 日志输出实现。StdOutImpl 标准日志控制台输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 全局默认主键类型 id-type: assign_id # 表名是否使用驼峰转下划线命名,只对表名生效。 默认true table-underline: true ``` 3. 编写mybatis plus文件 ```sql -- data-h2.sql DELETE FROM `user`; INSERT INTO `user` (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com'); -- schema-h2.sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( id BIGINT NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); ``` 实体类 ```java import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serial; import java.io.Serializable; @TableName(value = "`user`") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; private String email; @Serial @TableField(exist = false) private static final long serialVersionUID = 1L; } ``` DAO(mapper) ```java import com.yiyan.study.domain.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IUserDao extends BaseMapper { } ``` Service ```java import com.yiyan.study.domain.User; import com.baomidou.mybatisplus.extension.service.IService; public interface IUserService extends IService { } ``` ServiceImpl ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yiyan.study.domain.User; import com.yiyan.study.service.IUserService; import com.yiyan.study.dao.IUserDao; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements IUserService{ } ``` mapper.xml ```xml ``` 4. 编写测试用例 ```java package com.yiyan.study; import com.yiyan.study.dao.IUserDao; import com.yiyan.study.domain.User; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @Slf4j @SpringBootTest @DisplayName("MyBatis Plus Test") public class MyBatisPlusTest { @Autowired private IUserDao userDao; @Test @DisplayName("MyBatis Plus Select Test") public void testSelect() { log.info(("----- selectAll method test ------")); List userList = userDao.selectList(null); for (User user : userList) { log.info(user.toString()); } } @Test @DisplayName("MyBatis Plus Insert Test") public void testInsert() { log.info(("----- insert method test ------")); User user = new User(); user.setName("Alex"); user.setAge(18); user.setEmail("alex@gmail.com"); int result = userDao.insert(user); log.info("insert result: {}", result); User selectedById = userDao.selectById(user.getId()); log.info("insert user info: {}", selectedById); } @Test @DisplayName("MyBatis Plus Update Test") public void testUpdate() { log.info(("----- update method test ------")); User user = userDao.selectList(null).get(0); log.info("before update: {}", user); user.setName("Updated Name"); int result = userDao.updateById(user); log.info("update result: {}", result); User selectedById = userDao.selectById(user.getId()); log.info("after update: {}", selectedById); } @Test @DisplayName("MyBatis Plus Delete Test") public void testDelete() { log.info("----- delete method test ------"); List users = userDao.selectList(null); log.info("before delete user list size: {}", users.size()); User user = users.get(0); userDao.deleteById(user.getId()); log.info("delete user id: {}", user.getId()); List usersAfterDelete = userDao.selectList(null); log.info("after delete user list size: {}", usersAfterDelete.size()); } } ``` ![springboot3-mybatis-plus](https://minio.codestack.online/blog/202312242308202.gif) ## 集成Redis【Redisson】 1. 导入依赖 ```xml 3.25.0 org.projectlombok lombok org.springframework.boot spring-boot-starter org.redisson redisson-spring-boot-starter ${redisson.version} org.springframework.boot spring-boot-starter-test test ``` 2. 编写配置文件 ```yml # application.yml spring: # ======== Redis配置 ======== redis: redisson: file: classpath:redisson.yaml ``` ```yaml # redisson.yaml # 编码。默认值: org.redisson.codec.JsonJacksonCodec codec: ! {} # 线程池数量。默认值: 当前处理核数量 * 2 threads: 16 # Netty线程池数量。默认值: 当前处理核数量 * 2 nettyThreads: 32 # 传输模式。默认值: NIO transportMode: "NIO" # 监控锁的看门狗超时,单位:毫秒。默认值: 30000 lockWatchdogTimeout: 30000 # 是否保持订阅发布顺序。默认值: true keepPubSubOrder: true # Redisson 单实例配置 singleServerConfig: # 节点地址。格式:redis://host:port address: "redis://127.0.0.1:6379" # 密码。默认值: null password: null # 数据库编号。默认值: 0 database: 0 # 客户端名称(在Redis节点里显示的客户端名称)。默认值: null clientName: null # 连接超时,单位:毫秒。默认值: 10000 connectTimeout: 10000 # 命令等待超时,单位:毫秒。默认值: 3000 timeout: 3000 # 命令失败重试次数。默认值: 3 retryAttempts: 3 # 命令重试发送时间间隔,单位:毫秒。默认值: 1500 retryInterval: 1500 # 最小空闲连接数。默认值: 32 connectionMinimumIdleSize: 24 # 连接池大小。默认值: 64 connectionPoolSize: 64 # 单个连接最大订阅数量。默认值: 5 subscriptionsPerConnection: 5 # 发布和订阅连接的最小空闲连接数。默认值: 1 subscriptionConnectionMinimumIdleSize: 1 # 发布和订阅连接池大小。默认值: 50 subscriptionConnectionPoolSize: 50 # DNS监测时间间隔,单位:毫秒。默认值: 5000 dnsMonitoringInterval: 5000 # 连接空闲超时,单位:毫秒。默认值: 10000 idleConnectionTimeout: 10000 ``` 3. 编写Redis操作工具 ```java package com.yiyan.study; import lombok.RequiredArgsConstructor; import org.redisson.api.RAtomicDouble; import org.redisson.api.RAtomicLong; import org.redisson.api.RBucket; import org.redisson.api.RList; import org.redisson.api.RMap; import org.redisson.api.RScoredSortedSet; import org.redisson.api.RSet; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Redis工具类 */ @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class RedisService { private final RedissonClient redissonClient; // ============================= String类型操作 ============================ /** * 将值存储到Redis中 * * @param key 键 * @param value 值 */ public void setString(String key, T value) { RBucket bucket = redissonClient.getBucket(key); bucket.set(value); } /** * 将值存储到Redis中 * * @param key 键 * @param value 值 * @param timeout 过期时间 * @param timeUnit 时间单位 */ public void setString(String key, T value, long timeout, TimeUnit timeUnit) { RBucket bucket = redissonClient.getBucket(key); bucket.set(value, timeout, timeUnit); } /** * 根据键获取Redis中的值 * * @param key 键 * @return 值 */ public T getString(String key) { RBucket bucket = redissonClient.getBucket(key); return bucket.get(); } // ============================= Hash类型操作 ============================ /** * 将值存储到Redis中 * * @param key 键 * @param field hash键 * @param value 值 */ public boolean addToHash(String key, Object field, T value) { RMap hash = redissonClient.getMap(key); return hash.fastPut(field, value); } /** * 将值存储到Redis中 * * @param key 键 * @param field hash键 * @param value 值 * @param timeout 过期时间 * @param timeUnit 时间单位 */ public boolean addToHash(String key, Object field, T value, long timeout, ChronoUnit timeUnit) { RMap hash = redissonClient.getMap(key); boolean fastPut = hash.fastPut(field, value); boolean expire = hash.expire(Instant.now().plus(timeout, timeUnit)); return fastPut && expire; } /** * 根据键和Hash键获取Redis中的值 * * @param key 键 * @param field hash键 * @return 值 */ public T getFromHash(String key, Object field) { RMap hash = redissonClient.getMap(key); return hash.get(field); } /** * 根据键获取Redis中的值 * * @param key 键 * @return 值 */ public Map getFromHash(String key) { RMap hash = redissonClient.getMap(key); return hash.readAllMap(); } /** * 根据键和Hash键更新Redis中的值 * * @param key 键 * @param field hash键 * @param value 值 * @return 更新成功返回true,否则返回false */ public boolean updateToHash(String key, Object field, T value) { RMap hash = redissonClient.getMap(key); return hash.fastReplace(field, value); } /** * 根据Key,删除Hash类型的数据 * * @param key 键 * @param hashKeys hash键 * @return 删除成功的数量 */ public long removeFromHash(String key, T... hashKeys) { RMap hash = redissonClient.getMap(key); return hash.fastRemove(hashKeys); } // ============================= List类型操作 ============================ /** * 向List数据类型中添加值 * * @param key 键 * @param value 值 */ public boolean addToList(String key, T value) { RList list = redissonClient.getList(key); return list.add(value); } /** * 向List数据类型中添加值 * * @param key 键 * @param value 值 */ public boolean addToList(String key, List value) { RList list = redissonClient.getList(key); return list.addAll(value); } /** * 向List数据类型中添加值 * * @param key 键 * @param value 值 * @param timeout 过期时间 * @param timeUnit 时间单位 */ public boolean addToList(String key, T value, long timeout, ChronoUnit timeUnit) { RList list = redissonClient.getList(key); list.add(value); return list.expire(Instant.now().plus(timeout, timeUnit)); } /** * 从List数据类型中获取值 * * @param key 键 * @param start 起始位置 * @param end 结束位置 * @return 值 */ public List getFromList(String key, int start, int end) { RList list = redissonClient.getList(key); return list.range(start, end); } /** * 获取List数据类型中的所有值 * * @param key 键 * @return 值 */ public List getFromList(String key) { RList list = redissonClient.getList(key); return list.readAll(); } /** * 移除集合左侧第一个元素 * * @param key 键 */ public void removeListLeft(String key) { RList list = redissonClient.getList(key); list.fastRemove(0); } /** * 移除集合右侧第一个元素 * * @param key 键 */ public void removeListRight(String key) { RList list = redissonClient.getList(key); list.fastRemove(list.size() - 1); } /** * 移除集合指定位置元素 * * @param key 键 * @param index 索引 */ public void removeFromList(String key, int index) { RList list = redissonClient.getList(key); list.fastRemove(index); } /** * 移除集合指定元素 * * @param key 键 * @param value 值 */ public boolean removeFromList(String key, T value) { RList list = redissonClient.getList(key); return list.removeIf(o -> o.equals(value)); } // ============================= Set类型操作 ============================ /** * 添加值到Set数据类型中 * * @param key 键 * @param value 值 */ public boolean addToSet(String key, T value) { RSet set = redissonClient.getSet(key); return set.add(value); } /** * 添加值到Set数据类型中 * * @param key 键 * @param value 值 * @param timeout 过期时间 * @param timeUnit 时间单位 * @return 是否成功 */ public boolean addToSet(String key, T value, long timeout, ChronoUnit timeUnit) { RSet set = redissonClient.getSet(key); boolean add = set.add(value); boolean expire = set.expire(Instant.now().plus(timeout, timeUnit)); return add && expire; } /** * 添加值到Set数据类型中 * * @param key 键 * @param values 值 * @return 是否成功 */ public boolean addToSet(String key, List values) { RSet set = redissonClient.getSet(key); return set.addAll(values); } /** * 添加值到Set数据类型中 * * @param key 键 * @param values 值 * @param timeout 过期时间 * @param timeUnit 时间单位 * @return 是否成功 */ public boolean addToSet(String key, List values, long timeout, ChronoUnit timeUnit) { RSet set = redissonClient.getSet(key); set.addAllCounted(values); return set.expire(Instant.now().plus(timeout, timeUnit)); } /** * 获取Set的所有元素。 * * @param key 键 * @return 所有值 */ public Set getFromSet(String key) { RSet set = redissonClient.getSet(key); return set.readAll(); } /** * 从Set数据类型中删除值 * * @param key 键 * @param values 值 */ public void removeFromSet(String key, List values) { RSet set = redissonClient.getSet(key); values.forEach(set::remove); } /** * 从Set数据类型中删除值 * * @param key 键 * @param value 值 */ public boolean removeFromSet(String key, T value) { RSet set = redissonClient.getSet(key); return set.remove(value); } // ============================= ZSet类型操作 ============================ /** * 添加值到ZSet数据类型中 * * @param key 键 * @param value 值 * @param score 分值 */ public void addToZSet(String key, T value, double score) { RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); sortedSet.add(score, value); } /** * 在ZSet数据类型中添加值 * * @param key 键 * @param value 值 * @param score 分值 * @param timeout 过期时间 * @param timeUnit 时间单位 */ public void addToZSet(String key, T value, double score, long timeout, ChronoUnit timeUnit) { RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); sortedSet.add(score, value); sortedSet.expire(Instant.now().plus(timeout, timeUnit)); } /** * 获取ZSet的范围元素。 * * @param key 键 * @param start 起始位置 * @param end 结束位置 * @return Set类型的值 */ public Set getFromZSet(String key, int start, int end) { RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); return new HashSet<>(sortedSet.valueRange(start, end)); } /** * 删除ZSet数据类型中的值 * * @param key 键 * @param values 值 */ public void removeFromZSet(String key, List values) { RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); sortedSet.removeAll(values); } /** * 删除ZSet数据类型中的值 * * @param key 键 * @param value 值 */ public void removeFromZSet(String key, T value) { RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); sortedSet.remove(value); } // ============================= Common ============================ /** * 判断Key是否存在 * * @param key 键 * @return 存在返回true,否则返回false */ public boolean exists(String key) { return redissonClient.getBucket(key).isExists(); } /** * 删除Key * * @param key 键 */ public boolean remove(String key) { long delete = redissonClient.getKeys().delete(key); return delete > 0; } /** * 设置Key的过期时间 * * @param key 键 * @param timeout 过期时间 * @param timeUnit 时间单位 * @return 设置成功返回true,否则返回false */ public boolean expire(String key, long timeout, ChronoUnit timeUnit) { return redissonClient.getBucket(key).expire(Instant.now().plus(timeout, timeUnit)); } /** * 获取Key的过期时间 * * @param key 键 * @return 过期时间 */ public Long getExpire(String key) { return redissonClient.getBucket(key).getExpireTime(); } /** * 递增操作 * * @param key 键 * @param delta 增加的值 * @return 递增后的值,如果键不存在,则返回-1 */ public long increment(String key, long delta) { RAtomicLong atomicLong = redissonClient.getAtomicLong(key); return atomicLong.addAndGet(delta); } /** * 递减操作 * * @param key 键 * @param delta 减少的值 * @return 递减后的值,如果键不存在,则返回-1 */ public long decrement(String key, long delta) { RAtomicLong atomicLong = redissonClient.getAtomicLong(key); return atomicLong.decrementAndGet(); } /** * 递增操作 * * @param key 键 * @param delta 增加的值 * @return 递增后的值,如果键不存在,则返回-1 */ public double increment(String key, double delta) { RAtomicDouble atomicDouble = redissonClient.getAtomicDouble(key); return atomicDouble.addAndGet(delta); } /** * 递减操作 * * @param key 键 * @param delta 减少的值 * @return 递减后的值,如果键不存在,则返回-1 */ public double decrement(String key, double delta) { RAtomicDouble atomicDouble = redissonClient.getAtomicDouble(key); return atomicDouble.decrementAndGet(); } } ``` 4. 编写测试用例 ```java package com.yiyan.study; import jakarta.annotation.Resource; import jodd.util.ThreadUtil; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.time.LocalDateTime; import java.util.List; @Slf4j @SpringBootTest @DisplayName("Redis 集成测试类") public class RedisApplicationTest { @Resource private RedisService redisService; private static final String STRING_KEY = "redis:string"; private static final String LIST_KEY = "redis:list"; private static final String SET_KEY = "redis:set"; private static final String HASH_KEY = "redis:hash"; @Test @DisplayName("Redis String 数据类型测试") public void redisStringTest() { log.info("Redis String 数据类型测试"); redisService.setString(STRING_KEY, LocalDateTime.now().toString()); String redisGetStringData = redisService.getString(STRING_KEY); log.info("Redis String Get:{}", redisGetStringData); boolean remove = redisService.remove(STRING_KEY); log.info("Redis String Remove:{}", remove); redisGetStringData = redisService.getString(STRING_KEY); log.info("Redis String Get After Delete:{}", redisGetStringData); } @Test @DisplayName("Redis List 数据类型测试") public void redisListTest() { log.info("Redis List 数据类型测试"); // 填充数据 List list = Lists.newArrayList(); for (int i = 0; i < 5; i++) { list.add(LocalDateTime.now().getNano()); ThreadUtil.sleep(5); } boolean addItemResult = redisService.addToList(LIST_KEY, LocalDateTime.now().getNano()); log.info("Redis List Add item:{}", addItemResult); redisService.getFromList(LIST_KEY) .forEach(s -> log.info("Redis List Get After Add Item:{}", s)); boolean addListDataResult = redisService.addToList(LIST_KEY, list); log.info("Redis List Add List:{}", addListDataResult); redisService.getFromList(LIST_KEY) .forEach(s -> log.info("Redis List Get After Add List:{}", s)); redisService.getFromList(LIST_KEY, 0, 2) .forEach(s -> log.info("Redis List Get By Index:{}", s)); log.info("Redis List Size Before Delete:{}", redisService.getFromList(LIST_KEY).size()); redisService.removeFromList(LIST_KEY, 0); log.info("Redis List Size After Delete:{}", redisService.getFromList(LIST_KEY).size()); boolean remove = redisService.remove(LIST_KEY); log.info("Redis List Remove:{}", remove); } @Test @DisplayName("Redis Set 数据类型测试") public void redisSetTest() { log.info("Redis Set 数据类型测试"); // 填充数据 List list = Lists.newArrayList(); for (int i = 0; i < 5; i++) { list.add(LocalDateTime.now().getNano()); ThreadUtil.sleep(5); } boolean addItemResult = redisService.addToSet(SET_KEY, LocalDateTime.now().getNano()); log.info("Redis Set Add item:{}", addItemResult); redisService.getFromSet(SET_KEY) .forEach(s -> log.info("Redis Set Get After Add Item:{}", s)); boolean addListDataResult = redisService.addToSet(SET_KEY, list); log.info("Redis Set Add List:{}", addListDataResult); redisService.getFromSet(SET_KEY) .forEach(s -> log.info("Redis Set Get After Add List:{}", s)); log.info("Redis Set Size Before Delete:{}", redisService.getFromSet(SET_KEY).size()); redisService.removeFromSet(SET_KEY, LocalDateTime.now().getNano()); log.info("Redis Set Size After Delete:{}", redisService.getFromSet(SET_KEY).size()); boolean remove = redisService.remove(SET_KEY); log.info("Redis Set Remove:{}", remove); } @Test @DisplayName("Redis Hash 数据类型测试") public void redisHashTest() { log.info("Redis Hash 数据类型测试"); Integer key = LocalDateTime.now().getNano(); boolean addItemResult = redisService .addToHash(HASH_KEY, key, LocalDateTime.now().toString()); log.info("Redis Hash Add item:{}", addItemResult); redisService.getFromHash(HASH_KEY) .forEach((k, v) -> log.info("Redis Hash Get After Add Item:{} - {}", k, v.toString())); log.info("Redis Hash Get By Key:{}", redisService.getFromHash(HASH_KEY, key).toString()); log.info("Redis Hash Size Before Delete:{}", redisService.getFromHash(HASH_KEY).size()); redisService.removeFromHash(HASH_KEY, key); log.info("Redis Hash Size After Delete:{}", redisService.getFromHash(HASH_KEY).size()); boolean remove = redisService.remove(HASH_KEY); log.info("Redis Hash Remove:{}", remove); } } ``` ![springboot3-redis](https://minio.codestack.online/blog/202312250215242.gif) ## Spring Security 实现动态鉴权 ### 依赖版本 - [x] JDK 17 - [x] Spring Boot 3.2.0 - [x] Spring Security 6.2.0 > 为了能够不需要额外配置就能启动项目,看到配置效果。用例采用模拟数据,可自行修改为对应的ORM操作 ### 编写Spring Security基础配置 #### 导入依赖 ```xml 4.4.0 33.0.0-jre org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.projectlombok lombok com.auth0 java-jwt ${java-jwt.version} com.google.guava guava ${guava.version} org.apache.commons commons-lang3 ``` #### 测试Spring Security > 默认配置下,Spring Security form表单登录的用户名为user,密码启动时在控制台输出。 编写测试Controller ```java package com.yiyan.study.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试接口 */ @RestController public class SecurityController { @GetMapping("/hello") public String hello() { return "hello spring security"; } } ``` 访问接口测试 ![springboot3-security-default-test](https://minio.codestack.online/blog/202312250254041.gif) ### 编写Spring Security基础文件 #### 创建Spring Security模拟数据 ```java package com.yiyan.study.config; import lombok.Getter; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Spring Security 模拟数据 */ public class SecurityConstant { /** * 模拟用户数据。key:用户名,value:密码 */ public static final Map USER_MAP = new ConcurrentHashMap<>(); /** * 模拟权限数据。key:接口地址,value:所需权限 */ public static final Map PERMISSION_MAP = new ConcurrentHashMap<>(); /** * 用户权限数据。key:用户名,value:权限 */ public static final Map> USER_PERMISSION_MAP = new ConcurrentHashMap<>(); /** * 白名单 */ public static final String[] WHITELIST = {"/login"}; static { // 填充模拟用户数据 USER_MAP.put("admin", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy"); USER_MAP.put("user", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy"); // 填充用户权限 USER_PERMISSION_MAP.put("admin", List.of(PERMISSION.ADMIN, PERMISSION.USER)); USER_PERMISSION_MAP.put("user", List.of(PERMISSION.USER)); // 填充接口权限 PERMISSION_MAP.put("/user", new SecurityConfig(PERMISSION.USER.getValue())); PERMISSION_MAP.put("/admin", new SecurityConfig(PERMISSION.ADMIN.getValue())); } /** * 模拟权限 */ @Getter public enum PERMISSION { ADMIN("admin"), USER("user"); private final String value; private PERMISSION(String value) { this.value = value; } } } ``` #### 实现 UserDetails ```java package com.yiyan.study.config; import lombok.Builder; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * Spring Security用户信息 */ @Data @Builder public class SecurityUserDetails implements UserDetails { private String username; private String password; private List permissions; public SecurityUserDetails(String username, String password, List permissions) { this.username = username; this.password = password; this.permissions = permissions; } @Override public Collection getAuthorities() { return permissions.stream() .map(permission -> new SimpleGrantedAuthority(permission.getValue())) .collect(Collectors.toList()); } @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 true; } } ``` #### 实现`UserDetailsService`,重写`loadUserByUsername`()方法 ```java package com.yiyan.study.config; import io.micrometer.common.util.StringUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; @Service public class SecurityUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 获取用户信息 String password = SecurityConstant.USER_MAP.get(username); if (StringUtils.isBlank(password)) { throw new UsernameNotFoundException("用户名或密码错误"); } // 获取用户权限 List permission = SecurityConstant.USER_PERMISSION_MAP.get(username); // 返回SecurityUserDetails return SecurityUserDetails.builder() .username(username) .password(password) .permissions(permission) .build(); } } ``` ### 创建自定义过滤器,用于实现对TOKEN进行鉴权 #### JWT工具类 ```java package com.yiyan.study.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.Date; import java.util.List; /** * JWT工具类 */ public class JwtUtils { /** * 默认JWT标签头 */ public static final String HEADER = "Authorization"; /** * JWT配置信息 */ private static JwtConfig jwtConfig; private JwtUtils() { } /** * 初始化参数 * * @param header JWT标签头 * @param tokenHead Token头 * @param issuer 签发者 * @param secretKey 密钥 最小长度:4 * @param expirationTime Token过期时间 单位:秒 * @param issuers 签发者列表 校验签发者时使用 * @param audience 接受者 */ public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime, List issuers, String audience) { jwtConfig = new JwtConfig(); jwtConfig.setHeader(StringUtils.isNotBlank(header) ? header : HEADER); jwtConfig.setTokenHead(tokenHead); jwtConfig.setIssuer(issuer); jwtConfig.setSecretKey(secretKey); jwtConfig.setExpirationTime(expirationTime); if (CollectionUtils.isEmpty(issuers)) { issuers = Collections.singletonList(issuer); } jwtConfig.setIssuers(issuers); jwtConfig.setAudience(audience); jwtConfig.setAlgorithm(Algorithm.HMAC256(jwtConfig.getSecretKey())); } /** * 初始化参数 */ public static void initialize(String header, String issuer, String secretKey, long expirationTime) { initialize(header, null, issuer, secretKey, expirationTime, null, null); } /** * 初始化参数 */ public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime) { initialize(header, tokenHead, issuer, secretKey, expirationTime, null, null); } /** * 生成 Token * * @param subject 主题 * @return Token */ public static String generateToken(String subject) { return generateToken(subject, jwtConfig.getExpirationTime()); } /** * 生成 Token * * @param subject 主题 * @param expirationTime 过期时间 * @return Token */ public static String generateToken(String subject, long expirationTime) { Date now = new Date(); Date expiration = new Date(now.getTime() + expirationTime * 1000); return JWT.create() .withSubject(subject) .withIssuer(jwtConfig.getIssuer()) .withAudience(jwtConfig.getAudience()) .withIssuedAt(now) .withExpiresAt(expiration) .sign(jwtConfig.getAlgorithm()); } /** * 获取Token数据体 */ public static String getTokenContent(String token) { if (StringUtils.isNotBlank(jwtConfig.getTokenHead())) { token = token.substring(jwtConfig.getTokenHead().length()).trim(); } return token; } /** * 验证 Token * * @param token token * @return 验证通过返回true,否则返回false */ public static boolean isValidToken(String token) { try { token = getTokenContent(token); Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getSecretKey()); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); return true; } catch (JWTVerificationException exception) { // Token验证失败 return false; } } /** * 判断Token是否过期 * * @param token token * @return 过期返回true,否则返回false */ public static boolean isTokenExpired(String token) { try { token = getTokenContent(token); Algorithm algorithm = Algorithm.HMAC256(jwtConfig.secretKey); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); Date expirationDate = JWT.decode(token).getExpiresAt(); return expirationDate != null && expirationDate.before(new Date()); } catch (JWTVerificationException exception) { // Token验证失败 return false; } } /** * 获取 Token 中的主题 * * @param token token * @return 主题 */ public static String getSubject(String token) { token = getTokenContent(token); return JWT.decode(token).getSubject(); } /** * 获取当前Jwt配置信息 */ public static JwtConfig getCurrentConfig() { return jwtConfig; } @Data public static class JwtConfig { /** * JwtToken Header标签 */ private String header; /** * Token头 */ private String tokenHead; /** * 签发者 */ private String issuer; /** * 密钥 */ private String secretKey; /** * Token 过期时间 */ private long expirationTime; /** * 签发者列表 */ private List issuers; /** * 接受者 */ private String audience; /** * 加密算法 */ private Algorithm algorithm; } } ``` #### 配置JWT application.yml 添加配置 ```yml server: port: 8080 # ======== JWT配置 ======== jwt: secret: 1234567890123456 expirationTime: 604800 issuer: springboot3-security header: Authorization tokenHead: Bearer ``` 配置JWT启动时加载配置项 ```java package com.yiyan.study.config; import com.yiyan.study.utils.JwtUtils; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * JWT 配置 */ @Slf4j @Component public class JwtConfig { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.issuer}") private String issuer; @Value("${jwt.expirationTime}") private long expirationTime; @Value("${jwt.header}") private String header; @Value("${jwt.tokenHead}") private String tokenHead; @PostConstruct public void jwtInit() { JwtUtils.initialize(header, tokenHead, issuer, secretKey, expirationTime); log.info("JwtUtils初始化完成"); } } ``` #### 自定义拦截器 ```java package com.yiyan.study.config; import com.yiyan.study.utils.JwtUtils; import jakarta.annotation.Resource; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; /** * 自定义过滤器 */ @Component public class MyAuthenticationFilter extends OncePerRequestFilter { @Resource private SecurityUserDetailsService securityUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader()); // 读取请求头中的token if (StringUtils.isNotBlank(requestToken)) { // 判断token是否有效 boolean verifyToken = JwtUtils.isValidToken(requestToken); if (!verifyToken) { filterChain.doFilter(request, response); } // 解析token中的用户信息 String subject = JwtUtils.getSubject(requestToken); if (StringUtils.isNotBlank(subject) && SecurityContextHolder.getContext().getAuthentication() == null) { SecurityUserDetails userDetails = (SecurityUserDetails) securityUserDetailsService.loadUserByUsername(subject); // 保存用户信息到当前会话 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); // 将authentication填充到安全上下文 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } ``` #### 修改Controller 的登录接口 ```java package com.yiyan.study.controller; import com.yiyan.study.utils.JwtUtils; import jakarta.annotation.Resource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 测试接口 */ @RestController public class SecurityController { @Resource private AuthenticationManager authenticationManager; @GetMapping("/hello") public String hello() { return "hello spring security"; } @GetMapping("/user") public String helloUser() { return "Hello User"; } @GetMapping("/admin") public String helloAdmin() { return "Hello Admin"; } @PostMapping("/login") public String doLogin(@RequestParam("username") String username, @RequestParam("password") String password) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); Authentication authentication = authenticationManager.authenticate(authenticationToken); // 判断是否验证成功 if (null == authentication) { throw new UsernameNotFoundException("用户名或密码错误"); } return JwtUtils.generateToken(username); } } ``` ### 编写Spring Security配置文件 > Spring Security 升级到6.x后,配置方式与前版本不同,多个旧的配置类被启用。新版本采用lambda表达式的方式进行配置,核心配置项没变化。 ```java package com.yiyan.study.config; import jakarta.annotation.Resource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * Spring Security配置类 */ @Configuration @EnableWebSecurity public class SecurityConfiguration { @Resource private UserDetailsService userDetailsService; @Resource private MyAuthenticationFilter myAuthenticationFilter; /** * 鉴权管理类 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } /** * 加密类 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * Spring Security 过滤链 */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // 禁用明文验证 .httpBasic(AbstractHttpConfigurer::disable) // 关闭csrf .csrf(AbstractHttpConfigurer::disable) // 禁用默认登录页 .formLogin(AbstractHttpConfigurer::disable) // 禁用默认登出页 .logout(AbstractHttpConfigurer::disable) // 禁用session .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 配置拦截信息 .authorizeHttpRequests(authorization -> authorization // 允许所有的OPTIONS请求 .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 放行白名单 .requestMatchers(SecurityConstant.WHITELIST).permitAll() // 根据接口所需权限进行动态鉴权 .anyRequest().access((authentication, object) -> { // 获取当前的访问路径 String requestURI = object.getRequest().getRequestURI(); PathMatcher pathMatcher = new AntPathMatcher(); // 白名单请求直接放行 for (String url : SecurityConstant.WHITELIST) { if (pathMatcher.match(url, requestURI)) { return new AuthorizationDecision(true); } } // 获取访问该路径所需权限 Map permissionMap = SecurityConstant.PERMISSION_MAP; List apiNeedPermissions = new ArrayList<>(); for (Map.Entry config : permissionMap.entrySet()) { if (pathMatcher.match(config.getKey(), requestURI)) { apiNeedPermissions.add(config.getValue()); } } // 如果接口没有配置权限则直接放行 if (apiNeedPermissions.isEmpty()) { return new AuthorizationDecision(true); } // 获取当前登录用户权限信息 Collection authorities = authentication.get().getAuthorities(); // 判断当前用户是否有足够的权限访问 for (ConfigAttribute configAttribute : apiNeedPermissions) { // 将访问所需资源和用户拥有资源进行比对 String needAuthority = configAttribute.getAttribute(); for (GrantedAuthority grantedAuthority : authorities) { if (needAuthority.trim().equals(grantedAuthority.getAuthority())) { // 权限匹配放行 return new AuthorizationDecision(true); } } } return new AuthorizationDecision(false); }) ) // 注册重写后的UserDetailsService实现 .userDetailsService(userDetailsService) // 注册自定义拦截器 .addFilterBefore(myAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .build(); } } ``` ### 测试 ![springboot3-security](https://minio.codestack.online/blog/202312250425115.gif) ## SpringBoot 基于Logback定制日志框架 ### 依赖版本 - [x] JDK 17 - [x] Spring Boot 3.2.0 ### 日志门面和日志实现 日志门面(如Slf4j)就是一个标准,同JDBC一样来制定“规则”,把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口。而Logback、log4j等具体的日志系统就如同MySQL驱动、PGSQL驱动一样,才是日志功能的真正实现。 ![img](https://minio.codestack.online/blog/202312250943102.jpg) ### SpringBoot默认日志框架 SpringBoot使用Slf4j作为日志门面,Logback作为默认的日志实现。在SpringBoot的pom.xml中,依赖为: ```xml org.springframework.boot spring-boot-starter-logging ``` 此依赖在SpringBoot Starter包中包含,所以在SpringBoot项目中导入 `spring-boot-starter` 后可直接使用: ```xml org.springframework.boot spring-boot-starter ``` ### 打印日志 1. 基于`LoggerFactory`创建日志记录器实例 ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LoggingApplication { private static final Logger logger = LoggerFactory.getLogger(LoggingApplication.class); public static void main(String[] args) { logger.info("LoggingApplication start..."); SpringApplication.run(LoggingApplication.class, args); logger.info("LoggingApplication end..."); } } ``` ![springboot3-logging-factory](https://minio.codestack.online/blog/202312250959282.gif) 2. 引入`lombok`依赖,通过注解`@Slf4j`创建日志记录器实例 ```xml org.springframework.boot spring-boot-starter org.projectlombok lombok ``` ```java import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @Slf4j @SpringBootApplication public class LoggingApplication { public static void main(String[] args) { log.info("LoggingApplication start..."); SpringApplication.run(LoggingApplication.class, args); log.info("LoggingApplication end..."); } } ``` ![springboot3-logging-lombok](https://minio.codestack.online/blog/202312251002949.gif) ### 配置Logback #### 默认配置 SpringBoot推荐将配置文件名称命名为`logback-spring.xml`表示这是SpringBoot下Logback专用的配置,可以使用SpringBoot 的高级Profile功能,它的内容类似于这样: ```xml ``` 最外层由`configuration`包裹,一旦编写,那么就会替换默认的配置,所以如果内部什么都不写的话,那么会导致我们的SpringBoot项目没有配置任何日志输出方式,控制台也不会打印日志。 > 也可在配置文件中指定要使用的日志配置,如`application.yml`中通过配置 `logging.config`指定要使用的日志配置。 ```yml logging: config: classpath:logback-spring.xml ``` ![springboot3-logging-noneconfig](https://minio.codestack.online/blog/202312251008703.gif) #### 基于SpringBoot默认Logback配置进行定制 > SpringBoot 的默认Logback文件可在依赖项中找到。路径为:`org/springframework/boot/logging/logback/defaults.xml` ![springboot3-logging-logback-config-default](https://minio.codestack.online/blog/202312251027766.gif) 在配置文件中对默认配置进行引用,及编写定制配置覆盖默认配置。 ```xml ${CONSOLE_LOG_THRESHOLD} ${CONSOLE_LOG_PATTERN} ${CONSOLE_LOG_CHARSET} ``` ![springboot3-logging-config-my](https://minio.codestack.online/blog/202312251017298.gif) #### 配置日志信息输出到文件 `logback-spring.xml` 添加如下内容: ```XML ${FILE_LOG_THRESHOLD} ${FILE_LOG_PATTERN} ${FILE_LOG_CHARSET} ${LOG_FILE} ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7} ``` ![springboot3-logging-logback-config-tofile](https://minio.codestack.online/blog/202312251039051.gif) ### MDC机制 #### Slf4j官方解释 > `MDC`全称`Mapped Diagnostic Context`(映射诊断上下文),“映射诊断上下文” > 本质上是由日志记录框架维护的映射,其中应用程序代码提供键值对,然后可以由日志记录框架将其插入日志消息中。MDC数据在过滤消息或触发某些操作时也非常有用。 > > 通俗的说就是可以将特定的信息(如请求Id,链路Id),通过MDC机制注入到当前日志线程的上下文中,将信息在日志中记录、输出。 ![image-20231225104700128](https://minio.codestack.online/blog/202312251047192.png) #### MDC的使用 MDC 常用的三个方法: - MDC.put(key,value) 注入值 - MDC.remove(key) 移除指定的MDC注入信息 - MDC.clear() 移除所有当前线程的MDC注入信息 MDC注入属性配置: 在`logback-spring.xml` 文件的输出格式中添加需要注入的KEY信息与代码注入的信息相对应。假定我需要注入当前用户的Id信息,且设置key为`user-id` 代码: ```java import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.util.UUID; @Slf4j @SpringBootApplication public class LoggingApplication { public static void main(String[] args) { SpringApplication.run(LoggingApplication.class, args); String mdcKey = "user-id"; log.info("Logging before MDC put..."); MDC.put("mdcKey", UUID.randomUUID().toString()); log.info("Logging after MDC put..."); MDC.clear(); log.info("Logging after MDC clear..."); } } ``` logback-spring.xml ```xml ${CONSOLE_LOG_THRESHOLD} ${CONSOLE_LOG_PATTERN} ${CONSOLE_LOG_CHARSET} ``` ![springboot3-logging-logback-mdc](https://minio.codestack.online/blog/202312251110505.gif) > 使用方式: > > 1. 通过注册拦截器或过滤器,在业务开始前获取到相关信息(请求Id,操作人Id),将相关信息通过MDC自动注入到当前线程中,实现关键信息记录。 > 2. 在创建子线程时,需要通过`MDC.getCopyOfContextMap()`方法获取到当前线程的MDC注入相关信息,传递给子线程,才能在子线打印日志信息时获取到父线程MDC的注入信息。 ### Logback参考配置 ```xml ${CONSOLE_LOG_THRESHOLD} ${CONSOLE_LOG_PATTERN} ${CONSOLE_LOG_CHARSET} ${FILE_LOG_THRESHOLD} ${FILE_LOG_PATTERN} ${FILE_LOG_CHARSET} ${LOG_FILE} ${FILE_NAME_PATTERN} ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START} ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE} ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP} ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY} ``` ## 接入 SpringDoc 实现接口文档 ### 依赖版本 - [x] JDK 17 - [x] Spring Boot 3.2.0 - [x] SpringDoc 2.3.0 工程源码:[Gitee](https://gitee.com/sparklers/springboot3-study) ### 导入依赖 ```xml 17 17 UTF-8 2.3.0 org.springframework.boot spring-boot-starter-web org.springdoc springdoc-openapi-starter-webmvc-ui ${springdoc.version} org.projectlombok lombok ``` ### 创建SpringDoc配置文件 #### application.yml 配置 ```yml server: port: 8080 # ======== SpringDocs文档配置 ======== springdocs: title: SpringBoot3.2.0 API Docs description: SpringBoot3 + OpenAPI Docs version: 0.0.1 scheme: Bearer header: Authorization ``` #### OpenAPI配置文件 ```java import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "springdocs") @Setter @Getter public class SpringDocsProperties { private String title; private String description; private String version; private String header; private String scheme; } ``` ```java import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import jakarta.annotation.Resource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * SpringDoc配置文件 */ @Configuration public class SpringDocConfig { @Resource private SpringDocsProperties springDocsProperties; @Bean public OpenAPI openApi() { return new OpenAPI() // 文档描述信息 .info(new Info() .title(springDocsProperties.getTitle()) .description(springDocsProperties.getDescription()) .version(springDocsProperties.getVersion()) ) // 添加全局的header参数 .addSecurityItem(new SecurityRequirement() .addList(springDocsProperties.getHeader())) .components(new Components() .addSecuritySchemes(springDocsProperties.getHeader(), new SecurityScheme() .name(springDocsProperties.getHeader()) .scheme(springDocsProperties.getScheme()) .bearerFormat("JWT") .type(SecurityScheme.Type.HTTP)) ); } } ``` ### OpenAPI 与 Swagger的 API对应 - `@Api` → `@Tag` - `@ApiIgnore` → `@Parameter(hidden = true)` or `@Operation(hidden = true)` or `@Hidden` - `@ApiImplicitParam` → `@Parameter` - `@ApiImplicitParams` → `@Parameters` - `@ApiModel` → `@Schema` - `@ApiModelProperty(hidden = true)` → `@Schema(accessMode = READ_ONLY)` - `@ApiModelProperty` → `@Schema` - `@ApiOperation(value = "foo", notes = "bar")` → `@Operation(summary = "foo", description = "bar")` - `@ApiParam` → `@Parameter` - `@ApiResponse(code = 404, message = "foo")` → `@ApiResponse(responseCode = "404", description = "foo")` ### 演示 ![springboot3-springdoc](https://minio.codestack.online/blog/202312251227157.gif)