From 1e5eaab9d3637b3e3eecdafe7b86aecd4c5b223b Mon Sep 17 00:00:00 2001
From: Charles7c <charles7c@126.com>
Date: Sun, 11 Dec 2022 15:06:21 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=96=B0=E5=A2=9E?=
 =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=9B=BE=E7=89=87=E9=AA=8C=E8=AF=81=E7=A0=81?=
 =?UTF-8?q?=20API=EF=BC=88=E5=BC=95=E5=85=A5=20Redisson=E3=80=81Hutool?=
 =?UTF-8?q?=E3=80=81Easy=20Captcha=20=E4=BE=9D=E8=B5=96=EF=BC=8C=E8=AF=A6?=
 =?UTF-8?q?=E6=83=85=E5=8F=AF=E8=A7=81=20README=20=E4=BB=8B=E7=BB=8D?=
 =?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     |  54 ++++-
 continew-admin-common/pom.xml                 |   7 +
 .../config/jackson/BigNumberSerializer.java   |  55 +++++
 .../config/jackson/JacksonConfiguration.java  |  91 ++++++++
 .../charles7c/cnadmin/common/model/vo/R.java  | 101 ++++++++
 .../cnadmin/common/util/RedisUtils.java       | 215 ++++++++++++++++++
 continew-admin-system/pom.xml                 |   6 +
 .../config/properties/CaptchaProperties.java  | 136 +++++++++++
 .../cnadmin/auth/model/vo/CaptchaVO.java      |  45 ++++
 .../ContinewAdminApplication.java             |  12 +-
 .../controller/auth/CaptchaController.java    |  69 ++++++
 .../src/main/resources/application-dev.yml    |  30 +++
 .../src/main/resources/application-prod.yml   |  30 +++
 .../src/main/resources/application.yml        |   6 +-
 pom.xml                                       |  35 ++-
 15 files changed, 875 insertions(+), 17 deletions(-)
 create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/BigNumberSerializer.java
 create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/JacksonConfiguration.java
 create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/R.java
 create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/RedisUtils.java
 create mode 100644 continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/config/properties/CaptchaProperties.java
 create mode 100644 continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/vo/CaptchaVO.java
 rename continew-admin-webapi/src/main/java/top/charles7c/{ => cnadmin}/ContinewAdminApplication.java (88%)
 create mode 100644 continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/CaptchaController.java

diff --git a/README.md b/README.md
index 9d92591f..92936db9 100644
--- a/README.md
+++ b/README.md
@@ -5,28 +5,66 @@
 
 ### 简介
 
-ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Spring Boot、Undertow 等。
+ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Spring Boot、Undertow、Redis、Redisson、Hutool 等。
 
 ### 技术栈
 
