refactor: 基于 novel 项目 & Spring Cloud 2022 & Spring Cloud Alibaba 2022 重构

This commit is contained in:
xiongxiaoyang
2023-03-30 16:15:56 +08:00
parent d68ce51c82
commit 3d098eea5e
505 changed files with 14127 additions and 24067 deletions

View File

@@ -0,0 +1,54 @@
<?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>
<groupId>io.github.xxyopen</groupId>
<artifactId>novel-cloud</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>novel-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,78 @@
package io.github.xxyopen.novel.common.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* JWT 工具类
*
* @author xiongxiaoyang
* @date 2022/5/17
*/
@UtilityClass
@Slf4j
public class JwtUtils {
/**
* JWT 加密密钥
*/
private static final String SECRET = "E66559580A1ADF48CDD928516062F12E";
/**
* 定义系统标识头常量
*/
private static final String HEADER_SYSTEM_KEY = "systemKeyHeader";
/**
* 根据用户ID生成JWT
*
* @param uid 用户ID
* @param systemKey 系统标识
* @return JWT
*/
public String generateToken(Long uid, String systemKey) {
return Jwts.builder()
.setHeaderParam(HEADER_SYSTEM_KEY, systemKey)
.setSubject(uid.toString())
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)))
.compact();
}
/**
* 解析JWT返回用户ID
*
* @param token JWT
* @param systemKey 系统标识
* @return 用户ID
*/
public Long parseToken(String token, String systemKey) {
Jws<Claims> claimsJws;
try {
claimsJws = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token);
// OK, we can trust this JWT
// 判断该 JWT 是否属于指定系统
if (Objects.equals(claimsJws.getHeader().get(HEADER_SYSTEM_KEY), systemKey)) {
return Long.parseLong(claimsJws.getBody().getSubject());
}
} catch (JwtException e) {
log.warn("JWT解析失败:{}", token);
// don't trust the JWT!
}
return null;
}
}

View File

@@ -0,0 +1,45 @@
package io.github.xxyopen.novel.common.auth;
import lombok.experimental.UtilityClass;
/**
* 用户信息 持有类
*
* @author xiongxiaoyang
* @date 2022/5/18
*/
@UtilityClass
public class UserHolder {
/**
* 当前线程用户ID
*/
private static final ThreadLocal<Long> userIdTL = new ThreadLocal<>();
/**
* 当前线程作家ID
*/
private static final ThreadLocal<Long> authorIdTL = new ThreadLocal<>();
public void setUserId(Long userId) {
userIdTL.set(userId);
}
public Long getUserId() {
return userIdTL.get();
}
public void setAuthorId(Long authorId) {
authorIdTL.set(authorId);
}
public Long getAuthorId() {
return authorIdTL.get();
}
public void clear() {
userIdTL.remove();
authorIdTL.remove();
}
}

View File

@@ -0,0 +1,33 @@
package io.github.xxyopen.novel.common.constant;
/**
* AMQP 相关常量
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
public class AmqpConsts {
/**
* 小说信息改变 MQ
*/
public static class BookChangeMq {
/**
* 小说信息改变交换机
*/
public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
/**
* Elasticsearch book 索引更新的队列
*/
public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
/**
* Redis book 缓存更新的队列
*/
public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
}
}

View File

