-
在企业级应用开发中,数据持久化是核心模块之一,负责将业务数据存储到数据库并提供读写操作。MyBatis-Plus(简称MP)是MyBatis的增强工具,在MyBatis基础上简化了CRUD操作,提供了代码生成器、分页插件、条件构造器等强大功能,大幅提升开发效率。本文将详细讲解Spring Boot集成MyBatis-Plus的完整流程,实现数据库表设计、实体类映射、CRUD操作、分页查询、条件查询等核心功能,结合实际业务场景提供完整代码示例,帮助开发者快速掌握MyBatis-Plus的实战应用。一、核心技术栈与环境准备1. 技术栈选型核心框架:Spring Boot 2.7.x;数据持久化:MyBatis-Plus 3.5.x;数据库:MySQL 8.0;核心依赖:Spring Web、MyBatis-Plus Starter、MySQL Connector Java、Lombok、Validation;开发工具:IntelliJ IDEA、Navicat(数据库管理)、Postman。2. 数据库环境准备创建MySQL数据库(名称:springboot_mp_demo),并创建用户表(user),SQL语句如下: -- 创建数据库 CREATE DATABASE IF NOT EXISTS springboot_mp_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 使用数据库 USE springboot_mp_demo; -- 创建用户表 CREATE TABLE IF NOT EXISTS `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID(主键)', `username` varchar(50) NOT NULL COMMENT '用户名(唯一)', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `email` varchar(100) NOT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态(1:正常,0:禁用)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; 3. 项目初始化与依赖配置通过Spring Initializr创建项目,引入核心依赖,pom.xml文件如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.15</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>springboot-mp-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-mp-demo</name> <description>Spring Boot 集成 MyBatis-Plus 实战项目</description> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> <mysql-connector.version>8.0.33</mysql-connector.version> </properties> <dependencies> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus Starter(核心依赖,自动集成MyBatis) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency><!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>${mysql-connector.version}</version> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 数据校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- MyBatis-Plus 代码生成器(可选,快速生成代码) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- 代码生成器模板引擎(Freemarker) --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 二、核心配置(application.yml)配置数据库连接信息、MyBatis-Plus核心参数(如 mapper 扫描路径、日志级别、分页插件等),application.yml文件如下: spring: # 数据库配置 datasource: url: jdbc:mysql://localhost:3306/springboot_mp_demo?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root # 你的MySQL用户名 password: root # 你的MySQL密码 driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.0+ 驱动类 # MyBatis-Plus 配置 mybatis-plus: # mapper.xml 文件扫描路径(默认classpath:/mapper/**/*.xml) mapper-locations: classpath:/mapper/**/*.xml # 实体类扫描路径(配置后可省略@TableName注解的schema属性) type-aliases-package: com.example.springbootmpdemo.entity # 全局配置 global-config: db-config: # 主键生成策略(AUTO:自增,NONE:手动输入,INPUT:手动输入,ASSIGN_ID:雪花算法,ASSIGN_UUID:UUID) id-type: AUTO # 数据库表前缀(如果表名有前缀,如t_user,可配置为t_,实体类无需写前缀) table-prefix: # 逻辑删除字段(软删除,需在实体类对应字段添加@TableLogic注解) logic-delete-field: status # 逻辑删除值(1:正常,0:删除/禁用) logic-not-delete-value: 1 logic-delete-value: 0 # 配置日志(STDOUT_LOGGING:控制台日志,可替换为LOG4J2等) configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启驼峰命名自动映射(数据库字段下划线转实体类驼峰命名) map-underscore-to-camel-case: true # 服务器配置 server: port: 8081 servlet: context-path: / 三、MyBatis-Plus核心功能实现1. 项目目录结构 src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── springbootmpdemo/ │ │ ├── SpringbootMpDemoApplication.java # 启动类 │ │ ├── entity/ # 实体类(与数据库表映射) │ │ │ └── User.java │ │ ├── mapper/ # Mapper接口(继承BaseMapper) │ │ │ └── UserMapper.java │ │ ├── service/ # 业务层(继承IService/ServiceImpl) │ │ │ ├── UserService.java │ │ │ └── impl/ │ │ │ └── UserServiceImpl.java │ │ ├── controller/ # 控制层 │ │ │ └── UserController.java │ │ ├── dto/ # 请求/响应DTO │ │ │ ├── UserQueryDTO.java │ │ │ ├── UserRequestDTO.java │ │ │ └── UserResponseDTO.java │ │ ├── exception/ # 异常处理(同第一篇文章结构) │ │ ├── config/ # 配置类 │ │ │ └── MyBatisPlusConfig.java # MP分页插件配置 │ │ └── util/ # 工具类 │ └── resources/ │ ├── application.yml # 核心配置 │ └── mapper/ # mapper.xml文件目录 │ └── UserMapper.xml └── test/ # 测试目录 2. 启动类配置(添加@MapperScan注解)在启动类上添加@MapperScan注解,指定Mapper接口扫描路径,让Spring Boot自动扫描并注册Mapper接口: package com.example.springbootmpdemo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * @MapperScan:扫描Mapper接口所在包 */ @SpringBootApplication @MapperScan("com.example.springbootmpdemo.mapper") public class SpringbootMpDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootMpDemoApplication.class, args); System.out.println("Spring Boot + MyBatis-Plus 项目启动成功!"); } } 3. 实体类(User.java)与数据库表映射通过MyBatis-Plus的注解实现实体类与数据库表的映射,核心注解包括@TableName(表名映射)、@TableId(主键映射)、@TableField(字段映射)、@TableLogic(逻辑删除)等: package com.example.springbootmpdemo.entity;import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime;/** * 用户实体类(与user表映射) * @Data:Lombok注解,生成getter/setter等 * @TableName:指定数据库表名(如果实体类名与表名一致,可省略) * @EqualsAndHashCode:生成equals和hashCode方法 */ @Data @EqualsAndHashCode(callSuper = false) @TableName("user") public class User { /** * 主键ID * @TableId:标识主键,type指定主键生成策略(与配置文件全局配置一致,可省略) */ @TableId(type = IdType.AUTO) private Long id; /** * 用户名 * @TableField:字段映射(如果实体类字段与数据库字段一致,可省略) * value:数据库字段名 * unique:是否唯一(与数据库表的唯一约束对应) */ @TableField(value = "username", unique = true) private String username; /** * 密码 */ private String password; /** * 邮箱 */ @TableField(unique = true) private String email; /** * 手机号 * exist:是否为数据库字段(false表示非数据库字段,仅用于
-
Spring Boot作为Spring生态的核心子项目,以“约定优于配置”为核心理念,大幅简化了Spring应用的初始化搭建与开发流程,无需繁琐的XML配置,即可快速构建可运行的企业级应用。本文将从零开始,搭建一个标准的企业级RESTful API项目,涵盖项目初始化、目录结构设计、核心依赖引入、接口开发、全局异常处理、数据校验、日志配置等核心功能,提供完整可运行的代码示例,帮助开发者快速上手Spring Boot基础开发。一、核心技术栈与环境准备1. 技术栈选型核心框架:Spring Boot 2.7.x(稳定版);核心依赖:Spring Web(RESTful API开发)、Spring Boot DevTools(热部署)、Lombok(简化代码)、Validation(数据校验)、Spring Boot Starter Logging(日志);开发工具:IntelliJ IDEA 2023、Postman(接口测试);JDK版本:JDK 1.8+;构建工具:Maven 3.6+。2. 项目初始化(两种方式)方式一:通过Spring Initializr(在线初始化),访问地址:https://start.spring.io/,填写项目信息(Group、Artifact、Name等),选择对应依赖(Spring Web、Lombok、Validation),下载项目压缩包后解压导入IDEA。方式二:通过IDEA直接创建,打开IDEA → New Project → 选择Spring Initializr,配置项目基本信息,选择依赖后直接创建,IDEA会自动完成项目初始化与依赖下载。3. 核心依赖配置(pom.xml)项目初始化后,pom.xml文件核心依赖如下,可根据需求调整版本与依赖: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.15</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>springboot-rest-api-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-rest-api-demo</name> <description>Spring Boot RESTful API 基础实战项目</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- Spring Web 核心依赖(RESTful API开发) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Lombok 简化代码(消除getter/setter/构造器等) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 数据校验依赖(参数校验) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 热部署依赖(开发时无需重启项目) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven插件(打包运行) --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 二、标准项目目录结构设计为保证项目的可维护性、可扩展性,遵循“分层架构”设计思想,标准目录结构如下,各层职责清晰,便于团队协作开发: src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── springbootrestapidemo/ # 项目根包 │ │ ├── SpringbootRestApiDemoApplication.java # 项目启动类 │ │ ├── controller/ # 控制层(接口暴露) │ │ │ └── UserController.java # 用户相关接口 │ │ ├── entity/ # 实体类层(映射数据库/请求响应模型) │ │ │ ├── User.java # 用户实体 │ │ │ └── dto/ # 数据传输对象(请求/响应专用) │ │ │ ├── UserRequestDTO.java │ │ │ └── UserResponseDTO.java │ │ ├── service/ # 业务逻辑层 │ │ │ ├── UserService.java # 业务接口 │ │ │ └── impl/ # 业务实现类 │ │ │ └── UserServiceImpl.java │ │ ├── exception/ # 异常处理层 │ │ │ ├── GlobalExceptionHandler.java # 全局异常处理器 │ │ │ ├── BusinessException.java # 自定义业务异常 │ │ │ └── ErrorCode.java # 错误码枚举 │ │ ├── util/ # 工具类层 │ │ │ └── ResultUtil.java # 响应结果工具类 │ │ └── config/ # 配置层 │ │ └── WebConfig.java # Web相关配置 │ └── resources/ # 资源文件目录 │ ├── application.yml # 核心配置文件(替代application.properties) │ ├── application-dev.yml # 开发环境配置 │ └── application-prod.yml # 生产环境配置 └── test/ # 测试目录(与main目录结构对应) └── java/ └── com/ └── example/ └── springbootrestapidemo/ └── controller/ └── UserControllerTest.java 三、核心功能代码实现1. 项目启动类(入口类)启动类是Spring Boot项目的入口,通过@SpringBootApplication注解开启自动配置、组件扫描等核心功能,必须放在项目根包下(保证组件扫描范围): package com.example.springbootrestapidemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Spring Boot 项目启动类 * @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan */ @SpringBootApplication public class SpringbootRestApiDemoApplication { public static void main(String[] args) { // 启动Spring Boot应用 SpringApplication.run(SpringbootRestApiDemoApplication.class, args); System.out.println("项目启动成功!访问地址:http://localhost:8080"); } } 2. 实体类与DTO设计实体类(Entity)用于映射业务模型,DTO(Data Transfer Object)用于请求参数接收与响应结果返回,避免直接暴露实体类,保证数据传输的安全性与灵活性。(1)用户实体类(User.java): package com.example.springbootrestapidemo.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; /** * 用户实体类(业务模型) * Lombok注解:@Data(生成getter/setter/toString等)、@NoArgsConstructor(无参构造)、@AllArgsConstructor(全参构造) */ @Data @NoArgsConstructor @AllArgsConstructor public class User { // 用户ID private Long id; // 用户名(唯一) private String username; // 密码(实际开发中需加密存储) private String password; // 邮箱 private String email; // 手机号 private String phone; // 创建时间 private LocalDateTime createTime; // 更新时间 private LocalDateTime updateTime; } (2)用户请求DTO(UserRequestDTO.java):用于接收前端传递的用户新增/修改参数,并添加数据校验规则: package com.example.springbootrestapidemo.entity.dto; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; /** * 用户请求DTO(接收前端参数) */ @Data public class UserRequestDTO { // 用户名:非空,长度3-20位 @NotBlank(message = "用户名不能为空") @Length(min = 3, max = 20, message = "用户名长度必须在3-20位之间") private String username; // 密码:非空,长度6-16位,包含字母和数字 @NotBlank(message = "密码不能为空") @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(.{6,16})$", message = "密码必须6-16位,包含字母和数字") private String password; // 邮箱:非空,格式正确 @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; // 手机号:非空,格式正确(11位手机号) @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone; } (3)用户响应DTO(UserResponseDTO.java):用于向前端返回用户数据,隐藏敏感字段(如password): package com.example.springbootrestapidemo.entity.dto; import lombok.Data; import java.time.LocalDateTime; /** * 用户响应DTO(返回前端数据) */ @Data public class UserResponseDTO { private Long id; private String username; private String email; private String phone; private LocalDateTime createTime; private LocalDateTime updateTime; } 3. 业务逻辑层(Service)业务逻辑层负责核心业务处理,通过接口与实现类分离的方式,降低耦合度,便于后续扩展与测试。本文采用内存模拟数据存储(实际开发中替换为数据库交互)。(1)业务接口(UserService.java): package com.example.springbootrestapidemo.service; import com.example.springbootrestapidemo.entity.User; import com.example.springbootrestapidemo.entity.dto.UserRequestDTO; import java.util.List; /** * 用户业务逻辑接口 */ public interface UserService { /** * 新增用户 * @param userRequestDTO 用户请求参数 * @return 新增后的用户 */ User addUser(UserRequestDTO userRequestDTO); /** * 根据ID查询用户 * @param id 用户ID * @return 用户信息 */ User getUserById(Long id); /** * 查询所有用户(分页,本文简化为不分页) * @return 用户列表 */ List<User> getAllUsers(); /** * 根据ID修改用户 * @param id 用户ID * @param userRequestDTO 修改参数 * @return 修改后的用户 */ User updateUser(Long id, UserRequestDTO userRequestDTO); /** * 根据ID删除用户 * @param id 用户ID * @return 删除结果(true/false) */ Boolean deleteUser(Long id); } (2)业务实现类(UserServiceImpl.java): package com.example.springbootrestapidemo.service.impl; import com.example.springbootrestapidemo.entity.User; import com.example.springbootrestapidemo.entity.dto.UserRequestDTO; import com.example.springbootrestapidemo.exception.BusinessException; import com.example.springbootrestapidemo.exception.ErrorCode; import com.example.springbootrestapidemo.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; /** * 用户业务逻辑实现类 * @Slf4j:Lombok注解,生成日志对象 * @Service:Spring注解,标识为业务层组件,自动注入 */ @Service @Slf4j public class UserServiceImpl implements UserService { // 内存存储用户数据(ConcurrentHashMap保证线程安全) private static final ConcurrentMap<Long, User> USER_MAP = new ConcurrentHashMap<>(); // 自增ID生成器(AtomicLong保证原子性) private static final AtomicLong USER_ID_GENERATOR = new AtomicLong(1); @Override public User addUser(UserRequestDTO userRequestDTO) { // 1. 业务校验:用户名是否已存在 boolean usernameExists = USER_MAP.values().stream() .anyMatch(user -> user.getUsername().equals(userRequestDTO.getUsername())); if (usernameExists) { log.error("用户名已存在:{}", userRequestDTO.getUsername()); throw new BusinessException(ErrorCode.USERNAME_ALREADY_EXISTS); } // 2. 构建User对象 User user = new User(); Long userId = USER_ID_GENERATOR.getAndIncrement(); user.setId(userId); user.setUsername(userRequestDTO.getUsername()); user.setPassword(userRequestDTO.getPassword()); // 实际开发中需加密(如BCrypt) user.setEmail(userRequestDTO.getEmail()); user.setPhone(userRequestDTO.getPhone()); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); // 3. 存储用户数据 USER_MAP.put(userId, user); log.info("新增用户成功,用户ID:{}", userId); return user; } @Override public User getUserById(Long id) { // 1. 校验ID是否存在 User user = USER_MAP.get(id); if (user == null) { log.error("用户不存在,用户ID:{}", id); throw new BusinessException(ErrorCode.USER_NOT_FOUND); } return user; } @Override public List<User> getAllUsers() { // 返回所有用户列表 return new ArrayList<>(USER_MAP.values()); } @Override public User updateUser(Long id, UserRequestDTO userRequestDTO) { // 1. 校验用户是否存在 User user = getUserById(id); // 2. 业务校验:用户名是否重复(排除当前用户) boolean usernameExists = USER_MAP.values().stream() .anyMatch(u -> u.getUsername().equals(userRequestDTO.getUsername()) && !u.getId().equals(id)); if (usernameExists) { log.error("用户名已存在:{}", userRequestDTO.getUsername()); throw new BusinessException(ErrorCode.USERNAME_ALREADY_EXISTS); } // 3. 更新用户信息 user.setUsername(userRequestDTO.getUsername()); user.setPassword(userRequestDTO.getPassword()); // 实际开发中需加密 user.setEmail(userRequestDTO.getEmail()); user.setPhone(userRequestDTO.getPhone()); user.setUpdateTime(LocalDateTime.now()); // 4. 保存更新后的数据 USER_MAP.put(id, user); log.info("修改用户成功,用户ID:{}", id); return user; } @Override public Boolean deleteUser(Long id) { // 1. 校验用户是否存在 getUserById(id); // 2. 删除用户数据 USER_MAP.remove(id); log.info("删除用户成功,用户ID:{}", id); return true; } } 4. 控制层(Controller)控制层负责暴露RESTful API接口,接收前端请求,调用业务层处理,返回响应结果。通过@RestController注解标识为REST接口控制器,自动返回JSON格式数据。 package com.example.springbootrestapidemo.controller; import com.example.springbootrestapidemo.entity.User; import com.example.springbootrestapidemo.entity.dto.UserRequestDTO; import com.example.springbootrestapidemo.entity.dto.UserResponseDTO; import com.example.springbootrestapidemo.service.UserService; import com.example.springbootrestapidemo.util.ResultUtil; import com.example.springbootrestapidemo.util.ResultVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors; /** * 用户接口控制器(RESTful API) * @RestController = @Controller + @ResponseBody(自动返回JSON) * @RequestMapping:配置接口基础路径 */ @RestController @RequestMapping("/api/v1/users") @Slf4j public class UserController { // 注入业务层对象(@Resource:按名称注入,@Autowired:按类型注入) @Resource private UserService userService; /** * 新增用户 * @PostMapping:POST请求方式,对应新增操作 * @Validated:开启参数校验(配合DTO中的校验注解) * @RequestBody:接收JSON格式请求体 */ @PostMapping public ResultVO<UserResponseDTO> addUser(@Validated @RequestBody UserRequestDTO userRequestDTO) { log.info("接收新增用户请求,参数:{}", userRequestDTO); User user = userService.addUser(userRequestDTO); // 实体类转换为响应DTO(BeanUtils.copyProperties:属性拷贝) UserResponseDTO responseDTO = new UserResponseDTO(); BeanUtils.copyProperties(user, responseDTO); return ResultUtil.success(responseDTO, "新增用户成功"); } /** * 根据ID查询用户 * @GetMapping("/{id}"):GET请求方式,路径参数ID * @PathVariable:获取路径参数 */ @GetMapping("/{id}") public ResultVO<UserResponseDTO> getUserById(@PathVariable Long id) { log.info("接收查询用户请求,用户ID:{}", id); User user = userService.getUserById(id); UserResponseDTO responseDTO = new UserResponseDTO(); BeanUtils.copyProperties(user, responseDTO); return ResultUtil.success(responseDTO); } /** * 查询所有用户 * @GetMapping:GET请求方式,对应查询操作 */ @GetMapping public ResultVO<List<UserResponseDTO>> getAllUsers() { log.info("接收查询所有用户请求"); List<User> userList = userService.getAllUsers(); // 批量转换为响应DTO List<UserResponseDTO> responseDTOList = userList.stream() .map(user -> { UserResponseDTO dto = new UserResponseDTO(); BeanUtils.copyProperties(user, dto); return dto; }) .collect(Collectors.toList()); return ResultUtil.success(responseDTOList); } /** * 根据ID修改用户 * @PutMapping("/{id}"):PUT请求方式,对应修改操作 */ @PutMapping("/{id}") public ResultVO<UserResponseDTO> updateUser(@PathVariable Long id, @Validated @RequestBody UserRequestDTO userRequestDTO) { log.info("接收修改用户请求,用户ID:{},参数:{}", id, userRequestDTO); User user = userService.updateUser(id, userRequestDTO); UserResponseDTO responseDTO = new UserResponseDTO(); BeanUtils.copyProperties(user, responseDTO); return ResultUtil.success(responseDTO, "修改用户成功"); } /** * 根据ID删除用户 * @DeleteMapping("/{id}"):DELETE请求方式,对应删除操作 */ @DeleteMapping("/{id}") public ResultVO<Boolean> deleteUser(@PathVariable Long id) { log.info("接收删除用户请求,用户ID:{}", id); Boolean result = userService.deleteUser(id); return ResultUtil.success(result, "删除用户成功"); } } 5. 全局异常处理与响应工具(1)错误码枚举(ErrorCode.java):统一管理错误码与错误信息,便于维护: package com.example.springbootrestapidemo.exception; import lombok.AllArgsConstructor; import lombok.Getter; /** * 错误码枚举 */ @Getter @AllArgsConstructor public enum ErrorCode { // 通用错误 SUCCESS(200, "操作成功"), PARAM_ERROR(400, "参数校验失败"), USER_NOT_FOUND(404, "用户不存在"), USERNAME_ALREADY_EXISTS(409, "用户名已存在"), SYSTEM_ERROR(500, "系统内部错误"); // 错误码 private final Integer code; // 错误信息 private final String message; } (2)自定义业务异常(BusinessException.java):处理业务层面异常: package com.example.springbootrestapidemo.exception; import lombok.Getter; /** * 自定义业务异常 */ @Getter public class BusinessException extends RuntimeException { // 错误码 private final Integer code; // 错误信息 private final String message; // 构造方法(接收错误码枚举) public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); this.message = errorCode.getMessage(); } // 构造方法(自定义错误信息) public BusinessException(Integer code, String message) { super(message); this.code = code; this.message = message; } } (3)全局异常处理器(GlobalExceptionHandler.java):统一捕获所有异常,返回标准化响应,避免直接向客户端暴露异常堆栈信息: package com.example.springbootrestapidemo.exception; import com.example.springbootrestapidemo.util.ResultUtil; import com.example.springbootrestapidemo.util.ResultVO; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; /** * 全局异常处理器 * @RestControllerAdvice:全局异常处理注解,作用于所有@RestController */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理自定义业务异常 */ @ExceptionHandler(BusinessException.class) public ResultVO<Void> handleBusinessException(BusinessException e, HttpServletRequest request) { log.error("业务异常:请求路径={},错误码={},错误信息={}", request.getRequestURI(), e.getCode(), e.getMessage(), e); return ResultUtil.fail(e.getCode(), e.getMessage()); } /** * 处理参数校验异常(@Validated注解触发) */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { BindingResult bindingResult = e.getBindingResult(); // 获取第一个校验失败的字段与信息 FieldError fieldError = bindingResult.getFieldError(); String errorMsg = fieldError != null ? fieldError.getDefaultMessage() : "参数校验失败"; log.error("参数校验异常:请求路径={},错误信息={}", request.getRequestURI(), errorMsg, e); return ResultUtil.fail(ErrorCode.PARAM_ERROR.getCode(), errorMsg); } /** * 处理系统异常(兜底异常处理) */ @ExceptionHandler(Exception.class) public ResultVO<Void> handleSystemException(Exception e, HttpServletRequest request) { log.error("系统异常:请求路径={},错误信息={}", request.getRequestURI(), e.getMessage(), e); return ResultUtil.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage()); } } (4)响应结果工具类(ResultUtil.java)与响应VO(ResultVO.java):统一响应格式,便于前端解析: package com.example.springbootrestapidemo.util; import com.example.springbootrestapidemo.exception.ErrorCode; import lombok.Data; /** * 统一响应VO(View Object) * @param <T> 响应数据类型 */ @Data public class ResultVO<T> { // 状态码(200=成功,其他=失败) private Integer code; // 响应信息 private String message; // 响应数据 private T data; // 私有构造方法(通过静态方法创建对象) private ResultVO(Integer code, String message, T data) { this.code = code; this.message = message; this.data = data; } } // ResultUtil.java package com.example.springbootrestapidemo.util; import com.example.springbootrestapidemo.exception.ErrorCode; /** * 响应结果工具类(构建统一响应VO) */ public class ResultUtil { /** * 成功响应(无数据,默认信息) */ public static <T> ResultVO<T> success() { return new ResultVO<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), null); } /** * 成功响应(有数据,默认信息) */ public static <T> ResultVO<T> success(T data) { return new ResultVO<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMessage(), data); } /** * 成功响应(有数据,自定义信息) */ public static <T> ResultVO<T> success(T data, String message) { return new ResultVO<>(ErrorCode.SUCCESS.getCode(), message, data); } /** * 失败响应(自定义错误码与信息) */ public static <T> ResultVO<T> fail(Integer code, String message) { return new ResultVO<>(code, message, null); } /** * 失败响应(通过错误码枚举) */ public static <T> ResultVO<T> fail(ErrorCode errorCode) { return new ResultVO<>(errorCode.getCode(), errorCode.getMessage(), null); } } 6. 核心配置文件(application.yml)Spring Boot支持application.yml(推荐)与application.properties两种配置文件格式,yml格式更简洁,支持分层结构。本文配置端口、日志级别、热部署等核心参数: # 核心配置文件(默认环境) spring: profiles: active: dev # 激活开发环境配置(application-dev.yml) devtools: restart: enabled: true # 开启热部署 additional-paths: src/main/java # 热部署监听目录 exclude: WEB-INF/** # 热部署排除目录 # 服务器配置 server: port: 8080 # 服务端口 servlet: context-path: / # 应用上下文路径(默认/) # 日志配置 logging: level: root: INFO # 根日志级别 com.example.springbootrestapidemo: DEBUG # 项目包日志级别(开发环境设为DEBUG,生产环境设为INFO) file: name: logs/springboot-rest-api.log # 日志文件路径 max-size: 10MB # 单个日志文件最大大小 max-history: 7 # 日志文件保留天数 # 开发环境配置(application-dev.yml) # spring: # profiles: dev # logging: # level: # com.example.springbootrestapidemo: DEBUG # 生产环境配置(application-prod.yml) # spring: # profiles: prod # logging: # level: # com.example.springbootrestapidemo: INFO # server: # port: 80 # 生产环境默认80端口 四、接口测试与项目运行1. 项目运行直接运行SpringbootRestApiDemoApplication类的main方法,控制台输出“项目启动成功!访问地址:http://localhost:8080”表示启动成功。2. 接口测试(使用Postman)(1)新增用户:POST http://localhost:8080/api/v1/users,请求体(JSON): { "username": "zhangsan", "password": "Zhangsan123", "email": "zhangsan@example.com", "phone": "13812345678" } 响应结果(成功): { "code": 200, "message": "新增用户成功", "data": { "id": 1, "username": "zhangsan", "email": "zhangsan@example.com", "phone": "13812345678", "createTime": "2024-05-20T15:30:00", "updateTime": "2024-05-20T15:30:00" } } (2)查询用户:GET http://localhost:8080/api/v1/users/1,响应结果包含用户详细信息。(3)修改用户:PUT http://localhost:8080/api/v1/users/1,请求体修改对应参数,响应修改后的用户信息。(4)删除用户:DELETE http://localhost:8080/api/v1/users/1,响应删除成功信息。五、项目优化与扩展建议1. 密码加密:实际开发中,用户密码需通过BCrypt、SHA256等算法加密存储,Spring Boot提供了BCryptPasswordEncoder工具类,可直接使用。2. 数据库集成:替换内存存储为MySQL、PostgreSQL等关系型数据库,引入Spring Boot Starter Data JPA或MyBatis-Plus实现数据持久化。3. 分页查询:查询所有用户接口添加分页功能,通过PageHelper或Spring Data JPA的Pageable实现。4. 接口文档:引入Swagger/Knife4j自动生成接口文档,便于前后端对接。5. 权限控制:后续可引入Spring Security或Shiro实现接口权限管控。
-
1、 使用Git实现revert的完整操作步骤【转载】cid:link_02、C++中new关键字用法示例详解【转载】cid:link_13、在C# WinForm项目中跨.cs文件传值的六种常用方案【转载】cid:link_24、 一文带你搞懂Java中Error和Exception的区别【转载】cid:link_35、 Java中实现Word和TXT之间互相转换的实用教程【转载】cid:link_46、MyBatis-Plus 默认不更新null的4种方法【转载】cid:link_57、SpringBoot接口防抖的5种高效方案【转载】cid:link_68、 Java中锁分类及在什么场景下使用【转载】cid:link_79、 Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】cid:link_810、 Caffeine结合Redis空值缓存实现多级缓存【转载】cid:link_911、在PostgreSQL中优雅高效地进行全文检索的完整过程【转载】cid:link_1012、MySQL CDC原理解析及实现方案【转载】cid:link_1113、 PostgreSQL优雅的进行递归查询的实战指南【转载】cid:link_1214、Redis 常用命令之基础、进阶与场景化实战案例【转载】https://bbs.huaweicloud.com/forum/thread-0212720487861500817-1-1.html15、Git中忽略文件机制的.gitignore与.git/info/exclude两种方式详解【转载】https://bbs.huaweicloud.com/forum/thread-0212720487688092711-1-1.html
-
了解RedisRedis(Remote Dictionary Server)是一个开源的高性能键值对存储数据库。它支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。Redis的特点包括:内存存储:Redis将数据存储在内存中,因此读写速度非常快,适用于对性能有较高要求的场景。持久化:Redis支持持久化将内存中的数据保存到硬盘上,以便在服务器重启后能够恢复数据。数据结构多样:Redis不仅仅支持简单的键值对存储,还支持丰富的数据结构,例如列表、集合、有序集合等,使其具备更多的功能和用途。高并发:Redis是单线程模型,通过使用异步I/O和非阻塞I/O来支持高并发。多语言支持:Redis支持多种编程语言的客户端,如Java、Python、C#等,便于开发人员在不同平台上使用。发布/订阅:Redis支持发布/订阅模式,允许客户端订阅一个或多个频道并接收对应频道的消息。事务支持:Redis支持事务,可以在一个事务中执行多个命令,并保证这些命令的原子性。由于Redis具有高性能、灵活的数据结构和丰富的功能,它被广泛用于缓存、消息队列、计数器、实时排行榜、会话管理等多种应用场景。需求&为什么需要接口限流需求:针对相同IP,60s的接口请求次数不能超过10000次接口限流是为了保护系统和服务,防止因为过多的请求而导致系统过载、性能下降甚至崩溃。以下是进行接口限流的几个主要原因:防止恶意攻击:接口限流可以防止恶意用户或者攻击者通过大量的请求来攻击系统,保护系统的稳定性和安全性。保护系统资源:对于一些计算密集型或者资源消耗较大的接口,限制请求的频率可以避免服务器资源被过度消耗,保障其他正常请求的处理。避免雪崩效应:当某个服务不可用或者响应时间过长时,如果没有限流措施,大量请求可能会涌入后端,导致更多的请求失败,产生雪崩效应。提升系统性能:限流可以控制并发请求数,避免过多的请求导致服务器负载过高,从而提升系统的整体性能和响应速度。提供公平资源分配:通过限流,可以实现对不同用户或者不同服务请求的公平分配,避免某些请求占用过多资源而影响其他请求。综上所述,进行接口限流是保护系统和提升性能的重要手段,对于高并发的系统尤为重要。通过合理设置限流策略,可以有效地平衡资源利用和系统稳定性,提供更好的用户体验。实现方案方案一:固定时间段思路:当用户在第一次访问该接口时,向Redis中设置一个包含了用户IP和接口方法名的key,value的值初始化为1(表示第一次访问当前接口),同时设置该key的过期时间(60秒),只要此Redis的key没有过期,每次访问都将value的值自增1次,用户每次访问接口前,先从Redis中拿到当前接口访问次数,如果发现访问次数大于规定的次数(超过10000次),则向用户返回接口访问失败的标识。实现:(一)拦截器1、添加Redis依赖:首先在pom.xml文件中添加Spring Data Redis依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>2、 配置Redis连接信息:在application.properties或application.yml中配置Redis的连接信息,包括主机、端口、密码等。3、创建限流拦截器:在项目中创建一个限流拦截器,用于对用户IP进行接口限流。拦截器可以实现HandlerInterceptor接口,并重写preHandle方法进行限流逻辑。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.concurrent.TimeUnit; public class RateLimitInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ipAddress = getIpAddress(request); String uri = request.getRequestURI().replace("/","_"); String key = "apiVisits:" + uri + ":" + ipAddress; // 判断是否已经达到限流次数 String value = redisTemplate.opsForValue().get(key); // key 不存在,则是第一次请求设置过期时间 if(StringUtils.isBlank(value)){ redisTemplate.opsForValue().increment(key, 1); redisTemplate.expire(key, time, TimeUnit.SECONDS); return true; } if (value != null && Integer.parseInt(value) > 10) { response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); return false; } // 未达到限流次数,自增 redisTemplate.opsForValue().increment(key, 1); return true; } private String getIpAddress(HttpServletRequest request) { // 从请求头或代理头中获取真实IP地址 String ipAddress = request.getHeader("X-Forwarded-For"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); } return ipAddress; }}4、注册拦截器:在配置类中注册自定义的限流拦截器。123456789101112131415import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configurationpublic class WebMvcConfig implements WebMvcConfigurer { @Autowired private RateLimitInterceptor rateLimitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**"); }}(二)AOP以注解+切面的方式实现,将需要进行限流的API加上注解即可1、创建注解12345678910111213141516171819@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface CurrentLimiting { /** * 缓存key */ String key() default "apiVisits:"; /** * 限流时间,单位秒 */ int time() default 5; /** * 限流次数 */ int count() default 10;}2、创建AOP切面12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849@Slf4j@Aspect@Component@RequiredArgsConstructorpublic class CurrentLimitingAspect { private final RedisTemplate redisTemplate; /** * 带有注解的方法之前执行 */ @SuppressWarnings("unchecked") @Before("@annotation(currentLimiting)") public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable { int time = currentLimiting.time(); int count = currentLimiting.count(); // 将接口方法和用户IP构建Redis的key String key = getCurrentLimitingKey(currentLimiting.key(), point); // 判断是否已经达到限流次数 String value = redisTemplate.opsForValue().get(key); if (value != null && Integer.parseInt(value) > count) { log.error("接口限流,key:{},count:{},currentCount:{}", key, count, value); throw new RuntimeException("访问过于频繁,请稍后再试!"); } // 未达到限流次数,自增 redisTemplate.opsForValue().increment(key, 1); // key 不存在,则是第一次请求设置过期时间 if(StringUtils.isBlank(value)){ redisTemplate.expire(key, time, TimeUnit.SECONDS); } } /** * 组装 redis 的 key */ private String getCurrentLimitingKey(String prefixKey,JoinPoint point) { StringBuilder sb = new StringBuilder(prefixKey); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); sb.append( Utils.getIpAddress(request) ); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); return sb.append("_").append( targetClass.getName() ) .append("_").append(method.getName()).toString(); }}缺陷:当在10:00访问接口,这个时候向Reids写入一条数据访问次数为1,在10:59的时候突然访问了9999次,然后redis过期,在11:00访问了9999次,这样出现的问题就是在10:59到11:00之间访问了9999+9999次。故以固定时间段的方式进行限流可能会不起作用,会存在Reids过期的临界点内造成大量的用户访问。方案二:滑动窗口思路:由于方案一的时间是固定的,我们可以把固定的时间段改成动态的,也就是在用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前一分钟内用户访问该接口的总次数。如果总次数大于限流次数,则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过10000次。实现:1、创建注解12345678910111213141516171819@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface CurrentLimiting { /** * 缓存key */ String key() default "apiVisits:"; /** * 限流时间,单位秒 */ int time() default 5; /** * 限流次数 */ int count() default 10;}2、创建AOP切面123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657@Slf4j@Aspect@Component@RequiredArgsConstructorpublic class CurrentLimitingAspect { private final RedisTemplate redisTemplate; /** * 带有注解的方法之前执行 */ @SuppressWarnings("unchecked") @Before("@annotation(currentLimiting)") public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable { int time = currentLimiting.time(); int count = currentLimiting.count(); // 将接口方法和用户IP构建Redis的key String key = getCurrentLimitingKey(currentLimiting.key(), point); // 使用Zset的 score 设置成用户访问接口的时间戳 ZSetOperations zSetOperations = redisTemplate.opsForZSet(); // 当前时间戳 long currentTime = System.currentTimeMillis(); zSetOperations.add(key, currentTime, currentTime); // 设置过期时间防止key不消失 redisTemplate.expire(key, time, TimeUnit.SECONDS); // 移除 time 秒之前的访问记录,动态时间段 zSetOperations.removeRangeByScore(key, 0, currentTime - time * 1000); // 获得当前时间窗口内的访问记录数 Long currentCount = zSetOperations.zCard(key); // 限流判断 if (currentCount > count) { log.error("接口限流,key:{},count:{},currentCount:{}", key, count, currentCount); throw new RuntimeException("访问过于频繁,请稍后再试!"); } } /** * 组装 redis 的 key */ private String getCurrentLimitingKey(String prefixKey,JoinPoint point) { StringBuilder sb = new StringBuilder(prefixKey); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); sb.append( Utils.getIpAddress(request) ); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); return sb.append("_").append( targetClass.getName() ) .append("_").append(method.getName()).toString(); }}
-
接口防抖是一种用于限制用户重复提交请求的机制。在Web开发中,用户可能会因为网络延迟或者多次点击按钮而导致多次提交同一个请求,这可能会对系统产生不必要的压力,或者导致数据异常。接口防抖就是为了解决这个问题而设计的。接口防抖的基本原理是,在接收到一个请求后,服务器会在一定的时间内暂时忽略后续相同的请求,直到这段时间过去,才会再次处理新的请求。这样可以有效地避免重复提交导致的问题。常见的接口防抖实现方式包括基于缓存、基于Token、基于拦截器等。在Spring Boot中,我们可以利用拦截器、过滤器或者切面等机制来实现接口防抖功能。下面我们就来看看在SpringBoot中如何实现这些操作。基于缓存实现防抖这里使用了ConcurrentHashMap作为缓存来实现,当然在实际操作的时候还可以使用Redis缓存或者是Memcached来作为缓存操作。123456789101112131415161718192021@Componentpublic class DebounceInterceptor { private static final ConcurrentHashMap<String, Long> CACHE = new ConcurrentHashMap<>(); private static final long EXPIRE_TIME = 5000; // 5秒内重复请求会被拦截 public boolean shouldIntercept(HttpServletRequest request) { String key = request.getMethod() + ":" + request.getRequestURI() + ":" + request.getParameterMap(); long now = System.currentTimeMillis(); if (CACHE.containsKey(key)) { long lastRequestTime = CACHE.get(key); if (now - lastRequestTime < EXPIRE_TIME) { return true; // 请求被拦截 } } CACHE.put(key, now); return false; // 请求通过 }}在上面的代码中,我们使用ConcurrentHashMap来存储请求信息,其中key是请求的方法、URL和参数,value是请求的时间戳。如果同样的请求在5秒内重复出现,则会被拦截。然后,我们创建一个拦截器来应用这个防抖逻辑,如下所示。12345678910111213141516public class DebounceInterceptor implements HandlerInterceptor { @Autowired private DebounceService debounceService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (debounceService.shouldIntercept(request)) { // 返回429状态码表示请求过多 response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); return false; } return true; }}最后,在配置类中注册这个拦截器。如下所示。12345678@Configurationpublic class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DebounceInterceptor()).addPathPatterns("/**"); }}基于Token实现防抖通过在请求中添加Token,在一定时间内禁止相同的Token重复提交,代码如下所示。1234567891011121314151617181920212223@Componentpublic class TokenInterceptor { private static final ConcurrentHashMap<String, Long> CACHE = new ConcurrentHashMap<>(); private static final long EXPIRE_TIME = 5000; // 5秒内重复Token会被拦截 public boolean shouldIntercept(HttpServletRequest request) { String token = request.getHeader("X-Request-Token"); if (StringUtils.isEmpty(token)) { return false; // 请求通过,没有Token } long now = System.currentTimeMillis(); if (CACHE.containsKey(token)) { long lastRequestTime = CACHE.get(token); if (now - lastRequestTime < EXPIRE_TIME) { return true; // 请求被拦截 } } CACHE.put(token, now); return false; // 请求通过 }}在这种方法中,我们需要在请求头中添加一个Token,然后使用ConcurrentHashMap来存储Token和请求时间的映射关系。如果相同的Token在5秒内重复出现,则会被拦截。然后,我们创建一个拦截器来应用这个防抖逻辑,与上面的方法类似。基于注解实现防抖通过自定义注解来实现防抖,使得只需要在需要防抖的接口上添加注解即可。定义注解类12345@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Debounce { long value() default 5000; // 默认5秒内重复请求会被拦截}创建切面类来实现拦截注解的操作。1234567891011121314151617181920212223242526@Aspect@Componentpublic class DebounceAspect { private static final ConcurrentHashMap<String, Long> CACHE = new ConcurrentHashMap<>(); @Autowired private HttpServletRequest request; @Around("@annotation(debounce)") public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounce) throws Throwable { String key = request.getMethod() + ":" + request.getRequestURI() + ":" + request.getParameterMap(); long now = System.currentTimeMillis(); if (CACHE.containsKey(key)) { long lastRequestTime = CACHE.get(key); if (now - lastRequestTime < debounce.value()) { return null; // 请求被拦截 } } CACHE.put(key, now); return joinPoint.proceed(); // 请求通过 }}
-
背景:已经运行的项目,数据采用的是mysql数据,使用springBatch框架做的数据同步。然后信创改造,最近公司采购了华为GaussDB,需要切换mysql数据库到GaussDB。由于springBtach框架最新版本也不支持GaussDB。存在的问题:1、直接切换数据库连接,项目启动时springBatch会检查数据库类型,直接报错。求助:在不更换springBatch框架的情况下,有没有代码改动最小的改造方案?
-
一、监控体系设计1.1 关键监控指标java@Componentpublic class SSOMetricsCollector { private final MeterRegistry meterRegistry; // 计数器指标 private final Counter loginSuccessCounter; private final Counter loginFailureCounter; private final Counter tokenIssuedCounter; private final Counter tokenRefreshedCounter; private final Counter tokenRevokedCounter; // 计时器指标 private final Timer loginProcessingTimer; private final Timer tokenValidationTimer; private final Timer userInfoRetrievalTimer; // 仪表盘指标 private final Gauge activeSessionsGauge; private final Gauge activeTokensGauge; private final Gauge concurrentRequestsGauge; public SSOMetricsCollector(MeterRegistry meterRegistry, SessionService sessionService, TokenService tokenService) { this.meterRegistry = meterRegistry; // 初始化计数器 loginSuccessCounter = Counter.builder("sso.login.success") .description("Successful login attempts") .tag("type", "total") .register(meterRegistry); loginFailureCounter = Counter.builder("sso.login.failure") .description("Failed login attempts") .tag("type", "total") .register(meterRegistry); tokenIssuedCounter = Counter.builder("sso.token.issued") .description("Tokens issued") .register(meterRegistry); // 初始化计时器 loginProcessingTimer = Timer.builder("sso.login.duration") .description("Time spent processing login requests") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); // 初始化仪表盘 activeSessionsGauge = Gauge.builder("sso.sessions.active", sessionService::getActiveSessionCount) .description("Number of active sessions") .register(meterRegistry); activeTokensGauge = Gauge.builder("sso.tokens.active", tokenService::getActiveTokenCount) .description("Number of active tokens") .register(meterRegistry); } public void recordLoginSuccess(String username, String clientId, long duration) { loginSuccessCounter.increment(); // 按用户和客户端统计 meterRegistry.counter("sso.login.success", "username", username, "client_id", clientId ).increment(); loginProcessingTimer.record(duration, TimeUnit.MILLISECONDS); } public void recordLoginFailure(String username, String clientId, String reason) { loginFailureCounter.increment(); meterRegistry.counter("sso.login.failure", "username", username, "client_id", clientId, "reason", reason ).increment(); } public void recordTokenIssued(String tokenType, String username, String clientId) { tokenIssuedCounter.increment(); meterRegistry.counter("sso.token.issued.detail", "token_type", tokenType, "username", username, "client_id", clientId ).increment(); } public Map<String, Object> getHealthMetrics() { Map<String, Object> metrics = new HashMap<>(); // 系统健康指标 metrics.put("status", "UP"); metrics.put("timestamp", Instant.now().toString()); // 性能指标 metrics.put("login_success_rate", calculateSuccessRate()); metrics.put("avg_login_duration_ms", getAverageLoginDuration()); metrics.put("active_sessions", activeSessionsGauge.value()); metrics.put("active_tokens", activeTokensGauge.value()); // 系统资源指标 Runtime runtime = Runtime.getRuntime(); metrics.put("memory_used_mb", (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024); metrics.put("memory_max_mb", runtime.maxMemory() / 1024 / 1024); metrics.put("available_processors", runtime.availableProcessors()); return metrics; } private double calculateSuccessRate() { double success = loginSuccessCounter.count(); double failure = loginFailureCounter.count(); double total = success + failure; return total > 0 ? (success / total) * 100 : 0; } private double getAverageLoginDuration() { return loginProcessingTimer.mean(TimeUnit.MILLISECONDS); }}1.2 实时监控端点java@RestController@RequestMapping("/api/monitor")public class MonitorController { @Autowired private SSOMetricsCollector metricsCollector; @Autowired private AuditLogService auditLogService; @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { Map<String, Object> healthInfo = new HashMap<>(); healthInfo.put("service", "SSO Service"); healthInfo.put("status", "UP"); healthInfo.put("version", "1.0.0"); healthInfo.put("timestamp", Instant.now().toString()); // 添加指标数据 healthInfo.put("metrics", metricsCollector.getHealthMetrics()); // 检查依赖服务 healthInfo.put("dependencies", checkDependencies()); return ResponseEntity.ok(healthInfo); } @GetMapping("/metrics") public ResponseEntity<Map<String, Object>> metrics() { Map<String, Object> metrics = new HashMap<>(); // JVM指标 metrics.put("jvm", getJvmMetrics()); // 应用指标 metrics.put("application", getApplicationMetrics()); // 业务指标 metrics.put("business", getBusinessMetrics()); // 性能指标 metrics.put("performance", getPerformanceMetrics()); return ResponseEntity.ok(metrics); } @GetMapping("/audit/recent") public ResponseEntity<Page<AuditLog>> getRecentAuditLogs( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "timestamp")); Page<AuditLog> logs = auditLogService.getRecentLogs(pageable); return ResponseEntity.ok(logs); } @GetMapping("/audit/search") public ResponseEntity<List<AuditLog>> searchAuditLogs( @RequestParam(required = false) String username, @RequestParam(required = false) String action, @RequestParam(required = false) String resource, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate) { List<AuditLog> logs = auditLogService.searchLogs( username, action, resource, fromDate, toDate); return ResponseEntity.ok(logs); } @GetMapping("/alerts") public ResponseEntity<List<Alert>> getActiveAlerts() { List<Alert> alerts = alertService.getActiveAlerts(); return ResponseEntity.ok(alerts); } private Map<String, Object> getJvmMetrics() { Map<String, Object> jvmMetrics = new HashMap<>(); Runtime runtime = Runtime.getRuntime(); jvmMetrics.put("memory", Map.of( "used", runtime.totalMemory() - runtime.freeMemory(), "max", runtime.maxMemory(), "free", runtime.freeMemory() )); jvmMetrics.put("threads", Map.of( "active", Thread.activeCount(), "peak", ManagementFactory.getThreadMXBean().getPeakThreadCount() )); jvmMetrics.put("gc", getGarbageCollectionMetrics()); return jvmMetrics; } private Map<String, Object> getGarbageCollectionMetrics() { Map<String, Object> gcMetrics = new HashMap<>(); for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { gcMetrics.put(gcBean.getName(), Map.of( "count", gcBean.getCollectionCount(), "time", gcBean.getCollectionTime() )); } return gcMetrics; }}二、结构化日志2.1 Logback配置xml<?xml version="1.0" encoding="UTF-8"?><configuration> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/> <property name="JSON_LOG_PATTERN" value='{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}", "level":"%level", "thread":"%thread", "logger":"%logger", "message":"%msg", "tenant":"%X{tenantId}", "user":"%X{username}", "traceId":"%X{traceId}", "spanId":"%X{spanId}"}%n'/> <!-- 控制台输出(开发环境) --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- JSON文件输出(生产环境) --> <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/sso-service.json</file> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <customFields>{"service":"sso-service","environment":"${ENV:-dev}"}</customFields> <includeContext>true</includeContext> <includeMdc>true</includeMdc> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/sso-service-%d{yyyy-MM-dd}.json.gz</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender> <!-- 审计日志单独文件 --> <appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/audit.log</file> <encoder> <pattern>${JSON_LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/audit-%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> </appender> <!-- 安全日志单独文件 --> <appender name="SECURITY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/security.log</file> <encoder> <pattern>${JSON_LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/security-%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> </appender> <!-- Logger配置 --> <logger name="com.example.sso.audit" level="INFO" additivity="false"> <appender-ref ref="AUDIT_FILE"/> </logger> <logger name="com.example.sso.security" level="WARN" additivity="false"> <appender-ref ref="SECURITY_FILE"/> </logger> <!-- 第三方库日志级别 --> <logger name="org.springframework.security" level="INFO"/> <logger name="org.springframework.web" level="INFO"/> <logger name="org.hibernate" level="WARN"/> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="JSON_FILE"/> </root> </configuration>2.2 审计日志服务java@Service@Slf4jpublic class AuditLogService { @Autowired private AuditLogRepository auditLogRepository; @Autowired private MDCService mdcService; public void logAuthenticationSuccess(String username, String clientId, String ipAddress, String userAgent) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("LOGIN_SUCCESS") .resource("AUTHENTICATION") .details(Map.of( "client_id", clientId, "ip_address", ipAddress, "user_agent", userAgent, "result", "SUCCESS" )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 同时输出到日志文件 log.info("用户 {} 登录成功,客户端: {},IP: {}", username, clientId, ipAddress); } public void logAuthenticationFailure(String username, String clientId, String ipAddress, String userAgent, String failureReason) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("LOGIN_FAILURE") .resource("AUTHENTICATION") .details(Map.of( "client_id", clientId, "ip_address", ipAddress, "user_agent", userAgent, "failure_reason", failureReason, "result", "FAILURE" )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 安全告警日志 log.warn("用户 {} 登录失败,原因: {},IP: {}", username, failureReason, ipAddress); } public void logTokenIssued(String username, String tokenType, String clientId, Map<String, Object> tokenDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("TOKEN_ISSUED") .resource("TOKEN") .details(Map.of( "token_type", tokenType, "client_id", clientId, "token_details", tokenDetails )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); } public void logAccess(String username, String resource, String action, boolean allowed, Map<String, Object> accessDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action(action) .resource(resource) .details(Map.of( "access_allowed", allowed, "access_details", accessDetails )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); } public void logAdminAction(String adminUsername, String action, String targetResource, Map<String, Object> actionDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(adminUsername) .action("ADMIN_" + action) .resource(targetResource) .details(actionDetails) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 管理员操作需要特别记录 log.info("管理员 {} 执行操作: {},目标资源: {}", adminUsername, action, targetResource); } public Page<AuditLog> searchLogs(String username, String action, String resource, LocalDate fromDate, LocalDate toDate, Pageable pageable) { Specification<AuditLog> spec = Specification.where(null); if (username != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("username"), username)); } if (action != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("action"), action)); } if (resource != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("resource"), resource)); } if (fromDate != null) { spec = spec.and((root, query, cb) -> cb.greaterThanOrEqualTo(root.get("timestamp"), fromDate.atStartOfDay())); } if (toDate != null) { spec = spec.and((root, query, cb) -> cb.lessThan(root.get("timestamp"), toDate.plusDays(1).atStartOfDay())); } return auditLogRepository.findAll(spec, pageable); } public List<AuditStat> getAuditStatistics(LocalDate fromDate, LocalDate toDate) { List<AuditStat> stats = new ArrayList<>(); // 按操作类型统计 List<Object[]> actionStats = auditLogRepository .countByActionType(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay()); for (Object[] row : actionStats) { AuditStat stat = new AuditStat(); stat.setCategory("ACTION_TYPE"); stat.setKey((String) row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } // 按用户统计 List<Object[]> userStats = auditLogRepository .countByUser(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay(), 10); // 前10个 for (Object[] row : userStats) { AuditStat stat = new AuditStat(); stat.setCategory("TOP_USERS"); stat.setKey((String) row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } // 按时间段统计 List<Object[]> hourlyStats = auditLogRepository .countByHour(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay()); for (Object[] row : hourlyStats) { AuditStat stat = new AuditStat(); stat.setCategory("HOURLY_DISTRIBUTION"); stat.setKey("HOUR_" + row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } return stats; }}三、安全审计跟踪3.1 合规性审计java@Componentpublic class ComplianceAuditService { @Autowired private AuditLogRepository auditLogRepository; @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行 public void generateDailyComplianceReport() { LocalDate yesterday = LocalDate.now().minusDays(1); ComplianceReport report = new ComplianceReport(); report.setReportDate(yesterday); report.setGeneratedAt(Instant.now()); // 收集审计数据 report.setLoginStats(getLoginStatistics(yesterday)); report.setAccessStats(getAccessStatistics(yesterday)); report.setSecurityEvents(getSecurityEvents(yesterday)); report.setAdminActions(getAdminActions(yesterday)); // 检查合规性规则 report.setComplianceChecks(runComplianceChecks(yesterday)); // 生成报告文件 String reportContent = generateReportContent(report); saveReportToStorage(reportContent, yesterday); // 发送报告给相关人员 sendReportByEmail(report); log.info("生成合规性报告完成: {}", yesterday); } private Map<String, Object> getLoginStatistics(LocalDate date) { Map<String, Object> stats = new HashMap<>(); // 总登录次数 Long totalLogins = auditLogRepository.countLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("total_logins", totalLogins); // 成功/失败次数 Long successLogins = auditLogRepository.countSuccessfulLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); Long failedLogins = auditLogRepository.countFailedLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("successful_logins", successLogins); stats.put("failed_logins", failedLogins); stats.put("success_rate", totalLogins > 0 ? (double) successLogins / totalLogins * 100 : 0); // 唯一用户数 Long uniqueUsers = auditLogRepository.countUniqueUsersByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("unique_users", uniqueUsers); return stats; } private List<ComplianceCheck> runComplianceChecks(LocalDate date) { List<ComplianceCheck> checks = new ArrayList<>(); // 检查1: 是否有多次失败登录 ComplianceCheck check1 = new ComplianceCheck(); check1.setCheckName("多次失败登录检查"); check1.setDescription("检查是否有账户存在多次失败登录"); List<FailedLoginAttempt> failedAttempts = auditLogRepository .findExcessiveFailedLogins(date.atStartOfDay(), date.plusDays(1).atStartOfDay(), 5); check1.setPassed(failedAttempts.isEmpty()); check1.setDetails(failedAttempts); checks.add(check1); // 检查2: 管理员操作审计 ComplianceCheck check2 = new ComplianceCheck(); check2.setCheckName("管理员操作审计"); check2.setDescription("检查管理员关键操作是否都有记录"); List<AdminAction> adminActions = auditLogRepository .findAdminActionsWithoutAudit(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); check2.setPassed(adminActions.isEmpty()); check2.setDetails(adminActions); checks.add(check2); // 检查3: 令牌使用合规性 ComplianceCheck check3 = new ComplianceCheck(); check3.setCheckName("令牌合规性检查"); check3.setDescription("检查令牌颁发和使用是否符合策略"); List<TokenViolation> tokenViolations = tokenService .findTokenPolicyViolations(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); check3.setPassed(tokenViolations.isEmpty()); check3.setDetails(tokenViolations); checks.add(check3); return checks; }}3.2 实时告警系统java@Servicepublic class RealTimeAlertService { @Autowired private AlertRepository alertRepository; @Autowired private NotificationService notificationService; private final Map<String, AlertRule> alertRules = new ConcurrentHashMap<>(); @PostConstruct public void initAlertRules() { // 初始化告警规则 alertRules.put("MULTIPLE_FAILED_LOGINS", AlertRule.builder() .ruleId("MULTIPLE_FAILED_LOGINS") .name("多次登录失败") .description("同一账户在短时间内多次登录失败") .threshold(5) .timeWindow(300) // 5分钟 .severity(AlertSeverity.HIGH) .enabled(true) .build()); alertRules.put("SUSPICIOUS_IP", AlertRule.builder() .ruleId("SUSPICIOUS_IP") .name("可疑IP地址") .description("来自异常地理位置的登录") .threshold(1) .timeWindow(3600) // 1小时 .severity(AlertSeverity.MEDIUM) .enabled(true) .build()); alertRules.put("TOKEN_ABUSE", AlertRule.builder() .ruleId("TOKEN_ABUSE") .name("令牌滥用") .description("同一令牌在短时间内被频繁使用") .threshold(100) .timeWindow(60) // 1分钟 .severity(AlertSeverity.HIGH) .enabled(true) .build()); } @EventListener public void handleLoginFailure(AuthenticationFailureEvent event) { String username = event.getUsername(); String ipAddress = event.getIpAddress(); // 检查是否触发告警规则 checkMultipleFailedLogins(username, ipAddress); checkSuspiciousLocation(username, ipAddress); } @EventListener public void handleTokenUsage(TokenUsageEvent event) { String token = event.getToken(); String username = event.getUsername(); // 检查令牌使用频率 checkTokenUsageFrequency(token, username); } private void checkMultipleFailedLogins(String username, String ipAddress) { AlertRule rule = alertRules.get("MULTIPLE_FAILED_LOGINS"); if (!rule.isEnabled()) { return; } // 查询最近失败次数 long failedCount = auditLogRepository.countFailedLogins( username, ipAddress, rule.getTimeWindow()); if (failedCount >= rule.getThreshold()) { // 触发告警 Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId(rule.getRuleId()) .severity(rule.getSeverity()) .title("多次登录失败告警") .description(String.format("用户 %s 在 %d 分钟内登录失败 %d 次", username, rule.getTimeWindow() / 60, failedCount)) .details(Map.of( "username", username, "ip_address", ipAddress, "failed_count", failedCount, "time_window_minutes", rule.getTimeWindow() / 60 )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); } } private void checkSuspiciousLocation(String username, String ipAddress) { // 检查IP地理位置 GeoLocation location = ipGeoService.getLocation(ipAddress); if (location == null) { return; } // 获取用户常用登录地点 List<GeoLocation> usualLocations = userService.getUsualLocations(username); // 检查是否与常用地点差异较大 boolean isSuspicious = usualLocations.stream() .noneMatch(usual -> isNearby(usual, location, 100)); // 100公里内 if (isSuspicious) { Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId("SUSPICIOUS_IP") .severity(AlertSeverity.MEDIUM) .title("可疑地理位置登录") .description(String.format("用户 %s 从异常位置登录: %s", username, location.getCity())) .details(Map.of( "username", username, "ip_address", ipAddress, "location", location.toString(), "usual_locations", usualLocations.toString() )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); } } private void checkTokenUsageFrequency(String token, String username) { AlertRule rule = alertRules.get("TOKEN_ABUSE"); if (!rule.isEnabled()) { return; } // 检查令牌使用频率 long usageCount = tokenUsageService.getUsageCount( token, rule.getTimeWindow()); if (usageCount >= rule.getThreshold()) { Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId(rule.getRuleId()) .severity(rule.getSeverity()) .title("令牌滥用告警") .description(String.format("令牌在 %d 秒内被使用 %d 次", rule.getTimeWindow(), usageCount)) .details(Map.of( "username", username, "token", maskToken(token), "usage_count", usageCount, "time_window_seconds", rule.getTimeWindow() )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); // 可以自动采取措施,如临时锁定账户 userService.temporarilyLockAccount(username, 300); // 锁定5分钟 } } private void saveAlert(Alert alert) { alertRepository.save(alert); log.warn("触发安全告警: {}", alert.getTitle()); } private void sendAlertNotifications(Alert alert) { // 发送给安全团队 notificationService.sendSecurityAlert(alert); // 发送给用户(如果是其账户相关) if (alert.getDetails().containsKey("username")) { String username = (String) alert.getDetails().get("username"); notificationService.sendUserSecurityAlert(username, alert); } // 记录到审计日志 auditLogService.logSecurityAlert(alert); } private String maskToken(String token) { if (token == null || token.length() <= 8) { return "***"; } return token.substring(0, 4) + "..." + token.substring(token.length() - 4); } private boolean isNearby(GeoLocation loc1, GeoLocation loc2, double maxDistanceKm) { // 计算两个地理位置之间的距离 double distance = calculateDistance(loc1, loc2); return distance <= maxDistanceKm; }}
-
一、多租户架构设计1.1 租户隔离策略java@Configurationpublic class MultiTenantConfig { @Bean public CurrentTenantIdentifierResolver tenantIdentifierResolver() { return new CurrentTenantIdentifierResolver() { @Override public String resolveCurrentTenantIdentifier() { // 从请求头获取租户ID RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); String tenantId = request.getHeader("X-Tenant-Id"); if (tenantId == null) { // 从域名解析租户 tenantId = extractTenantFromDomain(request.getServerName()); } return tenantId != null ? tenantId : "default"; } return "default"; } @Override public boolean validateExistingCurrentSessions() { return false; } }; } @Bean public MultiTenantConnectionProvider multiTenantConnectionProvider() { return new MultiTenantConnectionProvider() { @Override public Connection getConnection(String tenantIdentifier) throws SQLException { DataSource dataSource = getTenantDataSource(tenantIdentifier); return dataSource.getConnection(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { connection.close(); } @Override public boolean supportsAggressiveRelease() { return true; } private DataSource getTenantDataSource(String tenantId) { // 根据租户ID获取对应的数据源 return dataSourceMap.get(tenantId); } }; } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver tenantIdentifierResolver) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource()); em.setPackagesToScan("com.example.sso"); em.setJpaVendorAdapter(jpaVendorAdapter()); Map<String, Object> properties = new HashMap<>(); properties.put("hibernate.multiTenancy", "DATABASE"); properties.put("hibernate.tenant_identifier_resolver", tenantIdentifierResolver); properties.put("hibernate.multi_tenant_connection_provider", multiTenantConnectionProvider); em.setJpaPropertyMap(properties); return em; }}1.2 租户数据路由java@Componentpublic class TenantDataRouter { private final Map<String, TenantConfig> tenantConfigs = new ConcurrentHashMap<>(); public TenantConfig getTenantConfig(String tenantId) { return tenantConfigs.computeIfAbsent(tenantId, this::loadTenantConfig); } public String getTenantDatabase(String tenantId) { TenantConfig config = getTenantConfig(tenantId); if (config.getDatabaseStrategy() == DatabaseStrategy.SHARED) { return "shared_db"; } else if (config.getDatabaseStrategy() == DatabaseStrategy.ISOLATED) { return "tenant_db_" + tenantId; } else { // 分片策略 int shard = Math.abs(tenantId.hashCode()) % 10; return "shard_db_" + shard; } } public String getTenantRedisPrefix(String tenantId) { return "tenant:" + tenantId + ":"; } public String getTenantStoragePath(String tenantId) { TenantConfig config = getTenantConfig(tenantId); if (config.getStorageStrategy() == StorageStrategy.SHARED) { return "/storage/shared/" + tenantId + "/"; } else { return "/storage/tenants/" + tenantId + "/"; } } private TenantConfig loadTenantConfig(String tenantId) { // 从数据库或配置中心加载租户配置 TenantConfig config = new TenantConfig(); config.setTenantId(tenantId); config.setDatabaseStrategy(DatabaseStrategy.ISOLATED); config.setStorageStrategy(StorageStrategy.SHARED); config.setCustomLoginPageEnabled(true); config.setBrandingEnabled(true); config.setMaxUsers(1000); config.setActive(true); return config; }}二、租户定制化2.1 动态登录页面java@Controller@RequestMapping("/{tenantId}/login")public class TenantLoginController { @Autowired private TenantService tenantService; @GetMapping public String loginPage(@PathVariable String tenantId, Model model, HttpServletRequest request) { TenantConfig tenantConfig = tenantService.getTenantConfig(tenantId); if (!tenantConfig.isActive()) { return "error/tenant-inactive"; } // 设置租户特定的属性 model.addAttribute("tenantId", tenantId); model.addAttribute("tenantName", tenantConfig.getTenantName()); model.addAttribute("logoUrl", tenantConfig.getLogoUrl()); model.addAttribute("primaryColor", tenantConfig.getPrimaryColor()); model.addAttribute("backgroundColor", tenantConfig.getBackgroundColor()); // 自定义字段 if (tenantConfig.getCustomFields() != null) { model.addAttribute("customFields", tenantConfig.getCustomFields()); } // 检查是否有自定义模板 String templatePath = findTenantTemplate(tenantId); if (templatePath != null) { return templatePath; } return "login/default"; } @PostMapping public String processLogin(@PathVariable String tenantId, @RequestParam String username, @RequestParam String password, HttpServletRequest request) { // 验证租户状态 if (!tenantService.isTenantActive(tenantId)) { return "redirect:/error/tenant-inactive"; } // 租户特定的认证逻辑 Authentication authentication = tenantAuthenticationService .authenticate(tenantId, username, password); if (authentication != null && authentication.isAuthenticated()) { // 设置租户上下文 TenantContext.setCurrentTenant(tenantId); // 生成租户特定的令牌 String token = tokenService.generateTenantToken( authentication, tenantId ); // 重定向到租户首页 return "redirect:/" + tenantId + "/dashboard"; } return "redirect:/" + tenantId + "/login?error"; } private String findTenantTemplate(String tenantId) { // 检查是否存在租户自定义模板 String[] templateLocations = { "classpath:templates/tenants/" + tenantId + "/login.html", "classpath:templates/tenants/" + tenantId + "/login.ftl", "file:/var/www/templates/" + tenantId + "/login.html" }; for (String location : templateLocations) { Resource resource = resourceLoader.getResource(location); if (resource.exists()) { return "tenants/" + tenantId + "/login"; } } return null; }}2.2 租户品牌管理java@Servicepublic class TenantBrandingService { @Autowired private StorageService storageService; public TenantBranding getBranding(String tenantId) { TenantBranding branding = cacheService.get("branding:" + tenantId, TenantBranding.class); if (branding == null) { branding = loadBrandingFromStorage(tenantId); cacheService.put("branding:" + tenantId, branding, 1, TimeUnit.HOURS); } return branding; } public void updateBranding(String tenantId, BrandingUpdateRequest request) { TenantBranding branding = new TenantBranding(); branding.setTenantId(tenantId); branding.setLogoUrl(request.getLogoUrl()); branding.setFaviconUrl(request.getFaviconUrl()); branding.setPrimaryColor(request.getPrimaryColor()); branding.setSecondaryColor(request.getSecondaryColor()); branding.setFontFamily(request.getFontFamily()); branding.setCustomCss(request.getCustomCss()); branding.setCustomJs(request.getCustomJs()); // 上传Logo文件 if (request.getLogoFile() != null && !request.getLogoFile().isEmpty()) { String logoPath = uploadBrandingFile(tenantId, "logo", request.getLogoFile()); branding.setLogoUrl(logoPath); } // 保存到存储 saveBrandingToStorage(tenantId, branding); // 清除缓存 cacheService.evict("branding:" + tenantId); // 生成CSS文件 generateBrandingCss(tenantId, branding); } private String uploadBrandingFile(String tenantId, String type, MultipartFile file) { String fileName = type + "_" + System.currentTimeMillis() + getFileExtension(file.getOriginalFilename()); String path = "tenants/" + tenantId + "/branding/" + fileName; storageService.upload(path, file); return storageService.getPublicUrl(path); } private void generateBrandingCss(String tenantId, TenantBranding branding) { String css = String.format(""" :root { --primary-color: %s; --secondary-color: %s; --font-family: %s; } .tenant-%s .header { background-color: %s; } .tenant-%s .btn-primary { background-color: %s; border-color: %s; } %s """, branding.getPrimaryColor(), branding.getSecondaryColor(), branding.getFontFamily(), tenantId, branding.getPrimaryColor(), tenantId, branding.getPrimaryColor(), branding.getSecondaryColor(), branding.getCustomCss() ); String cssPath = "tenants/" + tenantId + "/branding/custom.css"; storageService.upload(cssPath, css.getBytes()); }}三、租户配额管理java@Servicepublic class TenantQuotaService { @Autowired private TenantUsageRepository usageRepository; @Autowired private TenantQuotaRepository quotaRepository; public boolean checkUserQuota(String tenantId) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); TenantUsage usage = usageRepository.findByTenantId(tenantId); if (quota == null || usage == null) { return true; // 无限制 } if (quota.getMaxUsers() > 0 && usage.getActiveUsers() >= quota.getMaxUsers()) { return false; } return true; } public boolean checkApiQuota(String tenantId, String apiEndpoint) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); if (quota == null) { return true; } // 检查API调用频率限制 String rateLimitKey = "ratelimit:" + tenantId + ":" + apiEndpoint; Long count = redisTemplate.opsForValue().increment(rateLimitKey); if (count == 1) { redisTemplate.expire(rateLimitKey, 60, TimeUnit.SECONDS); } Integer rateLimit = quota.getApiRateLimits().get(apiEndpoint); if (rateLimit != null && count > rateLimit) { return false; } return true; } public boolean checkStorageQuota(String tenantId, long additionalBytes) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); TenantUsage usage = usageRepository.findByTenantId(tenantId); if (quota == null || usage == null) { return true; } long newUsage = usage.getStorageUsed() + additionalBytes; if (quota.getMaxStorage() > 0 && newUsage > quota.getMaxStorage()) { return false; } return true; } public void recordApiCall(String tenantId, String apiEndpoint) { // 记录API调用 String key = "api_usage:" + tenantId + ":" + LocalDate.now().format(DateTimeFormatter.ISO_DATE); redisTemplate.opsForHash().increment(key, apiEndpoint, 1); redisTemplate.expire(key, 30, TimeUnit.DAYS); // 更新每日统计 updateDailyUsage(tenantId); } public TenantUsageStats getUsageStats(String tenantId, LocalDate startDate, LocalDate endDate) { TenantUsageStats stats = new TenantUsageStats(); stats.setTenantId(tenantId); stats.setPeriod(startDate + " - " + endDate); // 获取活跃用户数 stats.setActiveUsers(getActiveUsersCount(tenantId)); // 获取API调用统计 stats.setApiCalls(getApiCallsCount(tenantId, startDate, endDate)); // 获取存储使用量 stats.setStorageUsed(getStorageUsage(tenantId)); // 获取登录统计 stats.setLoginCount(getLoginCount(tenantId, startDate, endDate)); return stats; }}四、租户管理APIjava@RestController@RequestMapping("/api/admin/tenants")@PreAuthorize("hasRole('SUPER_ADMIN')")public class TenantAdminController { @Autowired private TenantService tenantService; @PostMapping public ResponseEntity<TenantResponse> createTenant( @RequestBody @Valid CreateTenantRequest request) { Tenant tenant = tenantService.createTenant(request); TenantResponse response = TenantResponse.builder() .tenantId(tenant.getTenantId()) .name(tenant.getName()) .status(tenant.getStatus()) .createdAt(tenant.getCreatedAt()) .adminEmail(tenant.getAdminEmail()) .build(); return ResponseEntity.status(HttpStatus.CREATED) .body(response); } @PutMapping("/{tenantId}") public ResponseEntity<TenantResponse> updateTenant( @PathVariable String tenantId, @RequestBody @Valid UpdateTenantRequest request) { Tenant tenant = tenantService.updateTenant(tenantId, request); TenantResponse response = TenantResponse.builder() .tenantId(tenant.getTenantId()) .name(tenant.getName()) .status(tenant.getStatus()) .updatedAt(tenant.getUpdatedAt()) .build(); return ResponseEntity.ok(response); } @PostMapping("/{tenantId}/suspend") public ResponseEntity<Void> suspendTenant( @PathVariable String tenantId, @RequestParam(required = false) String reason) { tenantService.suspendTenant(tenantId, reason); // 通知租户管理员 notificationService.sendTenantSuspendedNotification(tenantId, reason); return ResponseEntity.ok().build(); } @PostMapping("/{tenantId}/activate") public ResponseEntity<Void> activateTenant(@PathVariable String tenantId) { tenantService.activateTenant(tenantId); return ResponseEntity.ok().build(); } @GetMapping("/{tenantId}/usage") public ResponseEntity<TenantUsageResponse> getTenantUsage( @PathVariable String tenantId, @RequestParam(defaultValue = "30") int days) { LocalDate endDate = LocalDate.now(); LocalDate startDate = endDate.minusDays(days); TenantUsageResponse usage = tenantService.getTenantUsage( tenantId, startDate, endDate); return ResponseEntity.ok(usage); } @PostMapping("/{tenantId}/quota") public ResponseEntity<Void> updateQuota( @PathVariable String tenantId, @RequestBody @Valid QuotaUpdateRequest request) { tenantService.updateQuota(tenantId, request); // 记录配额变更 auditService.logQuotaChange(tenantId, request); return ResponseEntity.ok().build(); } @GetMapping("/{tenantId}/billing") public ResponseEntity<BillingInfoResponse> getBillingInfo( @PathVariable String tenantId, @RequestParam(defaultValue = "current") String period) { BillingInfoResponse billing = billingService.getBillingInfo( tenantId, period); return ResponseEntity.ok(billing); }}五、租户间数据隔离java@Componentpublic class TenantDataFilter { @Autowired private TenantContext tenantContext; public <T> Specification<T> tenantSpecification() { return (root, query, criteriaBuilder) -> { String currentTenant = tenantContext.getCurrentTenant(); if (currentTenant == null || "super_admin".equals(currentTenant)) { return criteriaBuilder.conjunction(); // 超级管理员可以看到所有 } return criteriaBuilder.equal(root.get("tenantId"), currentTenant); }; } public Predicate addTenantFilter(CriteriaBuilder cb, Root<?> root, String tenantId) { return cb.equal(root.get("tenantId"), tenantId); } public void validateTenantAccess(String resourceTenantId) { String currentTenant = tenantContext.getCurrentTenant(); if (!resourceTenantId.equals(currentTenant) && !"super_admin".equals(currentTenant)) { throw new AccessDeniedException("无权访问其他租户数据"); } }}// JPA Repository使用示例@Repositorypublic interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { default List<User> findAllByTenant() { return findAll(tenantDataFilter.tenantSpecification()); } default Page<User> findPageByTenant(Pageable pageable) { return findAll(tenantDataFilter.tenantSpecification(), pageable); } @Query("SELECT u FROM User u WHERE u.tenantId = :tenantId AND u.username = :username") User findByTenantAndUsername(@Param("tenantId") String tenantId, @Param("username") String username);}
-
一、集群部署架构1.1 Redis集群配置yamlspring: redis: cluster: nodes: - redis-node1:6379 - redis-node2:6379 - redis-node3:6379 max-redirects: 3 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 shutdown-timeout: 100ms1.2 会话同步策略java@Configurationpublic class SessionConfig { @Bean public HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); } @Bean public FindByIndexNameSessionRepository<?> sessionRepository() { MapSessionRepository sessionRepository = new MapSessionRepository(); // 配置会话同步 RedisIndexedSessionRepository redisSessionRepository = new RedisIndexedSessionRepository(redisConnectionFactory()); redisSessionRepository.setDefaultMaxInactiveInterval(1800); redisSessionRepository.setRedisKeyNamespace("spring:session"); return redisSessionRepository; }}二、负载均衡策略2.1 Nginx配置示例nginxupstream sso_servers { least_conn; # 最少连接策略 server sso1.example.com:8080 weight=3; server sso2.example.com:8080 weight=2; server sso3.example.com:8080 weight=1; keepalive 32;}server { listen 443 ssl; server_name sso.example.com; ssl_certificate /etc/ssl/certs/sso.crt; ssl_certificate_key /etc/ssl/private/sso.key; location / { proxy_pass http://sso_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 健康检查 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 10s; } # 健康检查端点 location /health { access_log off; return 200 "healthy\n"; }}三、缓存优化3.1 多级缓存策略java@Servicepublic class MultiLevelCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; private final Cache<String, Object> localCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public Object getWithCache(String key, Supplier<Object> loader) { // 1. 检查本地缓存 Object value = localCache.getIfPresent(key); if (value != null) { return value; } // 2. 检查Redis缓存 value = redisTemplate.opsForValue().get(key); if (value != null) { localCache.put(key, value); return value; } // 3. 从数据源加载 value = loader.get(); if (value != null) { // 异步写入缓存 CompletableFuture.runAsync(() -> { redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); localCache.put(key, value); }); } return value; } public void refreshCache(String key) { localCache.invalidate(key); redisTemplate.delete(key); }}3.2 令牌缓存优化java@Componentpublic class TokenCacheManager { private final Cache<String, Claims> tokenCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats() .build(); public Claims getCachedClaims(String token) { return tokenCache.getIfPresent(token); } public void cacheClaims(String token, Claims claims) { tokenCache.put(token, claims); } public void invalidateToken(String token) { tokenCache.invalidate(token); } public CacheStats getStats() { return tokenCache.stats(); }}四、数据库优化4.1 分库分表策略java@Componentpublic class ShardingStrategy { public String getDatabaseName(String userId) { // 根据用户ID哈希分库 int hash = Math.abs(userId.hashCode()); int dbIndex = hash % 4; // 4个数据库 return "sso_db_" + dbIndex; } public String getTableName(String userId, String tablePrefix) { // 根据时间分表 LocalDateTime now = LocalDateTime.now(); String month = now.format(DateTimeFormatter.ofPattern("yyyyMM")); return tablePrefix + "_" + month; }}4.2 读写分离配置java@Configurationpublic class DataSourceConfig { @Bean @Primary public DataSource dataSource() { HikariDataSource master = createDataSource("master"); HikariDataSource slave = createDataSource("slave"); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); RoutingDataSource routingDataSource = new RoutingDataSource(); routingDataSource.setDefaultTargetDataSource(master); routingDataSource.setTargetDataSources(targetDataSources); return routingDataSource; } @Bean public AbstractRoutingDataSource routingDataSource() { return new ReadWriteRoutingDataSource(); }}public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); return isReadOnly ? "slave" : "master"; }}五、异步处理5.1 异步认证处理java@Servicepublic class AsyncAuthService { @Autowired private ThreadPoolTaskExecutor authTaskExecutor; @Async("authTaskExecutor") public CompletableFuture<AuthResult> authenticateAsync(LoginRequest request) { // 异步执行认证逻辑 AuthResult result = performAuthentication(request); return CompletableFuture.completedFuture(result); } @Bean public ThreadPoolTaskExecutor authTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("auth-executor-"); executor.initialize(); return executor; }}六、性能监控6.1 Micrometer监控配置java@Configurationpublic class MetricsConfig { @Bean public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config() .commonTags("application", "sso-service") .commonTags("environment", System.getenv("ENV")); } @Bean public TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); }}@Servicepublic class AuthMetricsService { private final MeterRegistry meterRegistry; private final Counter loginSuccessCounter; private final Counter loginFailureCounter; private final Timer loginTimer; public AuthMetricsService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; loginSuccessCounter = Counter.builder("sso.login.success") .description("Successful login attempts") .register(meterRegistry); loginFailureCounter = Counter.builder("sso.login.failure") .description("Failed login attempts") .register(meterRegistry); loginTimer = Timer.builder("sso.login.duration") .description("Login processing time") .register(meterRegistry); } public void recordLoginSuccess(String username) { loginSuccessCounter.increment(); meterRegistry.counter("sso.login.user", "username", username).increment(); } public Timer.Sample startLoginTimer() { return Timer.start(meterRegistry); } public void stopLoginTimer(Timer.Sample sample, String status) { sample.stop(loginTimer); }}
-
一、CSRF防护机制1.1 Spring Security CSRF配置java@Configuration@EnableWebSecuritypublic class CsrfSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) .ignoringRequestMatchers( "/api/auth/login", "/api/auth/logout", "/webhook/**" ) ) .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); return http.build(); }}@Componentpublic class CsrfCookieFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); if (csrfToken != null) { Cookie cookie = new Cookie("XSRF-TOKEN", csrfToken.getToken()); cookie.setPath("/"); cookie.setHttpOnly(false); cookie.setSecure(request.isSecure()); response.addCookie(cookie); } filterChain.doFilter(request, response); }}1.2 自定义CSRF策略java@Componentpublic class CustomCsrfTokenRepository implements CsrfTokenRepository { private final RedisTemplate<String, String> redisTemplate; private static final String CSRF_KEY_PREFIX = "csrf:"; @Override public CsrfToken generateToken(HttpServletRequest request) { String tokenId = UUID.randomUUID().toString(); return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", tokenId); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String sessionId = request.getSession().getId(); if (token == null) { redisTemplate.delete(CSRF_KEY_PREFIX + sessionId); } else { redisTemplate.opsForValue().set( CSRF_KEY_PREFIX + sessionId, token.getToken(), 30, TimeUnit.MINUTES ); } } @Override public CsrfToken loadToken(HttpServletRequest request) { String sessionId = request.getSession().getId(); String token = redisTemplate.opsForValue().get(CSRF_KEY_PREFIX + sessionId); if (token != null) { return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); } return null; }}二、XSS攻击防护2.1 输入输出过滤java@Componentpublic class XssFilter { private static final HtmlPolicyBuilder POLICY_BUILDER = new HtmlPolicyBuilder() .allowElements("a", "b", "i", "em", "strong", "p", "br") .allowAttributes("href").onElements("a") .requireRelNofollowOnLinks(); public String sanitize(String html) { if (StringUtils.isEmpty(html)) { return html; } PolicyFactory policy = POLICY_BUILDER.toFactory(); return policy.sanitize(html); } public Map<String, Object> sanitizeMap(Map<String, Object> data) { return data.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue() instanceof String ? sanitize((String) entry.getValue()) : entry.getValue() )); }}2.2 响应头安全配置java@Configurationpublic class SecurityHeadersConfig { @Bean public SecurityFilterChain securityHeadersFilter(HttpSecurity http) throws Exception { http .headers(headers -> headers .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'") ) .xssProtection(xss -> xss .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK) ) .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) ) .frameOptions(frame -> frame .sameOrigin() ) .contentTypeOptions(contentType -> {}) ); return http.build(); }}三、重放攻击防范3.1 Nonce机制实现java@Servicepublic class NonceService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String NONCE_KEY_PREFIX = "nonce:"; private static final long NONCE_EXPIRE_SECONDS = 300; // 5分钟 public String generateNonce(String clientId) { String nonce = UUID.randomUUID().toString(); String key = NONCE_KEY_PREFIX + clientId + ":" + nonce; redisTemplate.opsForValue().set( key, String.valueOf(System.currentTimeMillis()), NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS ); return nonce; } public boolean validateNonce(String clientId, String nonce) { String key = NONCE_KEY_PREFIX + clientId + ":" + nonce; if (!Boolean.TRUE.equals(redisTemplate.hasKey(key))) { return false; } // 使用后删除,防止重用 redisTemplate.delete(key); return true; }}3.2 时间戳验证java@Componentpublic class TimestampValidator { private static final long MAX_TIME_DIFF = 5 * 60 * 1000; // 5分钟 public boolean validateTimestamp(long requestTimestamp) { long currentTime = System.currentTimeMillis(); long timeDiff = Math.abs(currentTime - requestTimestamp); return timeDiff <= MAX_TIME_DIFF; } public boolean validateRequest(String clientId, long timestamp, String nonce, String signature) { // 1. 验证时间戳 if (!validateTimestamp(timestamp)) { return false; } // 2. 验证Nonce NonceService nonceService = new NonceService(); if (!nonceService.validateNonce(clientId, nonce)) { return false; } // 3. 验证签名 String dataToSign = clientId + timestamp + nonce; String expectedSignature = calculateSignature(dataToSign); return expectedSignature.equals(signature); } private String calculateSignature(String data) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("计算签名失败", e); } }}四、暴力破解防护4.1 登录尝试限制java@Servicepublic class LoginAttemptService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String ATTEMPT_KEY_PREFIX = "login:attempts:"; private static final String BLOCKED_KEY_PREFIX = "login:blocked:"; private static final int MAX_ATTEMPTS = 5; private static final long BLOCK_DURATION = 15 * 60 * 1000; // 15分钟 public void loginSucceeded(String username, String ipAddress) { String key = ATTEMPT_KEY_PREFIX + username + ":" + ipAddress; redisTemplate.delete(key); } public void loginFailed(String username, String ipAddress) { String key = ATTEMPT_KEY_PREFIX + username + ":" + ipAddress; String attemptsStr = redisTemplate.opsForValue().get(key); int attempts = attemptsStr != null ? Integer.parseInt(attemptsStr) : 0; attempts++; redisTemplate.opsForValue().set(key, String.valueOf(attempts), 1, TimeUnit.HOURS); if (attempts >= MAX_ATTEMPTS) { blockUser(username, ipAddress); } } public boolean isBlocked(String username, String ipAddress) { String key = BLOCKED_KEY_PREFIX + username + ":" + ipAddress; if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { // 检查是否超过封锁时间 Long ttl = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); return ttl != null && ttl > 0; } return false; } private void blockUser(String username, String ipAddress) { String key = BLOCKED_KEY_PREFIX + username + ":" + ipAddress; redisTemplate.opsForValue().set(key, "blocked", BLOCK_DURATION, TimeUnit.MILLISECONDS); }}4.2 验证码集成java@Servicepublic class CaptchaService { @Autowired private RedisTemplate<String, String> redisTemplate; public CaptchaResponse generateCaptcha(String sessionId) { // 生成验证码文本 String captchaText = generateRandomText(6); // 生成验证码图片 BufferedImage image = generateCaptchaImage(captchaText); // 存储验证码 String key = "captcha:" + sessionId; redisTemplate.opsForValue().set(key, captchaText, 5, TimeUnit.MINUTES); // 转换为Base64 String imageBase64 = imageToBase64(image); return new CaptchaResponse(imageBase64, sessionId); } public boolean validateCaptcha(String sessionId, String userInput) { String key = "captcha:" + sessionId; String storedCaptcha = redisTemplate.opsForValue().get(key); if (storedCaptcha == null) { return false; } // 验证后删除 redisTemplate.delete(key); return storedCaptcha.equalsIgnoreCase(userInput); } public boolean isCaptchaRequired(String username, String ipAddress) { LoginAttemptService attemptService = new LoginAttemptService(); return attemptService.isBlocked(username, ipAddress); }}五、会话安全5.1 会话固定防护java@Configurationpublic class SessionFixationProtectionConfig { @Bean public SecurityFilterChain sessionFixationFilter(HttpSecurity http) throws Exception { http .sessionManagement(session -> session .sessionFixation(sessionFixation -> sessionFixation .migrateSession() ) .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredUrl("/login?expired") ); return http.build(); }}5.2 会话劫持检测java@Componentpublic class SessionHijackingDetector { public boolean detectHijacking(HttpServletRequest request, HttpSession session) { // 检查User-Agent String currentUserAgent = request.getHeader("User-Agent"); String storedUserAgent = (String) session.getAttribute("userAgent"); if (storedUserAgent != null && !storedUserAgent.equals(currentUserAgent)) { return true; } // 检查IP地址 String currentIp = getClientIp(request); String storedIp = (String) session.getAttribute("clientIp"); if (storedIp != null && !storedIp.equals(currentIp)) { return true; } // 更新会话属性 session.setAttribute("userAgent", currentUserAgent); session.setAttribute("clientIp", currentIp); return false; } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; }}六、安全最佳实践6.1 定期安全扫描java@Component@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行public class SecurityScanner { public void scanForVulnerabilities() { // 1. 检查弱密码用户 scanWeakPasswords(); // 2. 检查过期会话 cleanExpiredSessions(); // 3. 检查异常登录 analyzeLoginPatterns(); // 4. 生成安全报告 generateSecurityReport(); } private void scanWeakPasswords() { // 实现弱密码检测逻辑 } private void cleanExpiredSessions() { // 清理过期会话 }}
-
一、微服务SSO架构1.1 网关统一认证text┌─────────┐ ┌─────────────┐ ┌─────────────┐│ 客户端 │──▶│ API网关 │──▶│ 认证服务 │└─────────┘ └─────────────┘ └─────────────┘ │ │ ┌──────▼──────┐ ┌──────▼──────┐ │ 服务A │ │ 用户服务 │ └─────────────┘ └─────────────┘ │ ┌──────▼──────┐ │ 服务B │ └─────────────┘二、Spring Cloud Gateway集成2.1 网关配置yamlspring: cloud: gateway: routes: - id: auth-service uri: http://localhost:8081 predicates: - Path=/auth/** filters: - StripPrefix=1 - id: user-service uri: http://localhost:8082 predicates: - Path=/api/users/** filters: - name: JwtAuthFilter args: jwt-secret: ${JWT_SECRET} - id: product-service uri: http://localhost:8083 predicates: - Path=/api/products/** filters: - name: JwtAuthFilter - AddRequestHeader=X-User-Id, ${user.id}2.2 网关JWT过滤器java@Componentpublic class JwtAuthFilter extends AbstractGatewayFilterFactory<JwtAuthFilter.Config> { @Autowired private JwtTokenProvider tokenProvider; public JwtAuthFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); // 检查是否为公开端点 if (isPublicEndpoint(request.getPath().toString())) { return chain.filter(exchange); } // 获取令牌 String token = extractToken(request); if (token == null || !tokenProvider.validateToken(token)) { return unauthorized(exchange, "无效的认证令牌"); } // 验证通过,添加用户信息到请求头 String username = tokenProvider.getUsername(token); List<String> roles = tokenProvider.getRoles(token); ServerHttpRequest mutatedRequest = request.mutate() .header("X-User-Id", username) .header("X-User-Roles", String.join(",", roles)) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }; } private boolean isPublicEndpoint(String path) { return path.startsWith("/auth/login") || path.startsWith("/auth/register") || path.startsWith("/public/"); } private String extractToken(ServerHttpRequest request) { List<String> authHeaders = request.getHeaders().get("Authorization"); if (authHeaders != null && !authHeaders.isEmpty()) { String authHeader = authHeaders.get(0); if (authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } } return null; } private Mono<Void> unauthorized(ServerWebExchange exchange, String message) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); String body = String.format("{\"error\": \"Unauthorized\", \"message\": \"%s\"}", message); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes()); return response.writeWith(Mono.just(buffer)); } public static class Config { // 配置属性 }}三、服务间认证3.1 Feign客户端配置java@Configurationpublic class FeignConfig { @Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { // 从安全上下文获取当前用户的令牌 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getCredentials() instanceof String) { String token = (String) authentication.getCredentials(); requestTemplate.header("Authorization", "Bearer " + token); } else if (authentication instanceof JwtAuthenticationToken) { JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; requestTemplate.header("Authorization", "Bearer " + jwtAuth.getToken().getTokenValue()); } }; }}3.2 服务间JWT传播java@Servicepublic class ServiceClient { @Autowired private RestTemplate restTemplate; @Value("${service-b.url}") private String serviceBUrl; public String callServiceB() { // 获取当前用户的JWT令牌 String token = getCurrentUserToken(); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.set("X-Service-Request", "true"); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = restTemplate.exchange( serviceBUrl + "/api/data", HttpMethod.GET, entity, String.class ); return response.getBody(); } private String getCurrentUserToken() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof JwtAuthenticationToken) { JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; return jwtAuth.getToken().getTokenValue(); } else if (authentication instanceof AbstractOAuth2TokenAuthenticationToken) { AbstractOAuth2TokenAuthenticationToken<?> oauth2Auth = (AbstractOAuth2TokenAuthenticationToken<?>) authentication; return oauth2Auth.getToken().getTokenValue(); } throw new IllegalStateException("无法获取当前用户的认证令牌"); }}四、分布式会话管理4.1 Spring Session Redis配置java@Configuration@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } @Bean public HttpSessionIdResolver httpSessionIdResolver() { // 支持多种方式传递Session ID return new HeaderHttpSessionIdResolver("X-Auth-Token"); }}4.2 会话共享过滤器java@Componentpublic class SessionSharingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // 从请求头获取会话ID String sessionId = httpRequest.getHeader("X-Session-Id"); if (sessionId != null) { // 设置当前会话ID RequestContextHolder.currentRequestAttributes().setSessionId(sessionId); } chain.doFilter(request, response); // 将会话ID添加到响应头 if (sessionId == null) { HttpSession session = httpRequest.getSession(false); if (session != null) { httpResponse.setHeader("X-Session-Id", session.getId()); } } }}五、服务注册发现集成5.1 Eureka客户端配置java@SpringBootApplication@EnableDiscoveryClientpublic class AuthServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); }}5.2 动态服务发现java@Servicepublic class ServiceDiscoveryClient { @Autowired private DiscoveryClient discoveryClient; @Autowired private LoadBalancerClient loadBalancer; public String getServiceUrl(String serviceId) { List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); if (instances.isEmpty()) { throw new ServiceUnavailableException("服务 " + serviceId + " 不可用"); } ServiceInstance instance = loadBalancer.choose(serviceId); return instance.getUri().toString(); } public List<String> getAllAuthServices() { return discoveryClient.getServices().stream() .filter(service -> service.endsWith("-auth-service")) .collect(Collectors.toList()); }}六、配置中心集成6.1 Spring Cloud Config配置yaml# bootstrap.ymlspring: application: name: sso-service cloud: config: uri: http://config-server:8888 fail-fast: true retry: max-attempts: 6 max-interval: 10000 encrypt: key: ${CONFIG_ENCRYPT_KEY}6.2 动态JWT配置java@Configuration@RefreshScopepublic class DynamicJwtConfig { @Value("${jwt.secret:default-secret}") private String secret; @Value("${jwt.expiration:3600}") private Long expiration; @Bean @RefreshScope public JwtTokenProvider jwtTokenProvider() { return new JwtTokenProvider(secret, expiration); }}七、熔断与降级7.1 Resilience4j配置yamlresilience4j: circuitbreaker: instances: authService: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 retry: instances: authService: maxAttempts: 3 waitDuration: 500ms timelimiter: instances: authService: timeoutDuration: 3s7.2 认证服务熔断器java@Service@Slf4jpublic class AuthServiceWithCircuitBreaker { @Autowired private AuthService authService; @CircuitBreaker(name = "authService", fallbackMethod = "fallbackLogin") @TimeLimiter(name = "authService") @Retry(name = "authService") public CompletableFuture<AuthResponse> loginAsync(LoginRequest request) { return CompletableFuture.supplyAsync(() -> authService.login(request)); } private CompletableFuture<AuthResponse> fallbackLogin( LoginRequest request, Throwable throwable) { log.warn("认证服务降级处理,请求: {}, 异常: {}", request.getUsername(), throwable.getMessage()); // 返回降级响应 AuthResponse response = new AuthResponse(); response.setError("认证服务暂时不可用,请稍后重试"); response.setFallback(true); return CompletableFuture.completedFuture(response); }}八、监控与追踪8.1 Spring Cloud Sleuth集成yamlspring: sleuth: sampler: probability: 1.0 propagation: type: B3 baggage: remote-fields: - X-User-Id - X-Request-ID logging: pattern: level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"8.2 自定义追踪过滤器java@Componentpublic class TraceFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 添加追踪信息 String traceId = httpRequest.getHeader("X-B3-TraceId"); if (traceId == null) { traceId = generateTraceId(); } MDC.put("traceId", traceId); MDC.put("spanId", generateSpanId()); try { chain.doFilter(request, response); } finally { MDC.clear(); } } private String generateTraceId() { return UUID.randomUUID().toString().replace("-", ""); } private String generateSpanId() { return Long.toHexString(new Random().nextLong()); }}九、API版本管理9.1 版本化认证端点java@RestController@RequestMapping("/api/v{auth-api-version}/auth")public class VersionedAuthController { @PostMapping("/login") public ResponseEntity<?> loginV1(@PathVariable("auth-api-version") String version, @RequestBody LoginRequest request) { if ("1".equals(version)) { return loginV1(request); } else if ("2".equals(version)) { return loginV2(request); } return ResponseEntity.status(HttpStatus.NOT_FOUND) .body("不支持的API版本"); } private ResponseEntity<?> loginV1(LoginRequest request) { // V1版本实现 return ResponseEntity.ok("V1 response"); } private ResponseEntity<?> loginV2(LoginRequest request) { // V2版本实现,支持多因素认证 return ResponseEntity.ok("V2 response with MFA support"); }}十、微服务SSO最佳实践服务治理:使用API网关统一入口实施服务熔断和降级配置合理的超时和重试安全策略:服务间使用mTLS相互认证实施最小权限原则定期轮换服务凭证可观测性:集成分布式追踪收集认证相关指标设置告警规则部署策略:认证服务独立部署支持蓝绿部署配置自动伸缩数据一致性:使用分布式会话存储实现最终一致性定期清理过期数据
-
一、JWT核心原理1.1 JWT结构详解JWT由三部分组成,以点分隔:textHeader.Payload.SignatureHeader示例:json{ "alg": "HS256", "typ": "JWT"}Payload示例:json{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622, "roles": ["USER", "ADMIN"]}二、JWT工具类实现2.1 完整JWT工具类java@Component@Slf4jpublic class JwtTokenProvider { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long validityInMilliseconds; @Value("${jwt.refresh-expiration}") private long refreshValidityInMilliseconds; // 生成访问令牌 public String createAccessToken(String username, List<String> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("roles", roles); claims.put("type", "ACCESS"); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) .compact(); } // 生成刷新令牌 public String createRefreshToken(String username) { Claims claims = Jwts.claims().setSubject(username); claims.put("type", "REFRESH"); Date now = new Date(); Date validity = new Date(now.getTime() + refreshValidityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) .compact(); } // 解析用户名 public String getUsername(String token) { return getAllClaimsFromToken(token).getSubject(); } // 获取用户角色 @SuppressWarnings("unchecked") public List<String> getRoles(String token) { return getAllClaimsFromToken(token).get("roles", List.class); } // 获取过期时间 public Date getExpirationDate(String token) { return getAllClaimsFromToken(token).getExpiration(); } // 验证令牌是否过期 public boolean isTokenExpired(String token) { final Date expiration = getExpirationDate(token); return expiration.before(new Date()); } // 验证令牌有效性 public boolean validateToken(String token) { try { Jwts.parser() .setSigningKey(secretKey.getBytes()) .parseClaimsJws(token); return !isTokenExpired(token); } catch (JwtException | IllegalArgumentException e) { log.warn("无效的JWT令牌: {}", e.getMessage()); return false; } } // 获取令牌中的所有声明 private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secretKey.getBytes()) .parseClaimsJws(token) .getBody(); } // 从请求头提取令牌 public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; }}三、JWT过滤器配置3.1 JWT认证过滤器java@Component@Slf4jpublic class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider jwtTokenProvider; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { String username = jwtTokenProvider.getUsername(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); // 刷新令牌逻辑 refreshTokenIfNeeded(token, response); } } catch (Exception e) { log.error("无法设置用户认证: {}", e.getMessage()); } filterChain.doFilter(request, response); } private void refreshTokenIfNeeded(String token, HttpServletResponse response) { long expirationTime = jwtTokenProvider.getExpirationDate(token).getTime(); long currentTime = System.currentTimeMillis(); long remainingTime = expirationTime - currentTime; // 如果令牌将在30分钟内过期,则刷新 if (remainingTime < 30 * 60 * 1000) { String username = jwtTokenProvider.getUsername(token); List<String> roles = jwtTokenProvider.getRoles(token); String newToken = jwtTokenProvider.createAccessToken(username, roles); response.setHeader("X-New-Access-Token", newToken); } }}四、令牌黑名单实现4.1 黑名单管理java@Servicepublic class TokenBlacklistService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String BLACKLIST_KEY = "token:blacklist:"; // 添加令牌到黑名单 public void blacklistToken(String token, long ttlInSeconds) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); redisTemplate.opsForValue().set(key, "blacklisted", ttlInSeconds, TimeUnit.SECONDS); } // 检查令牌是否在黑名单中 public boolean isTokenBlacklisted(String token) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } // 从黑名单中移除令牌 public void removeFromBlacklist(String token) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); redisTemplate.delete(key); }}4.2 增强的JWT验证java@Componentpublic class EnhancedJwtTokenProvider extends JwtTokenProvider { @Autowired private TokenBlacklistService blacklistService; @Override public boolean validateToken(String token) { if (blacklistService.isTokenBlacklisted(token)) { return false; } return super.validateToken(token); }}五、配置文件示例5.1 application.ymlyamljwt: secret: "mySuperSecretKeyThatIsAtLeast32BytesLongForHS256" expiration: 3600000 # 1小时(毫秒) refresh-expiration: 86400000 # 24小时(毫秒) issuer: "sso-server" audience: "web-client" redis: host: localhost port: 6379 password: timeout: 2000ms database: 0 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0六、API接口设计6.1 认证控制器java@RestController@RequestMapping("/api/auth")public class AuthController { @Autowired private JwtTokenProvider tokenProvider; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenBlacklistService blacklistService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); User user = (User) authentication.getPrincipal(); List<String> roles = user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); // 生成令牌 String accessToken = tokenProvider.createAccessToken(user.getUsername(), roles); String refreshToken = tokenProvider.createRefreshToken(user.getUsername()); AuthResponse response = AuthResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(tokenProvider.getExpirationDate(accessToken).getTime()) .username(user.getUsername()) .roles(roles) .build(); return ResponseEntity.ok(response); } catch (AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("认证失败", e.getMessage())); } } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) { try { String refreshToken = request.getRefreshToken(); if (!tokenProvider.validateToken(refreshToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("刷新令牌无效或已过期")); } String username = tokenProvider.getUsername(refreshToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String newAccessToken = tokenProvider.createAccessToken(username, roles); AuthResponse response = AuthResponse.builder() .accessToken(newAccessToken) .refreshToken(refreshToken) // 可以重新生成刷新令牌 .tokenType("Bearer") .expiresIn(tokenProvider.getExpirationDate(newAccessToken).getTime()) .username(username) .roles(roles) .build(); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("令牌刷新失败", e.getMessage())); } } @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletRequest request) { String token = tokenProvider.resolveToken(request); if (token != null) { // 将令牌加入黑名单,剩余有效期同令牌有效期 long expirationTime = tokenProvider.getExpirationDate(token).getTime(); long currentTime = System.currentTimeMillis(); long ttlInSeconds = (expirationTime - currentTime) / 1000; if (ttlInSeconds > 0) { blacklistService.blacklistToken(token, ttlInSeconds); } // 清除安全上下文 SecurityContextHolder.clearContext(); } return ResponseEntity.ok("登出成功"); } @GetMapping("/validate") public ResponseEntity<?> validateToken(@RequestParam String token) { boolean isValid = tokenProvider.validateToken(token); Map<String, Object> response = new HashMap<>(); response.put("valid", isValid); if (isValid) { response.put("username", tokenProvider.getUsername(token)); response.put("roles", tokenProvider.getRoles(token)); response.put("expiresAt", tokenProvider.getExpirationDate(token)); } return ResponseEntity.ok(response); }}七、安全配置7.1 Spring Security配置java@Configuration@EnableWebSecuritypublic class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private CustomUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint()); return http.build(); } @Bean public AuthenticationEntryPoint jwtAuthenticationEntryPoint() { return (request, response, authException) -> { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write( "{\"error\": \"Unauthorized\", \"message\": \"无效或过期的令牌\"}" ); }; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; }}八、JWT优化实践8.1 令牌压缩(针对长令牌)java@Componentpublic class TokenCompressionUtil { // 压缩令牌(当payload很大时) public String compressToken(String token) { byte[] compressed = LZ4Factory.fastestInstance() .fastCompressor() .compress(token.getBytes(StandardCharsets.UTF_8)); return Base64.getUrlEncoder().encodeToString(compressed); } // 解压令牌 public String decompressToken(String compressedToken) { byte[] decoded = Base64.getUrlDecoder().decode(compressedToken); byte[] decompressed = LZ4Factory.fastestInstance() .fastDecompressor() .decompress(decoded, 1024 * 1024); // 1MB max return new String(decompressed, StandardCharsets.UTF_8); }}8.2 令牌轮换策略java@Servicepublic class TokenRotationService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String TOKEN_FAMILY_KEY = "token:family:"; // 创建令牌族(用于刷新令牌时轮换) public String createTokenFamily(String userId) { String familyId = UUID.randomUUID().toString(); String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; redisTemplate.opsForValue().set(key, "active", 30, TimeUnit.DAYS); return familyId; } // 验证令牌族 public boolean isValidTokenFamily(String userId, String familyId) { String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } // 撤销令牌族 public void revokeTokenFamily(String userId, String familyId) { String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; redisTemplate.delete(key); }}九、监控和审计9.1 JWT使用监控java@Component@Slf4jpublic class JwtUsageMonitor { private final MeterRegistry meterRegistry; public JwtUsageMonitor(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } // 记录令牌生成 public void recordTokenGeneration(String username) { meterRegistry.counter("jwt.tokens.generated", "username", username).increment(); log.info("为用户 {} 生成了新的JWT令牌", username); } // 记录令牌验证 public void recordTokenValidation(String username, boolean valid) { if (valid) { meterRegistry.counter("jwt.tokens.validated.success", "username", username).increment(); } else { meterRegistry.counter("jwt.tokens.validated.failed", "username", username).increment(); log.warn("用户 {} 的JWT令牌验证失败", username); } } // 记录令牌刷新 public void recordTokenRefresh(String username) { meterRegistry.counter("jwt.tokens.refreshed", "username", username).increment(); log.info("用户 {} 刷新了JWT令牌", username); }}十、最佳实践总结密钥管理:使用至少32字节的密钥定期轮换密钥密钥与代码分离存储令牌设计:访问令牌有效期1-2小时刷新令牌有效期7-30天包含必要的最小信息安全措施:强制HTTPS传输实现令牌黑名单防止重放攻击监控异常使用模式性能优化:合理设置令牌大小使用高效的签名算法缓存验证结果运维建议:记录所有令牌操作设置令牌使用阈值告警定期审计令牌策略
-
一、OAuth2.0核心概念OAuth2.0是一个授权框架,不是认证协议,但常被用于实现SSO。它定义了四种授权模式,其中授权码模式最适合Web应用的SSO场景。1.1 核心角色资源所有者:用户客户端:需要访问资源的应用授权服务器:颁发访问令牌资源服务器:保护资源的服务器1.2 授权流程(授权码模式)text1. 用户访问客户端2. 客户端重定向到授权服务器3. 用户登录并授权4. 授权服务器返回授权码5. 客户端用授权码交换访问令牌6. 客户端用访问令牌访问资源二、Spring Security OAuth2配置2.1 授权服务器配置java@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("webapp") .secret(passwordEncoder.encode("websecret")) .authorizedGrantTypes("authorization_code", "refresh_token", "password") .scopes("read", "write") .redirectUris("http://localhost:8081/login/oauth2/code/webapp") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(tokenStore()) .accessTokenConverter(accessTokenConverter()); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("signing-key"); return converter; }}2.2 资源服务器配置java@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .antMatchers("/api/**").authenticated() .and() .oauth2ResourceServer() .jwt(); }}2.3 安全配置java@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/oauth/**", "/login/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}三、JWT令牌集成3.1 JWT配置增强java@Configurationpublic class JwtConfig { @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("my-signing-key"); // 自定义JWT声明 DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); accessTokenConverter.setUserTokenConverter( new CustomUserAuthenticationConverter()); converter.setAccessTokenConverter(accessTokenConverter); return converter; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); }}3.2 自定义用户信息转换javapublic class CustomUserAuthenticationConverter implements UserAuthenticationConverter { @Override public Map<String, ?> convertUserAuthentication( Authentication authentication) { Map<String, Object> response = new LinkedHashMap<>(); response.put(USERNAME, authentication.getName()); if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { response.put(AUTHORITIES, AuthorityUtils.authorityListToSet( authentication.getAuthorities())); } // 添加自定义声明 if (authentication.getDetails() != null) { response.put("details", authentication.getDetails()); } return response; }}四、客户端应用集成4.1 客户端配置yamlspring: security: oauth2: client: registration: sso-server: client-id: webapp client-secret: websecret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/sso-server" scope: read,write provider: sso-server: authorization-uri: http://localhost:8080/oauth/authorize token-uri: http://localhost:8080/oauth/token user-info-uri: http://localhost:8080/userinfo user-name-attribute: name4.2 客户端安全配置java@Configuration@EnableWebSecuritypublic class ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() .and() .oauth2Login() .loginPage("/oauth2/authorization/sso-server") .defaultSuccessUrl("/home", true) .failureUrl("/login?error=true"); }}五、用户信息端点5.1 用户信息服务java@RestControllerpublic class UserInfoController { @GetMapping("/userinfo") public Map<String, Object> getUserInfo(@AuthenticationPrincipal OAuth2User principal) { Map<String, Object> userInfo = new HashMap<>(); userInfo.put("username", principal.getName()); userInfo.put("authorities", principal.getAuthorities()); userInfo.put("attributes", principal.getAttributes()); return userInfo; } @GetMapping("/api/me") public UserProfile getCurrentUser(@AuthenticationPrincipal(expression = "username") String username) { return userService.getUserProfile(username); }}六、令牌管理6.1 令牌增强器java@Componentpublic class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); // 添加额外信息到令牌 additionalInfo.put("organization", "Example Corp"); // 如果是密码模式,添加用户详情 if (authentication.getUserAuthentication() != null) { Object principal = authentication.getUserAuthentication().getPrincipal(); if (principal instanceof UserDetails) { UserDetails user = (UserDetails) principal; additionalInfo.put("username", user.getUsername()); additionalInfo.put("authorities", user.getAuthorities()); } } ((DefaultOAuth2AccessToken) accessToken) .setAdditionalInformation(additionalInfo); return accessToken; }}6.2 令牌存储策略java@Configurationpublic class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @Primary public TokenStore tokenStore() { // 使用Redis存储令牌(支持集群) RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); tokenStore.setPrefix("oauth:"); return tokenStore; } @Bean @ConditionalOnProperty(name = "oauth2.token.store", havingValue = "jwt") public TokenStore jwtTokenStore() { // 使用JWT令牌(无状态) return new JwtTokenStore(jwtAccessTokenConverter()); }}七、单点登出实现7.1 全局登出端点java@RestControllerpublic class LogoutController { @Autowired private TokenStore tokenStore; @PostMapping("/oauth/revoke-token") public ResponseEntity<?> revokeToken(@RequestParam("token") String tokenValue) { OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); if (accessToken != null) { tokenStore.removeAccessToken(accessToken); // 如果支持刷新令牌,也一并删除 OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.removeRefreshToken(refreshToken); } } return ResponseEntity.ok().build(); } @PostMapping("/global-logout") public ResponseEntity<?> globalLogout( @RequestParam("access_token") String accessToken, HttpServletRequest request) { // 1. 使当前令牌失效 revokeToken(accessToken); // 2. 获取所有活跃会话并通知登出 List<String> clientIds = getActiveClients(accessToken); notifyClientsLogout(clientIds); // 3. 清除安全上下文 SecurityContextHolder.clearContext(); // 4. 使HTTP会话失效 HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } return ResponseEntity.ok("登出成功"); }}八、安全加固措施8.1 防止CSRF攻击java@Configurationpublic class CsrfConfig { @Bean public CsrfTokenRepository csrfTokenRepository() { CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); repository.setCookiePath("/"); repository.setCookieDomain("example.com"); return repository; }}8.2 防止重放攻击java@Componentpublic class ReplayAttackProtection { @Autowired private RedisTemplate<String, String> redisTemplate; public boolean isReplayAttack(String token, String requestId) { String key = "token:nonce:" + token; // 检查请求ID是否已使用过 if (redisTemplate.hasKey(key)) { String existingRequestId = redisTemplate.opsForValue().get(key); return existingRequestId.equals(requestId); } // 存储新的请求ID,有效期5分钟 redisTemplate.opsForValue().set(key, requestId, 5, TimeUnit.MINUTES); return false; }}九、性能优化9.1 令牌缓存java@Configuration@EnableCachingpublic class TokenCacheConfig { @Bean public CacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); cacheManager.setCacheNames(Arrays.asList( "accessTokens", "refreshTokens", "authorizationCodes")); return cacheManager; } @Bean public TokenServices tokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); tokenServices.setSupportRefreshToken(true); tokenServices.setReuseRefreshToken(false); tokenServices.setAccessTokenValiditySeconds(3600); tokenServices.setRefreshTokenValiditySeconds(86400); return tokenServices; }}十、监控与日志10.1 审计日志java@Componentpublic class OAuth2AuditLogger { private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT"); public void logAuthorizationRequest(OAuth2Request request) { auditLogger.info("Authorization request: clientId={}, scope={}, approved={}", request.getClientId(), request.getScope(), request.isApproved()); } public void logTokenIssue(String clientId, String username) { auditLogger.info("Token issued: clientId={}, username={}", clientId, username); }}总结SpringBoot结合OAuth2.0实现SSO提供了企业级的认证授权解决方案。通过合理的配置和扩展,可以满足不同场景的需求。关键点包括:正确选择授权模式:Web应用使用授权码模式安全令牌管理:合理设置有效期,实现令牌刷新完善的登出机制:支持全局登出性能优化:合理使用缓存安全加固:防止CSRF、重放等攻击监控审计:记录关键操作日志
-
一、SSO核心概念与价值单点登录(Single Sign-On,SSO)是一种身份认证方案,允许用户使用单一凭证登录多个相互信任的应用系统。在微服务架构和分布式系统盛行的今天,SSO已成为企业级应用的标准配置。核心价值体现:用户体验提升:减少重复登录,提高操作效率安全管理集中化:统一认证入口,便于监控审计开发维护简化:各应用无需独立实现认证逻辑安全风险降低:统一密码策略和会话管理二、主流SSO协议对比2.1 OAuth 2.0适用场景:第三方应用授权、API访问控制优点:标准化程度高、生态系统完善缺点:协议复杂、学习曲线陡峭SpringBoot支持:spring-security-oauth2-autoconfigure2.2 OpenID Connect适用场景:身份认证标准化解决方案优点:基于OAuth 2.0,专为认证设计缺点:相对较新,部分企业接受度低SpringBoot支持:spring-security-oauth2-client2.3 JWT(JSON Web Token)适用场景:无状态认证、微服务间通信优点:自包含、跨语言、易于传输缺点:令牌无法撤销、需要额外存储机制SpringBoot支持:jjwt库2.4 SAML 2.0适用场景:企业级SSO、跨组织认证优点:成熟稳定、企业特性丰富缺点:XML格式复杂、实现繁琐SpringBoot支持:spring-security-saml2-core三、SpringBoot SSO架构设计3.1 典型架构模式集中式认证中心架构:text用户浏览器 │ ├─▶ 应用A(前端) │ │ │ └─▶ 统一认证中心 │ │ └─▶ 应用B(前端)─┘ │ └─▶ 用户数据存储技术组件划分:认证服务器:处理用户登录、令牌颁发资源服务器:保护API资源,验证令牌客户端应用:集成SSO的前端应用会话存储:Redis/数据库存储会话信息用户目录:LDAP/数据库存储用户数据3.2 关键设计原则安全性原则:最小权限原则:只授予必要权限防御性编程:验证所有输入参数安全传输:强制使用HTTPS定期审计:记录所有认证操作可用性原则:故障隔离:单点故障不影响整体负载均衡:支持水平扩展会话持久化:避免会话丢失优雅降级:部分故障时保持基本功能四、技术栈选择建议4.1 基础依赖配置xml<dependencies> <!-- Spring Security核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2客户端 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- JWT支持 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <!-- Redis会话存储 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency></dependencies>4.2 配置建议开发环境:使用内存存储简化配置开启详细日志便于调试使用自签名证书测试HTTPS生产环境:使用专业CA证书配置集群化Redis存储会话启用API网关进行流量控制配置监控告警系统五、安全考虑与最佳实践5.1 令牌安全使用HTTPS传输令牌设置合理的令牌有效期实现令牌刷新机制记录令牌使用日志5.2 会话管理限制并发会话数量实现会话超时机制支持跨域会话共享提供全局登出功能5.3 密码安全使用BCrypt等强哈希算法实施密码复杂度策略支持多因素认证记录登录失败尝试六、性能优化策略6.1 缓存策略java@Configuration@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000)); return cacheManager; }}6.2 数据库优化为认证相关表添加合适索引定期清理过期令牌和会话使用连接池管理数据库连接考虑读写分离策略七、监控与运维7.1 关键监控指标认证成功率/失败率平均认证响应时间活跃会话数量令牌颁发频率系统资源使用率7.2 日志记录策略yamllogging: level: org.springframework.security: DEBUG com.example.sso: INFO file: name: logs/sso-service.log max-size: 10MB max-history: 30八、部署架构建议8.1 开发测试环境单节点部署,简化运维使用内嵌数据库和缓存配置开发人员访问权限8.2 生产环境text负载均衡器 │ ├─▶ 认证节点1 ──┐ │ │ ├─▶ 认证节点2 ──┼─▶ Redis集群 │ │ └─▶ 认证节点3 ──┘ │ └─▶ 数据库集群高可用配置:至少3节点集群部署使用Keepalived实现VIP漂移配置数据库主从复制设置自动化备份策略总结SpringBoot SSO实现需要综合考虑安全性、性能、可扩展性和可维护性。建议从实际业务需求出发,选择合适的协议和架构模式。对于初创项目,可以从简单的JWT方案开始;对于企业级应用,建议采用OAuth 2.0或SAML 2.0。无论选择哪种方案,都要确保遵循安全最佳实践,建立完善的监控和应急响应机制。随着业务发展,SSO系统可能需要支持更复杂的场景,如多因素认证、生物识别认证、风险控制等。因此,在架构设计时应预留扩展点,确保系统能够平滑演进。
-
springboot静态资源映射规则SpringBoot 对静态资源的访问提供了自动配置支持,核心依赖是 WebMvcAutoConfiguration 自动配置类,其底层通过重写 addResourceHandlers 方法,借助 ResourceHandlerRegistry(资源处理器注册器)注册多个 ResourceHandler(资源处理器),分别定义了普通静态资源和 WebJars 资源的存放路径与 URL 访问路径映射关系,同时遵循 Controller 映射优先级高于静态资源映射的规则,确保请求匹配的合理性。springboot静态资源映射规则源码解析首先我们找到 spring-boot-autoconfigure 的jar包,点击 spring.factories 文件,找到 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,进入 WebMvcAutoConfiguration 核心配置类 SpringBoot 之所以能访问到静态资源,本质是 WebMvcAutoConfiguration 自动配置类中,通过 addResourceHandlers 方法注册了默认的资源处理器 下面具体简述该函数代码第一部分:加载缓存配置 点击进入 ResourceProperties 这是静态资源相关配置的绑定类,可以在 application.yml/properties 静态资源配置中进行设置和静态资源有关的参数,包括缓存时间等第二部分:注册 WebJars 专用资源处理器 Webjars 是以 jar 包的方式引入静态资源,Webjars 的网址为 https://www.webjars.org/由代码可知所有以 /webjars/ 开头的请求都会被该处理器处理,WebJars 组件的 Jar 包中静态资源默认存放在 classpath:META-INF/resources/webjars/ 目录下下面以引入 jQuery 为例进行演示使用方式是导入 org.webjars 的相关依赖 <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency>访问方式是访问 classpath:META-INF/resources/webjars/ 目录下的内容,例如访问 jquery 的路径为:localhost:8080/webjars/jquery/3.3.1/jquery.js 第三部分是:注册普通静态资源处理器 点击 getStaticPathPattern 函数进入 WebMvcProperties 类中,里面 staticPathPattern 注册 URL 访问路径模式,默认是 /**(所有未被 Controller 处理的请求),可通过配置修改 点击 getStaticLocations 函数进入 ResourceProperties 类中,里面 staticLocations 读取默认 / 自定义的静态资源目录,默认值为四个目录 优先级从高到低(同名文件会优先访问优先级高的目录):静态资源 classpath:/META-INF/resources/,即项目内实际存放路径 src/main/resources/META-INF/resources/静态资源 classpath:/resources/,即项目内实际存放路径 src/main/resources/resources/静态资源 classpath:/static/,即项目内实际存放路径 src/main/resources/static/静态资源 classpath:/public/,即项目内实际存放路径 src/main/resources/public/下面我们来看在 WebMvcAutoConfiguration 自动配置类中 SpringBoot 的默认欢迎页的自动配置核心welcomePageHandlerMapping 函数专门处理欢迎页的 HandlerMapping getWelcomePage() 函数用于查找静态资源目录和模板目录下的 index.html,严格遵循静态资源默认目录优先级进行查找,只要其中任意一个目录下有 index.html,就返回该资源路径。另外,如果通过 spring.web.resources.staticLocations 自定义了静态资源目录,getWelcomePage() 也会在自定义目录中查找 index.html 由代码可知这个方法的本质是向 SpringMVC 容器注册一个 WelcomePageHandlerMapping 实例,它是一个请求处理器映射器(HandlerMapping),专门处理根路径(/)请求,自动查找并返回项目中的 index.html 作为欢迎页。访问 http://localhost:8080/ 自动打开欢迎页的效果————————————————版权声明:本文为CSDN博主「期待のcode」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2401_84284464/article/details/154792579
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签