-| 名称                                                  | 版本         | 简介                                                         |
-| :---------------------------------------------------- | :----------- | :----------------------------------------------------------- |
-| [Spring Boot](https://spring.io/projects/spring-boot) | 2.7.6        | 简化新 Spring 应用的初始搭建以及开发过程。                   |
-| [Undertow](https://undertow.io/)                      | 2.2.20.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 |
-| [Lombok](https://projectlombok.org/)                  | 1.18.24      | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 |
+| 名称                                                         | 版本         | 简介                                                         |
+| :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- |
+| [Spring Boot](https://spring.io/projects/spring-boot)        | 2.7.6        | 简化新 Spring 应用的初始搭建以及开发过程。                   |
+| [Undertow](https://undertow.io/)                             | 2.2.20.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 |
+| [Redis](https://redis.io/)                                   | 6.2.7        | 高性能的 key-value 数据库。                                  |
+| [Redisson](https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D) | 3.18.1       | 不仅仅是一个 Redis Java 客户端,同其他 Redis Java 客户端有着很大的区别,相比之下其他客户端提供的功能还仅仅停留在作为数据库驱动层面上,比如仅针对 Redis 提供连接方式,发送命令和处理返回结果等。而 Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 |
+| Easy Captcha                                                 | 1.6.2        | Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目。 |
+| [Hutool](https://www.hutool.cn/)                             | 5.8.10       | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 |
+| [Lombok](https://projectlombok.org/)                         | 1.18.24      | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 |
 
 ### 项目结构
 
-采用按功能拆分模块的开发方式,项目结构如下:
+采用按功能拆分模块的开发方式,项目目录结构如下:
 
-> 项目结构按模块的层次顺序介绍,实际 IDE 中 `continew-admin-common` 模块会因为字母排序原因排在上方
+> 下方项目目录结构是按照模块的层次顺序进行介绍的,实际 IDE 中 `continew-admin-common` 模块会因为字母排序原因排在上方。
 >
 
 ```
 continew-admin  全局通用项目配置及依赖版本管理
   ├─continew-admin-webapi  API 模块(存放 Controller 层代码,打包部署的模块)
+  │  ├─src
+  │  │  ├─main
+  │  │  │  ├─java       工程源文件代码目录
+  │  │  │  │  └─top
+  │  │  │  │    └─charles7c
+  │  │  │  │      └─cnadmin
+  │  │  │  │        └─webapi
+  │  │  │  │          └─controller  
+  │  │  │  │            └─auth    认证相关 API
+  │  │  │  │      └─ContinewAdminApplication.java  启动入口
+  │  │  │  ├─resources  工程配置目录
   ├─continew-admin-system  系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)
+  │  ├─src    工程源文件代码目录
+  │  │  ├─main
+  │  │  │  ├─java
+  │  │  │  │  └─top
+  │  │  │  │    └─charles7c
+  │  │  │  │      └─cnadmin
+  │  │  │  │        └─auth    认证相关业务及配置
+  │  │  │  │          └─config       认证相关配置
+  │  │  │  │            └─properties   配置属性
+  │  │  │  │          └─model        认证相关模型
+  │  │  │  │            └─vo           认证相关 VO(View Object)
   ├─continew-admin-common  公共模块(存放公共工具类,公共配置等)
+  │  ├─src    工程源文件代码目录
+  │  │  ├─main
+  │  │  │  ├─java
+  │  │  │  │  └─top
+  │  │  │  │    └─charles7c
+  │  │  │  │      └─cnadmin
+  │  │  │  │        └─common
+  │  │  │  │          └─config     公共配置
+  │  │  │  │            └─jackson    Jackson 配置
+  │  │  │  │          └─model      公共模型
+  │  │  │  │            └─vo         公共 VO(View Object)
+  │  │  │  │          └─util       公共工具类
 ```
 
 ### License
diff --git a/continew-admin-common/pom.xml b/continew-admin-common/pom.xml
index f6ca1650..8157041f 100644
--- a/continew-admin-common/pom.xml
+++ b/continew-admin-common/pom.xml
@@ -58,5 +58,12 @@ limitations under the License.
                 </exclusion>
             </exclusions>
         </dependency>
+
+        <!-- ################ 工具库相关 ################ -->
+        <!-- Redisson(不仅仅是一个 Redis Java 客户端) -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/BigNumberSerializer.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/BigNumberSerializer.java
new file mode 100644
index 00000000..fc91c6b4
--- /dev/null
+++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/BigNumberSerializer.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.common.config.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
+
+/**
+ * 大数值序列化器(针对数值超出 JS 最大或最小值的情况,将其转换为字符串)
+ *
+ * @author Charles7c
+ * @since 2022/12/11 13:22
+ */
+@JacksonStdImpl
+public class BigNumberSerializer extends NumberSerializer {
+
+    /** 静态实例 */
+    public static final BigNumberSerializer SERIALIZER_INSTANCE = new BigNumberSerializer(Number.class);
+    /** JS:Number.MAX_SAFE_INTEGER */
+    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+    /** JS:Number.MIN_SAFE_INTEGER */
+    private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+    public BigNumberSerializer(Class<? extends Number> rawType) {
+        super(rawType);
+    }
+
+    @Override
+    public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+        // 序列化为字符串
+        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+            super.serialize(value, gen, provider);
+        } else {
+            gen.writeString(value.toString());
+        }
+    }
+}
diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/JacksonConfiguration.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/JacksonConfiguration.java
new file mode 100644
index 00000000..345d9aea
--- /dev/null
+++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/jackson/JacksonConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.common.config.jackson;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+
+/**
+ * Jackson 配置
+ *
+ * @author Charles7c
+ * @since 2022/12/11 13:23
+ */
+@Slf4j
+@Configuration
+public class JacksonConfiguration {
+
+    /**
+     * 全局配置序列化返回 JSON 处理
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer customizer() {
+        String dateTimeFormatPattern = "yyyy-MM-dd HH:mm:ss";
+        String dateFormatPattern = "yyyy-MM-dd";
+        String timeFormatPattern = "HH:mm:ss";
+
+        return builder -> {
+            // 针对 java.util.Date 的转换
+            builder.locale(Locale.CHINA);
+            builder.timeZone(TimeZone.getDefault());
+            builder.simpleDateFormat(dateTimeFormatPattern);
+
+            // 针对 Long、BigInteger、BigDecimal 的转换
+            JavaTimeModule javaTimeModule = new JavaTimeModule();
+            javaTimeModule.addSerializer(Long.class, BigNumberSerializer.SERIALIZER_INSTANCE);
+            javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.SERIALIZER_INSTANCE);
+            javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.SERIALIZER_INSTANCE);
+            javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
+
+            // 针对 LocalDateTime、LocalDate、LocalTime 的转换
+            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatPattern);
+            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
+            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
+
+            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormatPattern);
+            javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
+            javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));
+
+            DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeFormatPattern);
+            javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
+            javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
+            builder.modules(javaTimeModule);
+            log.info(">>>初始化 Jackson 配置<<<");
+        };
+    }
+}
diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/R.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/R.java
new file mode 100644
index 00000000..7494c279
--- /dev/null
+++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/R.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.common.model.vo;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import org.springframework.http.HttpStatus;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 响应信息
+ *
+ * @author Charles7c
+ * @since 2022/12/10 23:31
+ */
+@Data
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class R<V extends Serializable> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 是否成功 */
+    private boolean success;
+    /** 状态码 */
+    private int code;
+    /** 状态信息 */
+    private String msg;
+    /** 返回数据 */
+    private V data;
+    /** 时间戳 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime timestamp = LocalDateTime.now();
+
+    /** 成功状态码 */
+    private static final int SUCCESS_CODE = HttpStatus.OK.value();
+    /** 失败状态码 */
+    private static final int FAIL_CODE = HttpStatus.INTERNAL_SERVER_ERROR.value();
+
+    private R(boolean success, int code, String msg, V data) {
+        this.success = success;
+        this.code = code;
+        this.msg = msg;
+        this.data = data;
+    }
+
+    public static <V extends Serializable> R<V> ok() {
+        return new R<>(true, SUCCESS_CODE, "操作成功", null);
+    }
+
+    public static <V extends Serializable> R<V> ok(V data) {
+        return new R<>(true, SUCCESS_CODE, "操作成功", data);
+    }
+
+    public static <V extends Serializable> R<V> ok(String msg) {
+        return new R<>(true, SUCCESS_CODE, msg, null);
+    }
+
+    public static <V extends Serializable> R<V> ok(String msg, V data) {
+        return new R<>(true, SUCCESS_CODE, msg, data);
+    }
+
+    public static <V extends Serializable> R<V> fail() {
+        return new R<>(false, FAIL_CODE, "操作失败", null);
+    }
+
+    public static <V extends Serializable> R<V> fail(String msg) {
+        return new R<>(false, FAIL_CODE, msg, null);
+    }
+
+    public static <V extends Serializable> R<V> fail(V data) {
+        return new R<>(false, FAIL_CODE, "操作失败", data);
+    }
+
+    public static <V extends Serializable> R<V> fail(String msg, V data) {
+        return new R<>(false, FAIL_CODE, msg, data);
+    }
+
+    public static <V extends Serializable> R<V> fail(int code, String msg) {
+        return new R<>(false, code, msg, null);
+    }
+}
diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/RedisUtils.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/RedisUtils.java
new file mode 100644
index 00000000..bdacad19
--- /dev/null
+++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/RedisUtils.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.common.util;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import org.redisson.api.*;
+import org.redisson.config.Config;
+
+import cn.hutool.extra.spring.SpringUtil;
+
+/**
+ * Redis 工具类
+ *
+ * @author Charles7c
+ * @since 2022/12/11 12:00
+ */
+@Data
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class RedisUtils {
+
+    private static final RedissonClient REDISSON_CLIENT = SpringUtil.getBean(RedissonClient.class);
+
+    /* ################ 查询操作 ################ */
+    /**
+     * 获取缓存的基本对象列表
+     *
+     * @param keyPattern
+     *            缓存键表达式
+     * @return 基本对象列表
+     */
+    public static Collection<String> keys(final String keyPattern) {
+        Stream<String> stream = REDISSON_CLIENT.getKeys().getKeysStreamByPattern(getNameMapper().map(keyPattern));
+        return stream.map(key -> getNameMapper().unmap(key)).collect(Collectors.toList());
+    }
+
+    /**
+     * 是否存在指定缓存
+     *
+     * @param key
+     *            缓存键
+     */
+    public static Boolean hasKey(String key) {
+        RKeys rKeys = REDISSON_CLIENT.getKeys();
+        return rKeys.countExists(getNameMapper().map(key)) > 0;
+    }
+
+    /**
+     * 获取缓存剩余存活时间
+     *
+     * @param key
+     *            缓存键
+     * @return 剩余存活时间
+     */
+    public static <T> long getTimeToLive(final String key) {
+        RBucket<T> rBucket = REDISSON_CLIENT.getBucket(key);
+        return rBucket.remainTimeToLive();
+    }
+
+    /**
+     * 获取缓存的基本对象
+     *
+     * @param key
+     *            缓存键
+     * @return 缓存值
+     */
+    public static <T> T getCacheObject(final String key) {
+        RBucket<T> rBucket = REDISSON_CLIENT.getBucket(key);
+        return rBucket.get();
+    }
+
+    /* ################ 操作有效期 ################ */
+    /**
+     * 设置过期时间
+     *
+     * @param key
+     *            缓存键
+     * @param timeout
+     *            过期时间
+     * @return true 设置成功;false 设置失败
+     */
+    public static boolean expire(final String key, final long timeout) {
+        return expire(key, Duration.ofSeconds(timeout));
+    }
+
+    /**
+     * 设置过期时间
+     *
+     * @param key
+     *            缓存键
+     * @param duration
+     *            过期时间
+     * @return true 设置成功;false 设置失败
+     */
+    public static boolean expire(final String key, final Duration duration) {
+        RBucket rBucket = REDISSON_CLIENT.getBucket(key);
+        return rBucket.expire(duration);
+    }
+
+    /* ################ 操作基本对象 ################ */
+    /**
+     * 缓存基本对象(Integer、String、实体类等)
+     *
+     * @param key
+     *            缓存键
+     * @param value
+     *            缓存值
+     */
+    public static <T> void setCacheObject(final String key, final T value) {
+        setCacheObject(key, value, false);
+    }
+
+    /**
+     * 缓存基本对象,保留当前对象 TTL 有效期
+     *
+     * @param key
+     *            缓存键
+     * @param value
+     *            缓存值
+     * @param isSaveTtl
+     *            是否保留 TTL 有效期(例如: set 之前 ttl 剩余 90,set 之后还是为 90)
+     * @since Redis 6.X 以上使用 setAndKeepTTL 兼容 5.X 方案
+     */
+    public static <T> void setCacheObject(final String key, final T value, final boolean isSaveTtl) {
+        RBucket<T> bucket = REDISSON_CLIENT.getBucket(key);
+        if (isSaveTtl) {
+            try {
+                bucket.setAndKeepTTL(value);
+            } catch (Exception e) {
+                long timeToLive = bucket.remainTimeToLive();
+                setCacheObject(key, value, Duration.ofMillis(timeToLive));
+            }
+        } else {
+            bucket.set(value);
+        }
+    }
+
+    /**
+     * 缓存基本对象(Integer、String、实体类等)
+     *
+     * @param key
+     *            缓存键
+     * @param value
+     *            缓存值
+     * @param duration
+     *            时间
+     */
+    public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
+        RBatch batch = REDISSON_CLIENT.createBatch();
+        RBucketAsync<T> bucket = batch.getBucket(key);
+        bucket.setAsync(value);
+        bucket.expireAsync(duration);
+        batch.execute();
+    }
+
+    /**
+     * 删除缓存的基本对象
+     *
+     * @param key
+     *            缓存键
+     */
+    public static boolean deleteCacheObject(final String key) {
+        return REDISSON_CLIENT.getBucket(key).delete();
+    }
+
+    /**
+     * 删除缓存的基本对象列表
+     *
+     * @param keyPattern
+     *            缓存键表达式
+     */
+    public static void deleteKeys(final String keyPattern) {
+        REDISSON_CLIENT.getKeys().deleteByPattern(getNameMapper().map(keyPattern));
+    }
+
+    /**
+     * 格式化缓存键,将各子键用 : 拼接起来
+     *
+     * @param subKeys
+     *            子键列表
+     * @return 缓存键
+     */
+    public static String formatKey(String... subKeys) {
+        return String.join(":", subKeys);
+    }
+
+    public static NameMapper getNameMapper() {
+        Config config = REDISSON_CLIENT.getConfig();
+        if (config.isClusterConfig()) {
+            return config.useClusterServers().getNameMapper();
+        }
+        return config.useSingleServer().getNameMapper();
+    }
+}
diff --git a/continew-admin-system/pom.xml b/continew-admin-system/pom.xml
index f33e7e55..116d21d4 100644
--- a/continew-admin-system/pom.xml
+++ b/continew-admin-system/pom.xml
@@ -32,6 +32,12 @@ limitations under the License.
     <description>系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)</description>
 
     <dependencies>
+        <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) -->
+        <dependency>
+            <groupId>com.github.whvcse</groupId>
+            <artifactId>easy-captcha</artifactId>
+        </dependency>
+
         <!-- 公共模块(存放公共工具类,公共配置等) -->
         <dependency>
             <groupId>top.charles7c</groupId>
diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/config/properties/CaptchaProperties.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/config/properties/CaptchaProperties.java
new file mode 100644
index 00000000..5123ea89
--- /dev/null
+++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/config/properties/CaptchaProperties.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.auth.config.properties;
+
+import java.awt.*;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import com.wf.captcha.*;
+import com.wf.captcha.base.Captcha;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 验证码配置属性
+ *
+ * @author Charles7c
+ * @since 2022/12/11 13:35
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "captcha")
+public class CaptchaProperties {
+
+    /**
+     * 类型
+     */
+    private CaptchaTypeEnum type;
+
+    /**
+     * 缓存键的前缀
+     */
+    private String keyPrefix;
+
+    /**
+     * 过期时间
+     */
+    private Long expirationInMinutes = 2L;
+
+    /**
+     * 内容长度
+     */
+    private int length = 4;
+
+    /**
+     * 宽度
+     */
+    private int width = 111;
+
+    /**
+     * 高度
+     */
+    private int height = 36;
+
+    /**
+     * 字体
+     */
+    private String fontName;
+
+    /**
+     * 字体大小
+     */
+    private int fontSize = 25;
+
+    /**
+     * 获取验证码对象
+     *
+     * @return 验证码对象
+     */
+    public Captcha getCaptcha() {
+        Captcha captcha = ReflectUtil.newInstance(type.getClazz(), this.width, this.height);
+        captcha.setLen(length);
+        if (StrUtil.isNotBlank(this.fontName)) {
+            captcha.setFont(new Font(this.fontName, Font.PLAIN, this.fontSize));
+        }
+        return captcha;
+    }
+
+    /**
+     * 验证码类型枚举
+     */
+    @Getter
+    @RequiredArgsConstructor
+    public enum CaptchaTypeEnum {
+
+        /**
+         * 算术
+         */
+        ARITHMETIC(ArithmeticCaptcha.class),
+
+        /**
+         * 中文
+         */
+        CHINESE(ChineseCaptcha.class),
+
+        /**
+         * 中文闪图
+         */
+        CHINESE_GIF(ChineseGifCaptcha.class),
+
+        /**
+         * 闪图
+         */
+        GIF(GifCaptcha.class),
+
+        /**
+         * 特殊类型
+         */
+        SPEC(SpecCaptcha.class),;
+
+        /**
+         * 验证码字节码类型
+         */
+        private final Class<? extends Captcha> clazz;
+    }
+}
diff --git a/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/vo/CaptchaVO.java b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/vo/CaptchaVO.java
new file mode 100644
index 00000000..9cd36d43
--- /dev/null
+++ b/continew-admin-system/src/main/java/top/charles7c/cnadmin/auth/model/vo/CaptchaVO.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.auth.model.vo;
+
+import java.io.Serializable;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 验证码信息
+ *
+ * @author Charles7c
+ * @since 2022/12/11 13:55
+ */
+@Data
+@Accessors(chain = true)
+public class CaptchaVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 验证码唯一标识
+     */
+    private String uuid;
+
+    /**
+     * 验证码图片(Base64编码,带图片格式:data:image/gif;base64)
+     */
+    private String img;
+}
diff --git a/continew-admin-webapi/src/main/java/top/charles7c/ContinewAdminApplication.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/ContinewAdminApplication.java
similarity index 88%
rename from continew-admin-webapi/src/main/java/top/charles7c/ContinewAdminApplication.java
rename to continew-admin-webapi/src/main/java/top/charles7c/cnadmin/ContinewAdminApplication.java
index 07ff5541..be70123d 100644
--- a/continew-admin-webapi/src/main/java/top/charles7c/ContinewAdminApplication.java
+++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/ContinewAdminApplication.java
@@ -14,20 +14,22 @@
  * limitations under the License.
  */
 
-package top.charles7c;
+package top.charles7c.cnadmin;
 
 import java.net.InetAddress;
 
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
 import org.springframework.core.env.Environment;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-
 /**
  * 启动程序
  *
@@ -37,6 +39,8 @@ import lombok.extern.slf4j.Slf4j;
 @Slf4j
 @RestController
 @SpringBootApplication
+@Import(cn.hutool.extra.spring.SpringUtil.class)
+@ComponentScan(basePackages = {"top.charles7c.cnadmin", "cn.hutool.extra.spring"})
 public class ContinewAdminApplication {
 
     private static Environment env;
diff --git a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/CaptchaController.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/CaptchaController.java
new file mode 100644
index 00000000..dc8d95d6
--- /dev/null
+++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/auth/CaptchaController.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.charles7c.cnadmin.webapi.controller.auth;
+
+import java.time.Duration;
+
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.wf.captcha.base.Captcha;
+
+import cn.hutool.core.util.IdUtil;
+
+import top.charles7c.cnadmin.auth.config.properties.CaptchaProperties;
+import top.charles7c.cnadmin.auth.model.vo.CaptchaVO;
+import top.charles7c.cnadmin.common.model.vo.R;
+import top.charles7c.cnadmin.common.util.RedisUtils;
+
+/**
+ * 验证码 API
+ *
+ * @author Charles7c
+ * @since 2022/12/11 14:00
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/captcha")
+public class CaptchaController {
+
+    private final CaptchaProperties captchaProperties;
+
+    /**
+     * 获取图片验证码
+     *
+     * @return 验证码信息
+     */
+    @GetMapping("/img")
+    public R<CaptchaVO> getImageCaptcha() {
+        // 生成验证码
+        Captcha captcha = captchaProperties.getCaptcha();
+
+        // 保存验证码
+        String uuid = IdUtil.simpleUUID();
+        String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), uuid);
+        RedisUtils.setCacheObject(captchaKey, captcha.text(),
+            Duration.ofMinutes(captchaProperties.getExpirationInMinutes()));
+
+        // 返回验证码
+        CaptchaVO captchaVo = new CaptchaVO().setUuid(uuid).setImg(captcha.toBase64());
+        return R.ok(captchaVo);
+    }
+}
diff --git a/continew-admin-webapi/src/main/resources/application-dev.yml b/continew-admin-webapi/src/main/resources/application-dev.yml
index 1f311327..a79c9b3e 100644
--- a/continew-admin-webapi/src/main/resources/application-dev.yml
+++ b/continew-admin-webapi/src/main/resources/application-dev.yml
@@ -3,3 +3,33 @@ server:
   # HTTP 端口(默认 8080)
   port: 8000
 