@@ -0,0 +1,114 @@
package io.github.xxyopen.novel.common.constant;
/**
* API 路由常量
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class ApiRouterConsts {
private ApiRouterConsts() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
/**
* API请求路径前缀
*/
public static final String API_URL_PREFIX = "/api";
/**
* 前台门户系统请求路径前缀
*/
public static final String API_FRONT_URL_PREFIX = API_URL_PREFIX + "/front";
/**
* 作家管理系统请求路径前缀
*/
public static final String API_AUTHOR_URL_PREFIX = API_URL_PREFIX + "/author";
/**
* 平台后台管理系统请求路径前缀
*/
public static final String API_ADMIN_URL_PREFIX = API_URL_PREFIX + "/admin";
/**
* 微服务内部调用请求路径前缀
*/
public static final String API_INNER_URL_PREFIX = API_URL_PREFIX + "/inner";
/**
* 首页模块请求路径前缀
*/
public static final String HOME_URL_PREFIX = "/home";
/**
* 首页模块请求路径前缀
*/
public static final String NEWS_URL_PREFIX = "/news";
/**
* 小说模块请求路径前缀
*/
public static final String BOOK_URL_PREFIX = "/book";
/**
* 会员模块请求路径前缀
*/
public static final String USER_URL_PREFIX = "/user";
/**
* 资源(图片/视频/文档)模块请求路径前缀
*/
public static final String RESOURCE_URL_PREFIX = "/resource";
/**
* 搜索模块请求路径前缀
*/
public static final String SEARCH_URL_PREFIX = "/search";
/**
* 前台门户首页API请求路径前缀
*/
public static final String API_FRONT_HOME_URL_PREFIX = API_FRONT_URL_PREFIX + HOME_URL_PREFIX;
/**
* 前台门户新闻相关API请求路径前缀
*/
public static final String API_FRONT_NEWS_URL_PREFIX = API_FRONT_URL_PREFIX + NEWS_URL_PREFIX;
/**
* 前台门户小说相关API请求路径前缀
*/
public static final String API_FRONT_BOOK_URL_PREFIX = API_FRONT_URL_PREFIX + BOOK_URL_PREFIX;
/**
* 前台门户会员相关API请求路径前缀
*/
public static final String API_FRONT_USER_URL_PREFIX = API_FRONT_URL_PREFIX + USER_URL_PREFIX;
/**
* 前台门户资源(图片/视频/文档相关API请求路径前缀
*/
public static final String API_FRONT_RESOURCE_URL_PREFIX =
API_FRONT_URL_PREFIX + RESOURCE_URL_PREFIX;
/**
* 前台门户搜索相关API请求路径前缀
*/
public static final String API_FRONT_SEARCH_URL_PREFIX =
API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX;
/**
* 小说微服务内部调用接口
* */
public static final String API_INNER_BOOK_URL_PREFIX =
API_INNER_URL_PREFIX + BOOK_URL_PREFIX;
/**
* 会员微服务内部调用接口
* */
public static final String API_INNER_USER_URL_PREFIX =
API_INNER_URL_PREFIX + USER_URL_PREFIX;
}

View File

@@ -0,0 +1,175 @@
package io.github.xxyopen.novel.common.constant;
/**
* 缓存相关常量
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class CacheConsts {
/**
* 本项目 Redis 缓存前缀
*/
public static final String REDIS_CACHE_PREFIX = "Cache::Novel::";
/**
* Caffeine 缓存管理器
*/
public static final String CAFFEINE_CACHE_MANAGER = "caffeineCacheManager";
/**
* Redis 缓存管理器
*/
public static final String REDIS_CACHE_MANAGER = "redisCacheManager";
/**
* 首页小说推荐缓存
*/
public static final String HOME_BOOK_CACHE_NAME = "homeBookCache";
/**
* 最新新闻缓存
*/
public static final String LATEST_NEWS_CACHE_NAME = "latestNewsCache";
/**
* 小说点击榜缓存
*/
public static final String BOOK_VISIT_RANK_CACHE_NAME = "bookVisitRankCache";
/**
* 小说新书榜缓存
*/
public static final String BOOK_NEWEST_RANK_CACHE_NAME = "bookNewestRankCache";
/**
* 小说更新榜缓存
*/
public static final String BOOK_UPDATE_RANK_CACHE_NAME = "bookUpdateRankCache";
/**
* 首页友情链接缓存
*/
public static final String HOME_FRIEND_LINK_CACHE_NAME = "homeFriendLinkCache";
/**
* 小说分类列表缓存
*/
public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache";
/**
* 小说信息缓存
*/
public static final String BOOK_INFO_CACHE_NAME = "bookInfoCache";
/**
* 小说章节缓存
*/
public static final String BOOK_CHAPTER_CACHE_NAME = "bookChapterCache";
/**
* 小说内容缓存
*/
public static final String BOOK_CONTENT_CACHE_NAME = "bookContentCache";
/**
* 最近更新小说ID列表缓存
*/
public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache";
/**
* 图片验证码缓存 KEY
*/
public static final String IMG_VERIFY_CODE_CACHE_KEY =
REDIS_CACHE_PREFIX + "imgVerifyCodeCache::";
/**
* 用户信息缓存
*/
public static final String USER_INFO_CACHE_NAME = "userInfoCache";
/**
* 作家信息缓存
*/
public static final String AUTHOR_INFO_CACHE_NAME = "authorInfoCache";
/**
* 缓存配置常量
*/
public enum CacheEnum {
HOME_BOOK_CACHE(0, HOME_BOOK_CACHE_NAME, 60 * 60 * 24, 1),
LATEST_NEWS_CACHE(0, LATEST_NEWS_CACHE_NAME, 60 * 10, 1),
BOOK_VISIT_RANK_CACHE(2, BOOK_VISIT_RANK_CACHE_NAME, 60 * 60 * 6, 1),
BOOK_NEWEST_RANK_CACHE(0, BOOK_NEWEST_RANK_CACHE_NAME, 60 * 30, 1),
BOOK_UPDATE_RANK_CACHE(0, BOOK_UPDATE_RANK_CACHE_NAME, 60, 1),
HOME_FRIEND_LINK_CACHE(2, HOME_FRIEND_LINK_CACHE_NAME, 0, 1),
BOOK_CATEGORY_LIST_CACHE(0, BOOK_CATEGORY_LIST_CACHE_NAME, 0, 2),
BOOK_INFO_CACHE(0, BOOK_INFO_CACHE_NAME, 60 * 60 * 18, 500),
BOOK_CHAPTER_CACHE(0, BOOK_CHAPTER_CACHE_NAME, 60 * 60 * 6, 5000),
BOOK_CONTENT_CACHE(2, BOOK_CONTENT_CACHE_NAME, 60 * 60 * 12, 3000),
LAST_UPDATE_BOOK_ID_LIST_CACHE(0, LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME, 60 * 60, 10),
USER_INFO_CACHE(2, USER_INFO_CACHE_NAME, 60 * 60 * 24, 10000),
AUTHOR_INFO_CACHE(2, AUTHOR_INFO_CACHE_NAME, 60 * 60 * 48, 1000);
/**
* 缓存类型 0-本地 1-本地和远程 2-远程
*/
private int type;
/**
* 缓存的名字
*/
private String name;
/**
* 失效时间(秒) 0-永不失效
*/
private int ttl;
/**
* 最大容量
*/
private int maxSize;
CacheEnum(int type, String name, int ttl, int maxSize) {
this.type = type;
this.name = name;
this.ttl = ttl;
this.maxSize = maxSize;
}
public boolean isLocal() {
return type <= 1;
}
public boolean isRemote() {
return type >= 1;
}
public String getName() {
return name;
}
public int getTtl() {
return ttl;
}
public int getMaxSize() {
return maxSize;
}
}
}

