「youlai-boot」入门篇:从0到1搭建 Java、Spring Boot、Spring Security 企业级权限管理系统

🚀 作者主页: 有来技术

🔥 开源项目: youlai-mallvue3-element-adminyoulai-bootvue-uniapp-template

🌺 仓库主页: GitCodeGiteeGithub

💖 欢迎点赞 👍 收藏 ⭐评论 📝 如有错误敬请纠正!

前言

本文基于 JavaSpring Boot 3 ,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQLRedis ,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制,最终构建完整的企业级安全管理框架。

作为开源项目youlai-boot 的入门篇,本文旨在帮助前端开发者或后端初学者快速上手 Java 后端开发。通过一步步实践,掌握项目的核心逻辑与实现细节,不仅能放心使用,还能轻松扩展和二次开发。

环境准备

本章节介绍安装 Java 开发所需的环境,包括 JDK、Maven 和 IntelliJ IDEA(简称 IDEA),这些工具是 Java 开发的核心环境。


安装 JDK

JDK(Java Development Kit) 是 Java 开发工具包,包含编译器、运行时环境等,支持 Java 应用程序的开发与运行。

下载 JDK

访问以下链接,下载最新版本的 JDK 安装包:
https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe

安装 JDK

下载完成后,双击安装包,根据引导完成安装。

示例安装路径:D:\Java\jdk-17.0.3.1

配置环境变量

  • 打开 系统属性 -> 高级系统设置 -> 环境变量
  • 新建系统变量 JAVA_HOME,值为 D:\Java\jdk-17.0.3.1
  • Path 环境变量中,添加 %JAVA_HOME%\bin

验证安装

在命令行中执行以下命令,查看 Java 版本:

bash 复制代码
java -version

输出类似如下内容表示安装成功:


安装 Maven

Maven 是一个流行的 Java 构建和依赖管理工具,类似于前端的 npm,用于管理项目的构建流程及第三方依赖库。

下载 Maven

访问以下链接,下载最新的 bin.zip 文件:https://maven.apache.org/download.cgi

bin.zip 解压到本地目录,示例解压路径:D:\Soft\apache-maven-3.9.5

配置阿里云镜像

编辑配置文件 D:\Soft\apache-maven-3.9.5\conf\settings.xml,在 <mirrors> 节点中添加以下配置:

xml 复制代码
<mirrors>
    <mirror>
        <id>alimaven</id>
        <name>aliyun maven</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        <mirrorOf>central</mirrorOf>       
    </mirror>
</mirrors>

配置环境变量

  • 新建系统变量 M2_HOME,值为 D:\Soft\apache-maven-3.9.5
  • Path 环境变量中,添加 %M2_HOME%\bin

验证安装

在命令行中执行以下命令,查看 Maven 版本:

bash 复制代码
mvn -v

输出类似如下内容表示安装成功:


安装 IDEA

IntelliJ IDEA 是一款功能强大的 Java 集成开发环境(IDE),由 JetBrains 开发,广泛用于 Java 项目的开发、调试和运行。

下载 IDEA

访问以下链接,下载适合您系统的安装包:
https://www.jetbrains.com/idea/download

安装 IDEA

下载完成后,双击安装包,按引导完成安装即可。

具体的配置在创建项目之前说明。

安装 MySQL

安装 MySQL 服务

安装 MySQL 可视化工具

推荐使用 Navicat ,这是一款功能强大的数据库管理工具,但需要付费。如果你因未付费而遇到使用限制,可以选择 DBeaver 作为替代方案。

下载并安装 Navicat 后,你将获得 14 天的免费试用期。安装完成后,连接到 MySQL 服务,即可对数据库和表进行可视化操作,体验非常流畅。

Navicat 界面效果:

安装 Redis

安装 Redis 服务

安装 Redis 可视化工具

推荐使用开源的 AnotherRedisDesktopManager,这是一款功能强大且免费的 Redis 可视化工具。

安装步骤:

  1. 下载安装程序(.exe 文件)。
  2. 按照安装向导逐步操作,完成安装。

使用步骤:

  1. 打开软件,点击"新建连接"。
  2. 输入 Redis 服务器的连接信息(如主机地址、端口、密码等)。
  3. 连接成功后,即可对 Redis 数据进行可视化操作。

AnotherRedisDesktopManager 界面效果:

连接成功示例:

项目搭建

新建项目