+--- ### Redis 单机配置
+spring:
+  redis:
+    # 地址
+    host: ${REDIS_HOST:127.0.0.1}
+    # 端口(默认 6379)
+    port: ${REDIS_PORT:6379}
+    # 密码(未设置密码时可为空或注释掉)
+    password: ${REDIS_PWD:123456}
+    # 数据库索引
+    database: ${REDIS_DB:0}
+    # 连接超时时间
+    timeout: 10s
+    # 是否开启 SSL
+    ssl: false
+
+--- ### 验证码配置
+captcha:
+  # 类型
+  type: SPEC
+  # 缓存键的前缀
+  keyPrefix: CAPTCHA
+  # 过期时间
+  expirationInMinutes: 2
+  # 内容长度
+  length: 4
+  # 宽度
+  width: 111
+  # 高度
+  height: 36
\ No newline at end of file
diff --git a/continew-admin-webapi/src/main/resources/application-prod.yml b/continew-admin-webapi/src/main/resources/application-prod.yml
index a6a5ae5b..116a6459 100644
--- a/continew-admin-webapi/src/main/resources/application-prod.yml
+++ b/continew-admin-webapi/src/main/resources/application-prod.yml
@@ -3,3 +3,33 @@ server:
   # HTTP 端口(默认 8080)
   port: 18000
 