View File

@@ -0,0 +1,57 @@
package io.github.xxyopen.novel.common.constant;
/**
* 通用常量
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class CommonConsts {
/**
* 是
*/
public static final Integer YES = 1;
public static final String TRUE = "true";
/**
* 否
*/
public static final Integer NO = 0;
public static final String FALSE = "false";
/**
* 性别常量
*/
public enum SexEnum {
/**
* 男
*/
MALE(0, ""),
/**
* 女
*/
FEMALE(1, "");
SexEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
private int code;
private String desc;
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
}

View File

@@ -0,0 +1,201 @@
package io.github.xxyopen.novel.common.constant;
import lombok.Getter;
/**
* 数据库 常量
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class DatabaseConsts {
/**
* 用户信息表
*/
public static class UserInfoTable {
private UserInfoTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_USERNAME = "username";
}
/**
* 用户反馈表
*/
public static class UserFeedBackTable {
private UserFeedBackTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_USER_ID = "user_id";
}
/**
* 用户书架表
*/
public static class UserBookshelfTable {
private UserBookshelfTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_USER_ID = "user_id";
public static final String COLUMN_BOOK_ID = "book_id";
}
/**
* 作家信息表
*/
public static class AuthorInfoTable {
private AuthorInfoTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_USER_ID = "user_id";
}
/**
* 小说类别表
*/
public static class BookCategoryTable {
private BookCategoryTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_WORK_DIRECTION = "work_direction";
}
/**
* 小说表
*/
public static class BookTable {
private BookTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_CATEGORY_ID = "category_id";
public static final String COLUMN_BOOK_NAME = "book_name";
public static final String AUTHOR_ID = "author_id";
public static final String COLUMN_VISIT_COUNT = "visit_count";
public static final String COLUMN_WORD_COUNT = "word_count";
public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
}
/**
* 小说章节表
*/
public static class BookChapterTable {
private BookChapterTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_BOOK_ID = "book_id";
public static final String COLUMN_CHAPTER_NUM = "chapter_num";
public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
}
/**
* 小说内容表
*/
public static class BookContentTable {
private BookContentTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_CHAPTER_ID = "chapter_id";
}
/**
* 小说评论表
*/
public static class BookCommentTable {
private BookCommentTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_BOOK_ID = "book_id";
public static final String COLUMN_USER_ID = "user_id";
}
/**
* 新闻内容表
*/
public static class NewsContentTable {
private NewsContentTable() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
public static final String COLUMN_NEWS_ID = "news_id";
}
/**
* 通用列枚举类
*/
@Getter
public enum CommonColumnEnum {
ID("id"),
SORT("sort"),
CREATE_TIME("create_time"),
UPDATE_TIME("update_time");
private String name;
CommonColumnEnum(String name) {
this.name = name;
}
}
/**
* SQL语句枚举类
*/
@Getter
public enum SqlEnum {
LIMIT_1("limit 1"),
LIMIT_2("limit 2"),
LIMIT_5("limit 5"),
LIMIT_30("limit 30"),
LIMIT_500("limit 500");
private String sql;
SqlEnum(String sql) {
this.sql = sql;
}
}
}