打开 IDEA ,选择 Projects → New Project

  1. 项目名称 :输入 youlai-boot(可根据实际需求调整)。
  2. 项目类型 :选择 Maven
  3. JDK 版本 :选择 JDK 17(确保已安装 JDK 17)。
  4. 项目信息
    • Group :填写 com.youlai(可根据实际需求调整)。
    • Artifact :填写 youlai-boot(可根据实际需求调整)。
    • Package name :填写 com.youlai.boot(可根据实际需求调整)。

点击 Next,在左侧的依赖列表中勾选项目所需的依赖。

完成项目初始化后,项目结构如下:

配置开发环境

配置 JDK

通过 File → Project Structure (快捷键 Ctrl + Alt + Shift + S )打开项目结构配置面板,确保 ProjectModules 使用的 SDK 版本为前面安装的 JDK 17。

配置 Maven

通过 File → Settings (快捷键 Ctrl + Alt + S )打开设置面板,切换到 Maven 选项,并将 Maven 设置为前面安装到本地的版本。

验证配置

在 IDEA 的 Terminal 中输入以下命令 mvn -v,验证 Maven 是否正确使用了 JDK 17:

快速开始

创建第一个接口

  1. src/main/java 目录下的 com.youlai.boot 包中,新建一个名为 controller 的包。
  2. controller 包下创建一个名为 TestController 的 Java 类。
  3. TestController 类中添加一个简单的 hello-world 接口。

以下是 TestController 类的代码:

java 复制代码
/**
 * 测试接口
 *
 * @author youlai
 */
@RestController
public class TestController {

    @GetMapping("/hello-world")
    public String test() {
        return "hello world";
    }

}

启动测试

在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。

控制台显示 Tomcat 已在端口 8080 (http) ,表示应用成功启动

打开浏览器,访问 http://localhost:8080/hello-world,页面将显示 hello world,表示接口正常运行。

连接数据库

为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。

创建数据库

使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表

