mirror of
https://github.com/blossom-editor/blossom
synced 2024-11-17 14:39:21 +08:00
init
This commit is contained in:
parent
8995e2d58f
commit
b6c6684ff3
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 blossom-editor
|
||||
Copyright (c) 2023 小贼贼子
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Blossom 笔记软件
|
||||
|
||||
Blossom 是一个**云端存储的笔记软件**,你可以搭建自己的服务器来保存你的所有笔记,图片,个人计划安排,并在任意设备之间实时同步。
|
||||
|
||||
![](./doc/imgs/home_ld.jpg "Blossom 首页")
|
||||
|
||||
当前版本: `1.0.0`
|
||||
|
||||
> 更新日志
|
||||
|
||||
---
|
||||
|
||||
#### Blossom 具有以下优势:
|
||||
|
||||
1. **不再需要为同步设备数量付费**。
|
||||
2. **不再需要为公网访问付费**。
|
||||
3. **不再需要为软件付费**,基于[MIT协议](https://choosealicense.com/licenses/mit/)完全开源
|
||||
4. **自带截图功能**(仅windows),你不再需要使用其他截图工具截图后保存在本地再上传到文章了。
|
||||
5. **文章与图片都在你的服务器存储**,不需要再使用任何三方图床,不需要购买任何对象存储。
|
||||
6. **没有任何私有协议**,基于 Markdown 语法,采用约定格式拓展样式。迁移到其他软件可以正常显示。
|
||||
7. **所有文章和图片一键打包下载**,你可以一键导出迁移笔记。
|
||||
8. 多账号权限,可以和你的朋友一起使用。或者将自己的生活/工作笔记存放在不同账号中。
|
||||
|
||||
#### Blossom 具有以下功能:
|
||||
1. **文章编辑**:文章编写,以及文章公网访问权限设置。
|
||||
2. **双链笔记**:内部文章与外部链接引用形成的双链笔记。
|
||||
3. **图片管理**:按文章目录分类你的图片,或自定义图片目录。
|
||||
4. **快捷便签**:方便你记录随意的日常信息。
|
||||
5. **计划安排**:阶段性的长期计划,或者日历中的短期计划安排。
|
||||
6. **网站收藏**:常用网站快捷访问,或者说是一个书签功能。
|
||||
7. 笔记总字数折线图,笔记编辑数量热力图,服务器请求量折线图展示。
|
||||
|
||||
更多内容,可前往[源码仓库](https://gitee.com/blossom-projects/blossom)或[文档](https://www.wangyunf.com/blossomdoc)查看。
|
32
blossom-backend/.gitignore
vendored
Normal file
32
blossom-backend/.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**
|
||||
!**/src/test/**
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/logs/
|
149
blossom-backend/README.md
Normal file
149
blossom-backend/README.md
Normal file
@ -0,0 +1,149 @@
|
||||
# 一、项目介绍
|
||||
|
||||
私密信息:如`Git`账号等不在此列。
|
||||
---
|
||||
|
||||
# 二、系统基本信息
|
||||
|
||||
本项目为Web项目,除`SpringBoot`使用`2.x`最新版本外,其余不做强制版本要求,只需兼容`JDK8`即可。中间件版本跟随需求变更。
|
||||
|
||||
`SpringBoot`版本说明请查阅:https://spring.io/projects/spring-boot#support。
|
||||
|
||||
简略的注意事项:
|
||||
1. 接口文档使用`SmartDoc`,不要引入其他三方接口文档框架,如`Swagger`等。用法请查看第【4.1】项。
|
||||
2. 接口调用需授权时,请查看第【4.2】项的系统登录。
|
||||
3. 项目的模块说明请查看第【三】项。
|
||||
4. 需遵循第【五】项中说明的开发规范。
|
||||
5. 本项目依赖其他开源项目,本身为闭源项目。并遵循各类框架的开源协议。
|
||||
|
||||
---
|
||||
|
||||
# 三、模块说明
|
||||
|
||||
```
|
||||
root
|
||||
├─ aodc 相关文档
|
||||
├─ backend-admin 后台应用,当不需要多应用部署时,默认使用该应用对外提供所有服务,该应用只写 Controller 层。
|
||||
├─ backend-service 业务代码 Service 层与持久层 Mapper。
|
||||
| ├─ service-base 提供基础的用户登录,RBAC,文件上传,系统字典,参数等功能,通常所有 Service 模块都依赖该模块。
|
||||
| ├─ service-biz 业务代码,实现业务需求的模块。
|
||||
| └─ service-flow 工作流引擎,如果系统需要使用工作流,那么通常所有 Service 模块都依赖该模块。
|
||||
|
|
||||
├─ common 项目公共模块
|
||||
| ├─ common-base 基础对象(如公共响应 R.class),工具类,异常捕获,动态日志级别,Caffeine 等。
|
||||
| ├─ common-cache 缓存封装,Redis 封装,Redis Cache 封装。
|
||||
| ├─ common-db 数据库模块,使用 mybatis plus,dynamic-datasource,慢SQL监控。
|
||||
| ├─ common-es ElasticSearch 功能封装。
|
||||
| ├─ common-iaas 云厂商功能的封装,如对象存储,SMS短信服务,IM即时消息等。
|
||||
| └─ common-wechat 微信相关功能的封装。
|
||||
|
|
||||
├─ expand-sentinel 对 Alibaba Sentinel 的封装,拓展了一些功能,实现了一些本地流量查询接口。
|
||||
├─ expand-tracker 自研的链路追踪核心模块, 源自 tracker-core, 仅提供本地日志记录和日志中插入 Trace 的功能。
|
||||
└─ smart-doc 本地接口文档,在 Smart-Doc 上修改了样式,使之更符合开发阅读习惯。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 四、使用说明
|
||||
|
||||
---
|
||||
|
||||
## 4.1 SmartDoc 接口文档使用说明
|
||||
|
||||
本系统接口文档依赖于`smart-doc`,并修改了`smart-doc`的文档样式,所以需要本地编译`smart-doc`才可使用。<a href="https://smart-doc-group.github.io/#/zh-cn/diy/highlight">官方文档地址</a>
|
||||
|
||||
<u>**第一步:**</u>在 smart-doc 目录执行如下命令。
|
||||
```
|
||||
mvn clean install -Dmaven.test.skip=true -Dfile.encoding=UTF-8 -Dmaven.javadoc.skip=true
|
||||
```
|
||||
|
||||
<u>**第二步:**</u>搜索 `SmartDocConfig.class` 类,并执行该类生成接口文档。
|
||||
|
||||
---
|
||||
|
||||
## 4.2 系统登录
|
||||
|
||||
登录请访问`/<context-path>/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. 配置文件需要写明注释。
|
9
blossom-backend/adoc/restart-springboot.sh
Normal file
9
blossom-backend/adoc/restart-springboot.sh
Normal file
@ -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 正在启动,请查看日志 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓"
|
330
blossom-backend/adoc/sql/blossom.sql
Normal file
330
blossom-backend/adoc/sql/blossom.sql
Normal file
@ -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;
|
139
blossom-backend/backend/pom.xml
Normal file
139
blossom-backend/backend/pom.xml
Normal file
@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>blossom-backend</artifactId>
|
||||
<groupId>com.blossom</groupId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>backend</artifactId>
|
||||
|
||||
<properties>
|
||||
<java-jwt.version>4.3.0</java-jwt.version> <!-- JWT -->
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>common-base</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>common-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>common-db</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>common-iaas</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 简易链路追踪 -->
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>expand-tracker-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentinel MVC 接口自动拦截 -->
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>expand-sentinel-springmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentinel 指标查询拓展接口 -->
|
||||
<dependency>
|
||||
<groupId>com.blossom</groupId>
|
||||
<artifactId>expand-sentinel-metric</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>${java-jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 图片处理 -->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.20</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>backend-blossom</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- 解决加载tools.jar的问题 -->
|
||||
<includeSystemScope>true</includeSystemScope>
|
||||
<finalName>${project.build.finalName}</finalName>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.6.1</version>
|
||||
<configuration>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!--配置运行main函数-->
|
||||
<mainClass>com.blossom.backend.APP</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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)
|
||||
* <p>
|
||||
* 参考文档:
|
||||
* 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<String> DEFAULT_WHITE_LIST = new ArrayList<String>() {
|
||||
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;
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* 授权上下文
|
||||
* <p>
|
||||
* 设置: {@link RedisTokenValidateFilter}
|
||||
* 删除: {@link RedisTokenAuthFilterProxy#doFilter(ServletRequest, ServletResponse, FilterChain)}
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class AuthContext {
|
||||
|
||||
private static final ThreadLocal<AccessToken> 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<String, String> 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;
|
||||
}
|
||||
}
|
@ -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<AccessToken> 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<AccessToken> check() {
|
||||
return R.ok(authService.check());
|
||||
}
|
||||
}
|
@ -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 时效是不同的
|
||||
* <p>
|
||||
* 该类创建完成后会检查是否配置平台数据, 未自定义平台配置会创建默认配置
|
||||
*
|
||||
* @see AuthProperties#initAfterProcessorClient()
|
||||
*/
|
||||
private List<Client> clients;
|
||||
/**
|
||||
* platforms 转为 map, 方便查询
|
||||
*/
|
||||
private Map<String, Client> clientMap;
|
||||
/**
|
||||
* 白名单列表
|
||||
* 配置时不需要增加context-path,会自动拼接
|
||||
*/
|
||||
private List<String> whiteList;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 检查配置的授权类型是否正确
|
||||
initAfterProcessorTypeCheck();
|
||||
// 白名单后置处理
|
||||
initAfterProcessorWhiteList();
|
||||
// 平台配置后置处理
|
||||
initAfterProcessorClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 白名单后置处理
|
||||
*/
|
||||
private void initAfterProcessorWhiteList() {
|
||||
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
|
||||
Map<RequestMappingInfo, HandlerMethod> methodMap = mapping.getHandlerMethods();
|
||||
methodMap.forEach((key, value) -> {
|
||||
if (null != AnnotationUtil.getAnnotation(value.getMethod(), AuthIgnore.class)) {
|
||||
if (key.getPatternsCondition() != null) {
|
||||
Set<String> 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 的校验逻辑
|
||||
* <p>为 true 则每次登录 token 会替换前一个 token。
|
||||
* <p>为 false 则每次登录返回的 token 是一样的。
|
||||
*/
|
||||
private Boolean multiPlaceLogin;
|
||||
/**
|
||||
* 该平台允许的登录方式
|
||||
*/
|
||||
private List<String> 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<String> getGrantType() {
|
||||
return grantType;
|
||||
}
|
||||
|
||||
public void setGrantType(List<String> 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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();
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.blossom.backend.base.auth.enums;
|
||||
|
||||
/**
|
||||
* 授权类型
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum AuthTypeEnum {
|
||||
/**
|
||||
* 有状态的授权方式, Token 按自定义逻辑生成, 并存储在 Redis 中, 该种授权可以退出和主动删除
|
||||
*/
|
||||
redis,
|
||||
|
||||
/**
|
||||
* 无状态的授权方式, Token 使用 JWT
|
||||
*/
|
||||
jwt
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.blossom.backend.base.auth.enums;
|
||||
|
||||
/**
|
||||
* 打印请求日志的类型
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum LogTypeEnum {
|
||||
/**
|
||||
* 不打印请求日志
|
||||
*/
|
||||
none,
|
||||
/**
|
||||
* 打印简单的请求日志, 只有请求地址
|
||||
*/
|
||||
simple,
|
||||
/**
|
||||
* 答应详细的请求日志, 包含请求头, 请求参数, application/json 类型的请求体
|
||||
*/
|
||||
detail;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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: 未经过认证
|
||||
* <p>指身份验证是必需的, 没有提供身份验证或身份验证失败。如果请求已经包含授权凭据, 那么401状态码表示不接受这些凭据。
|
||||
*/
|
||||
INVALID_TOKEN ("AUTH-40101", "无效的授权信息",
|
||||
"无效的授权信息\n请求时的令牌错误, 可以通过 AUTH-40101 来判断登录超时来跳转至登录页等。"),
|
||||
|
||||
ANOTHER_DEVICE_LOGIN ("AUTH-40102", "账号已在其他设备登录",
|
||||
"账号已在其他设备登录。\n本账号在其他设备登录时, 本设备下次请求接口时会出现该错误, 该错误出现之后。\n再次使用该令牌访问时会响应 \"AUTH-40101\", 出现该错误通常需要提示用户后跳转至登录页。"),
|
||||
|
||||
/**
|
||||
* 403: 被禁止
|
||||
* <p>指示尽管请求有效, 但服务器拒绝响应它。与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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* 授权异常处理
|
||||
*
|
||||
* <p>并不是所有授权阶段发生的异常都在此捕获, 例如有些异常在过滤器无法通过 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());
|
||||
}
|
||||
}
|
@ -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<Object> apiResult = R.fault(
|
||||
authCode.getCode(),
|
||||
authCode.getMsg(),
|
||||
exception.getMessage());
|
||||
String resultJson = JsonUtil.toJson(apiResult);
|
||||
out.write(resultJson);
|
||||
out.flush();
|
||||
out.close();
|
||||
}
|
||||
|
||||
protected void printStackTrace(IRCode authCode, Exception exception) {
|
||||
boolean filterStackTrace = false;
|
||||
boolean onLine = false;
|
||||
|
||||
try {
|
||||
BaseProperties properties = SpringUtil.getBean(BaseProperties.class);
|
||||
filterStackTrace = properties.getEx().getStackTrace().equals(ExStackTrace.project);
|
||||
onLine = properties.getEx().getFormat().equals(ExFormat.line);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
log.error("{} | {}({})", ServletUtil.getUri(), exception.getClass().getSimpleName(),
|
||||
Optional.ofNullable(authCode.getMsg()).orElse(""));
|
||||
log.error(ExceptionUtil.printStackTrace(exception, filterStackTrace, onLine));
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
package com.blossom.backend.base.auth.filters;
|
||||
|
||||
import com.blossom.backend.base.auth.exception.AuthException;
|
||||
import com.blossom.backend.base.auth.exception.AuthRCode;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 请求链接防火墙
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class HttpFirewall {
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「.」的 URL 编码 %2e
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_ENCODED_PERIOD =
|
||||
Collections.unmodifiableList(Arrays.asList("%2e", "%2E"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「;」以及「;」的 URL 编码 %3b
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_SEMICOLON =
|
||||
Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「/」的 URL 编码 %2f
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_FORWARDSLASH =
|
||||
Collections.unmodifiableList(Arrays.asList("%2f", "%2F"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「//」以及「//」的 URL 编码 %2f%2f
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_DOUBLE_FORWARDSLASH =
|
||||
Collections.unmodifiableList(Arrays.asList("//", "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「\」以及「\」的 URL 编码 %5c
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_BACKSLASH =
|
||||
Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「」以及「」的 URL 编码 %00
|
||||
* (不是 null 而是空) 可转码 %00 查看, 转码后什么都没有
|
||||
*/
|
||||
private static final List<String> FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00"));
|
||||
|
||||
/**
|
||||
* 请求地址中不允许出现「%」以及「%」的 URL 编码 %25
|
||||
*/
|
||||
private static final String PERCENT = "%";
|
||||
private static final String ENCODED_PERCENT = "%25";
|
||||
|
||||
private final Set<String> encodedUrlBlockList = new HashSet<>();
|
||||
private final Set<String> decodedUrlBlockList = new HashSet<>();
|
||||
|
||||
public HttpFirewall() {
|
||||
// 请求地址中不允许出现「.」的 URL 编码 %2e
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_ENCODED_PERIOD);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_ENCODED_PERIOD);
|
||||
// 请求地址中不允许出现「;」以及「;」的 URL 编码 %3b
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_SEMICOLON);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_SEMICOLON);
|
||||
// 请求地址中不允许出现「/」的 URL 编码 %2f
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_FORWARDSLASH);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_FORWARDSLASH);
|
||||
// 请求地址中不允许出现「//」以及「//」的 URL 编码 %2f%2f
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
|
||||
// 请求地址中不允许出现「\」以及「\」的 URL 编码 %5c
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_BACKSLASH);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_BACKSLASH);
|
||||
// 请求地址中不允许出现「」以及「」的 URL 编码 %00
|
||||
this.encodedUrlBlockList.addAll(FORBIDDEN_NULL);
|
||||
this.decodedUrlBlockList.addAll(FORBIDDEN_NULL);
|
||||
// 请求地址中不允许出现 %, 以及 % 的 URL 编码 %25
|
||||
this.encodedUrlBlockList.add(ENCODED_PERCENT);
|
||||
this.decodedUrlBlockList.add(PERCENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许的请求方式
|
||||
*/
|
||||
private final Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();
|
||||
private static Set<String> createDefaultAllowedHttpMethods() {
|
||||
Set<String> result = new HashSet<>();
|
||||
result.add(HttpMethod.DELETE.name());
|
||||
result.add(HttpMethod.GET.name());
|
||||
result.add(HttpMethod.HEAD.name());
|
||||
result.add(HttpMethod.OPTIONS.name());
|
||||
result.add(HttpMethod.PATCH.name());
|
||||
result.add(HttpMethod.POST.name());
|
||||
result.add(HttpMethod.PUT.name());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行防火墙
|
||||
* @param servletRequest request
|
||||
*/
|
||||
public void wall(ServletRequest servletRequest) {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
|
||||
// 判断请求方式
|
||||
boolean isForbiddenMethod = rejectForbiddenHttpMethod(request);
|
||||
if (!isForbiddenMethod) {
|
||||
throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求方式错误 [" + request.getMethod() + "], 只允许如下请求: " + this.allowedHttpMethods);
|
||||
}
|
||||
|
||||
// 是否是无效的 url 请求
|
||||
boolean isNormalized = isNormalized(request);
|
||||
if (!isNormalized) {
|
||||
throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求链接格式错误, 请检查!");
|
||||
}
|
||||
|
||||
// 判断请求连接中的非法字符
|
||||
rejectedBlockListedUrls(request);
|
||||
|
||||
// 如果请求地址中包含不可打印的 ASCII 字符,请求则会被拒绝
|
||||
boolean isContainsOnlyPrintableAsciiCharacters = containsOnlyPrintableAsciiCharacters(request);
|
||||
if (!isContainsOnlyPrintableAsciiCharacters) {
|
||||
throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求链接包含非法字符, 请检查!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求方式
|
||||
* @param request request
|
||||
*/
|
||||
private boolean rejectForbiddenHttpMethod(HttpServletRequest request) {
|
||||
return this.allowedHttpMethods.contains(request.getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 URL 请求, 路径中不能包含
|
||||
* ./
|
||||
* /../
|
||||
* /.
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 是否
|
||||
*/
|
||||
private boolean isNormalized(HttpServletRequest request) {
|
||||
if (!isNormalized(request.getRequestURI())) {
|
||||
return false;
|
||||
}
|
||||
if (!isNormalized(request.getContextPath())) {
|
||||
return false;
|
||||
}
|
||||
if (!isNormalized(request.getServletPath())) {
|
||||
return false;
|
||||
}
|
||||
if (!isNormalized(request.getPathInfo())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 URL 请求, 路径中不能包含
|
||||
* ./
|
||||
* /../
|
||||
* /.
|
||||
*
|
||||
* @param path 请求路径
|
||||
* @return 是否
|
||||
*/
|
||||
private boolean isNormalized(String path) {
|
||||
if (path == null) {
|
||||
return true;
|
||||
}
|
||||
for (int i = path.length(); i > 0;) {
|
||||
int slashIndex = path.lastIndexOf('/', i - 1);
|
||||
int gap = i - slashIndex;
|
||||
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
|
||||
// ".", "/./" or "/."
|
||||
return false;
|
||||
}
|
||||
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
|
||||
return false;
|
||||
}
|
||||
i = slashIndex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果请求地址中包含不可打印的 ASCII 字符,请求则会被拒绝
|
||||
* @param request request
|
||||
* @return 是否
|
||||
*/
|
||||
private static boolean containsOnlyPrintableAsciiCharacters(HttpServletRequest request) {
|
||||
String requestUri = request.getRequestURI();
|
||||
int length = requestUri.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = requestUri.charAt(i);
|
||||
if (c < '\u0020' || c > '\u007e') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中不能包含 // 双斜杠
|
||||
* @param request request
|
||||
*/
|
||||
private void rejectedBlockListedUrls(HttpServletRequest request) {
|
||||
for (String forbidden : this.encodedUrlBlockList) {
|
||||
if (encodedUrlContains(request, forbidden)) {
|
||||
throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求被拒绝, 请求路径中包含非法字符 [" + forbidden + "]");
|
||||
}
|
||||
}
|
||||
for (String forbidden : this.decodedUrlBlockList) {
|
||||
if (decodedUrlContains(request, forbidden)) {
|
||||
throw new AuthException(AuthRCode.REQUEST_REJECTED.getCode(), "请求被拒绝, 请求路径中包含非法字符 [" + forbidden + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private boolean encodedUrlContains(HttpServletRequest request, String value) {
|
||||
if (valueContains(request.getContextPath(), value)) {
|
||||
return true;
|
||||
}
|
||||
return valueContains(request.getRequestURI(), value);
|
||||
}
|
||||
|
||||
private boolean decodedUrlContains(HttpServletRequest request, String value) {
|
||||
if (valueContains(request.getServletPath(), value)) {
|
||||
return true;
|
||||
}
|
||||
if (valueContains(request.getPathInfo(), value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean valueContains(String value, String contains) {
|
||||
return value != null && value.contains(contains);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.blossom.backend.base.auth.filters;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* request 封装类,用于实现多次读取 requet body
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class RequestBodyReaderWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private byte[] body;
|
||||
|
||||
public RequestBodyReaderWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
body = IoUtil.readBytes(request.getInputStream(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public void setInputStream(byte[] body) {
|
||||
this.body = body;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package com.blossom.backend.base.auth.filters;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import com.blossom.backend.base.auth.AuthProperties;
|
||||
import com.blossom.backend.base.auth.enums.LogTypeEnum;
|
||||
import com.blossom.common.base.util.SystemUtil;
|
||||
import com.blossom.common.base.util.json.JsonUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author wangyf
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
public class RequestLogFilter {
|
||||
/**
|
||||
* 配置文件内容
|
||||
*/
|
||||
private AuthProperties properties;
|
||||
|
||||
public RequestLogFilter(AuthProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
if (properties.getLogType().equals(LogTypeEnum.none)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示请求路径
|
||||
try {
|
||||
if (properties.getLogType().equals(LogTypeEnum.simple)) {
|
||||
HttpServletRequest req = ((HttpServletRequest) request);
|
||||
String uri = req.getRequestURI();
|
||||
log.info("[AUTHORIZ] IP:{} > {}", StrUtil.fillAfter(SystemUtil.getRemoteIp(req), ' ', 15), uri);
|
||||
}
|
||||
|
||||
// 显示请求详情,此配置会覆盖显示请求路径
|
||||
if (properties.getLogType().equals(LogTypeEnum.detail)) {
|
||||
HttpServletRequest req = ((HttpServletRequest) request);
|
||||
|
||||
Enumeration<String> headerNames = req.getHeaderNames();
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String key = headerNames.nextElement();
|
||||
String value = req.getHeader(key);
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
String requestBody = "";
|
||||
if (request.getContentType() != null && request.getContentType().contains("application/json")) {
|
||||
try {
|
||||
requestBody = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
|
||||
} catch (Exception e) {
|
||||
log.warn("请求体转换错误");
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("[AUTHORIZ] 请求详情 [{}:{}] From [{}]" +
|
||||
"\n请求参数: [{}]" +
|
||||
"\n请求体 : [{}]" +
|
||||
"\n请求头 : [{}]",
|
||||
req.getMethod(), req.getRequestURL().toString(), SystemUtil.getRemoteIp(req),
|
||||
StrUtil.isBlank(req.getQueryString()) ? "" : URLUtil.decode(req.getQueryString()),
|
||||
requestBody,
|
||||
JsonUtil.toPrettyJson(headers)
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("输出日志错误:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.blossom.backend.base.auth.filters;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 包装request, 提供多次读取请求体的机会
|
||||
*
|
||||
* @author wangyf
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(AuthConstant.AUTH_FILTER_WRAPPER_ORDER)
|
||||
public class RequestWrapperFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
// ContentType 为 "application/json" 格式才使用包装类
|
||||
// 这样是为了过滤上传文件等类型的请求
|
||||
if (request.getContentType() != null && request.getContentType().contains("application/json")) {
|
||||
ServletRequest requestWrapper = new RequestBodyReaderWrapper(request);
|
||||
filterChain.doFilter(requestWrapper, response);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.blossom.backend.base.auth.filters;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import com.blossom.backend.base.auth.AuthProperties;
|
||||
import com.blossom.common.base.util.spring.AntPathMatcherUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* [0]
|
||||
*
|
||||
* 白名单拦截器, 不一定作为拦截器链的入口
|
||||
* 通过获取配置文件中的白名单列表, 对请求(URI)进行判断
|
||||
* 支持 Ant 风格, 使用 {@link org.springframework.util.AntPathMatcher} 类进行匹配
|
||||
* 在白名单中的请求不会执行后续过滤器链, 即无法获取授权上下文
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
public class WhiteListFilter {
|
||||
|
||||
/**
|
||||
* 配置文件内容
|
||||
*/
|
||||
private final AuthProperties properties;
|
||||
|
||||
public WhiteListFilter(AuthProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果不在白名单 isWhiteRequest = false, 则需要走授权校验逻辑, 即继续代理过滤器链流程
|
||||
* 如果在白名单中 isWhiteRequest = true, 则中断过滤器链, 代理过滤器会调用后续原生过滤器
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
*/
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
log.debug("[AUTHORIZ] filter(000) 白名单校验");
|
||||
|
||||
String uri = ((HttpServletRequest)request).getRequestURI();
|
||||
// 如果请求路径在白名单中, 则允许略过请求
|
||||
for (String pattern : properties.getWhiteList()) {
|
||||
if (AntPathMatcherUtil.match(pattern, uri)) {
|
||||
request.setAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY,true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.blossom.backend.base.auth.interceptor;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.user.UserService;
|
||||
import com.blossom.backend.base.user.UserTypeEnum;
|
||||
import com.blossom.backend.base.user.pojo.UserEntity;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Slf4j
|
||||
public class UserTypeInterceptor implements HandlerInterceptor {
|
||||
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
public void setUserService(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @return 通过与否
|
||||
* @throws Exception 异常处理
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
//必须强转为HandlerMethod
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
|
||||
// 白名单不校验
|
||||
boolean isIgnore = handlerMethod.hasMethodAnnotation(AuthIgnore.class);
|
||||
if (isIgnore) {
|
||||
return true;
|
||||
}
|
||||
// GET请求校验
|
||||
if (HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UserEntity user = userService.selectById(AuthContext.getUserId());
|
||||
XzException404.throwBy(ObjUtil.isNull(user), "未查询到你的账户信息");
|
||||
XzException400.throwBy(UserTypeEnum.READONLY.getType().equals(user.getType()), "您的账号为只读账号, 无法使用该功能");
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.blossom.backend.base.auth.jwt;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import com.blossom.backend.base.auth.AuthProperties;
|
||||
import com.blossom.backend.base.auth.filters.AuthFilterProxy;
|
||||
import com.blossom.backend.base.auth.filters.HttpFirewall;
|
||||
import com.blossom.backend.base.auth.filters.RequestLogFilter;
|
||||
import com.blossom.backend.base.auth.filters.WhiteListFilter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT 过滤器实现
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(AuthConstant.AUTH_FILTER_PROXY)
|
||||
@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt", matchIfMissing = true)
|
||||
public class JWTAuthFilterProxy extends AuthFilterProxy {
|
||||
|
||||
/**
|
||||
* 防火墙对象
|
||||
*/
|
||||
private final HttpFirewall httpFirewall = new HttpFirewall();
|
||||
/**
|
||||
* 日志过滤器
|
||||
*/
|
||||
private final RequestLogFilter logFilter;
|
||||
/**
|
||||
* 白名单过滤器
|
||||
*/
|
||||
private final WhiteListFilter whiteListFilter;
|
||||
/**
|
||||
* JWT校验过滤器
|
||||
*/
|
||||
private final JWTValidateFilter validateFilter;
|
||||
|
||||
public JWTAuthFilterProxy(AuthProperties properties, JWTTokenEncoder tokenEncoder) {
|
||||
super(properties);
|
||||
this.logFilter = new RequestLogFilter(properties);
|
||||
this.whiteListFilter = new WhiteListFilter(properties);
|
||||
this.validateFilter = new JWTValidateFilter(tokenEncoder);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>1. 防火墙校验
|
||||
* <p>2. 授权校验
|
||||
* <ol>
|
||||
* <li>日志</li>
|
||||
* <li>白名单</li>
|
||||
* <li>授权校验</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @param chain chain
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
/*
|
||||
1. 执行防火墙
|
||||
*/
|
||||
httpFirewall.wall(request);
|
||||
|
||||
/*
|
||||
2. 授权校验
|
||||
*/
|
||||
// 2.1 日志
|
||||
logFilter.doFilter(request, response);
|
||||
// 2.2 白名单
|
||||
whiteListFilter.doFilter(request, response);
|
||||
// 2.3 token 校验
|
||||
validateFilter.doFilter(request, response);
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package com.blossom.backend.base.auth.jwt;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.exceptions.SignatureVerificationException;
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.blossom.backend.base.auth.token.TokenEncoder;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import com.blossom.common.base.util.DateUtils;
|
||||
import com.blossom.common.base.util.json.JsonUtil;
|
||||
import com.blossom.common.base.util.spring.SpringUtil;
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* jwt 在线解密解码:https://www.box3.cn/tools/jwt.html
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt")
|
||||
public class JWTTokenEncoder implements TokenEncoder, EnvironmentAware {
|
||||
/**
|
||||
* 加密字符串
|
||||
*/
|
||||
private final String secret = "T1h22WstOUaStOiUnAuCYif3Kw7DeLUciE8iWVAReqdnJTnJ7n4WYtE6x0UW";
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
private final Algorithm signer = Algorithm.HMAC256(secret);
|
||||
private Environment environment;
|
||||
private String appName;
|
||||
|
||||
@Override
|
||||
public void setEnvironment(@NotNull Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.appName = environment.getProperty(SpringUtil.APP_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* token 对象转为 jwt
|
||||
*
|
||||
* @param accessToken token
|
||||
* @return jwt 字符串
|
||||
*/
|
||||
@Override
|
||||
public String encode(AccessToken accessToken) {
|
||||
try {
|
||||
return JWT.create()
|
||||
.withIssuer(appName)
|
||||
// 主题,科目
|
||||
.withSubject(accessToken.getClientId())
|
||||
// 自定义字段
|
||||
.withClaim("expire", accessToken.getExpire())
|
||||
.withClaim("duration", accessToken.getDuration())
|
||||
.withClaim("loginTime", accessToken.getLoginTime())
|
||||
.withClaim("userId", accessToken.getUserId())
|
||||
.withClaim("metadata", JsonUtil.toJson(accessToken.getMetadata()))
|
||||
// 过期时间
|
||||
.withExpiresAt(DateUtils.date(accessToken.getExpire()))
|
||||
.sign(signer);
|
||||
} catch (Exception e) {
|
||||
log.error("授权发生异常:", e);
|
||||
throw new XzException500("JWT授权异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码 jwt
|
||||
*
|
||||
* @param token jwt
|
||||
* @return token 对象
|
||||
*/
|
||||
@Override
|
||||
public AccessToken decode(String token) {
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(signer).build();
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
AccessToken accessToken = new AccessToken();
|
||||
accessToken.setToken(token);
|
||||
|
||||
Long userId = jwt.getClaim("userId").asLong();
|
||||
if (userId == null || StrUtil.isBlank(jwt.getClaim("metadata").asString())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
accessToken.setExpire(jwt.getClaim("expire").asLong());
|
||||
accessToken.setDuration(jwt.getClaim("duration").asInt());
|
||||
accessToken.setLoginTime(jwt.getClaim("loginTime").asString());
|
||||
accessToken.setUserId(jwt.getClaim("userId").asLong());
|
||||
accessToken.setMetadata(JsonUtil.toMap(jwt.getClaim("metadata").asString()));
|
||||
return accessToken;
|
||||
} catch (SignatureVerificationException e) {
|
||||
log.error("token 解码时 Signature 错误");
|
||||
throw e;
|
||||
} catch (JWTDecodeException e) {
|
||||
log.error("token 解码错误");
|
||||
throw e;
|
||||
} catch (TokenExpiredException e) {
|
||||
log.error("token 解码超时");
|
||||
throw e;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("token 解码参数错误");
|
||||
throw e;
|
||||
} catch (JWTVerificationException e) {
|
||||
log.error("token 解码失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.blossom.backend.base.auth.jwt;
|
||||
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
import com.blossom.backend.base.auth.repo.TokenRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* JWT 存储, JWT 无状态无需存储
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "project.auth.type", havingValue = "jwt")
|
||||
public class JWTTokenRepository implements TokenRepository {
|
||||
|
||||
@Override
|
||||
public void saveToken(AccessToken accessToken) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessToken getToken(String token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String token) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUniqueToken(AccessToken accessToken) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueToken(String userId) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package com.blossom.backend.base.auth.jwt;
|
||||
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.exceptions.SignatureVerificationException;
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException;
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.TokenUtil;
|
||||
import com.blossom.backend.base.auth.exception.AuthException;
|
||||
import com.blossom.backend.base.auth.exception.AuthRCode;
|
||||
import com.blossom.common.base.util.ServletUtil;
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求验证, 校验请求是否携带 token, 未携带的进行拦截跑出错误
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
public class JWTValidateFilter {
|
||||
|
||||
private final JWTTokenEncoder jwtTokenEncoder;
|
||||
|
||||
public JWTValidateFilter(JWTTokenEncoder jwtTokenEncoder) {
|
||||
this.jwtTokenEncoder = jwtTokenEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行过滤器
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @throws IOException io
|
||||
* @throws ServletException servlet
|
||||
*/
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
log.debug("[AUTHORIZ] filter(100) 授权校验过滤器");
|
||||
|
||||
boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY));
|
||||
|
||||
// 解析请求头中的 token
|
||||
String token = getHeaderToken(request);
|
||||
|
||||
// 如果白名单且无token, 则直接放行
|
||||
if (isWhiteList && (StrUtil.isBlank(token) || AuthConstant.HEADER_TOKEN_PREFIX.trim().equals(token))) {
|
||||
return;
|
||||
}
|
||||
|
||||
AccessToken accessToken = null;
|
||||
try {
|
||||
try {
|
||||
accessToken = jwtTokenEncoder.decode(token);
|
||||
} catch (SignatureVerificationException e) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
} catch (JWTDecodeException e) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
} catch (TokenExpiredException e) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
} catch (JWTVerificationException e) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
}
|
||||
} catch (AuthException e) {
|
||||
if (!isWhiteList) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是白名单请求没有授权信息,则构造空的授权信息到上下文,异常由调用方捕获
|
||||
if (isWhiteList && null == accessToken) {
|
||||
AccessToken accessTokenNull = new AccessToken();
|
||||
accessTokenNull.setToken("");
|
||||
accessTokenNull.setUserId(0L);
|
||||
setAuthContext(accessTokenNull);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果白名单请求,且获得了token,则说明是登录状态请求白名单的接口,正常执行下一过滤器
|
||||
if (isWhiteList && null != accessToken) {
|
||||
// 将授权数据存放到上下文中,不执行后续过滤器
|
||||
setAuthContext(accessToken);
|
||||
return;
|
||||
}
|
||||
// 如果不是白名单请求,则抛出错误
|
||||
if (!isWhiteList && null == accessToken) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
// 将授权数据存放到上下文中,并执行下一过滤器
|
||||
setAuthContext(accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 从请求头中获取 Token
|
||||
*
|
||||
* @param servletRequest request
|
||||
* @return Token
|
||||
*/
|
||||
private String getHeaderToken(ServletRequest servletRequest) {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
|
||||
|
||||
// 从请求头获取
|
||||
String token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION);
|
||||
|
||||
// 从请求头获取全小写
|
||||
if (StrUtil.isBlank(token)) {
|
||||
token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toLowerCase());
|
||||
}
|
||||
|
||||
// 从请求头获取全大写
|
||||
if (StrUtil.isBlank(token)) {
|
||||
token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toUpperCase());
|
||||
}
|
||||
|
||||
// 从 cookie 获取
|
||||
if (StrUtil.isBlank(token)) {
|
||||
Cookie cookie = ServletUtil.getCookie(httpServletRequest, AuthConstant.HEADER_AUTHORIZATION);
|
||||
if (cookie != null) {
|
||||
token = cookie.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// 以 Bearer 开头 && 除此外长度 > 0
|
||||
token = TokenUtil.cutPrefix(token);
|
||||
if (null != token && token.length() > 0) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 保存到上下文
|
||||
*
|
||||
* @param accessToken 授权主体
|
||||
*/
|
||||
private void setAuthContext(AccessToken accessToken) {
|
||||
AuthContext.setContext(accessToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package com.blossom.backend.base.auth.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* token 对象
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
public class AccessToken implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 登录令牌
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 到期时间
|
||||
*/
|
||||
private Long expire;
|
||||
|
||||
/**
|
||||
* 授权方式
|
||||
*/
|
||||
private String grantType;
|
||||
|
||||
/**
|
||||
* 登录平台
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 请求是否刷新Token
|
||||
*/
|
||||
private Boolean requestRefresh;
|
||||
|
||||
/**
|
||||
* 用户 Token 唯一:
|
||||
* <p>为 false 则每次登录返回的 token 是一样的;
|
||||
* <p>为 true 则每次登录 token 会替换前一个 token
|
||||
*/
|
||||
private Boolean multiPlaceLogin;
|
||||
|
||||
/**
|
||||
* 授权时长, 单位为秒
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 登录日期
|
||||
*/
|
||||
private String loginTime;
|
||||
|
||||
/**
|
||||
* 权限列表
|
||||
*/
|
||||
private List<String> permissions;
|
||||
|
||||
/**
|
||||
* 用户元信息, 由用户自定义各类信息
|
||||
*/
|
||||
private Map<String, String> metadata;
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.blossom.backend.base.auth.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 登录信息
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LoginDTO extends LoginReq {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* userAgent
|
||||
*/
|
||||
private String userAgent;
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* 登录时间
|
||||
*/
|
||||
private String loginTime;
|
||||
/**
|
||||
* token 快照
|
||||
*/
|
||||
private String accessTokenSnapshot;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.blossom.backend.base.auth.pojo;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 登录事件
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public class LoginEvent extends ApplicationEvent {
|
||||
|
||||
public LoginEvent(LoginDTO source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.blossom.backend.base.auth.pojo;
|
||||
|
||||
import com.blossom.backend.base.auth.enums.GrantTypeEnum;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LoginReq extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* 登录方式,可见数据字典中[GrantType]部分, 必须为小写
|
||||
*
|
||||
* @see GrantTypeEnum
|
||||
*/
|
||||
@NotEmpty(message = "授权方式[GrantType]为必填项")
|
||||
private String grantType;
|
||||
/**
|
||||
* 客户端ID
|
||||
*/
|
||||
@NotEmpty(message = "客户端ID[ClientId]为必填项")
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 用户名, grantType = password 时必填
|
||||
*/
|
||||
private String username;
|
||||
/**
|
||||
* 密码, grantType = password 时必填
|
||||
*/
|
||||
private String password;
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
//package com.blossom.backend.base.auth.redis;
|
||||
//
|
||||
//import com.blossom.backend.base.auth.AuthConstant;
|
||||
//import com.blossom.backend.base.auth.AuthProperties;
|
||||
//import com.blossom.backend.base.auth.repo.TokenRepository;
|
||||
//import com.blossom.backend.base.auth.filters.AuthFilterProxy;
|
||||
//import com.blossom.backend.base.auth.filters.HttpFirewall;
|
||||
//import com.blossom.backend.base.auth.filters.RequestLogFilter;
|
||||
//import com.blossom.backend.base.auth.filters.WhiteListFilter;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
//import org.springframework.core.annotation.Order;
|
||||
//import org.springframework.data.redis.core.RedisTemplate;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
//import javax.servlet.FilterChain;
|
||||
//import javax.servlet.ServletException;
|
||||
//import javax.servlet.ServletRequest;
|
||||
//import javax.servlet.ServletResponse;
|
||||
//import java.io.IOException;
|
||||
//
|
||||
///**
|
||||
// * redis token 拦截器实现
|
||||
// *
|
||||
// * @author xzzz
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Component
|
||||
//@Order(AuthConstant.AUTH_FILTER_PROXY)
|
||||
//@ConditionalOnClass(RedisTemplate.class)
|
||||
//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis")
|
||||
//public class RedisTokenAuthFilterProxy extends AuthFilterProxy {
|
||||
// /**
|
||||
// * 防火墙对象
|
||||
// */
|
||||
// protected final HttpFirewall httpFirewall = new HttpFirewall();
|
||||
// /**
|
||||
// * 日期过滤器
|
||||
// */
|
||||
// protected final RequestLogFilter logFilter;
|
||||
// /**
|
||||
// * 白名单过滤器
|
||||
// */
|
||||
// protected final WhiteListFilter whiteListFilter;
|
||||
// /**
|
||||
// * 授权检查过滤器
|
||||
// */
|
||||
// private final RedisTokenValidateFilter redisTokenValidateFilter;
|
||||
// /**
|
||||
// * 唯一授权检查过滤器
|
||||
// */
|
||||
// private final RedisTokenUniqueFilter redisTokenUniqueFilter;
|
||||
// /**
|
||||
// * 授权重置过滤器
|
||||
// */
|
||||
// private final RedisTokenExpireResetFilter redisTokenExpireResetFilter;
|
||||
//
|
||||
// public RedisTokenAuthFilterProxy(AuthProperties properties, TokenRepository tokenRepository) {
|
||||
// super(properties);
|
||||
// this.logFilter = new RequestLogFilter(properties);
|
||||
// this.whiteListFilter = new WhiteListFilter(properties);
|
||||
// this.redisTokenValidateFilter = new RedisTokenValidateFilter(tokenRepository);
|
||||
// this.redisTokenUniqueFilter = new RedisTokenUniqueFilter(tokenRepository);
|
||||
// this.redisTokenExpireResetFilter = new RedisTokenExpireResetFilter(tokenRepository);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * <p>1. 防火墙校验
|
||||
// * <p>2. 授权校验
|
||||
// * <ol>
|
||||
// * <li>日志</li>
|
||||
// * <li>白名单</li>
|
||||
// * <li>授权校验</li>
|
||||
// * <li>授权续期</li>
|
||||
// * </ol>
|
||||
// *
|
||||
// * @param request request
|
||||
// * @param response response
|
||||
// * @param chain chain
|
||||
// */
|
||||
// @Override
|
||||
// protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
// /*
|
||||
// 1. 执行防火墙
|
||||
// */
|
||||
// httpFirewall.wall(request);
|
||||
//
|
||||
// /*
|
||||
// 2. 授权校验
|
||||
// */
|
||||
// // 2.1 日志
|
||||
// logFilter.doFilter(request, response);
|
||||
// // 2.2 白名单
|
||||
// whiteListFilter.doFilter(request, response);
|
||||
// // 2.3 token 校验
|
||||
// redisTokenValidateFilter.doFilter(request, response);
|
||||
// // 2.4 token 唯一校验
|
||||
// redisTokenUniqueFilter.doFilter(request, response);
|
||||
// // 2.5 过期时间刷新
|
||||
// redisTokenExpireResetFilter.doFilter(request, response);
|
||||
//
|
||||
// /*
|
||||
// 虚拟过滤器链执行完毕后交由原生过滤器继续执行
|
||||
// */
|
||||
// chain.doFilter(request, response);
|
||||
//
|
||||
// log.debug("[AUTHORIZ] **Proxy** >> 原生过滤器: request 请求结束");
|
||||
// }
|
||||
//
|
||||
//}
|
@ -0,0 +1,31 @@
|
||||
//package com.blossom.backend.base.auth.redis;
|
||||
//
|
||||
//import cn.hutool.core.lang.UUID;
|
||||
//import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
//import com.blossom.backend.base.auth.token.TokenEncoder;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
//import org.springframework.data.redis.core.RedisTemplate;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
///**
|
||||
// * token 编解码器, 简单生成一个 UUID token, 与 token 信息无加解密, 编解码关系
|
||||
// *
|
||||
// * @author xzzz
|
||||
// */
|
||||
//@Component
|
||||
//@ConditionalOnClass(RedisTemplate.class)
|
||||
//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis")
|
||||
//public class RedisTokenEncoder implements TokenEncoder {
|
||||
//
|
||||
// /**
|
||||
// * 生成一个 uuid 作为 token
|
||||
// *
|
||||
// * @param accessToken token
|
||||
// * @return uuid
|
||||
// */
|
||||
// @Override
|
||||
// public String encode(AccessToken accessToken) {
|
||||
// return UUID.randomUUID().toString(true);
|
||||
// }
|
||||
//}
|
@ -0,0 +1,60 @@
|
||||
package com.blossom.backend.base.auth.redis;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
import com.blossom.backend.base.auth.repo.TokenRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* [200]
|
||||
*
|
||||
* 令牌刷新, 对令牌进行刷新.
|
||||
* 不同类型的令牌刷新不同
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisTokenExpireResetFilter {
|
||||
|
||||
/**
|
||||
* 配置文件内容
|
||||
*/
|
||||
private final TokenRepository tokenRepository;
|
||||
|
||||
public RedisTokenExpireResetFilter(TokenRepository tokenRepository) {
|
||||
this.tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行过滤器
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @throws IOException io
|
||||
* @throws ServletException servlet
|
||||
*/
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
log.debug("[AUTHORIZ] filter(200) 令牌续期");
|
||||
|
||||
// 1. 上下文获取授权主体
|
||||
AccessToken accessToken = AuthContext.getContext();
|
||||
|
||||
// 2. 如果配置客户端有效 && 客户端允许刷新令牌
|
||||
if (accessToken != null && StrUtil.isNotBlank(accessToken.getToken()) && accessToken.getRequestRefresh()) {
|
||||
// 3.1 刷新存储中的授权信息
|
||||
accessToken.setExpire(System.currentTimeMillis() + (accessToken.getDuration() * 1000));
|
||||
tokenRepository.saveToken(accessToken);
|
||||
tokenRepository.saveUniqueToken(accessToken);
|
||||
// 3.2 刷新上下文授权信息
|
||||
AuthContext.setContext(accessToken);
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
//package com.blossom.backend.base.auth.redis;
|
||||
//
|
||||
//import cn.hutool.core.util.StrUtil;
|
||||
//import com.blossom.backend.base.auth.TokenUtil;
|
||||
//import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
//import com.blossom.backend.base.auth.repo.TokenRepository;
|
||||
//import com.blossom.common.base.exception.XzException500;
|
||||
//import com.blossom.common.base.util.json.JsonUtil;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.springframework.beans.factory.annotation.Autowired;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
//import org.springframework.data.redis.core.RedisTemplate;
|
||||
//import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
//import org.springframework.scheduling.annotation.Async;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
//import java.util.concurrent.TimeUnit;
|
||||
//
|
||||
///**
|
||||
// * redis token 存储
|
||||
// *
|
||||
// * @author xzzz
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Component
|
||||
//@ConditionalOnClass(RedisTemplate.class)
|
||||
//@ConditionalOnProperty(value = "project.auth.type", havingValue = "redis")
|
||||
//public class RedisTokenRepository implements TokenRepository {
|
||||
//
|
||||
// @Autowired
|
||||
// private StringRedisTemplate redisTemplate;
|
||||
//
|
||||
// @Override
|
||||
// public void saveToken(AccessToken accessToken) {
|
||||
// if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) {
|
||||
// throw new XzException500("无法保存空的AccessToken");
|
||||
// }
|
||||
// redisTemplate.opsForValue().set(
|
||||
// TokenUtil.buildTokenKey(accessToken.getToken()),
|
||||
// JsonUtil.toJson(accessToken),
|
||||
// accessToken.getDuration(),
|
||||
// TimeUnit.SECONDS);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public AccessToken getToken(String token) {
|
||||
// if (StrUtil.isBlank(token)) {
|
||||
// return null;
|
||||
// }
|
||||
// return JsonUtil.toObj(redisTemplate.opsForValue().get(TokenUtil.buildTokenKey(token)), AccessToken.class);
|
||||
// }
|
||||
//
|
||||
// @Async
|
||||
// @Override
|
||||
// public void remove(String token) {
|
||||
// log.info("异步删除 token");
|
||||
// redisTemplate.delete(TokenUtil.buildTokenKey(token));
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void saveUniqueToken(AccessToken accessToken) {
|
||||
// if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) {
|
||||
// throw new XzException500("无法保存空的AccessToken");
|
||||
// }
|
||||
//
|
||||
// if (accessToken.getMultiPlaceLogin()) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// redisTemplate.opsForValue().set(
|
||||
// TokenUtil.buildUniqueTokenKey(String.valueOf(accessToken.getUserId())),
|
||||
// accessToken.getToken(),
|
||||
// accessToken.getDuration(),
|
||||
// TimeUnit.SECONDS);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public String getUniqueToken(String userId) {
|
||||
// if (StrUtil.isBlank(userId)) {
|
||||
// return null;
|
||||
// }
|
||||
// return redisTemplate.opsForValue().get(TokenUtil.buildUniqueTokenKey(userId));
|
||||
// }
|
||||
//}
|
@ -0,0 +1,63 @@
|
||||
package com.blossom.backend.base.auth.redis;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
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.repo.TokenRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* redis 唯一token 拦截器, 用于处理用户多设备登录时的 唯一有效token 判断逻辑
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisTokenUniqueFilter {
|
||||
|
||||
/**
|
||||
* 配置文件内容
|
||||
*/
|
||||
private final TokenRepository tokenRepository;
|
||||
|
||||
public RedisTokenUniqueFilter(TokenRepository tokenRepository) {
|
||||
this.tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行过滤器
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @throws IOException io
|
||||
* @throws ServletException servlet
|
||||
*/
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY));
|
||||
|
||||
if (isWhiteList) {
|
||||
return;
|
||||
}
|
||||
|
||||
AccessToken accessToken = AuthContext.getContext();
|
||||
|
||||
/*
|
||||
* 如果不允许多地登录, 则要判断当前用户是否是唯一的用户
|
||||
*/
|
||||
if (!accessToken.getMultiPlaceLogin()) {
|
||||
String uniqueToken = tokenRepository.getUniqueToken(String.valueOf(accessToken.getUserId()));
|
||||
if (!accessToken.getToken().equals(uniqueToken)) {
|
||||
tokenRepository.remove(accessToken.getToken());
|
||||
throw new AuthException(AuthRCode.ANOTHER_DEVICE_LOGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.blossom.backend.base.auth.redis;
|
||||
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.base.auth.AuthConstant;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.TokenUtil;
|
||||
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.repo.TokenRepository;
|
||||
import com.blossom.common.base.util.ServletUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求验证, 校验请求是否携带 token, 未携带的进行拦截跑出错误
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisTokenValidateFilter {
|
||||
|
||||
private final TokenRepository tokenRepository;
|
||||
|
||||
public RedisTokenValidateFilter(TokenRepository tokenRepository) {
|
||||
this.tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行过滤器
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @throws IOException io
|
||||
* @throws ServletException servlet
|
||||
*/
|
||||
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
|
||||
log.debug("[AUTHORIZ] filter(100) 授权校验过滤器");
|
||||
|
||||
boolean isWhiteList = Boolean.TRUE.equals(request.getAttribute(AuthConstant.WHITE_LIST_ATTRIBUTE_KEY));
|
||||
|
||||
// 解析请求头中的 token
|
||||
String token = getHeaderToken(request);
|
||||
|
||||
// 如果白名单且无token, 则直接放行
|
||||
if (isWhiteList && StrUtil.isBlank(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据 token 取出 Redis 中的授权数据
|
||||
AccessToken accessToken = tokenRepository.getToken(token);
|
||||
|
||||
// 如果是白名单请求没有授权信息,则构造空的授权信息到上下文,异常由调用方捕获
|
||||
if (isWhiteList && null == accessToken) {
|
||||
AccessToken accessTokenNull = new AccessToken();
|
||||
accessTokenNull.setToken("");
|
||||
accessTokenNull.setUserId(0L);
|
||||
setAuthContext(accessTokenNull);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果白名单请求,且获得了token,则说明是登录状态请求白名单的接口,正常执行下一过滤器
|
||||
if (isWhiteList && null != accessToken) {
|
||||
// 将授权数据存放到上下文中,不执行后续过滤器
|
||||
setAuthContext(accessToken);
|
||||
return;
|
||||
}
|
||||
// 如果不是白名单请求,则抛出错误
|
||||
if (!isWhiteList && null == accessToken) {
|
||||
throw new AuthException(AuthRCode.INVALID_TOKEN);
|
||||
}
|
||||
// 将授权数据存放到上下文中,并执行下一过滤器
|
||||
setAuthContext(accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 从请求头中获取 Token
|
||||
*
|
||||
* @param servletRequest request
|
||||
* @return Token
|
||||
*/
|
||||
private String getHeaderToken(ServletRequest servletRequest) {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
|
||||
|
||||
// 从请求头获取
|
||||
String token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION);
|
||||
|
||||
// 从请求头获取全小写
|
||||
if (StrUtil.isBlank(token)) {
|
||||
token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toLowerCase());
|
||||
}
|
||||
|
||||
// 从请求头获取全大写
|
||||
if (StrUtil.isBlank(token)) {
|
||||
token = httpServletRequest.getHeader(AuthConstant.HEADER_AUTHORIZATION.toUpperCase());
|
||||
}
|
||||
|
||||
// 从 cookie 获取
|
||||
if (StrUtil.isBlank(token)) {
|
||||
Cookie cookie = ServletUtil.getCookie(httpServletRequest, AuthConstant.HEADER_AUTHORIZATION);
|
||||
if (cookie != null) {
|
||||
token = cookie.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// 以 Bearer 开头 && 除此外长度 > 0
|
||||
token = TokenUtil.cutPrefix(token);
|
||||
if (null != token && token.length() > 0) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 保存到上下文
|
||||
*
|
||||
* @param accessToken 授权主体
|
||||
*/
|
||||
private void setAuthContext(AccessToken accessToken) {
|
||||
AuthContext.setContext(accessToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.blossom.backend.base.auth.repo;
|
||||
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
|
||||
/**
|
||||
* token 存储
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public interface TokenRepository {
|
||||
|
||||
/**
|
||||
* 保存 token
|
||||
*
|
||||
* @param accessToken token 对象信息
|
||||
*/
|
||||
void saveToken(AccessToken accessToken);
|
||||
|
||||
/**
|
||||
* 获取 token 对象信息
|
||||
*
|
||||
* @param token token 令牌
|
||||
* @return token
|
||||
*/
|
||||
AccessToken getToken(String token);
|
||||
|
||||
/**
|
||||
* 删除 Token 信息
|
||||
*
|
||||
* @param token token 令牌
|
||||
*/
|
||||
void remove(String token);
|
||||
|
||||
/**
|
||||
* 保存唯一生效的 token 对象
|
||||
*
|
||||
* @param accessToken 唯一生效的 token 信息
|
||||
*/
|
||||
void saveUniqueToken(AccessToken accessToken);
|
||||
|
||||
/**
|
||||
* 获取唯一生效的 token 令牌
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 该用户有效的 token
|
||||
*/
|
||||
String getUniqueToken(String userId);
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.blossom.backend.base.auth.security;
|
||||
|
||||
/**
|
||||
* 密码加密方式接口
|
||||
*
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public interface PasswordEncoder {
|
||||
|
||||
/**
|
||||
* 明文加密
|
||||
* <p>
|
||||
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
|
||||
* greater hash combined with an 8-byte or greater randomly generated salt.
|
||||
*
|
||||
* @param rawPassword 密码原文
|
||||
* @return 密文
|
||||
*/
|
||||
String encode(CharSequence rawPassword);
|
||||
|
||||
/**
|
||||
* 密码判断,用未加密的密码与已加密的密码进行判断
|
||||
*
|
||||
* @param rawPassword 原始密码,未加密的密码
|
||||
* @param encodedPassword 已经加密过的密码
|
||||
* @return true 为密码正确
|
||||
*/
|
||||
boolean matches(CharSequence rawPassword, String encodedPassword);
|
||||
|
||||
/**
|
||||
* Returns true if the encoded password should be encoded again for better security,
|
||||
* else false. The default implementation always returns false.
|
||||
*
|
||||
* @param encodedPassword the encoded password to check
|
||||
* @return true if the encoded password should be encoded again for better security,
|
||||
* else false.
|
||||
*/
|
||||
default boolean upgradeEncoding(String encodedPassword) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.blossom.backend.base.auth.security;
|
||||
|
||||
import com.blossom.common.base.util.security.BCryptUtil;
|
||||
import com.blossom.common.base.util.security.SaltUtil;
|
||||
|
||||
/**
|
||||
* bcrypt 加解密
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public class PasswordEncoderBCrypt implements PasswordEncoder {
|
||||
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return BCryptUtil.encode(String.valueOf(rawPassword));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return BCryptUtil.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(SaltUtil.randomSalt());
|
||||
|
||||
String salt = "UVeESP5NgXwb8JmjCHUK";
|
||||
String password = "blos";
|
||||
PasswordEncoderBCrypt uuid = new PasswordEncoderBCrypt();
|
||||
System.out.println(uuid.encode(password + salt));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.blossom.backend.base.auth.security;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
|
||||
/**
|
||||
* md5 加解密匹配
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public class PasswordEncoderMD5 implements PasswordEncoder {
|
||||
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return SecureUtil.md5(rawPassword.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return SecureUtil.md5(rawPassword.toString()).equals(encodedPassword);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.blossom.backend.base.auth.security;
|
||||
|
||||
import com.blossom.common.base.util.security.SHA256Util;
|
||||
|
||||
/**
|
||||
* sha256 加解密匹配
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public class PasswordEncoderSHA256 implements PasswordEncoder{
|
||||
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return SHA256Util.encode(String.valueOf(rawPassword));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return SHA256Util.encode(String.valueOf(rawPassword)).equals(encodedPassword);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.blossom.backend.base.auth.security;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthProperties;
|
||||
import com.blossom.backend.base.auth.enums.PasswordEncoderEnum;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 密码处理
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Primary
|
||||
@Component
|
||||
public class PasswordManager implements PasswordEncoder {
|
||||
|
||||
private final AuthProperties authProperties;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public PasswordManager(AuthProperties authProperties) {
|
||||
this.authProperties = authProperties;
|
||||
if (authProperties.getPasswordEncoder() == PasswordEncoderEnum.md5) {
|
||||
passwordEncoder = new PasswordEncoderMD5();
|
||||
} else if (authProperties.getPasswordEncoder() == PasswordEncoderEnum.sha256) {
|
||||
passwordEncoder = new PasswordEncoderSHA256();
|
||||
} else {
|
||||
passwordEncoder = new PasswordEncoderBCrypt();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDefaultPassword() {
|
||||
return passwordEncoder.encode(authProperties.getDefaultPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* 明文加密, 如果密码需要加盐, 需要加盐后传入
|
||||
*
|
||||
* @param rawPassword 密码明文
|
||||
* @return 密码密文
|
||||
*/
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return passwordEncoder.encode(rawPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码判断,用未加密的密码与已加密的密码进行判断
|
||||
*
|
||||
* @param rawPassword 原始密码,未加密的密码
|
||||
* @param encodedPassword 已经加密过的密码
|
||||
* @return true 为密码正确
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.blossom.backend.base.auth.token;
|
||||
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
|
||||
/**
|
||||
* token 编解码器
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public interface TokenEncoder {
|
||||
|
||||
/**
|
||||
* token 编码成字符串
|
||||
*
|
||||
* @param accessToken token
|
||||
* @return 字符串
|
||||
*/
|
||||
String encode(AccessToken accessToken);
|
||||
|
||||
/**
|
||||
* token 解码
|
||||
*
|
||||
* @param string token 字符串
|
||||
* @return token
|
||||
*/
|
||||
default AccessToken decode(String string) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.blossom.backend.base.param;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 参数枚举
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum ParamEnum {
|
||||
|
||||
/**
|
||||
* 文章的 web 端访问路径
|
||||
*/
|
||||
WEB_ARTICLE_URL(false),
|
||||
|
||||
/**
|
||||
* 文章日志过期天数
|
||||
*/
|
||||
ARTICLE_LOG_EXP_DAYS(false),
|
||||
|
||||
/**
|
||||
* 和风天气KEY
|
||||
*/
|
||||
HEFENG_KEY(true),
|
||||
|
||||
/**
|
||||
* GITEE key
|
||||
*/
|
||||
GITEE_ACCESS_TOKEN(true),
|
||||
;
|
||||
|
||||
/**
|
||||
* 是否脱敏
|
||||
*/
|
||||
@Getter
|
||||
private final Boolean masking;
|
||||
|
||||
ParamEnum(Boolean masking) {
|
||||
this.masking = masking;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.blossom.backend.base.param;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统参数信息的持久化层
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Mapper
|
||||
public interface ParamMapper extends BaseMapper<ParamEntity> {
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.blossom.backend.base.param;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||
import com.blossom.backend.base.param.pojo.ParamReq;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.util.BeanUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 系统参数信息业务接口
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ParamService extends ServiceImpl<ParamMapper, ParamEntity> {
|
||||
|
||||
private static final Map<String, ParamEntity> CACHE = new HashMap<>(20);
|
||||
|
||||
@PostConstruct
|
||||
public void refresh() {
|
||||
log.info("[ BASE] 初始化系统参数缓存");
|
||||
CACHE.clear();
|
||||
List<ParamEntity> params = baseMapper.selectList(new QueryWrapper<>());
|
||||
if (CollUtil.isEmpty(params)) {
|
||||
return;
|
||||
}
|
||||
for (ParamEntity param : params) {
|
||||
CACHE.put(param.getParamName(), param);
|
||||
}
|
||||
}
|
||||
|
||||
public ParamEntity getValue(ParamEnum name) {
|
||||
return CACHE.get(name.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据多个名称查询
|
||||
*
|
||||
* @param masking 返回数据是否脱敏
|
||||
* @param names 参数名称
|
||||
* @return 返回参数 map
|
||||
*/
|
||||
public Map<String, String> selectMap(boolean masking, ParamEnum... names) {
|
||||
if (ArrayUtil.isEmpty(names)) {
|
||||
return new HashMap<>(0);
|
||||
}
|
||||
Map<String, String> result = new HashMap<>(names.length);
|
||||
for (ParamEnum name : names) {
|
||||
ParamEntity param = BeanUtil.toObj(CACHE.get(name.name()), ParamEntity.class);
|
||||
if (masking && name.getMasking()) {
|
||||
param.setParamValue(StrUtil.hide(param.getParamValue(), 0, Math.min(param.getParamValue().length(), 20)));
|
||||
}
|
||||
result.put(name.name(), param.getParamValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long update(ParamReq req) {
|
||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||
return req.getId();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.blossom.backend.base.param.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 系统参数信息的实体类
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@TableName("base_sys_param")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ParamEntity extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参数ID */
|
||||
@TableId
|
||||
private Long id;
|
||||
/** 参数名称 */
|
||||
private String paramName;
|
||||
/** 参数值 */
|
||||
private String paramValue;
|
||||
/** 参数说明 */
|
||||
private String paramDesc;
|
||||
/** 创建时间 */
|
||||
private Date creTime;
|
||||
/** 修改时间 */
|
||||
private Date updTime;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.blossom.backend.base.param.pojo;
|
||||
|
||||
import com.blossom.common.db.pojo.PageReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 系统参数分页请求类
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ParamPageReq extends PageReq implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参数ID */
|
||||
private Long id;
|
||||
/** 参数名称 */
|
||||
private String paramName;
|
||||
/** 参数值 */
|
||||
private String paramValue;
|
||||
/** 参数说明 */
|
||||
private String paramDesc;
|
||||
/** 创建时间 */
|
||||
private Date creTime;
|
||||
/** 修改时间 */
|
||||
private Date updTime;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.blossom.backend.base.param.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 系统参数请求实体
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ParamReq extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参数ID */
|
||||
private Long id;
|
||||
/** 参数名称 */
|
||||
private String paramName;
|
||||
/** 参数值 */
|
||||
private String paramValue;
|
||||
/** 参数说明 */
|
||||
private String paramDesc;
|
||||
/** 创建时间 */
|
||||
private Date creTime;
|
||||
/** 修改时间 */
|
||||
private Date updTime;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.blossom.backend.base.param.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 系统参数响应实体,会排除敏感字段
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ParamRes extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参数名称 */
|
||||
private String paramName;
|
||||
/** 参数值 */
|
||||
private String paramValue;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package com.blossom.backend.base.sys;
|
||||
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.sys.os.OSRes;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.spring.SpringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 系统功能
|
||||
*
|
||||
* @apiNote 服务在线检查, 文件上传
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sys")
|
||||
public class SysController {
|
||||
|
||||
@Autowired
|
||||
private SysService sysService;
|
||||
|
||||
/**
|
||||
* 服务在线检查
|
||||
*/
|
||||
@AuthIgnore
|
||||
@GetMapping("/alive")
|
||||
public R<String> checkAlive() {
|
||||
return R.ok(String.format("这里是 [%s] 服务器 [%s] 环境", SpringUtil.getAppName(), SpringUtil.getProfileAction()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象存储的配置信息
|
||||
*/
|
||||
@GetMapping("/osconfig")
|
||||
public R<OSRes> getOsConfig() {
|
||||
return R.ok(sysService.getOsConfig());
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.blossom.backend.base.sys;
|
||||
|
||||
import com.blossom.backend.base.sys.os.OSRes;
|
||||
import com.blossom.common.iaas.IaasProperties;
|
||||
import com.blossom.common.iaas.OSManager;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 系统
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Service
|
||||
public class SysService {
|
||||
|
||||
@Autowired
|
||||
private OSManager osManager;
|
||||
|
||||
/**
|
||||
* 对象存储的配置信息
|
||||
*/
|
||||
public OSRes getOsConfig() {
|
||||
IaasProperties props = osManager.getProp();
|
||||
OSRes res = new OSRes();
|
||||
res.setOsType(props.getOsType());
|
||||
// TODO 目前只有 BLOS 一种
|
||||
res.setBucketName("");
|
||||
res.setDefaultPath(props.getBlos().getDefaultPath());
|
||||
res.setDomain(props.getBlos().getDomain());
|
||||
return res;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.blossom.backend.base.sys.os;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 对象存储配置
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
public class OSRes {
|
||||
|
||||
/**
|
||||
* 对象存储类型
|
||||
*/
|
||||
private String osType;
|
||||
/**
|
||||
* bucket 名称
|
||||
*/
|
||||
private String bucketName;
|
||||
/**
|
||||
* 请求域名
|
||||
*/
|
||||
private String domain;
|
||||
/**
|
||||
* 保存路径
|
||||
*/
|
||||
private String defaultPath;
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.blossom.backend.base.user;
|
||||
|
||||
import com.blossom.backend.base.param.ParamEnum;
|
||||
import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.sys.SysService;
|
||||
import com.blossom.backend.base.user.pojo.*;
|
||||
import com.blossom.backend.config.BlConstants;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
|
||||
import com.blossom.backend.server.article.stat.ArticleStatService;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
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.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户 [User]
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@AllArgsConstructor
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
private final ArticleStatService articleService;
|
||||
private final SysService sysService;
|
||||
private final ParamService paramService;
|
||||
|
||||
/**
|
||||
* 查询 blossom 用户
|
||||
*
|
||||
* @return 用户信息
|
||||
* @apiNote blossom 当前只支持一个用户登录, 数据并为做区分. 当登录状态调用该接口时, 会返回服务器相关配置信息.
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public R<BlossomUserRes> user() {
|
||||
BlossomUserRes user = userService.selectById(AuthContext.getUserId()).to(BlossomUserRes.class);
|
||||
user.setOsRes(sysService.getOsConfig());
|
||||
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
|
||||
user.setParams(paramMap);
|
||||
return R.ok(user);
|
||||
}
|
||||
|
||||
@AuthIgnore
|
||||
@GetMapping("/info/open")
|
||||
public R<BlossomUserRes> userOpen(@RequestHeader(BlConstants.REQ_HEADER_USERID) Long userId) {
|
||||
if (userId == null) {
|
||||
return R.ok(new BlossomUserRes());
|
||||
}
|
||||
BlossomUserRes user = userService.selectById(userId).to(BlossomUserRes.class);
|
||||
ArticleStatRes stat = articleService.statCount(null, null, userId);
|
||||
user.setArticleWords(stat.getArticleWords());
|
||||
user.setArticleCount(stat.getArticleCount());
|
||||
return R.ok(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
*/
|
||||
@PostMapping("/upd")
|
||||
public R<?> update(@Validated @RequestBody UserUpdReq req) {
|
||||
UserEntity user = req.to(UserEntity.class);
|
||||
user.setId(AuthContext.getUserId());
|
||||
userService.updById(user);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@PostMapping("/upd/pwd")
|
||||
public R<?> updatePassword(@Validated @RequestBody UserUpdPwdReq req) {
|
||||
req.setUserId(AuthContext.getUserId());
|
||||
userService.updPassword(req);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public R<?> add(@Validated @RequestBody UserAddReq req) {
|
||||
UserEntity curUser = userService.getById(AuthContext.getUserId());
|
||||
if (curUser == null || !UserTypeEnum.ADMIN.getType().equals(curUser.getType())) {
|
||||
throw new XzException400("您没有权限添加用户");
|
||||
}
|
||||
userService.insert(req);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.blossom.backend.base.user;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.blossom.backend.base.user.pojo.UserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 用户
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<UserEntity> {
|
||||
|
||||
/**
|
||||
* 修改用户信息
|
||||
*/
|
||||
void updById(UserEntity user);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param password 新密码
|
||||
*/
|
||||
void updPwd(@Param("userId") Long userId, @Param("password") String password);
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.blossom.backend.base.user;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.security.PasswordEncoder;
|
||||
import com.blossom.backend.base.user.pojo.UserAddReq;
|
||||
import com.blossom.backend.base.user.pojo.UserEntity;
|
||||
import com.blossom.backend.base.user.pojo.UserUpdPwdReq;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.util.security.SaltUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.omg.PortableInterceptor.USER_EXCEPTION;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class UserService extends ServiceImpl<UserMapper, UserEntity> {
|
||||
|
||||
private static final Map<String, UserEntity> userCache = new HashMap<>();
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* 查询全部用户
|
||||
*/
|
||||
public List<UserEntity> listAll() {
|
||||
return baseMapper.selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
public UserEntity selectById(Long id) {
|
||||
return baseMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getId, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
*/
|
||||
public UserEntity selectByUsername(String username) {
|
||||
return baseMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, username));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void insert(UserAddReq req) {
|
||||
UserEntity user = req.to(UserEntity.class);
|
||||
user.setNickName(req.getUsername());
|
||||
user.setRealName(req.getUsername());
|
||||
user.setType(req.getType());
|
||||
user.setSalt(SaltUtil.randomSalt());
|
||||
user.setPassword(passwordEncoder.encode(req.getPassword() + user.getSalt()));
|
||||
baseMapper.insert(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户信息
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updById(UserEntity user) {
|
||||
baseMapper.updById(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updPassword(UserUpdPwdReq req) {
|
||||
XzException400.throwBy(!req.getNewPassword().equals(req.getConfirmPassword()), "两次输入密码不匹配");
|
||||
UserEntity user = selectById(req.getUserId());
|
||||
if (!passwordEncoder.matches(req.getPassword() + user.getSalt(), user.getPassword())) {
|
||||
throw new XzException400("密码错误");
|
||||
}
|
||||
|
||||
String newPwd = passwordEncoder.encode(req.getNewPassword() + user.getSalt());
|
||||
baseMapper.updPwd(req.getUserId(), newPwd);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.blossom.backend.base.user;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum UserTypeEnum {
|
||||
/**
|
||||
* 管理员
|
||||
*/
|
||||
ADMIN(1),
|
||||
/**
|
||||
* 普通用户
|
||||
*/
|
||||
NORMAL(2),
|
||||
/**
|
||||
* 只读用户
|
||||
*/
|
||||
READONLY(3),
|
||||
;
|
||||
|
||||
@Getter
|
||||
private final Integer type;
|
||||
|
||||
UserTypeEnum(Integer type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.backend.base.sys.os.OSRes;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户响应
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BlossomUserRes extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickName;
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private String location;
|
||||
/**
|
||||
* 文章数
|
||||
*/
|
||||
private Integer articleCount;
|
||||
/**
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer articleWords;
|
||||
|
||||
/**
|
||||
* 对象存储信息, 非登录状态不返回该字段
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private OSRes osRes;
|
||||
|
||||
/**
|
||||
* 系统参数, paramName: paramValue
|
||||
*/
|
||||
private Map<String, String> params;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserAddReq extends AbstractPOJO {
|
||||
|
||||
@NotBlank(message = "用户名为必填项")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码为必填项")
|
||||
private String password;
|
||||
|
||||
@NotNull(message = "用户类型为必填项")
|
||||
private Integer type;
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@TableName("base_user")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserEntity extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer type;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
/**
|
||||
* 用户手机号
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 用户密码
|
||||
*/
|
||||
private String password;
|
||||
/**
|
||||
* 密码加盐
|
||||
*/
|
||||
private String salt;
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickName;
|
||||
/**
|
||||
* 真实姓名
|
||||
*/
|
||||
private String realName;
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 用户的位置
|
||||
*/
|
||||
private String location;
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserUpdPwdReq extends AbstractPOJO {
|
||||
|
||||
private Long userId;
|
||||
|
||||
@NotBlank(message = "旧密码为必填项")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "新密码为必填项")
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码为必填项")
|
||||
private String confirmPassword;
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 用户请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserUpdReq extends AbstractEntity {
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名为必填项")
|
||||
private String username;
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@NotBlank(message = "昵称为必填项")
|
||||
private String nickName;
|
||||
/**
|
||||
* 说明
|
||||
*/
|
||||
@NotBlank(message = "说明为必填项")
|
||||
private String remark;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private String location;
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
@NotBlank(message = "用户头像为必填项")
|
||||
private String avatar;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.blossom.backend.config;
|
||||
|
||||
public class BlConstants {
|
||||
|
||||
public static final String REQ_HEADER_USERID = "Blossom-User-Id";
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.blossom.backend.config;
|
||||
|
||||
import cn.hutool.core.thread.ThreadFactoryBuilder;
|
||||
import com.blossom.common.base.util.spring.SpringUtil;
|
||||
import com.blossom.expand.tracker.core.adapter.spring.TrackerTaskDecorator;
|
||||
import com.blossom.expand.tracker.core.common.TrackerConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* @author xzzz
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class SpringTaskExecutorConfig {
|
||||
|
||||
@Bean("taskExecutor")
|
||||
public Executor taskExecutor(Environment env) {
|
||||
log.info("[TRACKERS] 已经适配框架 : Spring Task");
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 核心线程池10
|
||||
executor.setCorePoolSize(10);
|
||||
// 最大线程池20
|
||||
executor.setMaxPoolSize(20);
|
||||
// 队列容量
|
||||
executor.setQueueCapacity(200);
|
||||
// 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。
|
||||
executor.setKeepAliveSeconds(60);
|
||||
// 线程名称前缀
|
||||
executor.setThreadNamePrefix(env.getProperty(SpringUtil.APP_NAME) + "-task-");
|
||||
// 用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
// 等待时间
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
// 增加 TaskDecorator 属性的配置
|
||||
executor.setTaskDecorator(new TrackerTaskDecorator(
|
||||
"SPRING_ASYNC_TASK", TrackerConstants.SPAN_TYPE_SPRING_ASYNC));
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
// 处理未捕获异常日志打印
|
||||
final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) ->
|
||||
log.error("异步线程执行失败。异常信息 => {} : ", e.getMessage(), e);
|
||||
ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
|
||||
threadFactoryBuilder.setNamePrefix(env.getProperty(SpringUtil.APP_NAME) + "-task-");
|
||||
threadFactoryBuilder.setUncaughtExceptionHandler(uncaughtExceptionHandler);
|
||||
executor.setThreadFactory(threadFactoryBuilder.build());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务线程池
|
||||
*/
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.setPoolSize(10);
|
||||
taskScheduler.setThreadNamePrefix("bl-sche-");
|
||||
final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> log.error("定时任务执行失败。异常信息 => {} : ", e.getMessage(), e);
|
||||
ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
|
||||
threadFactoryBuilder.setNamePrefix("bl-sche-");
|
||||
threadFactoryBuilder.setUncaughtExceptionHandler(uncaughtExceptionHandler);
|
||||
taskScheduler.setThreadFactory(threadFactoryBuilder.build());
|
||||
taskScheduler.initialize();
|
||||
return taskScheduler;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@
|
||||
package com.blossom.backend.config;
|
||||
|
||||
import com.blossom.backend.base.auth.interceptor.UserTypeInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
|
||||
/**
|
||||
* @author jasmineXz
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class WebConfigurer implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 拦截所有请求,通过判断是否有 @myInterceptor 注解 决定是否需要登录
|
||||
registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserTypeInterceptor myInterceptor() {
|
||||
log.info("123123213");
|
||||
return new UserTypeInterceptor();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.blossom.backend.server;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文件夹类型, 不同的文件夹具有不同的字段要素, 例如图片文件夹不需要公开
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum FolderTypeEnum {
|
||||
/**
|
||||
* 文章文件夹
|
||||
*/
|
||||
ARTICLE(1),
|
||||
|
||||
/**
|
||||
* 图片文件夹
|
||||
*/
|
||||
PICTURE(2);
|
||||
|
||||
@Getter
|
||||
private final Integer type;
|
||||
|
||||
FolderTypeEnum(Integer type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.blossom.backend.server;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* 文档中有特殊意义的标签
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
public enum TagEnum {
|
||||
/**
|
||||
* 标识该文件夹是个专题
|
||||
*/
|
||||
subject,
|
||||
/**
|
||||
* 标识该文章是个目录
|
||||
*/
|
||||
toc,
|
||||
;
|
||||
|
||||
public static boolean isSubject(String tags) {
|
||||
if (StrUtil.isBlank(tags)) {
|
||||
return false;
|
||||
}
|
||||
return tags.toLowerCase().contains(TagEnum.subject.name());
|
||||
}
|
||||
|
||||
public static boolean isToc(String tags) {
|
||||
if (StrUtil.isBlank(tags)) {
|
||||
return false;
|
||||
}
|
||||
return tags.toLowerCase().contains(TagEnum.toc.name());
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
package com.blossom.backend.server.article.draft;
|
||||
|
||||
import cn.hutool.core.net.URLEncodeUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.server.article.draft.pojo.*;
|
||||
import com.blossom.backend.server.article.open.ArticleOpenService;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||
import com.blossom.backend.server.doc.DocTypeEnum;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.DateUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 文章 [Article]
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/article")
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleService baseService;
|
||||
private final ArticleOpenService openService;
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @apiNote 返回简单字段
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public R<List<ArticleInfoSimpleRes>> listAll(ArticleQueryReq req) {
|
||||
req.setUserId(AuthContext.getUserId());
|
||||
return R.ok(baseService.listAll(req), ArticleInfoSimpleRes.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ID查询文章
|
||||
*
|
||||
* @param id 文章ID
|
||||
* @param showToc 返回目录
|
||||
* @param showMarkdown 返回 markdown 内容
|
||||
* @param showHtml 返回 html 内容
|
||||
* @return 文章信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public R<ArticleInfoRes> info(@RequestParam("id") Long id,
|
||||
@RequestParam("showToc") Boolean showToc,
|
||||
@RequestParam("showMarkdown") Boolean showMarkdown,
|
||||
@RequestParam("showHtml") Boolean showHtml) {
|
||||
if (showToc == null) {
|
||||
showToc = false;
|
||||
}
|
||||
if (showMarkdown == null) {
|
||||
showMarkdown = false;
|
||||
}
|
||||
if (showHtml == null) {
|
||||
showHtml = false;
|
||||
}
|
||||
ArticleEntity article = baseService.selectById(id, showToc, showMarkdown, showHtml);
|
||||
XzException400.throwBy(ObjUtil.isNull(article), "文章不存在");
|
||||
ArticleInfoRes res = article.to(ArticleInfoRes.class);
|
||||
res.setTags(DocUtil.toTagList(article.getTags()));
|
||||
res.setType(DocTypeEnum.A.getType());
|
||||
|
||||
if (YesNo.YES.getValue().equals(article.getOpenStatus())) {
|
||||
ArticleOpenEntity open = openService.selectById(id, false, false, false);
|
||||
if (open != null) {
|
||||
res.setOpenTime(open.getOpenTime());
|
||||
res.setOpenVersion(open.getOpenVersion());
|
||||
res.setSyncTime(open.getSyncTime());
|
||||
}
|
||||
}
|
||||
|
||||
return R.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增文章
|
||||
*
|
||||
* @param req 文章对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public R<Long> insert(@Validated @RequestBody ArticleAddReq req) {
|
||||
ArticleEntity article = req.to(ArticleEntity.class);
|
||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
article.setUserId(AuthContext.getUserId());
|
||||
return R.ok(baseService.insert(article));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改文章
|
||||
*
|
||||
* @param req 文章对象
|
||||
* @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 {@link ArticleService#updateContentById(ArticleEntity)}
|
||||
*/
|
||||
@PostMapping("/upd")
|
||||
public R<Long> insert(@Validated @RequestBody ArticleUpdReq req) {
|
||||
ArticleEntity article = req.to(ArticleEntity.class);
|
||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
return R.ok(baseService.update(article));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存正文内容
|
||||
*/
|
||||
@PostMapping("/upd/content")
|
||||
public R<ArticleUpdContentRes> updateContent(@Validated @RequestBody ArticleUpdContentReq content) {
|
||||
ArticleEntity upd = content.to(ArticleEntity.class);
|
||||
upd.setReferences(content.getReferences());
|
||||
upd.setUserId(AuthContext.getUserId());
|
||||
int words = baseService.updateContentById(upd);
|
||||
ArticleUpdContentRes res = new ArticleUpdContentRes();
|
||||
res.setWords(words);
|
||||
res.setUpdTime(DateUtils.date());
|
||||
return R.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 星标文章
|
||||
*
|
||||
* @param req 文章对象
|
||||
*/
|
||||
@PostMapping("/star")
|
||||
public R<Long> star(@Validated @RequestBody ArticleStarReq req) {
|
||||
return R.ok(baseService.update(req.to(ArticleEntity.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文章
|
||||
*
|
||||
* @param id 文章ID
|
||||
* @param response 文章流
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
public void download(@RequestParam("id") Long id, HttpServletResponse response) throws IOException {
|
||||
ArticleEntity article = baseService.selectById(id, false, true, false);
|
||||
if (StrUtil.isBlank(article.getMarkdown())) {
|
||||
throw new IllegalArgumentException("文章内容为空,无法下载");
|
||||
}
|
||||
try (InputStream is = new ByteArrayInputStream(article.getMarkdown().getBytes(StandardCharsets.UTF_8)); BufferedInputStream bis = new BufferedInputStream(is)) {
|
||||
OutputStream os = response.getOutputStream();
|
||||
String filename = URLEncodeUtil.encode(article.getName() + ".md");
|
||||
// 设置强制下载不打开
|
||||
response.setContentType("application/force-download");
|
||||
// 将请求头暴露给前端
|
||||
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + filename);
|
||||
byte[] buffer = new byte[1024];
|
||||
int i = bis.read(buffer);
|
||||
while (i != -1) {
|
||||
os.write(buffer, 0, i);
|
||||
i = bis.read(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.blossom.backend.server.article.draft;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Mapper
|
||||
public interface ArticleMapper extends BaseMapper<ArticleEntity> {
|
||||
|
||||
/**
|
||||
* 查询全部
|
||||
*/
|
||||
List<ArticleEntity> listAll(ArticleEntity entity);
|
||||
|
||||
/**
|
||||
* 根据ID修改
|
||||
*/
|
||||
void updById(ArticleEntity entity);
|
||||
|
||||
/**
|
||||
* 修改文章内容
|
||||
*/
|
||||
void updContentById(ArticleEntity entity);
|
||||
|
||||
/**
|
||||
* 修改某段日期内修改的文章数据
|
||||
*
|
||||
* @param beginUpdTime 开始修改日期
|
||||
* @param endUpdTime 结束修改日期
|
||||
*/
|
||||
ArticleStatRes statCount(@Param("beginUpdTime") String beginUpdTime,
|
||||
@Param("endUpdTime") String endUpdTime,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 同步版本号, 将文章的 version 同步到 openVersion, 只有 open_status 为 1 才会修改成功
|
||||
*
|
||||
* @param articleId 文章ID
|
||||
*/
|
||||
void sync(@Param("articleId") Long articleId);
|
||||
|
||||
/**
|
||||
* 递增 UV, PV
|
||||
*
|
||||
* @param article 文章ID
|
||||
* @param pv pv
|
||||
* @param uv uv
|
||||
*/
|
||||
void uvAndPv(@Param("articleId") Long article, @Param("pv") Integer pv, @Param("uv") Integer uv);
|
||||
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package com.blossom.backend.server.article.draft;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.server.TagEnum;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
|
||||
import com.blossom.backend.server.article.log.ArticleLogService;
|
||||
import com.blossom.backend.server.article.reference.ArticleReferenceService;
|
||||
import com.blossom.backend.server.article.view.ArticleViewService;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
||||
import com.blossom.backend.server.utils.ArticleUtil;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
||||
|
||||
private final ArticleReferenceService referenceService;
|
||||
private final ArticleViewService viewService;
|
||||
private final ArticleLogService logService;
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*/
|
||||
public List<ArticleEntity> listAll(ArticleQueryReq req) {
|
||||
List<ArticleEntity> articles = baseMapper.listAll(req.to(ArticleEntity.class));
|
||||
if (CollUtil.isEmpty(articles)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return articles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 树状列表
|
||||
*
|
||||
* @return DocTreeRes
|
||||
*/
|
||||
public List<DocTreeRes> listTree(ArticleQueryReq req) {
|
||||
List<ArticleEntity> articles = baseMapper.listAll(req.to(ArticleEntity.class));
|
||||
List<DocTreeRes> articleTrees = new ArrayList<>(articles.size());
|
||||
for (ArticleEntity article : articles) {
|
||||
articleTrees.add(DocUtil.toDocTree(article));
|
||||
}
|
||||
return articleTrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
public ArticleEntity selectById(Long id, boolean showToc, boolean showMarkdown, boolean showHtml) {
|
||||
QueryWrapper<ArticleEntity> where = new QueryWrapper<>();
|
||||
List<String> column = CollUtil.newArrayList("id", "pid", "name", "icon", "tags", "sort", "cover", "describes", "star_status",
|
||||
"open_status", "pv", "uv", "likes", "words", "version", "cre_time", "upd_time");
|
||||
if (showToc) {
|
||||
column.add(TagEnum.toc.name());
|
||||
}
|
||||
if (showMarkdown) {
|
||||
column.add("markdown");
|
||||
}
|
||||
if (showHtml) {
|
||||
column.add("html");
|
||||
}
|
||||
where.select(column);
|
||||
where.eq("id", id).last("limit 1");
|
||||
return baseMapper.selectOne(where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long insert(ArticleEntity req) {
|
||||
baseMapper.insert(req);
|
||||
return req.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改, 该接口只能修改文章的基本信息, 正文及版本修改请使用 {@link ArticleService#updateContentById(ArticleEntity)}
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long update(ArticleEntity req) {
|
||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||
baseMapper.updById(req);
|
||||
return req.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改文章内容, 并统计字数
|
||||
*
|
||||
* @return 返回文章字数
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer updateContentById(ArticleEntity req) {
|
||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||
if (req.getMarkdown() != null) {
|
||||
req.setWords(ArticleUtil.statWords(req.getMarkdown()));
|
||||
}
|
||||
if (req.getHtml() != null) {
|
||||
req.setHtml(req.getHtml().replaceAll("<p><br></p>", ""));
|
||||
}
|
||||
baseMapper.updContentById(req);
|
||||
referenceService.bind(req.getUserId(), req.getId(), req.getName(), req.getReferences());
|
||||
logService.insert(req.getId(), 0, req.getMarkdown());
|
||||
return req.getWords();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步版本号, 将文章的 version 同步到 openVersion, 只有 open_status 为 1 才会修改成功
|
||||
*/
|
||||
public void sync(Long id) {
|
||||
baseMapper.sync(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增UV和PV数据
|
||||
* PV 接口每调用一次都会递增, UV数据每个IP每天只会递增一次
|
||||
*
|
||||
* @param ip ip
|
||||
* @param userAgent userAgent
|
||||
* @param id 文章ID
|
||||
*/
|
||||
@Async
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void uvAndPv(String ip, String userAgent, Long id) {
|
||||
int uv = 0;
|
||||
if (viewService.uv(ip, userAgent, id)) {
|
||||
uv = 1;
|
||||
}
|
||||
baseMapper.uvAndPv(id, 1, uv);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章新增请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleAddReq extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
@Min(value = 0, message = "[上级菜单ID] 不能小于0")
|
||||
@NotNull(message = "[上级菜单] 为必填项")
|
||||
private Long pid;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
@NotBlank(message = "[文章名称] 为必填项")
|
||||
private String name;
|
||||
/**
|
||||
* 文章图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
private String cover;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String describes;
|
||||
/**
|
||||
* 颜色
|
||||
*/
|
||||
private String color;
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@TableName("blossom_article")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleEntity extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 文章图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private String tags;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
private String cover;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String describes;
|
||||
/**
|
||||
* star状态
|
||||
*/
|
||||
private Integer starStatus;
|
||||
/**
|
||||
* 公开状态
|
||||
*/
|
||||
private Integer openStatus;
|
||||
/**
|
||||
* 公开版本
|
||||
*/
|
||||
private Integer openVersion;
|
||||
/**
|
||||
* 页面的查看数
|
||||
*/
|
||||
private Integer pv;
|
||||
/**
|
||||
* 独立的访问次数,每日IP重置
|
||||
*/
|
||||
private Integer uv;
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likes;
|
||||
/**
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer version;
|
||||
/**
|
||||
* 颜色
|
||||
*/
|
||||
private String color;
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
private String toc;
|
||||
/**
|
||||
* Markdown 内容
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* Html 内容
|
||||
*/
|
||||
private String html;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Date creTime;
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date updTime;
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
//region ============================== 非数据库字段 ==============================
|
||||
/**
|
||||
* 引用集合
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<ArticleReferenceReq> references;
|
||||
|
||||
/**
|
||||
* 父ID集合
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Long> pids;
|
||||
|
||||
//endregion
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章响应
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleInfoRes extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 文章图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
private String cover;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String describes;
|
||||
/**
|
||||
* star状态
|
||||
*/
|
||||
private Integer starStatus;
|
||||
/**
|
||||
* 页面的查看数
|
||||
*/
|
||||
private Integer pv;
|
||||
/**
|
||||
* 独立的访问次数,每日IP重置
|
||||
*/
|
||||
private Integer uv;
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likes;
|
||||
/**
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Integer version;
|
||||
/**
|
||||
* Markdown 内容
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* Html 内容
|
||||
*/
|
||||
private String html;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Date creTime;
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date updTime;
|
||||
/**
|
||||
* 颜色
|
||||
*/
|
||||
private String color;
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
private String toc;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private Integer type;
|
||||
/**
|
||||
* 开放状态
|
||||
*/
|
||||
private Integer openStatus;
|
||||
/**
|
||||
* 公开同步时间
|
||||
*/
|
||||
private Date syncTime;
|
||||
/**
|
||||
* 公开时间
|
||||
*/
|
||||
private Date openTime;
|
||||
/**
|
||||
* 公开版本
|
||||
*/
|
||||
private Integer openVersion;
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章响应
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleInfoSimpleRes extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 文章图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
private String cover;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String describes;
|
||||
/**
|
||||
* star状态
|
||||
*/
|
||||
private Integer starStatus;
|
||||
/**
|
||||
* 开放状态
|
||||
*/
|
||||
private Integer openStatus;
|
||||
/**
|
||||
* 页面的查看数
|
||||
*/
|
||||
private Integer pv;
|
||||
/**
|
||||
* 独立的访问次数,每日IP重置
|
||||
*/
|
||||
private Integer uv;
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likes;
|
||||
/**
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Integer version;
|
||||
/**
|
||||
* 颜色
|
||||
*/
|
||||
private String color;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.common.db.pojo.PageReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章查询请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleQueryReq extends PageReq implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 上级ID集合
|
||||
*/
|
||||
private List<Long> pids;
|
||||
/**
|
||||
* star状态, YesNoEnum
|
||||
*/
|
||||
private Integer starStatus;
|
||||
/**
|
||||
* 公开状态
|
||||
*/
|
||||
private Integer openStatus;
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 文章 star
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleStarReq extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* 文章ID
|
||||
*/
|
||||
@Min(value = 0, message = "[文章ID] 不能小于0")
|
||||
@NotNull(message = "[文章ID] 为必填项")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* star 状态
|
||||
*/
|
||||
@Min(value = 0, message = "[star 状态] 不能小于0")
|
||||
@Max(value = 1, message = "[star 状态] 不能大于1")
|
||||
@NotNull(message = "[star 状态] 为必填项")
|
||||
private Integer starStatus;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 文章统计对象
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
public class ArticleStatRes implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 文章数
|
||||
*/
|
||||
private Integer articleCount;
|
||||
/**
|
||||
* 字数
|
||||
*/
|
||||
private Integer articleWords;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.backend.server.article.draft.ArticleController;
|
||||
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 修改文章内容
|
||||
*
|
||||
* <p>
|
||||
* {@link ArticleController#updateContent(ArticleUpdContentReq)}
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleUpdContentReq extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Min(value = 0, message = "[文章ID] 不能小于0")
|
||||
@NotNull(message = "[文章ID] 为必填项")
|
||||
private Long id;
|
||||
/**
|
||||
* 名称, 用于引用关系表中的名称冗余
|
||||
*/
|
||||
@NotBlank(message = "文章名称为必填项")
|
||||
private String name;
|
||||
/**
|
||||
* markdown 内容
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* html 内容
|
||||
*/
|
||||
private String html;
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
private String toc;
|
||||
/**
|
||||
* 引用链接集合
|
||||
*/
|
||||
private List<ArticleReferenceReq> references;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.backend.server.article.draft.ArticleController;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 修改文章内容
|
||||
* <p>
|
||||
* {@link ArticleController#updateContent(ArticleUpdContentReq)}
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleUpdContentRes extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 文章ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 文章版本
|
||||
*/
|
||||
private Integer version;
|
||||
/**
|
||||
* 字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date updTime;
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.blossom.backend.server.article.draft.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文章修改请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleUpdReq extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Min(value = 0, message = "[文章ID] 不能小于0")
|
||||
@NotNull(message = "[文章ID] 为必填项")
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
@Min(value = 0, message = "[上级菜单ID] 不能小于0")
|
||||
@NotNull(message = "[上级菜单] 为必填项")
|
||||
private Long pid;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
@NotBlank(message = "[文章名称] 为必填项")
|
||||
private String name;
|
||||
/**
|
||||
* 文章图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 封面
|
||||
*/
|
||||
private String cover;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String describes;
|
||||
/** 颜色 */
|
||||
private String color;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.blossom.backend.server.article.log;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.blossom.backend.server.article.log.pojo.ArticleLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
|
||||
/**
|
||||
* 文章记录
|
||||
*/
|
||||
@Mapper
|
||||
public interface ArticleLogMapper extends BaseMapper<ArticleLogEntity> {
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.blossom.backend.server.article.log;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.base.param.ParamEnum;
|
||||
import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||
import com.blossom.backend.server.article.log.pojo.ArticleLogEntity;
|
||||
import com.blossom.common.base.util.DateUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 文章记录
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ArticleLogService extends ServiceImpl<ArticleLogMapper, ArticleLogEntity> {
|
||||
|
||||
private final ParamService paramService;
|
||||
|
||||
/**
|
||||
* 新增记录
|
||||
*/
|
||||
@Async
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void insert(Long articleId, Integer version, String markdown) {
|
||||
ArticleLogEntity log = new ArticleLogEntity();
|
||||
log.setArticleId(articleId);
|
||||
log.setVersion(version);
|
||||
log.setMarkdown(markdown);
|
||||
log.setCreTime(DateUtils.date());
|
||||
baseMapper.insert(log);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 每天凌晨5点执行
|
||||
*/
|
||||
@Scheduled(cron = "0 0 05 * * ?")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void refreshWeather() {
|
||||
ParamEntity param = paramService.getValue(ParamEnum.ARTICLE_LOG_EXP_DAYS);
|
||||
int expireDay = -60;
|
||||
if (param != null) {
|
||||
expireDay = Integer.parseInt(param.getParamValue());
|
||||
}
|
||||
if (expireDay > 0) {
|
||||
expireDay = expireDay * -1;
|
||||
}
|
||||
|
||||
log.info("[BLOSSOM] 删除{}日前的编辑记录", Math.abs(expireDay));
|
||||
Date expireDate = DateUtils.offsetDay(DateUtils.date(), expireDay);
|
||||
LambdaQueryWrapper<ArticleLogEntity> where = new LambdaQueryWrapper<>();
|
||||
where.lt(ArticleLogEntity::getCreTime, DateUtils.toYMD(expireDate));
|
||||
baseMapper.delete(where);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.blossom.backend.server.article.log.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 文章记录
|
||||
*/
|
||||
@Data
|
||||
@TableName("blossom_article_log")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleLogEntity extends AbstractPOJO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** ID */
|
||||
@TableId
|
||||
private Long id;
|
||||
/** 文章ID */
|
||||
private Long articleId;
|
||||
/** 版本 */
|
||||
private Integer version;
|
||||
/** 文章内容 */
|
||||
private String markdown;
|
||||
/** 修改日期 */
|
||||
private Date creTime;
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.blossom.backend.server.article.open;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleInfoRes;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenReq;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenRes;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenSyncReq;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.server.article.view.ArticleViewService;
|
||||
import com.blossom.backend.server.article.view.pojo.ArticleViewEntity;
|
||||
import com.blossom.backend.server.doc.DocTypeEnum;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.ServletUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 公开文章 [Article]
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/article/open")
|
||||
public class ArticleOpenController {
|
||||
|
||||
private final ArticleService articleService;
|
||||
private final ArticleOpenService openService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询公开文章, 只返回 html 内容
|
||||
*
|
||||
* @param id 文章ID
|
||||
* @return
|
||||
*/
|
||||
@AuthIgnore
|
||||
@GetMapping("/info")
|
||||
public R<ArticleInfoRes> infoOpen(@RequestParam("id") Long id, HttpServletRequest request) {
|
||||
ArticleOpenEntity open = openService.selectById(id, true, false, true);
|
||||
XzException404.throwBy(ObjUtil.isNull(open), "文章不存在");
|
||||
|
||||
ArticleInfoRes res = open.to(ArticleInfoRes.class);
|
||||
res.setOpenTime(open.getOpenTime());
|
||||
res.setOpenVersion(open.getOpenVersion());
|
||||
res.setSyncTime(open.getSyncTime());
|
||||
res.setType(DocTypeEnum.A.getType());
|
||||
|
||||
ArticleEntity article = articleService.selectById(id, false, false, false);
|
||||
if (article != null) {
|
||||
res.setTags(DocUtil.toTagList(article.getTags()));
|
||||
res.setName(article.getName());
|
||||
res.setUv(article.getUv());
|
||||
res.setLikes(article.getLikes());
|
||||
}
|
||||
|
||||
articleService.uvAndPv(ServletUtil.getIP(request), ServletUtil.getUserAgent(request), id);
|
||||
|
||||
return R.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开文章
|
||||
*
|
||||
* @param req 文章对象
|
||||
*/
|
||||
@PostMapping
|
||||
public R<Long> open(@Validated @RequestBody ArticleOpenReq req) {
|
||||
req.setUserId(AuthContext.getUserId());
|
||||
return R.ok(openService.open(req));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步公开文章
|
||||
*
|
||||
* @param req 文章对象
|
||||
*/
|
||||
@PostMapping("/sync")
|
||||
public R<ArticleOpenRes> sync(@Validated @RequestBody ArticleOpenSyncReq req) {
|
||||
openService.sync(req.getId());
|
||||
ArticleOpenEntity openEntity = openService.selectById(req.getId(), false, false, false);
|
||||
return R.ok(openEntity.to(ArticleOpenRes.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.blossom.backend.server.article.open;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 公开文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Mapper
|
||||
public interface ArticleOpenMapper extends BaseMapper<ArticleOpenEntity> {
|
||||
|
||||
/**
|
||||
* 公开文章
|
||||
*
|
||||
* @param id 公开文章的ID
|
||||
*/
|
||||
void open(Long id);
|
||||
|
||||
/**
|
||||
* 关闭公开
|
||||
*
|
||||
* @param id 关闭公开文章的ID
|
||||
*/
|
||||
void close(Long id);
|
||||
|
||||
/**
|
||||
* 同步公开
|
||||
*
|
||||
* @param id 公开文章的ID
|
||||
*/
|
||||
void sync(Long id);
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package com.blossom.backend.server.article.open;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.server.TagEnum;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenReq;
|
||||
import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 公开文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ArticleOpenService extends ServiceImpl<ArticleOpenMapper, ArticleOpenEntity> {
|
||||
|
||||
private final ArticleService articleService;
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
public ArticleOpenEntity selectById(Long id, boolean showToc, boolean showMarkdown, boolean showHtml) {
|
||||
QueryWrapper<ArticleOpenEntity> where = new QueryWrapper<>();
|
||||
List<String> column = CollUtil.newArrayList("id", "pid", "words", "open_version", "open_time", "sync_time");
|
||||
if (showToc) {
|
||||
column.add(TagEnum.toc.name());
|
||||
}
|
||||
if (showMarkdown) {
|
||||
column.add("markdown");
|
||||
}
|
||||
if (showHtml) {
|
||||
column.add("html");
|
||||
}
|
||||
where.select(column);
|
||||
where.eq("id", id).last("limit 1");
|
||||
return baseMapper.selectOne(where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开或关闭公开访问
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long open(ArticleOpenReq req) {
|
||||
ArticleEntity article = articleService.getById(req.getId());
|
||||
ArticleEntity entity = req.to(ArticleEntity.class);
|
||||
/*
|
||||
* 公开文章 将 article 表插入到 article_open 表
|
||||
*/
|
||||
if (YesNo.YES.getValue().equals(req.getOpenStatus())) {
|
||||
XzException400.throwBy(article.getOpenStatus().equals(YesNo.YES.getValue()), "文章已[" + req.getId() + "]已允许公开访问, 若要同步最新文章内容, 请使用同步");
|
||||
entity.setOpenVersion(article.getVersion());
|
||||
baseMapper.open(req.getId());
|
||||
}
|
||||
/*
|
||||
* 取消公开 删除 article_open 表数据
|
||||
*/
|
||||
else if (YesNo.NO.getValue().equals(req.getOpenStatus())) {
|
||||
entity.setOpenVersion(0);
|
||||
XzException400.throwBy(article.getOpenStatus().equals(YesNo.NO.getValue()), "文章[" + req.getId() + "]未公开, 无法取消公开访问");
|
||||
baseMapper.close(req.getId());
|
||||
}
|
||||
|
||||
articleService.update(entity);
|
||||
return req.getId();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 同步公开访问的文章正文等信息
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void sync(Long id) {
|
||||
ArticleOpenEntity article = baseMapper.selectById(id);
|
||||
XzException400.throwBy(ObjUtil.isNull(article), "文章[" + id + "]未公开, 无法同步");
|
||||
baseMapper.sync(id);
|
||||
articleService.sync(id);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.blossom.backend.server.article.open.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 公开文章
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blossom_article_open")
|
||||
public class ArticleOpenEntity extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 公开版本的字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 公开版本
|
||||
*/
|
||||
private Integer openVersion;
|
||||
/**
|
||||
* 公开时间
|
||||
*/
|
||||
private Date openTime;
|
||||
/**
|
||||
* 同步时间
|
||||
*/
|
||||
private Date syncTime;
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
private String toc;
|
||||
/**
|
||||
* markdown 内容
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* html 内容
|
||||
*/
|
||||
private String html;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.blossom.backend.server.article.open.pojo;
|
||||
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 文章公开
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArticleOpenReq extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* 文章ID
|
||||
*/
|
||||
@Min(value = 0, message = "[文章ID] 不能小于0")
|
||||
@NotNull(message = "[文章ID] 为必填项")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公开状态
|
||||
*/
|
||||
@Min(value = 0, message = "[open 状态] 不能小于0")
|
||||
@Max(value = 1, message = "[open 状态] 不能大于1")
|
||||
@NotNull(message = "[open 状态] 为必填项")
|
||||
private Integer openStatus;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.blossom.backend.server.article.open.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 文章公开响应
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
public class ArticleOpenRes implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 文件夹ID
|
||||
*/
|
||||
private Long pid;
|
||||
/**
|
||||
* 字数
|
||||
*/
|
||||
private Integer words;
|
||||
/**
|
||||
* 公开文章版本
|
||||
*/
|
||||
private Integer openVersion;
|
||||
/**
|
||||
* 初次公开时间
|
||||
*/
|
||||
private Date openTime;
|
||||
/**
|
||||
* 同步时间
|
||||
*/
|
||||
private Date syncTime;
|
||||
/**
|
||||
* markdown 正文
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* html 正文
|
||||
*/
|
||||
private String html;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user