View File

@@ -0,0 +1,152 @@
package io.github.xxyopen.novel.common.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 错误码枚举类。
* <p>
* 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 错误产生来源分为 A/B/C A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题; B
* 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999大类之间的
* 步长间距预留 100。
* <p>
* 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@Getter
@AllArgsConstructor
public enum ErrorCodeEnum {
/**
* 正确执行后的返回
*/
OK("00000", "一切 ok"),
/**
* 一级宏观错误码,用户端错误
*/
USER_ERROR("A0001", "用户端错误"),
/**
* 二级宏观错误码,用户注册错误
*/
USER_REGISTER_ERROR("A0100", "用户注册错误"),
/**
* 用户未同意隐私协议
*/
USER_NO_AGREE_PRIVATE_ERROR("A0101", "用户未同意隐私协议"),
/**
* 注册国家或地区受限
*/
USER_REGISTER_AREA_LIMIT_ERROR("A0102", "注册国家或地区受限"),
/**
* 用户验证码错误
*/
USER_VERIFY_CODE_ERROR("A0240", "用户验证码错误"),
/**
* 用户名已存在
*/
USER_NAME_EXIST("A0111", "用户名已存在"),
/**
* 用户账号不存在
*/
USER_ACCOUNT_NOT_EXIST("A0201", "用户账号不存在"),
/**
* 用户密码错误
*/
USER_PASSWORD_ERROR("A0210", "用户密码错误"),
/**
* 二级宏观错误码,用户请求参数错误
*/
USER_REQUEST_PARAM_ERROR("A0400", "用户请求参数错误"),
/**
* 用户登录已过期
*/
USER_LOGIN_EXPIRED("A0230", "用户登录已过期"),
/**
* 访问未授权
*/
USER_UN_AUTH("A0301", "访问未授权"),
/**
* 用户请求服务异常
*/
USER_REQ_EXCEPTION("A0500", "用户请求服务异常"),
/**
* 请求超出限制
*/
USER_REQ_MANY("A0501", "请求超出限制"),
/**
* 用户评论异常
*/
USER_COMMENT("A2000", "用户评论异常"),
/**
* 用户评论异常
*/
USER_COMMENTED("A2001", "用户已发表评论"),
/**
* 作家发布异常
*/
AUTHOR_PUBLISH("A3000", "作家发布异常"),
/**
* 小说名已存在
*/
AUTHOR_BOOK_NAME_EXIST("A3001", "小说名已存在"),
/**
* 用户上传文件异常
*/
USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
/**
* 用户上传文件类型不匹配
*/
USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
/**
* 一级宏观错误码,系统执行出错
*/
SYSTEM_ERROR("B0001", "系统执行出错"),
/**
* 二级宏观错误码,系统执行超时
*/
SYSTEM_TIMEOUT_ERROR("B0100", "系统执行超时"),
/**
* 一级宏观错误码,调用第三方服务出错
*/
THIRD_SERVICE_ERROR("C0001", "调用第三方服务出错"),
/**
* 一级宏观错误码,中间件服务出错
*/
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错");
/**
* 错误码
*/
private final String code;
/**
* 中文描述
*/
private final String message;
}

View File

@@ -0,0 +1,25 @@
package io.github.xxyopen.novel.common.constant;
/**
* 消息发送器的类型
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
public class MessageSenderTypeConsts {
private MessageSenderTypeConsts() {
throw new IllegalStateException("Constant class");
}
/**
* 注册成功的邮件发送器
*/
public static final String REGISTER_MAIL_SENDER = "registerMailSender";
/**
* 秒杀活动的系统通知发送器
*/
public static final String SECKILL_SYS_NOTICE_SENDER = "seckillSysNoticeSender";
}