+--- ### Redis 单机配置
+spring:
+  redis:
+    # 地址
+    host: ${REDIS_HOST:127.0.0.1}
+    # 端口(默认 6379)
+    port: ${REDIS_PORT:6379}
+    # 密码(未设置密码时可为空或注释掉)
+    password: ${REDIS_PWD:123456}
+    # 数据库索引
+    database: ${REDIS_DB:0}
+    # 连接超时时间
+    timeout: 10s
+    # 是否开启 SSL
+    ssl: false
+
+--- ### 验证码配置
+captcha:
+  # 类型
+  type: SPEC
+  # 缓存键的前缀
+  keyPrefix: CAPTCHA
+  # 过期时间
+  expirationInMinutes: 2
+  # 内容长度
+  length: 4
+  # 宽度
+  width: 111
+  # 高度
+  height: 36
\ No newline at end of file
diff --git a/continew-admin-webapi/src/main/resources/application.yml b/continew-admin-webapi/src/main/resources/application.yml
index 737620ea..9ad1949c 100644
--- a/continew-admin-webapi/src/main/resources/application.yml
+++ b/continew-admin-webapi/src/main/resources/application.yml
@@ -52,11 +52,9 @@ spring:
     date-format: yyyy-MM-dd HH:mm:ss
     # 序列化配置(Bean -> JSON)
     serialization:
-      # 是否格式化输出
-      indent_output: false
-      # 忽略无法转换的对象
+      # 允许序列化无属性的 Bean
       fail_on_empty_beans: false
     # 反序列化配置(JSON -> Bean)
     deserialization:
-      # 忽略 JSON 中不存在的属性
+      # 允许反序列化不存在的属性
       fail_on_unknown_properties: false
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3c207e52..6f16546a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,11 @@ limitations under the License.
     </parent>
 
     <properties>
+        <!-- ### 工具库相关 ### -->
+        <redisson.version>3.18.1</redisson.version>
+        <easy-captcha.version>1.6.2</easy-captcha.version>
+        <hutool.version>5.8.10</hutool.version>
+
         <!-- ### 基础环境相关 ### -->
         <revision>0.0.1-SNAPSHOT</revision>
         <java.version>1.8</java.version>
@@ -54,6 +59,28 @@ limitations under the License.
     <!-- 全局依赖版本管理 -->
     <dependencyManagement>
         <dependencies>
+            <!-- ################ 工具库相关 ################ -->
+            <!-- Redisson(不仅仅是一个 Redis Java 客户端) -->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) -->
+            <dependency>
+                <groupId>com.github.whvcse</groupId>
+                <artifactId>easy-captcha</artifactId>
+                <version>${easy-captcha.version}</version>
+            </dependency>
+
+            <!-- Hutool(小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”) -->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
             <!-- ################ 本项目子模块相关 ################ -->
             <!-- API 模块(存放 Controller 层代码,打包部署的模块) -->
             <dependency>
@@ -92,6 +119,12 @@ limitations under the License.
             <optional>true</optional>
         </dependency>
 
+        <!-- Hutool(小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”) -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
         <!-- Lombok(在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁) -->
         <dependency>
             <groupId>org.projectlombok</groupId>
@@ -118,7 +151,7 @@ limitations under the License.
                 <configuration>
                     <java>
                         <importOrder>
-                            <order>java,javax,org,com,top.charles7c,</order>
+                            <order>java,javax,lombok,org,com,cn,top.charles7c,</order>
                         </importOrder>
                         <removeUnusedImports/>
                         <eclipse>