/login`
+
+请求体:
+```json
+{
+ // 授权客户端的ID, 见配置文件 prohect.auth.clients.client-id
+ "clientId": "web",
+ // 授权客户端的登录方式, 见配置文件 prohect.auth.clients.grant-type
+ "grantType":"password",
+ "username": "1",
+ "password": "123"
+}
+```
+登录返回的 token 信息需要放在请求头的 `Authorization` 中, 并以 `Bearer `开头, 例如
+
+```
+Authorization:Bearer ac27fd57303d4bddb0229c3b6d71b611
+```
+
+> token 的用法遵循 Bearer Token 规范, 可见:https://learning.postman.com/docs/sending-requests/authorization/#bearer-token
+---
+
+# 五、代码规范
+
+## 5.1 API接口规范
+1. **查询请求一律使用`GET`,对数据有变更时一律使用`POST`**。
+2. `GET`请求如果传入参数`>=5`个时,需使用`@ModelAttribute`接收对象。
+3. `GET`不要传入请求体。
+4. URL中**不要包含大写字母,不要包含下划线`_`,不要使用驼峰,分割字符串需使用`-`**。
+5. API不需要严格按照`Restful`设计,但仍需要遵循一些规则。
+ 1. 尽量不要包含动词,如查询用户:不要使用`GET /getUser`,而是使用`GET /user`
+ 2. 为了协调多种开发习惯,不使用`put`请求,不使用`delete`请求,不使用`head`请求等等。
+ 3. 基础增删改查路径为:列表`list`,分页`page`,详情`info`,新增修改`save`,删除`del`
+6. 接口统一返回`Http`响应码`200`,除非强制要求按规范返回响应码。
+7. 自定义响应码至少5位数,响应码可以是英文+数字的组合,但数字的前三位必须是规范的`Http`响应码。
+ 1. 【正例】:`40001`,`AUTH-40101`,`40402`,`50003`。
+ 2. 【反例】:使用`40400`表示用户未授权。使用`30000`表示服务器处理异常。
+8. 对请求参数或请求体有加签需求时,签名一律放在`Header`中。
+9. 授权信息一律放在请求头的`Authorization`字段中。
+
+## 5.2 框架规范
+
+### 5.2.1 `Spring`
+1. `common`包,`expand`包下的对象要求使用`spring.factories`自动装配。
+2. `backend-service`包下的对象要求使用`ServiceXXXScan`指定扫描路径,如`com.blossom.service.base.ServiceBaseScan`。
+3. 没有多实现类需求时,不需要创建接口,如各类业务`Service`,多数在整个项目周期内都不会有多个实现类,增加接口徒增工作量。但多个实现类时有必要使用接口(例如:`common.iaas.OSManager.class`)
+4. 功能为单独模块时,需使用`spring.factories`自动装配,不要在`SpringBootApplication`中指定扫描路径。
+5. 不优先使用`spring-security`作为授权框,该框架过于复杂,不同人使用时差异过大,除非有非常非常严格的代码审查与安全扫描。
+6. 不强制要求使用`validator`做参数校验,内部调用时,`service`内仍然会做必填等校验。
+7. 在保证逻辑清晰且实现唯一的情况下,为了防止相同逻辑在多处实现,允许循环依赖,但仍然需要劲量避免循环依赖。
+8. 优先使用`@Autowired`注入对象,不使用`@Resource`。JDK11中移除了`javax.annotation`包,需要手动加入才可使用`@Resource`。
+9. 使用自定义配置时,不建议使用`@Value`注解,而是统一使用`ConfigurationProperties`创建配置对象类。为了统一维护以及方便查找。
+10. 优先使用`Springboot`默认适配框架。
+ 1. 使用`jackson`作为`json`序列化工具,不要使用`fastjson`,`gson`或其他工具。
+ 2. 使用`logback`作为日志工具类。
+ 3. 使用`hikari`作为数据库连接池。
+
+### 5.2.2 `MyBatis`
+1. 使用驼峰命名转换`mybatis.configuration.map-underscore-to-camel-case: true`,`mapper.xml`文件中不要使用`resultMap`。
+2. `Mapper.java`接口使用`@Mapper`注解,扫描时通过`@MapperScan(annotationClass = Mapper.class)`查找,不要使用路径的方式配置。这样是为了将`Mapper.java`文件于业务service等放在同一目录下。
+3. 涉及到多表查询,复杂查询,使用xml显式编写SQL语句,不要使用`MyBatis Plus`,原则上只有单表简单查询时允许使用`MyBatis Plus`。
+4. `Entity`类中不需要使用`@TableField`标明数据库字段。
+5. `Entity`类中包含非数据库字段时,需要使用`@TableField(exist = false)`标明该字段非数据库字段。
+
+### 5.2.3 其他
+1. 使用`hutool`作为工具类,不要使用`common-lang3`,不要使用`guava`,不要使用`spring`自带工具类。
+2. 本地缓存工具类使用`caffeine`或基于Java自己实现,不要使用`hutool`(存在性能问题),不要使用`guava`。
+
+
+## 5.3 代码规范
+1. 本项目代码根据业务归类。
+ 1. 【正例】:用户`user`相关的`service,mapper,pojo`等都在`com.xx.user`路径下,不要根据类型分类。
+ 2. 【反例】:所有业务的`Service`类都在同一个路径`com.xx.service`下。
+ 3. 由于可能存在多个应用,所以`Controller`在应用所在的目录下,而不根据业务归类。
+2. 请求实体类使用`XxxReq.class`
+3. 响应实体类使用`XxxRes.class`
+4. 数据库实体类使用`XxxEntity.class`
+5. 非必要情况下不需要使用DTO,多数情况下只是多了一层无必要的转换。
+ 1. 若要求多应用服务,且共用模块,可以直接使用Entity来代替DTO。
+ 2. 若需要使用 Dubbo 等RPC框,则需要对外提供DTO来作为接口参数。
+6. 所有实体类需要继承`AbstractPOJO.class`,以便使用`to()`方法转换对象。
+7. 实体类中所有对象使用封装类型,尤其注意 Boolean 类型的字段,非封装类型生成的 `getter` 方法为 `is` 。
+8. 由于接口文档依赖于注释,所以`Controller`,实体类的注释必须填写。
+9. 配置文件需要写明注释。
diff --git a/blossom-backend/adoc/restart-springboot.sh b/blossom-backend/adoc/restart-springboot.sh
new file mode 100644
index 0000000..89725e7
--- /dev/null
+++ b/blossom-backend/adoc/restart-springboot.sh
@@ -0,0 +1,9 @@
+#!/dash
+# 重启 blossom
+pid=`ps aux | grep backend-blossom.jar | grep -v grep | awk '{print $2}'`
+echo "进程ID : " $pid
+kill -9 $pid
+echo "进程" $pid "已被杀死"
+echo "开始重启 backend-blossom 服务器"
+nohup java -Xms1024m -Xmx1024m -jar /usr/local/jasmine/blossom/backend/backend-blossom.jar &
+echo "backend-blossom 正在启动,请查看日志 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓"
diff --git a/blossom-backend/adoc/sql/blossom.sql b/blossom-backend/adoc/sql/blossom.sql
new file mode 100644
index 0000000..13e125c
--- /dev/null
+++ b/blossom-backend/adoc/sql/blossom.sql
@@ -0,0 +1,330 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server : 0_内网:【root&jasmine888】
+ Source Server Type : MySQL
+ Source Server Version : 80033
+ Source Host : 192.168.31.99:3306
+ Source Schema : blossom
+
+ Target Server Type : MySQL
+ Target Server Version : 80033
+ File Encoding : 65001
+
+ Date: 06/08/2023 22:20:26
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for base_sys_param
+-- ----------------------------
+DROP TABLE IF EXISTS `base_sys_param`;
+CREATE TABLE `base_sys_param` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '参数ID',
+ `param_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数名称',
+ `param_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数值',
+ `param_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '参数说明',
+ `open_state` int NOT NULL COMMENT '开放状态 [YesNo]',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `unq_param_name`(`param_name`) USING BTREE COMMENT '参数名称唯一'
+) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统参数' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of base_sys_param
+-- ----------------------------
+INSERT INTO `base_sys_param` VALUES (1, 'WEB_ARTICLE_URL', 'https://www.domain.com/blossom/#/articles?articleId=', 'WEB端文章地址,用于PC端直接调往WEB端阅读文章', 1, '2023-04-04 08:20:57', '2023-08-06 22:19:07');
+INSERT INTO `base_sys_param` VALUES (3, 'ARTICLE_LOG_EXP_DAYS', '30', '文章修改记录保存天数, 超过该天数将被删除', 1, '2023-08-02 17:46:58', '2023-08-02 18:03:43');
+INSERT INTO `base_sys_param` VALUES (11, 'HEFENG_KEY', 'ABC', '和风天气的KEY', 1, '2023-07-31 19:28:54', '2023-08-06 22:19:11');
+INSERT INTO `base_sys_param` VALUES (21, 'GITEE_ACCESS_TOKEN', 'ABC', '[过时配置]GITEE API 的访问 token', 1, '2023-07-31 20:12:05', '2023-08-06 22:20:12');
+
+-- ----------------------------
+-- Table structure for base_user
+-- ----------------------------
+DROP TABLE IF EXISTS `base_user`;
+CREATE TABLE `base_user` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+ `type` tinyint(1) NOT NULL DEFAULT 2 COMMENT '用户类型: 1:管理员; 2:普通用户; 3:只读用户;',
+ `username` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
+ `phone` varchar(13) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户手机号',
+ `password` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户密码',
+ `salt` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '密码加盐',
+ `nick_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '昵称',
+ `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '真实姓名',
+ `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户头像',
+ `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '备注',
+ `cre_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '创建人ID,名称',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `upd_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '修改人ID,名称',
+ `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+ `del_by` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '0,SYS' COMMENT '删除人ID,名称',
+ `del_time` bigint NOT NULL DEFAULT 0 COMMENT '删除时间',
+ `location` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '101100101' COMMENT '和风天气的位置, 官方文档:https://github.com/qwd/LocationList/blob/master/China-City-List-latest.csv',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `unq_user_username`(`username`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10002 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of base_user
+-- ----------------------------
+INSERT INTO `base_user` VALUES (1, 1, 'blos', '', '$2a$10$SgMx8T/06595PEq3EA9US.ja1oHxpIDG/XnERmBXS.wYS8qbxAGDa', 'UVeESP5NgXwb8JmjCHUK', '用户', 'blos', '', '预设管理员账号, 用户名密码都是 blos', '0,SYS', '2023-08-04 16:48:28', '0,SYS', '2023-08-06 22:17:35', '0', 0, '101100101');
+
+-- ----------------------------
+-- Table structure for blossom_article
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_article`;
+CREATE TABLE `blossom_article` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `pid` bigint NOT NULL COMMENT '文件夹ID',
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文章名称',
+ `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文章图标',
+ `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标签集合',
+ `sort` int NOT NULL DEFAULT 1 COMMENT '排序',
+ `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '封面',
+ `describes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '描述',
+ `star_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'star状态',
+ `open_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '公开状态',
+ `open_version` int NOT NULL DEFAULT 0 COMMENT '公开版本',
+ `pv` int NOT NULL DEFAULT 0 COMMENT '页面的查看数',
+ `uv` int NOT NULL DEFAULT 0 COMMENT '独立的访问次数,每日IP重置',
+ `likes` int NOT NULL DEFAULT 0 COMMENT '点赞数',
+ `words` int NOT NULL DEFAULT 0 COMMENT '文章字数',
+ `version` int NOT NULL DEFAULT 0 COMMENT '版本',
+ `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色',
+ `toc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '目录解析',
+ `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Markdown 内容',
+ `html` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Html内容',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 20153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章,Article' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_article
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_article_log
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_article_log`;
+CREATE TABLE `blossom_article_log` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `article_id` bigint NOT NULL COMMENT '文章ID',
+ `version` int NOT NULL DEFAULT 0 COMMENT '版本',
+ `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文章内容',
+ `cre_time` datetime NOT NULL COMMENT '修改日期',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章记录,ArticleLog' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_article_log
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_article_open
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_article_open`;
+CREATE TABLE `blossom_article_open` (
+ `id` bigint NOT NULL COMMENT '文章ID',
+ `pid` bigint NOT NULL COMMENT '文件夹ID',
+ `words` int NOT NULL COMMENT '字数',
+ `open_version` int NOT NULL DEFAULT 1 COMMENT '版本',
+ `open_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '公开时间',
+ `sync_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '同步时间',
+ `toc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '目录',
+ `markdown` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Markdown 内容',
+ `html` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT 'Html内容',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '公开文章,ArticleOpen' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_article_open
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_article_reference
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_article_reference`;
+CREATE TABLE `blossom_article_reference` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `source_id` bigint NOT NULL COMMENT '文章ID',
+ `source_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文章名称',
+ `target_Id` bigint NOT NULL COMMENT '引用文章ID',
+ `target_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '引用名称',
+ `target_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '图片链接',
+ `type` tinyint NOT NULL COMMENT '引用类型: 10:图片; 11:文章; 21:外部文章',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 9148 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_article_reference
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_article_view
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_article_view`;
+CREATE TABLE `blossom_article_view` (
+ `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `article_id` bigint NOT NULL COMMENT '文章ID',
+ `type` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '事件类型 1:uv; 2:like',
+ `ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '地址,IPV4',
+ `user_agent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '设备',
+ `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '省',
+ `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '市',
+ `cre_day` date NOT NULL COMMENT '日期 yyyy-MM-dd',
+ `cre_time` datetime NOT NULL COMMENT '日期',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_view_articleid`(`article_id`) USING BTREE COMMENT '文章ID',
+ INDEX `idx_view_ip`(`ip`) USING BTREE COMMENT 'IP',
+ INDEX `idx_view_creday`(`cre_day`) USING BTREE COMMENT '日期'
+) ENGINE = InnoDB AUTO_INCREMENT = 49 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文章访问记录,ArticleView' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_article_view
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_folder
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_folder`;
+CREATE TABLE `blossom_folder` (
+ `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
+ `pid` bigint UNSIGNED NOT NULL COMMENT '父id',
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件夹名称',
+ `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图标',
+ `tags` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标签',
+ `open_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '开放状态',
+ `sort` int UNSIGNED NOT NULL DEFAULT 1 COMMENT '排序',
+ `cover` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '封面图片',
+ `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色',
+ `describes` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '备注',
+ `store_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '/' COMMENT '存储地址',
+ `subject_words` int NOT NULL DEFAULT 0 COMMENT '专题字数',
+ `subject_upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '专题的最后修改时间',
+ `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1:文章;2:图片',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `upd_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 12035 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '文件夹,Folder' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_folder
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_note
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_note`;
+CREATE TABLE `blossom_note` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '内容',
+ `top` tinyint(1) NOT NULL DEFAULT 0 COMMENT '置顶',
+ `top_time` datetime NULL DEFAULT NULL COMMENT '置顶时间',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_note
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_picture
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_picture`;
+CREATE TABLE `blossom_picture` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `pid` bigint NOT NULL DEFAULT -1 COMMENT '文件夹ID',
+ `source_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '原文件名',
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件名',
+ `path_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件路径',
+ `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件访问url',
+ `rate` tinyint NOT NULL DEFAULT 0 COMMENT '评分 {0,1,2,3,4,5}',
+ `star_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '收藏 0:否,1:是',
+ `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '文件后缀',
+ `size` bigint NOT NULL DEFAULT 0 COMMENT '文件大小',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建日期',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UNI_FILE_UID_FNAME`(`url`) USING BTREE COMMENT '相同路径的图片不能有两条',
+ UNIQUE INDEX `unq_pic_pathname`(`path_name`) USING BTREE COMMENT '路径唯一'
+) ENGINE = InnoDB AUTO_INCREMENT = 305774931235323969 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '图片,Picture' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_picture
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_plan
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_plan`;
+CREATE TABLE `blossom_plan` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `group_id` bigint NOT NULL COMMENT '分组ID',
+ `type` tinyint(1) NOT NULL COMMENT '计划类型: daily, day',
+ `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标题',
+ `content` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '内容',
+ `plan_month` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '日期所在月份',
+ `plan_date` date NULL DEFAULT NULL COMMENT '日期: day',
+ `plan_start_time` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '开始时间: daily, day',
+ `plan_end_time` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '结束时间',
+ `color` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '颜色',
+ `position` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '该计划在该组计划的位置 head, tail, all',
+ `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图片名称, 或图片地址',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 142 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '计划,Plan' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_plan
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_stat
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_stat`;
+CREATE TABLE `blossom_stat` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `type` tinyint(1) NOT NULL COMMENT '统计类型: 1:每日编辑文章数; 2:每月总字数;',
+ `stat_date` date NOT NULL COMMENT '统计日期',
+ `stat_value` int NOT NULL DEFAULT 0 COMMENT '统计数值',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 218 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_stat
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for blossom_web
+-- ----------------------------
+DROP TABLE IF EXISTS `blossom_web`;
+CREATE TABLE `blossom_web` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '网页名称',
+ `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '网页url',
+ `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '图标',
+ `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '图片, 图片的优先级高于图标',
+ `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '类型 ',
+ `sort` int NOT NULL DEFAULT 1 COMMENT '排序',
+ `cre_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `user_id` bigint NOT NULL DEFAULT 1 COMMENT '用户ID',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 292 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '[FS] 网站收藏' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of blossom_web
+-- ----------------------------
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/blossom-backend/backend/pom.xml b/blossom-backend/backend/pom.xml
new file mode 100644
index 0000000..5a7750c
--- /dev/null
+++ b/blossom-backend/backend/pom.xml
@@ -0,0 +1,139 @@
+
+
+
+ blossom-backend
+ com.blossom
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ backend
+
+
+ 4.3.0
+
+
+
+
+
+ com.blossom
+ common-base
+
+
+
+ com.blossom
+ common-cache
+
+
+
+ com.blossom
+ common-db
+
+
+
+ com.blossom
+ common-iaas
+
+
+
+
+ com.blossom
+ expand-tracker-core
+
+
+
+
+ com.blossom
+ expand-sentinel-springmvc
+
+
+
+
+ com.blossom
+ expand-sentinel-metric
+
+
+
+
+ com.auth0
+ java-jwt
+ ${java-jwt.version}
+
+
+
+
+ net.coobird
+ thumbnailator
+ 0.4.20
+
+
+
+
+
+ backend-blossom
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ true
+ ${project.build.finalName}
+
+
+
+
+ repackage
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.1.0
+
+
+ maven-compiler-plugin
+ 3.6.1
+
+ ${maven.compiler.target}
+
+ ${project.build.sourceEncoding}
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.blossom.backend.APP
+
+
+
+
+ repackage
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java b/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java
new file mode 100644
index 0000000..b6731b1
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/APP.java
@@ -0,0 +1,28 @@
+package com.blossom.backend;
+
+import com.blossom.common.base.BaseConstants;
+import com.blossom.expand.tracker.core.Tracker;
+import com.blossom.expand.tracker.core.common.TrackerConstants;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * BLOSSOM
+ *
+ * @author xzzz
+ */
+@EnableAsync
+@EnableScheduling
+@SpringBootApplication
+public class APP {
+
+ public static void main(String[] args) {
+ Tracker.start("APPLICATION_START", TrackerConstants.SPAN_TYPE_APPLICATION_RUN);
+ SpringApplication.run(APP.class, args);
+ BaseConstants.desc();
+ Tracker.end();
+ }
+
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java
new file mode 100644
index 0000000..56c733d
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AbstractAuthService.java
@@ -0,0 +1,134 @@
+package com.blossom.backend.base.auth;
+
+import com.blossom.backend.base.auth.exception.AuthException;
+import com.blossom.backend.base.auth.exception.AuthRCode;
+import com.blossom.backend.base.auth.pojo.AccessToken;
+import com.blossom.backend.base.auth.pojo.LoginDTO;
+import com.blossom.backend.base.auth.pojo.LoginEvent;
+import com.blossom.backend.base.auth.pojo.LoginReq;
+import com.blossom.backend.base.auth.repo.TokenRepository;
+import com.blossom.backend.base.auth.security.PasswordEncoder;
+import com.blossom.backend.base.auth.token.TokenEncoder;
+import com.blossom.backend.base.user.pojo.UserEntity;
+import com.blossom.common.base.util.DateUtils;
+import com.blossom.common.base.util.ServletUtil;
+import com.blossom.common.base.util.json.JsonUtil;
+import org.springframework.context.ApplicationContext;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.TreeMap;
+
+/**
+ * 授权处理抽象类
+ *
+ * @author xzzz
+ */
+public abstract class AbstractAuthService {
+ protected AuthProperties properties;
+ protected TokenRepository tokenRepository;
+ protected TokenEncoder tokenEncoder;
+ protected PasswordEncoder passwordEncoder;
+ private ApplicationContext applicationContext;
+
+ public AbstractAuthService(AuthProperties properties,
+ TokenRepository tokenRepository,
+ TokenEncoder tokenEncoder,
+ PasswordEncoder passwordEncoder,
+ ApplicationContext applicationContext) {
+ this.properties = properties;
+ this.tokenRepository = tokenRepository;
+ this.tokenEncoder = tokenEncoder;
+ this.passwordEncoder = passwordEncoder;
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * 登录
+ *
+ * @param request 请求
+ * @param req 登录请求参数
+ * @return 令牌对象
+ */
+ public AccessToken login(HttpServletRequest request, LoginReq req) {
+ LoginDTO login = req.to(LoginDTO.class);
+ AccessToken accessToken = req.to(AccessToken.class);
+ accessToken.setMetadata(new TreeMap<>());
+ loginByPassword(accessToken, login);
+ fillConfig(accessToken);
+ genToken(accessToken);
+ saveToken(accessToken);
+ publishEvent(request, accessToken, login);
+ return accessToken;
+ }
+
+ /**
+ * 为AccessToken填充配置信息, 如过期日期等
+ */
+ private void fillConfig(AccessToken accessToken) {
+ AuthProperties.Client client = properties.getClientMap().get(accessToken.getClientId());
+ if (client == null) {
+ throw new AuthException(AuthRCode.INVALID_CLIENT_ID);
+ }
+ accessToken.setDuration(client.getDuration());
+ accessToken.setExpire(System.currentTimeMillis() + (accessToken.getDuration() * 1000));
+ accessToken.setRequestRefresh(client.getRequestRefresh());
+ accessToken.setMultiPlaceLogin(client.getMultiPlaceLogin());
+ accessToken.setLoginTime(DateUtils.now());
+ }
+
+ /**
+ * 登录时生成TOKEN令牌
+ */
+ private void genToken(AccessToken accessToken) {
+ accessToken.setToken(tokenEncoder.encode(accessToken));
+ }
+
+ /**
+ * 保存 accessToken 到存储介质中
+ */
+ private void saveToken(AccessToken accessToken) {
+ tokenRepository.saveToken(accessToken);
+ tokenRepository.saveUniqueToken(accessToken);
+ }
+
+ /**
+ * 发布登录事件
+ *
+ * @param request request
+ * @param accessToken token
+ * @param login 登录信息
+ */
+ private void publishEvent(HttpServletRequest request, AccessToken accessToken, LoginDTO login) {
+ String ip = ServletUtil.getIP(request);
+ String userAgent = ServletUtil.getUserAgent(request);
+ login.setUserId(accessToken.getUserId());
+ login.setIp(ip);
+ login.setUserAgent(userAgent);
+ login.setLoginTime(accessToken.getLoginTime());
+ login.setAccessTokenSnapshot(JsonUtil.toJson(accessToken));
+ applicationContext.publishEvent(new LoginEvent(login));
+ }
+
+ /**
+ * 根据用户名密码登录
+ *
+ * @param accessToken token
+ * @param req 请求参数, 包含用户名和密码
+ */
+ protected abstract void loginByPassword(AccessToken accessToken, LoginDTO req);
+
+ /**
+ * 填充 metadata 内容
+ *
+ * @param accessToken token
+ * @param user 用户信息
+ */
+ protected void fillUserDetail(AccessToken accessToken, UserEntity user) {
+ if (user == null) {
+ throw new AuthException(AuthRCode.USER_NOT_EXIST);
+ }
+ accessToken.setUserId(user.getId());
+ accessToken.getMetadata().put("userId", String.valueOf(user.getId()));
+ accessToken.getMetadata().put("username", user.getUsername());
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java
new file mode 100644
index 0000000..cf55f16
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthConstant.java
@@ -0,0 +1,70 @@
+package com.blossom.backend.base.auth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 授权模块静态参数
+ *
+ * @author xzzz
+ * @since 0.0.1
+ */
+public class AuthConstant {
+
+ /**
+ * token 所在的请求头名称
+ */
+ public static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * token 前缀, 遵循 Oauth2.0 规范 (RFC6750: https://tools.ietf.org/html/rfc6750)
+ *
+ * 参考文档:
+ * https://tools.ietf.org/html/RFC6750
+ * https://learning.postman.com/docs/sending-requests/authorization/
+ */
+ public static final String HEADER_TOKEN_PREFIX = "Bearer ";
+
+ /**
+ * Client 配置不踢出其他前一个 Token 时, token_unique 中记录的值
+ */
+ public static final String UNIQUE_TOKEN_EVERY_WHERE = "Client允许多处登录";
+
+ /**
+ * 标识请求时白名单, 为后续过滤器判断使用
+ */
+ public static final String WHITE_LIST_ATTRIBUTE_KEY = "IS_WHITE_LIST";
+
+ /**
+ * 默认忽略的请求
+ */
+ public static final List DEFAULT_WHITE_LIST = new ArrayList() {
+ private static final long serialVersionUID = -1;
+ {
+ // 登录接口
+ this.add("/login");
+ // 获取图片验证码接口
+ this.add("/captcha/image");
+ // 获取手机验证码接口
+ this.add("/captcha/phone");
+ // 一些默认的本地静态资源, 一些非前后端分离的静态文件, 如某些框架自带的操作界面(如 swagger)等
+ this.add("/favicon.ico");
+ this.add("/**/**.js");
+ this.add("/**/**.css");
+ // swagger 默认请求路径
+ this.add("/doc.html");
+ this.add("/webjars/**");
+ this.add("/swagger-resources");
+ this.add("/v2/**");
+ }
+ };
+
+ /**
+ * 请求体包装过滤器的顺序
+ */
+ public static final int AUTH_FILTER_WRAPPER_ORDER = -101;
+ /**
+ * 代理过滤器的顺序
+ */
+ public static final int AUTH_FILTER_PROXY = -100;
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java
new file mode 100644
index 0000000..092920d
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthContext.java
@@ -0,0 +1,104 @@
+package com.blossom.backend.base.auth;
+
+import com.blossom.backend.base.auth.pojo.AccessToken;
+import com.blossom.backend.base.auth.redis.RedisTokenValidateFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 授权上下文
+ *
+ * 设置: {@link RedisTokenValidateFilter}
+ * 删除: {@link RedisTokenAuthFilterProxy#doFilter(ServletRequest, ServletResponse, FilterChain)}
+ *
+ * @author xzzz
+ * @since 0.0.1
+ */
+public class AuthContext {
+
+ private static final ThreadLocal TOKEN_CONTEXT = new ThreadLocal<>();
+
+ // region
+
+ /**
+ * 获取授权上下文
+ *
+ * @return 授权信息
+ */
+ public static AccessToken getContext() {
+ return TOKEN_CONTEXT.get();
+ }
+
+ /**
+ * 设置上下文信息
+ *
+ * @param accessToken 授权信息
+ */
+ public static void setContext(AccessToken accessToken) {
+ TOKEN_CONTEXT.set(accessToken);
+ }
+
+ /**
+ * 删除上下文
+ */
+ public static void removeContext() {
+ TOKEN_CONTEXT.remove();
+ }
+
+ /**
+ * 获取token 信息
+ *
+ * @return token
+ */
+ public static String getToken() {
+ return TOKEN_CONTEXT.get().getToken();
+ }
+
+ // endregion
+
+ /**
+ * 获取UID
+ *
+ * @return 用户ID
+ */
+ public static Long getUserId() {
+ AccessToken accessToken = getContext();
+ if (accessToken == null) {
+ return null;
+ }
+ return getContext().getUserId();
+ }
+
+ /**
+ * 获取BID
+ *
+ * @return 商铺 ID
+ */
+ public static Map getUserMetadata() {
+ AccessToken accessToken = getContext();
+ if (accessToken == null) {
+ return new HashMap<>(0);
+ }
+ return accessToken.getMetadata();
+ }
+
+ /**
+ * 用户用户ID及用户名
+ */
+ public static String getIdAndName() {
+ String nameAndId = "0,Null";
+ try {
+ AccessToken accessToken = getContext();
+ if (accessToken != null) {
+ nameAndId = getUserId() + "," + getUserMetadata().get("username");
+ }
+ } catch (Exception ignored) {
+ // 空的上下文时忽略
+ }
+ return nameAndId;
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java
new file mode 100644
index 0000000..62af39e
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthController.java
@@ -0,0 +1,70 @@
+package com.blossom.backend.base.auth;
+
+import cn.hutool.core.util.StrUtil;
+import com.blossom.backend.base.auth.pojo.LoginReq;
+import com.blossom.backend.base.auth.annotation.AuthIgnore;
+import com.blossom.backend.base.auth.pojo.AccessToken;
+import com.blossom.common.base.exception.XzException400;
+import com.blossom.common.base.pojo.R;
+import lombok.AllArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 登录授权
+ *
+ * @author xzzz
+ */
+@RestController
+@AllArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+
+ /**
+ * 登录[OP]
+ *
+ * @param req 请求对象
+ * @return token信息
+ */
+ @AuthIgnore
+ @PostMapping("login")
+ public R login(HttpServletRequest request, @Validated @RequestBody LoginReq req) {
+ XzException400.throwBy(StrUtil.isBlank(req.getClientId()), "客户端ID[ClientId]为必填项");
+ XzException400.throwBy(StrUtil.isBlank(req.getGrantType()), "授权方式[GrantType]为必填项");
+ return R.ok(authService.login(request, req));
+ }
+
+ /**
+ * 用户退出
+ *
+ * @apiNote 如果为 JWT 授权方式, 则退出功能无效, 调用该接口后 Token 仍然可以正常使用。
+ * 客户端可以正常调用该接口以校验 Token 是否有效, 然后自行从 Storage 或 Cookie 中删除 Token 即可。
+ */
+ @PostMapping("logout")
+ public R> logout() {
+ authService.logout(AuthContext.getToken());
+ return R.ok();
+ }
+
+ /**
+ * 用户主动注册
+ */
+ @PostMapping("register")
+ public void register() {
+
+ }
+
+ /**
+ * 检查 token 状态
+ */
+ @GetMapping("check")
+ public R check() {
+ return R.ok(authService.check());
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java
new file mode 100644
index 0000000..791e989
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthProperties.java
@@ -0,0 +1,275 @@
+package com.blossom.backend.base.auth;
+
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ClassLoaderUtil;
+import cn.hutool.core.util.StrUtil;
+import com.blossom.backend.base.auth.annotation.AuthIgnore;
+import com.blossom.backend.base.auth.enums.AuthTypeEnum;
+import com.blossom.backend.base.auth.enums.GrantTypeEnum;
+import com.blossom.backend.base.auth.enums.LogTypeEnum;
+import com.blossom.backend.base.auth.enums.PasswordEncoderEnum;
+import com.blossom.common.base.exception.XzException500;
+import com.blossom.common.base.util.spring.SpringUtil;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 授权配置项
+ *
+ * @author xzzz
+ */
+@Slf4j
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "project.auth")
+public class AuthProperties implements EnvironmentAware, ApplicationContextAware {
+
+ /**
+ * 是否启用授权功能
+ */
+ private Boolean enabled = Boolean.TRUE;
+ /**
+ * 使用JWT
+ */
+ private AuthTypeEnum type = AuthTypeEnum.jwt;
+ /**
+ * 默认密码
+ */
+ private String defaultPassword = "123456";
+ /**
+ * 日志类型
+ */
+ private LogTypeEnum logType = LogTypeEnum.none;
+ /**
+ * 默认加密方式
+ */
+ private PasswordEncoderEnum passwordEncoder = PasswordEncoderEnum.bcrypt;
+ /**
+ * 授权平台配置,不同的平台类型生成的 token 时效是不同的
+ *
+ * 该类创建完成后会检查是否配置平台数据, 未自定义平台配置会创建默认配置
+ *
+ * @see AuthProperties#initAfterProcessorClient()
+ */
+ private List clients;
+ /**
+ * platforms 转为 map, 方便查询
+ */
+ private Map clientMap;
+ /**
+ * 白名单列表
+ * 配置时不需要增加context-path,会自动拼接
+ */
+ private List whiteList;
+
+ @PostConstruct
+ public void init() {
+ // 检查配置的授权类型是否正确
+ initAfterProcessorTypeCheck();
+ // 白名单后置处理
+ initAfterProcessorWhiteList();
+ // 平台配置后置处理
+ initAfterProcessorClient();
+ }
+
+ /**
+ * 白名单后置处理
+ */
+ private void initAfterProcessorWhiteList() {
+ RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
+ Map methodMap = mapping.getHandlerMethods();
+ methodMap.forEach((key, value) -> {
+ if (null != AnnotationUtil.getAnnotation(value.getMethod(), AuthIgnore.class)) {
+ if (key.getPatternsCondition() != null) {
+ Set urls = key.getPatternsCondition().getPatterns();
+ whiteList.addAll(urls);
+ }
+ }
+ });
+
+ String contentPath = env.getProperty(SpringUtil.SERVLET_CONTEXT_PATH);
+ // 设置默认登陆页白名单
+ if (CollUtil.isEmpty(whiteList)) {
+ this.whiteList = new ArrayList<>();
+ }
+ whiteList.addAll(AuthConstant.DEFAULT_WHITE_LIST);
+ this.whiteList = this.whiteList.stream()
+ .map(whiteUrl -> Optional.ofNullable(contentPath).orElse("") + whiteUrl)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 平台配置后置处理,设置默认平台类型
+ */
+ private void initAfterProcessorClient() {
+ // 初始化客户端列表
+ if (CollUtil.isEmpty(this.clients)) {
+ this.clientMap = new HashMap<>(1);
+ this.clients = new ArrayList<>(1);
+ }
+
+ final Client defaultClient = Client.getDefault();
+
+ log.info("[AUTHORIZ] 授权类型:{}, Client:客户端, Expire(h):授权时间(小时), Refresh:请求刷新授权, Unique:用户Token唯一", type);
+ log.info("[AUTHORIZ] ┌──────────┬───────────┬─────────┬────────────┐");
+ log.info("[AUTHORIZ] | ClientId | Expire(h) | Refresh | MultiPlace |");
+ log.info("[AUTHORIZ] ├──────────┼───────────┼─────────┼────────────┤");
+ for (Client client : this.clients) {
+ // 配置文件有配置,但配置不全,则未配置的参数使用默认配置
+ if (client.getDuration() == null || client.getDuration() == 0) {
+ client.setDuration(defaultClient.getDuration());
+ }
+ if (client.getRequestRefresh() == null) {
+ client.setRequestRefresh(defaultClient.getRequestRefresh());
+ }
+ if (client.getMultiPlaceLogin() == null) {
+ client.setMultiPlaceLogin(defaultClient.getMultiPlaceLogin());
+ }
+ if (CollUtil.isEmpty(client.getGrantType())) {
+ client.setGrantType(defaultClient.getGrantType());
+ }
+ if (AuthTypeEnum.jwt.equals(type)) {
+ client.setRequestRefresh(false);
+ client.setMultiPlaceLogin(true);
+ }
+ log.info("[AUTHORIZ] | {}| {}| {}| {}| {}",
+ StrUtil.fillAfter(client.getClientId(), StrUtil.C_SPACE, 9),
+ StrUtil.fillAfter(client.getDuration() / 3600L + "", StrUtil.C_SPACE, 10),
+ StrUtil.fillAfter(String.valueOf(client.getRequestRefresh()), StrUtil.C_SPACE, 8),
+ StrUtil.fillAfter(String.valueOf(client.getMultiPlaceLogin()), StrUtil.C_SPACE, 11),
+ client.getGrantType()
+ );
+ }
+ // 转换为 map, 有相同的 type, 后者会覆盖前者
+ this.clientMap = this.clients.stream().collect(Collectors.toMap(Client::getClientId, Function.identity(), (key1, key2) -> key2));
+ log.info("[AUTHORIZ] └──────────┴───────────┴─────────┴────────────┘");
+ }
+
+ private void initAfterProcessorTypeCheck() {
+ if (type.equals(AuthTypeEnum.redis) && !ClassLoaderUtil.isPresent("org.springframework.data.redis.core.RedisTemplate")) {
+ String msg = "授权方式 [project.auth.type = redis] 配置错误, 当前项目未使用 Redis, 无法使用 Redis 授权方式.";
+ log.error(msg);
+ throw new XzException500(msg);
+ }
+ }
+
+ /**
+ * 授权客户端的配置
+ */
+ public static class Client {
+ /**
+ * 编码
+ */
+ private String clientId;
+ /**
+ * 默认过期时间, 单位为秒
+ */
+ private Integer duration;
+ /**
+ * 每次请求刷新
+ */
+ private Boolean requestRefresh;
+ /**
+ * 登录时是否踢出上一个登录的 token,注意,该配置修改后,并不会改变在之前下发的 Token 的校验逻辑
+ * 为 true 则每次登录 token 会替换前一个 token。
+ *
为 false 则每次登录返回的 token 是一样的。
+ */
+ private Boolean multiPlaceLogin;
+ /**
+ * 该平台允许的登录方式
+ */
+ private List grantType;
+
+ /**
+ * 获取默认平台配置
+ *
+ * @return 平台配置
+ * @see AuthProperties#initAfterProcessorClient
+ */
+ public static Client getDefault() {
+ Client client = new Client();
+ client.setDuration(60 * 60 * 4);
+ client.setRequestRefresh(Boolean.TRUE);
+ client.setMultiPlaceLogin(Boolean.TRUE);
+ client.setGrantType(CollUtil.newArrayList(GrantTypeEnum.PASSWORD.getType()));
+ return client;
+ }
+
+ public Client() {
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public Integer getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Integer duration) {
+ this.duration = duration;
+ }
+
+ public Boolean getRequestRefresh() {
+ return requestRefresh;
+ }
+
+ public void setRequestRefresh(Boolean requestRefresh) {
+ this.requestRefresh = requestRefresh;
+ }
+
+ public Boolean getMultiPlaceLogin() {
+ return multiPlaceLogin;
+ }
+
+ public void setMultiPlaceLogin(Boolean multiPlaceLogin) {
+ this.multiPlaceLogin = multiPlaceLogin;
+ }
+
+ public List getGrantType() {
+ return grantType;
+ }
+
+ public void setGrantType(List grantType) {
+ this.grantType = grantType;
+ }
+ }
+
+ /**
+ * 环境配置
+ */
+ private Environment env;
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ this.env = environment;
+ }
+
+ private ApplicationContext applicationContext;
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java
new file mode 100644
index 0000000..c08ccdd
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/AuthService.java
@@ -0,0 +1,76 @@
+package com.blossom.backend.base.auth;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import com.blossom.backend.base.auth.exception.AuthException;
+import com.blossom.backend.base.auth.exception.AuthRCode;
+import com.blossom.backend.base.auth.pojo.LoginDTO;
+import com.blossom.backend.base.auth.security.PasswordEncoder;
+import com.blossom.backend.base.auth.pojo.AccessToken;
+import com.blossom.backend.base.auth.repo.TokenRepository;
+import com.blossom.backend.base.auth.token.TokenEncoder;
+import com.blossom.backend.base.user.UserService;
+import com.blossom.backend.base.user.pojo.UserEntity;
+import com.blossom.common.base.enums.YesNo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+
+/**
+ * 授权服务
+ *
+ * @author xzzz
+ */
+@Slf4j
+@Service
+public class AuthService extends AbstractAuthService {
+
+ @Autowired
+ private UserService userService;
+
+ public AuthService(AuthProperties properties,
+ TokenRepository tokenRepository,
+ TokenEncoder tokenEncoder,
+ PasswordEncoder passwordEncoder,
+ ApplicationContext applicationContext) {
+ super(properties, tokenRepository, tokenEncoder, passwordEncoder, applicationContext);
+ }
+
+ /**
+ * 根据用户名密码登录
+ */
+ @Override
+ protected void loginByPassword(AccessToken accessToken, LoginDTO login) {
+ AuthException.throwBy(StrUtil.isBlank(login.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
+ UserEntity user = userService.selectByUsername(login.getUsername());
+ AuthException.throwBy(ObjUtil.isNull(user), AuthRCode.USERNAME_OR_PWD_FAULT);
+ AuthException.throwBy(!passwordEncoder.matches(login.getPassword() + user.getSalt(), user.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
+ fillUserDetail(accessToken, user);
+ }
+
+ /**
+ * 退出
+ *
+ * @param token token 令牌
+ */
+ public void logout(String token) {
+ tokenRepository.remove(token);
+ }
+
+ /**
+ * 用户注册
+ */
+ public void register() {
+
+ }
+
+ /**
+ * 检查 AccessToken 信息
+ *
+ * @return
+ */
+ public AccessToken check() {
+ return AuthContext.getContext();
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java
new file mode 100644
index 0000000..8114f81
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/TokenUtil.java
@@ -0,0 +1,40 @@
+package com.blossom.backend.base.auth;
+
+import cn.hutool.core.util.StrUtil;
+import com.blossom.common.base.util.spring.SpringUtil;
+
+
+/**
+ * @author xzzz
+ * @since 0.0.1
+ */
+public class TokenUtil {
+
+ private static final String TOKEN_KEY = "auth:token";
+ private static final String TOKEN_UNIQUE_KEY = "auth:token_unique";
+
+ public static String buildTokenKey(String token) {
+ return SpringUtil.getAppName() + ":" + TOKEN_KEY + ":" + token;
+ }
+
+ public static String buildUniqueTokenKey(String token) {
+ return SpringUtil.getAppName() + ":" + TOKEN_UNIQUE_KEY + ":" + token;
+ }
+
+ /**
+ * 截取 token 前缀(Bearer), 返回 token
+ *
+ * @param tokenStr token 字符串
+ * @return token
+ */
+ public static String cutPrefix(String tokenStr) {
+ if (tokenStr == null || tokenStr.length() == 0) {
+ return null;
+ }
+ if (StrUtil.startWith(tokenStr, AuthConstant.HEADER_TOKEN_PREFIX)) {
+ return StrUtil.replace(tokenStr, AuthConstant.HEADER_TOKEN_PREFIX, "");
+ }
+ return tokenStr;
+ }
+
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java
new file mode 100644
index 0000000..83f73d6
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AssignUser.java
@@ -0,0 +1,12 @@
+package com.blossom.backend.base.auth.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 拦截器中为接口参数添加用户ID
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AssignUser {
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java
new file mode 100644
index 0000000..010a062
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/AuthIgnore.java
@@ -0,0 +1,14 @@
+package com.blossom.backend.base.auth.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 白名单注解标识
+ *
+ * @author xzzz
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AuthIgnore {
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java
new file mode 100644
index 0000000..65036c3
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/annotation/PermCheck.java
@@ -0,0 +1,21 @@
+package com.blossom.backend.base.auth.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 权限校验
+ *
+ * @author xzzz
+ * @since 0.0.1
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface PermCheck {
+
+ /*
+ 所需要的授权
+ */
+ String value();
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java
new file mode 100644
index 0000000..1fd130c
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/AuthTypeEnum.java
@@ -0,0 +1,18 @@
+package com.blossom.backend.base.auth.enums;
+
+/**
+ * 授权类型
+ *
+ * @author xzzz
+ */
+public enum AuthTypeEnum {
+ /**
+ * 有状态的授权方式, Token 按自定义逻辑生成, 并存储在 Redis 中, 该种授权可以退出和主动删除
+ */
+ redis,
+
+ /**
+ * 无状态的授权方式, Token 使用 JWT
+ */
+ jwt
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java
new file mode 100644
index 0000000..3c43c6e
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/GrantTypeEnum.java
@@ -0,0 +1,27 @@
+package com.blossom.backend.base.auth.enums;
+
+import lombok.Getter;
+
+/**
+ * 授权方式
+ *
+ * @author xzzz
+ */
+public enum GrantTypeEnum {
+
+ /**
+ * 暂时只支持 password
+ */
+ PASSWORD("password", "密码登录, 需要传入用户名和密码");
+
+ @Getter
+ private final String type;
+
+ @Getter
+ private final String desc;
+
+ GrantTypeEnum(String type, String desc) {
+ this.type = type;
+ this.desc = desc;
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java
new file mode 100644
index 0000000..0f8409c
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/LogTypeEnum.java
@@ -0,0 +1,21 @@
+package com.blossom.backend.base.auth.enums;
+
+/**
+ * 打印请求日志的类型
+ *
+ * @author xzzz
+ */
+public enum LogTypeEnum {
+ /**
+ * 不打印请求日志
+ */
+ none,
+ /**
+ * 打印简单的请求日志, 只有请求地址
+ */
+ simple,
+ /**
+ * 答应详细的请求日志, 包含请求头, 请求参数, application/json 类型的请求体
+ */
+ detail;
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java
new file mode 100644
index 0000000..1bda661
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/enums/PasswordEncoderEnum.java
@@ -0,0 +1,21 @@
+package com.blossom.backend.base.auth.enums;
+
+/**
+ * 密码加密方式
+ *
+ * @author xzzz
+ */
+public enum PasswordEncoderEnum {
+ /**
+ * md5 与 sha256 为匹配 hash 值.
+ */
+ md5,
+ /**
+ * md5 与 sha256 为匹配 hash 值.
+ */
+ sha256,
+ /**
+ * bcrypt 安全性最高, 但加解密最慢.
+ */
+ bcrypt;
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java
new file mode 100644
index 0000000..e06920c
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthException.java
@@ -0,0 +1,39 @@
+package com.blossom.backend.base.auth.exception;
+
+import cn.hutool.core.util.StrUtil;
+import com.blossom.common.base.exception.XzAbstractException;
+import com.blossom.common.base.pojo.IRCode;
+import com.blossom.common.base.pojo.RCode;
+
+/**
+ * 授权相关异常
+ *
+ * @author xzzz
+ */
+public class AuthException extends XzAbstractException {
+
+ public AuthException(AuthRCode authRCode) {
+ super(authRCode);
+ }
+
+ public AuthException(String code, String message) {
+ super(code, message);
+ }
+
+ public static void throwBy(boolean expression, IRCode authRCode) {
+ if (authRCode == null) {
+ authRCode = RCode.BAD_REQUEST;
+ }
+ throwBy(expression, authRCode.getCode(), authRCode.getMsg());
+ }
+
+ public static void throwBy(boolean expression, String code, String msg) {
+ if (expression) {
+ if (StrUtil.isBlank(msg)) {
+ msg = RCode.BAD_REQUEST.getMsg();
+ }
+ throw new AuthException(code, msg);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java
new file mode 100644
index 0000000..b84a226
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/AuthRCode.java
@@ -0,0 +1,77 @@
+package com.blossom.backend.base.auth.exception;
+
+
+import com.blossom.common.base.pojo.IRCode;
+import lombok.Getter;
+
+/**
+ * 授权响应码
+ *
+ * @author xzzz
+ */
+public enum AuthRCode implements IRCode {
+
+ /**
+ * 400: 请求路径不合法
+ */
+ REQUEST_REJECTED ("AUTH-40001", "请求不合法","请求不合法:请求路径中可能包含不规范的内容。"),
+
+ /**
+ * 400: 登录参数请求参数不合法
+ */
+ INVALID_GRANT_TYPE ("AUTH-40002", "无效的授权方式","无效的授权方式:[GrantType] 字段错误, 请查看传入值是否在数据字典[GrantType]中。"),
+ INVALID_CLIENT_ID ("AUTH-40003", "无效的客户端","无效的客户端:[ClientId] 字段错误, 请求的客户端没有在服务器配置。"),
+
+ /**
+ * 400: 登录时发生错误
+ */
+ USERNAME_OR_PWD_FAULT ("AUTH-40004", "用户名或密码错误","用户名或密码错误, 或用户名不存在。"),
+ CAPTCHA_FAULT ("AUTH-40005", "验证码错误","验证码错误, 或手机号不存在。"),
+ USER_NOT_ENABLED ("AUTH-40010", "用户已禁用, 暂时无法登录","用户已禁用, 暂时无法登录。"),
+
+ /**
+ * 401: 未经过认证
+ * 指身份验证是必需的, 没有提供身份验证或身份验证失败。如果请求已经包含授权凭据, 那么401状态码表示不接受这些凭据。
+ */
+ INVALID_TOKEN ("AUTH-40101", "无效的授权信息",
+ "无效的授权信息\n请求时的令牌错误, 可以通过 AUTH-40101 来判断登录超时来跳转至登录页等。"),
+
+ ANOTHER_DEVICE_LOGIN ("AUTH-40102", "账号已在其他设备登录",
+ "账号已在其他设备登录。\n本账号在其他设备登录时, 本设备下次请求接口时会出现该错误, 该错误出现之后。\n再次使用该令牌访问时会响应 \"AUTH-40101\", 出现该错误通常需要提示用户后跳转至登录页。"),
+
+ /**
+ * 403: 被禁止
+ *
指示尽管请求有效, 但服务器拒绝响应它。与401状态码不同, 提供身份验证不会改变结果。
+ */
+ PERMISSION_DENIED ("AUTH-40302", "你没有权限访问该资源","你没有权限访问该资源。\n没有权限访问对应 API 接口或服务器资源。"),
+
+ /**
+ * 404:找不到请求
+ *
+ */
+ USER_NOT_EXIST ("AUTH-40401", "用户不存在","用户不存在。\n通常不会返回该信息, 而是根据 [GrantType] 提示相应参数错误"),
+ ;
+
+ @Getter
+ final String code;
+ @Getter
+ final String msg;
+ @Getter
+ final String desc;
+
+ AuthRCode(String code, String msg, String desc) {
+ this.code = code;
+ this.msg = msg;
+ this.desc = desc;
+ }
+
+ public static AuthRCode getByCode(String code) {
+ for (AuthRCode value : AuthRCode.values()) {
+ if (value.getCode().equals(code)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java
new file mode 100644
index 0000000..379134a
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/exception/ExceptionAdviceByAuth.java
@@ -0,0 +1,33 @@
+package com.blossom.backend.base.auth.exception;
+
+import com.blossom.common.base.BaseProperties;
+import com.blossom.common.base.exception.AbstractExceptionAdvice;
+import com.blossom.common.base.exception.XzAbstractException;
+import com.blossom.common.base.pojo.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.annotation.Order;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 授权异常处理
+ *
+ *
并不是所有授权阶段发生的异常都在此捕获, 例如有些异常在过滤器无法通过 Advice 捕获, 该种异常不需要继承 {@link XzAbstractException}
+ *
+ * @author xzzz
+ */
+@Slf4j
+@Order(-1)
+@RestControllerAdvice
+public class ExceptionAdviceByAuth extends AbstractExceptionAdvice {
+
+ public ExceptionAdviceByAuth(BaseProperties baseProperties) {
+ super(baseProperties);
+ }
+
+ @ExceptionHandler(AuthException.class)
+ public R> authExceptionHandler(XzAbstractException exception) {
+ printExLog(exception, exception.getMessage());
+ return R.fault(exception.getCode(), exception.getMessage());
+ }
+}
diff --git a/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java
new file mode 100644
index 0000000..b3579f1
--- /dev/null
+++ b/blossom-backend/backend/src/main/java/com/blossom/backend/base/auth/filters/AuthFilterProxy.java
@@ -0,0 +1,128 @@
+package com.blossom.backend.base.auth.filters;
+
+import com.blossom.backend.base.auth.AuthContext;
+import com.blossom.backend.base.auth.AuthProperties;
+import com.blossom.backend.base.auth.exception.AuthException;
+import com.blossom.backend.base.auth.exception.AuthRCode;
+import com.blossom.common.base.BaseProperties;
+import com.blossom.common.base.enums.ExFormat;
+import com.blossom.common.base.enums.ExStackTrace;
+import com.blossom.common.base.pojo.IRCode;
+import com.blossom.common.base.pojo.R;
+import com.blossom.common.base.pojo.RCode;
+import com.blossom.common.base.util.ExceptionUtil;
+import com.blossom.common.base.util.ServletUtil;
+import com.blossom.common.base.util.json.JsonUtil;
+import com.blossom.common.base.util.spring.SpringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Optional;
+
+/**
+ * 授权过滤器抽象类, 授权有 JWT 和 有状态TOKEN 两种实现方式, 所以具体的过滤器逻辑需要
+ * 子类重写 {@link AuthFilterProxy#doFilterInternal } 方法
+ *
+ * @author xzzz
+ */
+@Slf4j
+public abstract class AuthFilterProxy extends GenericFilterBean {
+ /**
+ * 授权配置
+ */
+ protected final AuthProperties properties;
+
+ public AuthFilterProxy(AuthProperties properties) {
+ this.properties = properties;
+ }
+
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (!properties.getEnabled()) {
+ chain.doFilter(request, response);
+ } else {
+ log.debug("[AUTHORIZ] ================================ 代理过滤器 [开始] ================================");
+ try {
+ this.doFilterInternal(request, response, chain);
+ } catch (Exception e) {
+ log.debug("[AUTHORIZ] **Proxy** >> 代理过滤器执行异常: {}", e.getMessage());
+ onAuthenticationFailure(request, response, e);
+ } finally {
+ // 无论执行成功与否都需要清空上下文
+ AuthContext.removeContext();
+ log.debug("[AUTHORIZ] **Proxy** << 代理过滤器: response 清空上下文: {}", JsonUtil.toJson(AuthContext.getContext()));
+ }
+ log.debug("[AUTHORIZ] ================================ 代理过滤器 [结束] ================================");
+ }
+ }
+
+ /**
+ * 执行过滤器, 由不同的子过滤器实现具体的过滤器内容
+ *
+ * @param request
+ * @param response
+ * @param chain
+ * @throws IOException
+ * @throws ServletException
+ */
+ protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
+
+ /**
+ * 执行时出现异常, 在此捕获并统一处理
+ *
+ * @param request request
+ * @param response response
+ * @param exception ex
+ */
+ protected void onAuthenticationFailure(ServletRequest request, ServletResponse response, Exception exception) throws IOException {
+ HttpServletResponse resp = (HttpServletResponse) response;
+
+ resp.setContentType("application/json;charset=utf-8");
+ // 要求所有响应都为 200
+ resp.setStatus(200);
+
+ PrintWriter out = resp.getWriter();
+
+ // 自定义响应码
+ IRCode authCode = RCode.INTERNAL_SERVER_ERROR;
+ if (exception instanceof AuthException) {
+ authCode = AuthRCode.getByCode(((AuthException) exception).getCode());
+ }
+
+ printStackTrace(authCode, exception);
+
+ R