View File

@@ -0,0 +1,45 @@
package io.github.xxyopen.novel.common.constant;
/**
* 系统配置相关常量
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class SystemConfigConsts {
private SystemConfigConsts() {
throw new IllegalStateException(CONST_INSTANCE_EXCEPTION_MSG);
}
/**
* Http 请求认证 Header
*/
public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
/**
* 前台门户系统标识
*/
public static final String NOVEL_FRONT_KEY = "front";
/**
* 作家管理系统标识
*/
public static final String NOVEL_AUTHOR_KEY = "author";
/**
* 后台管理系统标识
*/
public static final String NOVEL_ADMIN_KEY = "admin";
/**
* 图片上传目录
*/
public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
/**
* 常量类实例化异常信息
*/
public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
}

View File

@@ -0,0 +1,32 @@
package io.github.xxyopen.novel.common.json.deserializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import org.springframework.boot.jackson.JsonComponent;
/**
* JSON 全局反序列化器
*
* @author xiongxiaoyang
* @date 2022/5/21
*/
@JsonComponent
public class GlobalJsonDeserializer {
/**
* 字符串反序列化器:过滤特殊字符,解决 XSS 攻击
*/
public static class StringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
return jsonParser.getValueAsString()
.replace("<", "&lt;")
.replace(">", "&gt;");
}
}
}

View File

@@ -0,0 +1,22 @@
package io.github.xxyopen.novel.common.json.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* 用户名序列化器(敏感信息,不应该在页面上完全显示)
*
* @author xiongxiaoyang
* @date 2022/5/20
*/
public class UsernameSerializer extends JsonSerializer<String> {
@Override
public void serialize(String s, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(s.substring(0, 4) + "****" + s.substring(8));
}
}

View File

@@ -0,0 +1,33 @@
package io.github.xxyopen.novel.common.req;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.Data;
/**
* 分页请求数据格式封装所有分页请求的Dto类都应继承该类
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@Data
public class PageReqDto {
/**
* 请求页码,默认第 1 页
*/
@Parameter(description = "请求页码,默认第 1 页")
private int pageNum = 1;
/**
* 每页大小,默认每页 10 条
*/
@Parameter(description = "每页大小,默认每页 10 条")
private int pageSize = 10;
/**
* 是否查询所有,默认不查所有 为 true 时pageNum 和 pageSize 无效
*/
@Parameter(hidden = true)
private boolean fetchAll = false;
}

View File

@@ -0,0 +1,63 @@
package io.github.xxyopen.novel.common.resp;
import java.util.List;
import lombok.Getter;
/**
* 分页响应数据格式封装
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@Getter
public class PageRespDto<T> {
/**
* 页码
*/
private final long pageNum;
/**
* 每页大小
*/
private final long pageSize;
/**
* 总记录数
*/
private final long total;
/**
* 分页数据集
*/
private final List<? extends T> list;
/**
* 该构造函数用于通用分页查询的场景 接收普通分页数据和普通集合
*/
public PageRespDto(long pageNum, long pageSize, long total, List<T> list) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.list = list;
}
public static <T> PageRespDto<T> of(long pageNum, long pageSize, long total, List<T> list) {
return new PageRespDto<>(pageNum, pageSize, total, list);
}
/**
* 获取分页数
*/
public long getPages() {
if (this.pageSize == 0L) {
return 0L;
} else {
long pages = this.total / this.pageSize;
if (this.total % this.pageSize != 0L) {
++pages;
}
return pages;
}
}
}

View File

@@ -0,0 +1,87 @@
package io.github.xxyopen.novel.common.resp;
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.util.Objects;
/**
* Http Rest 响应工具及数据格式封装
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@Getter
public class RestResp<T> {
/**
* 响应码
*/
@Schema(description = "错误码00000-没有错误")
private String code;
/**
* 响应消息
*/
@Schema(description = "响应消息")
private String message;
/**
* 响应数据
*/
@Schema(description = "响应数据")
private T data;
private RestResp() {
this.code = ErrorCodeEnum.OK.getCode();
this.message = ErrorCodeEnum.OK.getMessage();
}
private RestResp(ErrorCodeEnum errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
private RestResp(T data) {
this();
this.data = data;
}
/**
* 业务处理成功,无数据返回
*/
public static RestResp<Void> ok() {
return new RestResp<>();
}
/**
* 业务处理成功,有数据返回
*/
public static <T> RestResp<T> ok(T data) {
return new RestResp<>(data);
}
/**
* 业务处理失败
*/
public static RestResp<Void> fail(ErrorCodeEnum errorCode) {
return new RestResp<>(errorCode);
}
/**
* 系统错误
*/
public static RestResp<Void> error() {
return new RestResp<>(ErrorCodeEnum.SYSTEM_ERROR);
}
/**
* 判断是否成功
*/
public boolean isOk() {
return Objects.equals(this.code, ErrorCodeEnum.OK.getCode());
}
}