sql 复制代码
    -- ----------------------------
    -- 1. 创建数据库
    -- ----------------------------
    CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

    -- ----------------------------
    -- 2. 创建表 && 数据初始化
    -- ----------------------------
    use youlai_boot;
	
	-- ----------------------------
    -- Table structure for sys_user
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user` (
        `id` int NOT NULL AUTO_INCREMENT,
        `username` varchar(64) NULL DEFAULT NULL COMMENT '用户名',
        `nickname` varchar(64) NULL DEFAULT NULL COMMENT '昵称',
        `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别(1-男 2-女 0-保密)',
        `password` varchar(100) NULL DEFAULT NULL COMMENT '密码',
        `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',
        `avatar` varchar(255) NULL DEFAULT '' COMMENT '用户头像',
        `mobile` varchar(20) NULL DEFAULT NULL COMMENT '联系方式',
        `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
        `email` varchar(128) NULL DEFAULT NULL COMMENT '用户邮箱',
        `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
        `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
        `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
        `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
        `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
        PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;

    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0);
    INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', now(), NULL, now(), NULL, 0);
    INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);

添加依赖

项目 pom.xml 添加 MySQL 驱动和 Mybatis-Plus 依赖:

xml 复制代码
<!-- MySQL 8 驱动 -->
<dependency>
	<groupId>com.mysql</groupId>
	<artifactId>mysql-connector-j</artifactId>
	<version>9.1.0</version>
	<scope>runtime</scope>
</dependency>

<!-- Druid 数据库连接池 -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.2.24</version>
</dependency>

<!-- MyBatis Plus Starter-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
	<version>3.5.9</version>
</dependency>

配置数据源

src/main/resources/application.properties 文件修改为 src/main/resources/application.yml,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容:

yaml 复制代码
server:
  port: 8080
  
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone==Asia/Shanghai&&characterEncoding=utf8
    username: root
    password: 123456

mybatis-plus:
  configuration:
    # 驼峰命名映射
    map-underscore-to-camel-case: true
    # 打印 sql 日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto # 主键策略
      logic-delete-field: is_deleted # 全局逻辑删除字段(可选)

增删改查接口

安装 MybatisX 插件

在 IDEA 中依次点击 File → Settings (快捷键 Ctrl + Alt + S ),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。

自动代码生成

在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。

输入数据库的 主机地址用户名密码 ,测试连接成功后点击 OK 保存。

配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。

设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。

点击 Finish 生成,自动生成相关代码。

MybatisX 生成的代码存在以下问题:

  • SysUserMapper.java 文件未标注 @Mapper 注解,导致无法被 Spring Boot 识别为 Mybatis 的 Mapper 接口。如果已配置 @MapperScan,可以省略此注解,但最简单的方法是直接在 SysUserMapper.java 文件中添加 @Mapper 注解。注意避免导入错误的包。

添加增删改查接口

controller 包下创建 UserController.java,编写用户管理接口:

java 复制代码
/**
 * 用户控制层
 *
 * @author youlai
 * @since 2024/12/04
 */
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    /**
     * 获取用户列表
     */
    @GetMapping
    public List<SysUser> listUsers() {
        return userService.list();
    }

    /**
     * 获取用户详情
     */
    @GetMapping("/{id}")
    public SysUser getUserById(@PathVariable Long id) {
        return userService.getById(id);
    }

    /**
     * 新增用户
     */
    @PostMapping
    public String createUser(@RequestBody SysUser user) {
        userService.save(user);
        return "用户创建成功";
    }

    /**
     * 更新用户信息
     */
    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody SysUser user) {
        userService.updateById(user);
        return "用户更新成功";
    }

    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.removeById(id);
        return "用户删除成功";
    }

}

接口测试

重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。

其他增删改接口可以通过后续整合接口文档进行测试。

集成 Knife4j 接口文档

Knife4j 是基于 Swagger2OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。

添加依赖

pom.xml 文件中引入 Knife4j 的依赖:

xml 复制代码
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

配置接口文档

application.yml 文件中进行配置。注意,packages-to-scan 需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。

yaml 复制代码
# springdoc-openapi 项目配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径
# knife4j的增强配置,不需要增强可以不配
knife4j:
  enable: true
  # 是否为生产环境,true 表示生产环境,接口文档将被禁用
  production: false
  setting:
    language: zh_cn # 设置文档语言为中文

添加接口文档配置,在 com.youlai.boot.config 添加 OpenApiConfig 接口文档配置

java 复制代码
package com.youlai.boot.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;

/**
 * OpenAPI 接口文档配置
 *
 * @author youlai
 */
@Configuration
@RequiredArgsConstructor
@Slf4j
public class OpenApiConfig {

    private final Environment environment;

    /**
     * 接口信息
     */

    @Bean
    public OpenAPI openApi() {

        String appVersion = environment.getProperty("project.version", "1.0.0");

        return new OpenAPI()
                .info(new Info()
                        .title("系统接口文档")
                        .version(appVersion)
                )
                // 配置全局鉴权参数-Authorize
                .components(new Components()
                        .addSecuritySchemes(HttpHeaders.AUTHORIZATION,
                                new SecurityScheme()
                                        .name(HttpHeaders.AUTHORIZATION)
                                        .type(SecurityScheme.Type.APIKEY)
                                        .in(SecurityScheme.In.HEADER)
                                        .scheme("Bearer")
                                        .bearerFormat("JWT")
                        )
                );
    }


    /**
     * 全局自定义扩展
     * <p>
     * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。
     * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。
     */
    @Bean
    public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
        return openApi -> {
            // 全局添加鉴权参数
            if (openApi.getPaths() != null) {
                openApi.getPaths().forEach((s, pathItem) -> {
                    // 登录接口/验证码不需要添加鉴权参数
                    if ("/api/v1/auth/login".equals(s)) {
                        return;
                    }
                    // 接口添加鉴权参数
                    pathItem.readOperations()
                            .forEach(operation ->
                                    operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
                            );
                });
            }
        };
    }

}

完善接口文档

完善接口描述

在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例:

java 复制代码
@Tag(name = "用户接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    @Operation(summary = "获取用户列表")
    @GetMapping
    public List<SysUser> listUsers() {
        return userService.list();
    }

    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    public SysUser getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        return userService.getById(id);
    }

    @Operation(summary = "新增用户")
    @PostMapping
    public String createUser(@RequestBody SysUser user) {
        userService.save(user);
        return "新增用户成功";
    }

    @Operation(summary = "修改用户")
    @PutMapping("/{id}")
    public String updateUser(
            @Parameter(description = "用户ID") @PathVariable Long id,
            @RequestBody SysUser user
    ) {
        userService.updateById(user);
        return "修改用户成功";
    }

    @Operation(summary = "删除用户")
    @DeleteMapping("/{id}")
    public String deleteUser(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        userService.removeById(id);
        return "用户删除成功";
    }

}

完善实体类描述

SysUser 实体类中为每个字段添加 @Schema 注解,用于在接口文档中显示字段的详细说明及示例值:

java 复制代码
@Schema(description = "用户对象")
@TableName(value = "sys_user")
@Data
public class SysUser implements Serializable {

    @Schema(description = "用户ID", example = "1")
    @TableId(type = IdType.AUTO)
    private Integer id;

    @Schema(description = "用户名", example = "admin")
    private String username;

    @Schema(description = "昵称", example = "管理员")
    private String nickname;

    @Schema(description = "性别(1-男,2-女,0-保密)", example = "1")
    private Integer gender;

    @Schema(description = "用户头像URL", example = "https://example.com/avatar.png")
    private String avatar;

    @Schema(description = "联系方式", example = "13800000000")
    private String mobile;

    @Schema(description = "用户邮箱", example = "admin@example.com")
    private String email;
    
    // ... 
}

使用接口文档

完成以上步骤后,重新启动应用并访问生成的接口文档。

通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值:

接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据:

集成 Redis 缓存

Redis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。

添加依赖

pom.xml 文件中添加 Spring Boot Redis 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 Redis 连接

application.yml 文件中配置 Redis 连接信息:

yaml 复制代码
spring:
  data:
    redis:
      database: 0    # Redis 数据库索引
      host: localhost  # Redis 主机地址
      port: 6379  # Redis 端口
      # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串
      password: 123456
      timeout: 10s

自定义序列化

Spring Boot 默认使用 JdkSerializationRedisSerializer 进行序列化。我们可以通过自定义 RedisTemplate,将其修改为更易读的 StringJSON 序列化方式:

java 复制代码
/**
 *  Redis 自动装配配置
 *
 * @author youlai
 * @since 2024/12/5
 */
@Configuration
public class RedisConfig {

    /**
     * 自定义 RedisTemplate
     * <p>
     * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
     *
     * @param redisConnectionFactory {@link RedisConnectionFactory}
     * @return {@link RedisTemplate}
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());

        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(RedisSerializer.json());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

单元测试

src/test/java 目录的 com.youlai.boot 包下创建 RedisTests 单元测试类,用于验证数据的存储与读取。

java 复制代码
@SpringBootTest
@Slf4j
class RedisTests {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private SysUserService userService;

    @Test
    void testSetAndGet() {
        Long userId = 1L;
        // 1. 从数据库中获取用户信息
        SysUser user = userService.getById(userId);
        log.info("从数据库中获取用户信息: {}", user);

        // 2. 将用户信息缓存到 Redis
        redisTemplate.opsForValue().set("user:" + userId, user);

        // 3. 从 Redis 中获取缓存的用户信息
        SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get("user:" + userId);
        log.info("从 Redis 中获取用户信息: {}", cachedUser);
    }
}

点击测试类方法左侧的 ▶️ 图标运行单元测试。

运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。

完善 Web 框架

统一响应处理


为什么需要统一响应?

默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。

下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。


定义统一业务状态码

com.youlai.boot.common.result 包下创建 ResultCode 枚举,错误码规范参考 阿里开发手册-错误码设计

java 复制代码
package com.youlai.boot.common.result;

import java.io.Serializable;
import lombok.Getter;

/**
 * 统一业务状态码枚举
 *
 * @author youlai
 */
@Getter
public enum ResultCode implements Serializable {

    SUCCESS("00000", "操作成功"),
    TOKEN_INVALID("A0230", "Token 无效或已过期"),
    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
    SYSTEM_ERROR("B0001", "系统错误");

    private final String code;
    private final String message;

    ResultCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
}

创建统一响应结构

定义 Result 类,封装响应码、消息和数据。

java 复制代码
package com.youlai.boot.common.result;

import lombok.Data;
import java.io.Serializable;

/**
 * 统一响应结构
 *
 * @author youlai
 **/
@Data
public class Result<T> implements Serializable {
    // 响应码
    private String code;
    // 响应数据
    private T data;
    // 响应信息
    private String msg;

    /**
     * 成功响应
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMsg());
        result.setData(data);
        return result;
    }

    /**
     * 失败响应
     */
    public static <T> Result<T> failed(ResultCode resultCode) {
        Result<T> result = new Result<>();
        result.setCode(resultCode.getCode());
        result.setMsg(resultCode.getMsg());
        return result;
    }

    /**
     * 失败响应(系统默认错误)
     */
    public static <T> Result<T> failed() {
        Result<T> result = new Result<>();
        result.setCode(ResultCode.SYSTEM_ERROR.getCode());
        result.setMsg(ResultCode.SYSTEM_ERROR.getMsg());
        return result;
    }

}

封装接口返回结果

调整接口代码,返回统一的响应格式。

java 复制代码
@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public Result<SysUser> getUserById(
    @Parameter(description = "用户ID") @PathVariable Long id
) {
    SysUser user = userService.getById(id);
    return Result.success(user);
}

效果预览

接口返回结构变为标准格式:

通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。

全局异常处理


为什么需要全局异常处理

如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。

以下接口模拟了一个业务逻辑中的异常:

java 复制代码
@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public Result<SysUser> getUserById(
    @Parameter(description = "用户ID") @PathVariable Long id
) {
    // 模拟异常
    int i = 1 / 0;

    SysUser user = userService.getById(id);
    return Result.success(user);
}

当发生异常时,默认返回的数据格式如下所示:

这类非标准的响应格式既不直观,也不利于前后端协作。

全局异常处理器

com.youlai.boot.common.exception 包下创建全局异常处理器,用于捕获和处理系统异常。

java 复制代码
package com.youlai.boot.common.exception;

import com.youlai.boot.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

    /**
     * 处理系统异常
     * <p>
     * 兜底异常处理,处理未被捕获的异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public <T> Result<T> handleNullPointerException(Exception e) {
        log.error(e.getMessage(), e);
        return Result.failed("系统异常:" + e.getMessage());
    }

}

验证全局异常处理

再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范:

自定义业务异常

在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException,可以实现更灵活的异常处理机制。

java 复制代码
package com.youlai.boot.common.exception;

import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;

/**
 * 自定义业务异常
 *
 * @author youlai
 */
@Getter
public class BusinessException extends RuntimeException {

    public ResultCode resultCode;

    public BusinessException(ResultCode errorCode) {
        super(errorCode.getMsg());
        this.resultCode = errorCode;
    }

    public BusinessException(String message) {
        super(message);
    }

}

在全局异常处理器中添加业务异常处理逻辑

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public <T> Result<T> handleBusinessException(BusinessException e) {
        log.error(e.getMessage(), e);
        if(e.getResultCode()!=null){
            return Result.failed(e.getResultCode());
        }
        return Result.failed(e.getMessage());
    }

}

模拟业务异常

java 复制代码
    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    public Result<SysUser> getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        SysUser user = userService.getById(-1);
        // 模拟异常
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        return Result.success(user);
    }

请求不存在的用户时,响应如下:

通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。

日志输出配置

日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具

配置 logback-spring.xml 日志文件

src/main/resources 目录下,新增 logback-spring.xml 配置文件。基于 Spring Boot "约定优于配置" 的设计理念,项目默认会自动加载并使用该配置文件。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<configuration>

    <!-- SpringBoot默认logback的配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
    <property name="LOG_HOME" value="/logs/${APP_NAME}"/>

    <!-- 1. 输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- <withJansi>true</withJansi>-->
        <!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 2. 输出到文件  -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 当前记录的日志文档完整路径 -->
        <file>${LOG_HOME}/log.log</file>
        <encoder>
            <!--日志文档输出格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按大小和时间记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动后的日志文件命名模式 -->
            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 单个日志文件的最大大小 -->
            <maxFileSize>10MB</maxFileSize>
            <!-- 最大保留30天的日志 -->
            <maxHistory>30</maxHistory>
            <!-- 总日志文件大小不超过3GB -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <!-- 临界值过滤器,输出大于INFO级别日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <!-- 根日志记录器配置 -->
    <root level="INFO">
        <!-- 引用上面定义的两个appender,日志将同时输出到控制台和文件 -->
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

查看日志输出效果

添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息:

集成 Spring Security

Spring Security 是一个强大的安全框架,可用于身份认证和权限管理。

添加依赖

pom.xml 添加 Spring Security 依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

获取用户认证信息

从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。

定义用户认证信息类 UserDetails

创建 com.youlai.boot.security.model 包,新建 SysUserDetails 用户认证信息对象,继承 Spring Security 的 UserDetails 接口

java 复制代码
/**
 * Spring Security 用户认证信息对象
 * <p>
 * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。
 * 实现了 {@link UserDetails} 接口,提供用户的核心信息。
 *
 * @author youlai
 */
@Data
@NoArgsConstructor
public class SysUserDetails implements UserDetails {

    /**
     * 用户ID
     */
    private Integer userId;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 账号是否启用(true:启用,false:禁用)
     */
    private Boolean enabled;

    /**
     * 用户角色权限集合
     */
    private Collection<SimpleGrantedAuthority> authorities;

    /**
     * 根据用户认证信息初始化用户详情对象
     */
    public SysUserDetails(SysUser user) {
        this.userId = user.getId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.enabled = ObjectUtil.equal(user.getStatus(), 1);

        // 初始化角色权限集合
        this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
                ? user.getRoles().stream()
                // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add)
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toSet())
                : Collections.emptySet();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

获取用户认证信息服务类

创建 com.youlai.boot.security.service 包,新建 SysUserDetailsService 用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口

java 复制代码
/**
 * 用户认证信息加载服务类
 * <p>
 * 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法,
 * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。
 *
 * @author youlai
 */
@Service
@RequiredArgsConstructor
public class SysUserDetailsService implements UserDetailsService {

    private final SysUserService userService;

    /**
     * 根据用户名加载用户的认证信息
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户基本信息
        SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>()
                .eq(SysUser::getUsername, username)
        );
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        // 模拟设置角色,实际应从数据库获取用户角色信息
        Set<String> roles = Set.of("ADMIN");
        user.setRoles(roles);

        // 模拟设置权限,实际应从数据库获取用户权限信息
        Set<String> perms = Set.of("sys:user:query");
        user.setPerms(perms);

        // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象
        return new SysUserDetails(user);
    }
}

认证鉴权异常处理

com.youlai.boot.common.util 添加响应工具类 ResponseUtils

java 复制代码
@Slf4j
public class ResponseUtils {

    /**
     * 异常消息返回(适用过滤器中处理异常响应)
     *
     * @param response  HttpServletResponse
     * @param resultCode 响应结果码
     */
    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
        // 根据不同的结果码设置HTTP状态
        int status = switch (resultCode) {
            case ACCESS_UNAUTHORIZED, 
            	ACCESS_TOKEN_INVALID 
                    -> HttpStatus.UNAUTHORIZED.value();
            default -> HttpStatus.BAD_REQUEST.value();
        };

        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        try (PrintWriter writer = response.getWriter()) {
            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
            writer.print(jsonResponse);
            writer.flush(); // 确保将响应内容写入到输出流
        } catch (IOException e) {
            log.error("响应异常处理失败", e);
        }
    }

}
功能 AuthenticationEntryPoint AccessDeniedHandler
对应异常 AuthenticationException AccessDeniedException
适用场景 用户未认证(无凭证或凭证无效) 用户已认证但无权限
返回 HTTP 状态码 401 Unauthorized 403 Forbidden
常见使用位置 用于处理身份认证失败的全局入口逻辑 用于处理权限不足时的逻辑

用户未认证处理器

java 复制代码
/**
 * 未认证处理器
 *
 * @author youlai
 */
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        if (authException instanceof BadCredentialsException) {
            // 用户名或密码错误
            ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);
        } else {
            // token 无效或者 token 过期
            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
        }
    }
    
}

无权限访问处理器

java 复制代码
/**
 * 无权限访问处理器
 *
 * @author youlai
 */
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
    }

}

注意事项

需要在全局异常不能捕获认证和鉴权异常,不然不能交给 spring security 异常处理器处理

java 复制代码
public class GlobalExceptionHandler {

    /**
     * 处理系统异常
     * <p>
     * 兜底异常处理,处理未被捕获的异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public <T> Result<T> handleNullPointerException(Exception e) throws Exception {
        // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理
        if (e instanceof AccessDeniedException
                || e instanceof AuthenticationException) {
            throw e;
        }

        log.error(e.getMessage(), e);
        return Result.failed("系统异常,请联系管理员");
    }

}

认证授权配置

com.youlai.boot.config 包下新建 SecurityConfig 用来 Spring Security 安全配置

java 复制代码
/**
 * Spring Security 安全配置
 *
 * @author youlai
 */
@Configuration
@EnableWebSecurity  // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链
@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)
public class SecurityConfig {

    /**
     * 忽略认证的 URI 地址
     */
    private final String[] IGNORE_URIS = {"/api/v1/auth/login"};

    /**
     * 配置安全过滤链,用于定义哪些请求需要认证或授权
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 配置认证与授权规则
        http
                .authorizeHttpRequests(requestMatcherRegistry ->
                        requestMatcherRegistry
                                .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证
                                .anyRequest().authenticated() // 其他请求必须认证
                )
                // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT)
                .sessionManagement(configurer ->
                        configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF)
                .csrf(AbstractHttpConfigurer::disable)
                // 禁用默认的表单登录功能
                .formLogin(AbstractHttpConfigurer::disable)
                // 禁用 HTTP Basic 认证(统一使用 JWT 认证)
                .httpBasic(AbstractHttpConfigurer::disable)
                // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
                .headers(headers ->
                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
                )
                // 异常处理
                .exceptionHandling(configurer -> {
                    configurer
                            .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器
                            .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器
                });
            
            ;

        return http.build();
    }

    /**
     * 配置密码加密器
     *
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
     /**
     * 用于配置不需要认证的 URI 地址
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> {
            web.ignoring().requestMatchers(
                    "/v3/api-docs/**",
                    "/swagger-ui/**",
                    "/swagger-ui.html",
                    "/webjars/**",
                    "/doc.html"
            );
        };
    }

    /**
     *认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}

Token 工具类

com.youlai.boot.security.manager 包下新建 JwtTokenManager ,用于生成和解析 token

java 复制代码
/**
 * JWT Token 管理类
 *
 * @author youlai
 */
@Service
public class JwtTokenManager {

    /**
     * JWT 密钥,用于签名和解签名
     */
    private final String secretKey = " SecretKey012345678901234567890123456789012345678901234567890123456789";

    /**
     * 访问令牌有效期(单位:秒), 默认 1 小时
     */
    private final Integer accessTokenTimeToLive = 3600;

    /**
     *  生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token
     *
     * @param authentication 用户认证信息
     * @return JWT 访问令牌
     */
    public String generateToken(Authentication authentication) {
        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
        Map<String, Object> payload = new HashMap<>();
        // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入
        payload.put("userId", userDetails.getUserId());

        // 将用户的角色和权限信息放入 JWT 载荷中,例如:["ROLE_ADMIN", "sys:user:query"]
        Set<String> authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toSet());
        payload.put("authorities", authorities);

        Date now = new Date();
        payload.put(JWTPayload.ISSUED_AT, now);

        // 设置过期时间 -1 表示永不过期
        if (accessTokenTimeToLive != -1) {
            Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive);
            payload.put(JWTPayload.EXPIRES_AT, expiresAt);
        }
        payload.put(JWTPayload.SUBJECT, authentication.getName());
        payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());

        return JWTUtil.createToken(payload, secretKey.getBytes());
    }


    /**
     * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息
     *
     * @param token JWT Token
     * @return Authentication 对象
     */
    public Authentication parseToken(String token) {

        JWT jwt = JWTUtil.parseToken(token);
        JSONObject payloads = jwt.getPayloads();
        SysUserDetails userDetails = new SysUserDetails();
        userDetails.setUserId(payloads.getInt("userId")); // 用户ID
        userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
        // 角色集合
        Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray("authorities")
                .stream()
                .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
                .collect(Collectors.toSet());

        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
    }

    /**
     *  验证 JWT Token 是否有效
     *
     * @param token JWT Token 不携带 Bearer 前缀
     * @return 是否有效
     */
    public boolean validateToken(String token) {
        JWT jwt = JWTUtil.parseToken(token);
        // 检查 Token 是否有效(验签 + 是否过期)
        return jwt.setKey(secretKey.getBytes()).validate(0);
    }

}

登录认证接口

com.youlai.boot.controller 包下新建 AuthController

java 复制代码
/**
 * 认证控制器
 *
 * @author youlai
 */
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    // 认证管理器 - 用于执行认证
    private final AuthenticationManager authenticationManager;

    // JWT 令牌服务类 - 用于生成 JWT 令牌
    private final JwtTokenManager jwtTokenManager;

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<String> login(
            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
            @Parameter(description = "密码", example = "123456") @RequestParam String password
    ) {

        // 1. 创建用于密码认证的令牌(未认证)
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username.trim(), password);

        // 2. 执行认证(认证中)
        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        // 3. 认证成功后生成 JWT 令牌(已认证)
        String accessToken = jwtTokenManager.generateToken(authentication);

        return Result.success(accessToken);
    }
}

访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token

访问 https://jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。

访问鉴权

我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。

验证解析 Token 过滤器

新建 com.youlai.boot.security.filter 添加 JwtValidationFilter 过滤器 用于验证和解析token

java 复制代码
/**
 * JWT Token 验证和解析过滤器
 * <p>
 * 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。
 * 如果 Token 无效或解析失败,直接返回错误响应。
 * </p>
 *
 * @author youlai
 */
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final String BEARER_PREFIX = "Bearer ";

    private final JwtTokenManager jwtTokenManager;

    public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {
        this.jwtTokenManager = jwtTokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        try {
            if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) {
                // 去除 Bearer 前缀
                token = token.substring(BEARER_PREFIX.length());
                // 校验 JWT Token ,包括验签和是否过期
                boolean isValidate = jwtTokenService.validateToken(token);
                if (!isValidate) {
                    writeErrMsg(response, ResultCode.TOKEN_INVALID);
                    return;
                }
                // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中
                Authentication authentication = jwtTokenManager.parseToken(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            SecurityContextHolder.clearContext();
            writeErrMsg(response, ResultCode.TOKEN_INVALID);
            return;
        }
        
        // 无 Token 或 Token 验证通过时,继续执行过滤链。
        // 如果请求不在白名单内(例如登录接口、静态资源等),
        // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。
        // 例如:
        // - 匹配到 permitAll() 的规则会直接放行。
        // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。
        // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。
        filterChain.doFilter(request, response);
    }

    /**
     * 异常消息返回
     *
     * @param response  HttpServletResponse
     * @param resultCode 响应结果码
     */
    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
        int status = switch (resultCode) {
            case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
            default -> HttpStatus.BAD_REQUEST.value();
        };

        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        try (PrintWriter writer = response.getWriter()) {
            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
            writer.print(jsonResponse);
            writer.flush();
        } catch (IOException e) {
            // 日志记录:捕获响应写入失败异常
            // LOGGER.error("Error writing response", e);
        }
    }
}

添加 JWT 验证和解析过滤器

在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。

java 复制代码
/**
 * Spring Security 安全配置
 *
 * @author youlai
 */
@RequiredArgsConstructor
public class SecurityConfig {

    // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作
    private final JwtTokenManager jwtTokenManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      	return http 
                // ... 
                // JWT 验证和解析过滤器
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class)
		  .build();
    }

    // ...
}

获取用户列表接口

java 复制代码
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    @Operation(summary = "获取用户列表")
    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:query')")
    public List<SysUser> listUsers() {
        return userService.list();
    }
    
}

访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize("hasAuthority('sys:user:query')") 标记说明用户需要有 sys:user:query的权限,也就是所谓的鉴权。

正常访问

不携带 token 访问

携带错误/过期的 token

有访问权限

用户拥有的权限 sys:user:query

java 复制代码
public class UserController {

    @Operation(summary = "获取用户列表")
    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:query')")  // 需要 sys:user:query 权限
    public List<SysUser> listUsers() {
        return userService.list();
    }
    
}

无访问权限

用户没有拥有的权限 sys:user:info

java 复制代码
public class UserController {

    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('sys:user:info')") // 需要 sys:user:info 权限
    public Result<SysUser> getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        SysUser user = userService.getById(id);
    }     
}

结语

通过本文,您将了解企业级后端开发的核心技能和项目从搭建到部署的完整流程。如有兴趣,欢迎访问开源项目:https://gitee.com/youlaiorg,关注公众号 有来技术,或添加微信(微信号:haoxianrui)参与开源交流。

相关推荐
tekin几秒前
Go、Java、Python、C/C++、PHP、Rust 语言全方位对比分析
java·c++·golang·编程语言对比·python 语言·php 语言·编程适用场景
李长渊哦1 小时前
Java 虚拟机(JVM)方法区详解
java·开发语言·jvm
陌殇殇2 小时前
002 SpringCloudAlibaba整合 - Feign远程调用、Loadbalancer负载均衡
java·spring cloud·微服务
猎人everest3 小时前
SpringBoot应用开发入门
java·spring boot·后端
山猪打不过家猪5 小时前
ASP.NET Core Clean Architecture
java·数据库·asp.net
AllowM5 小时前
【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!
java·算法·leetcode
不会Hello World的小苗6 小时前
Java——列表(List)
java·python·list
二十七剑7 小时前
jvm中各个参数的理解
java·jvm
东阳马生架构8 小时前
JUC并发—9.并发安全集合四
java·juc并发·并发安全的集合
计算机小白一个8 小时前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