View File

@@ -0,0 +1,159 @@
<?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>novel-core</artifactId>
<groupId>io.github.xxyopen</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>novel-config</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Undertow instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 缓存相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 请求参数校验相关 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- MQ 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- ShardingSphere-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>${shardingsphere-jdbc.version}</version>
</dependency>
<!-- Spring Boot 管理和监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Redisson 相关 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Aop 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- OpenAPI 3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>io.github.xxyopen</groupId>
<artifactId>novel-common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,44 @@
package io.github.xxyopen.novel.config;
import io.github.xxyopen.novel.common.constant.AmqpConsts;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AMQP 配置类
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Configuration
public class AmqpConfig {
/**
* 小说信息改变交换机
*/
@Bean
public FanoutExchange bookChangeExchange() {
return new FanoutExchange(AmqpConsts.BookChangeMq.EXCHANGE_NAME);
}
/**
* Elasticsearch book 索引更新队列
*/
@Bean
public Queue esBookUpdateQueue() {
return new Queue(AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE);
}
/**
* Elasticsearch book 索引更新队列绑定到小说信息改变交换机
*/
@Bean
public Binding esBookUpdateQueueBinding() {
return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange());
}
}

View File

@@ -0,0 +1,92 @@
package io.github.xxyopen.novel.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.github.xxyopen.novel.common.constant.CacheConsts;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
/**
* 缓存配置类
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
@Configuration
public class CacheConfig {
/**
* Caffeine 缓存管理器
*/
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length);
// 类型推断 var 非常适合 for 循环JDK 10 引入JDK 11 改进
for (var c : CacheConsts.CacheEnum.values()) {
if (c.isLocal()) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats()
.maximumSize(c.getMaxSize());
if (c.getTtl() > 0) {
caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl()));
}
caches.add(new CaffeineCache(c.getName(), caffeine.build()));
}
}
cacheManager.setCaches(caches);
return cacheManager;
}
/**
* Redis 缓存管理器
*/
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(
connectionFactory);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);
Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>(
CacheConsts.CacheEnum.values().length);
// 类型推断 var 非常适合 for 循环JDK 10 引入JDK 11 改进
for (var c : CacheConsts.CacheEnum.values()) {
if (c.isRemote()) {
if (c.getTtl() > 0) {
cacheMap.put(c.getName(),
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
.entryTtl(Duration.ofSeconds(c.getTtl())));
} else {
cacheMap.put(c.getName(),
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
}
}
}
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
defaultCacheConfig, cacheMap);
redisCacheManager.setTransactionAware(true);
redisCacheManager.initializeCaches();
return redisCacheManager;
}
}

View File

@@ -0,0 +1,28 @@
package io.github.xxyopen.novel.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Mybatis-Plus 配置类
*
* @author xiongxiaoyang
* @date 2022/5/16
*/
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,25 @@
package io.github.xxyopen.novel.config;
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* OpenApi 配置类
*
* @author xiongxiaoyang
* @date 2022/9/1
*/
@Configuration
@Profile("dev")
@OpenAPIDefinition(info = @Info(title = "novel 项目接口文档", version = "v3.2.0", license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0")))
@SecurityScheme(type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME, description = "登录 token")
public class OpenApiConfig {
}

View File

@@ -0,0 +1,15 @@
package io.github.xxyopen.novel.config;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Xss 过滤配置属性
*
* @author xiongxiaoyang
* @date 2022/5/17
*/
@ConfigurationProperties(prefix = "novel.xss")
public record XssProperties(Boolean enabled, List<String> excludes) {
}

View File

@@ -0,0 +1,23 @@
package io.github.xxyopen.novel.config.annotation;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 分布式锁-Key 注解
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Documented
@Retention(RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface Key {
String expr() default "";
}

View File

@@ -0,0 +1,31 @@
package io.github.xxyopen.novel.config.annotation;
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 分布式锁 注解
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface Lock {
String prefix();
boolean isWait() default false;
long waitTime() default 3L;
ErrorCodeEnum failCode() default ErrorCodeEnum.OK;
}

View File

@@ -0,0 +1,86 @@
package io.github.xxyopen.novel.config.aspect;
import io.github.xxyopen.novel.config.annotation.Key;
import io.github.xxyopen.novel.config.annotation.Lock;
import io.github.xxyopen.novel.config.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁 切面
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Aspect
@Component
@RequiredArgsConstructor
public class LockAspect {
private final RedissonClient redissonClient;
private static final String KEY_PREFIX = "Lock";
private static final String KEY_SEPARATOR = "::";
@Around(value = "@annotation(io.github.xxyopen.novel.config.annotation.Lock)")
@SneakyThrows
public Object doAround(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
Lock lock = targetMethod.getAnnotation(Lock.class);
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
joinPoint.getArgs());
RLock rLock = redissonClient.getLock(lockKey);
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
try {
return joinPoint.proceed();
} finally {
rLock.unlock();
}
}
throw new BusinessException(lock.failCode());
}
private String buildLockKey(String prefix, Method method, Object[] args) {
StringBuilder builder = new StringBuilder();
if (StringUtils.hasText(prefix)) {
builder.append(KEY_SEPARATOR).append(prefix);
}
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
builder.append(KEY_SEPARATOR);
if (parameters[i].isAnnotationPresent(Key.class)) {
Key key = parameters[i].getAnnotation(Key.class);
builder.append(parseKeyExpr(key.expr(), args[i]));
}
}
return builder.toString();
}
private String parseKeyExpr(String expr, Object arg) {
if (!StringUtils.hasText(expr)) {
return arg.toString();
}
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
return expression.getValue(arg, String.class);
}
}

View File

@@ -0,0 +1,26 @@
package io.github.xxyopen.novel.config.exception;
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 自定义业务异常,用于处理用户请求时,业务错误时抛出
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BusinessException extends RuntimeException {
private final ErrorCodeEnum errorCodeEnum;
public BusinessException(ErrorCodeEnum errorCodeEnum) {
// 不调用父类 Throwable的fillInStackTrace() 方法生成栈追踪信息,提高应用性能
// 构造器之间的调用必须在第一行
super(errorCodeEnum.getMessage(), null, false, false);
this.errorCodeEnum = errorCodeEnum;
}
}

View File

@@ -0,0 +1,47 @@
package io.github.xxyopen.novel.config.exception;
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.common.resp.RestResp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 通用的异常处理器
*
* @author xiongxiaoyang
* @date 2022/5/11
*/
@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {
/**
* 处理数据校验异常
*/
@ExceptionHandler(BindException.class)
public RestResp<Void> handlerBindException(BindException e) {
log.error(e.getMessage(), e);
return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
}
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public RestResp<Void> handlerBusinessException(BusinessException e) {
log.error(e.getMessage(), e);
return RestResp.fail(e.getErrorCodeEnum());
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
public RestResp<Void> handlerException(Exception e) {
log.error(e.getMessage(), e);
return RestResp.error();
}
}

View File

@@ -0,0 +1,74 @@
package io.github.xxyopen.novel.config.filter;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import io.github.xxyopen.novel.config.XssProperties;
import io.github.xxyopen.novel.config.wrapper.XssHttpServletRequestWrapper;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 防止 XSS 攻击的过滤器
*
* @author xiongxiaoyang
* @date 2022/5/17
*/
@Component
@ConditionalOnProperty(value = "novel.xss.enabled", havingValue = "true")
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
@EnableConfigurationProperties(value = {XssProperties.class})
@RequiredArgsConstructor
public class XssFilter implements Filter {
private final XssProperties xssProperties;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (handleExcludeUrl(req)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
(HttpServletRequest) servletRequest);
filterChain.doFilter(xssRequest, servletResponse);
}
private boolean handleExcludeUrl(HttpServletRequest request) {
if (CollectionUtils.isEmpty(xssProperties.excludes())) {
return false;
}
String url = request.getServletPath();
for (String pattern : xssProperties.excludes()) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
@Override
public void destroy() {
Filter.super.destroy();
}
}

View File

@@ -0,0 +1,48 @@
package io.github.xxyopen.novel.config.interceptor;
import io.github.xxyopen.novel.common.auth.JwtUtils;
import io.github.xxyopen.novel.common.auth.UserHolder;
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* Token 解析拦截器
*
* @author xiongxiaoyang
* @date 2022/5/27
*/
@Component
@RequiredArgsConstructor
public class TokenParseInterceptor implements HandlerInterceptor {
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取登录 JWT
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
if (StringUtils.hasText(token)) {
// 解析 token 并保存
UserHolder.setUserId(JwtUtils.parseToken(token, SystemConfigConsts.NOVEL_FRONT_KEY));
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
/**
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 清理当前线程保存的用户数据
UserHolder.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

View File

@@ -0,0 +1,43 @@
package io.github.xxyopen.novel.config.wrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap;
import java.util.Map;
/**
* XSS 过滤处理
*
* @author xiongxiaoyang
* @date 2022/5/17
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Map<String, String> REPLACE_RULE = new HashMap<>();
static {
REPLACE_RULE.put("<", "&lt;");
REPLACE_RULE.put(">", "&gt;");
}
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapeValues = new String[length];
for (int i = 0; i < length; i++) {
escapeValues[i] = values[i];
int index = i;
REPLACE_RULE.forEach(
(k, v) -> escapeValues[index] = escapeValues[index].replaceAll(k, v));
}
return escapeValues;
}
return new String[0];
}
}

View File

@@ -0,0 +1,142 @@
#--------------------- Spring Cloud 配置-------------------
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.10.110:8848
openfeign:
lazy-attributes-resolution: true
feign:
sentinel:
enabled: true
--- #--------------------------通用配置-------------------------
spring:
jackson:
generator:
# JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。
# 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成,
# 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽)
write-numbers-as-strings: true
--- #---------------------数据库配置---------------------------
spring:
datasource:
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
# ShardingSphere-JDBC 配置
# 配置是 ShardingSphere-JDBC 中唯一与应用开发者交互的模块,
# 通过它可以快速清晰的理解 ShardingSphere-JDBC 所提供的功能。
shardingsphere:
# 是否开启分库分表
enabled: false
props:
# 是否在日志中打印 SQL
sql-show: true
# 模式配置
mode:
# 单机模式
type: Standalone
repository:
# 文件持久化
type: File
props:
# 元数据存储路径
path: .shardingsphere
# 使用本地配置覆盖持久化配置
overwrite: true
# 数据源配置
datasource:
names: ds_0
ds_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
# 规则配置
rules:
# 数据分片
sharding:
tables:
# book_content 表
book_content:
# 数据节点
actual-data-nodes: ds_$->{0}.book_content$->{0..9}
# 分表策略
table-strategy:
standard:
# 分片列名称
sharding-column: chapter_id
# 分片算法名称
sharding-algorithm-name: bookContentSharding
sharding-algorithms:
bookContentSharding:
# 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持
type: INLINE
props:
# 分片算法的行表达式
algorithm-expression: book_content$->{chapter_id % 10}
--- #---------------------中间件配置---------------------------
spring:
data:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
# password: 123456
# RabbitMQ 配置
rabbitmq:
addresses: "amqp://xxyopen:test123456@192.168.10.110"
virtual-host: novel
template:
retry:
# 开启重试
enabled: true
# 最大重试次数
max-attempts: 3
# 第一次和第二次重试之间的持续时间
initial-interval: "3s"
--- #----------------------安全配置---------------------------
# Actuator 端点管理
management:
# 端点公开配置
endpoints:
# 通过 HTTP 公开的 Web 端点
web:
exposure:
# 公开所有的 Web 端点
include: "*"
info:
env:
# 公开所有以 info. 开头的环境属性
enabled: true
health:
rabbit:
# 关闭 rabbitmq 的健康检查
enabled: true
elasticsearch:
# 关闭 elasticsearch 的健康检查
enabled: true
--- #---------------------自定义配置----------------------------
novel:
# XSS 过滤配置
xss:
# 过滤开关
enabled: true
# 排除链接
excludes:
- /system/notice/*

View File

@@ -0,0 +1,15 @@
spring:
cloud:
nacos:
config:
server-addr: 192.168.10.110:8848
file-extension: yml
extension-configs[0]:
dataid: novel-mysql.yml
refresh: true
extension-configs[1]:
dataid: novel-redis.yml
refresh: true
extension-configs[2]:
dataid: novel-rabbitmq.yml
refresh: true

19
novel-core/pom.xml Normal file
View File

@@ -0,0 +1,19 @@
<?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">
<modules>
<module>novel-common</module>
<module>novel-config</module>
</modules>
<parent>
<artifactId>novel-cloud</artifactId>
<groupId>io.github.xxyopen</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>novel-core</artifactId>
<packaging>pom</packaging>
</project>