mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-15 19:45:06 +08:00
Merge remote-tracking branch 'origin/dev' into feature/springdoc
# Conflicts: # yudao-dependencies/pom.xml # yudao-framework/yudao-spring-boot-starter-web/pom.xml # yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java # yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
This commit is contained in:
@ -40,6 +40,7 @@
|
||||
|
||||
<module>yudao-spring-boot-starter-flowable</module>
|
||||
<module>yudao-spring-boot-starter-captcha</module>
|
||||
<module>yudao-spring-boot-starter-websocket</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
|
@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.common.util.validation;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
|
@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.9.ALL</version>
|
||||
<version>4.35.32.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
|
@ -17,6 +17,10 @@
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xingyuv</groupId>
|
||||
<artifactId>spring-boot-starter-captcha-plus</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -29,11 +33,6 @@
|
||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码相关 -->
|
||||
<dependency>
|
||||
<groupId>com.anji-plus</groupId>
|
||||
<artifactId>spring-boot-starter-captcha</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -2,12 +2,9 @@ package cn.iocoder.yudao.framework.captcha.core.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
|
||||
import com.anji.captcha.model.vo.PointVO;
|
||||
import org.redisson.api.RLock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
|
||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.captcha.core.service;
|
||||
import com.anji.captcha.service.CaptchaCacheService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
@ -92,8 +92,22 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
entities.forEach(this::insert);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入,适合大量数据插入
|
||||
*
|
||||
* @param entities 实体们
|
||||
* @param size 插入数量 Db.saveBatch 默认为1000
|
||||
*/
|
||||
default void insertBatch(Collection<T> entities, int size) {
|
||||
Db.saveBatch(entities, size);
|
||||
}
|
||||
|
||||
default void updateBatch(T update) {
|
||||
update(update, new QueryWrapper<>());
|
||||
}
|
||||
|
||||
default void updateBatch(Collection<T> entities, int size) {
|
||||
Db.updateBatchById(entities, size);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -129,6 +129,8 @@ public class YudaoWebSecurityConfigurerAdapter {
|
||||
.antMatchers(buildAppApi("/**")).permitAll()
|
||||
// 1.5 验证码captcha 允许匿名访问
|
||||
.antMatchers("/captcha/get", "/captcha/check").permitAll()
|
||||
// 1.6 webSocket 允许匿名访问
|
||||
.antMatchers("/websocket/message").permitAll()
|
||||
// ②:每个项目的自定义规则
|
||||
.and().authorizeRequests(registry -> // 下面,循环设置自定义规则
|
||||
authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
|
||||
|
@ -68,6 +68,12 @@
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- xss -->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -2,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.JsoupXssCleaner;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.XssFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
|
||||
import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
* 设置 API 前缀,仅仅匹配 controller 包下的
|
||||
*
|
||||
* @param configurer 配置
|
||||
* @param api API 配置
|
||||
* @param api API 配置
|
||||
*/
|
||||
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
|
||||
@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
* 创建 XssFilter Bean,解决 Xss 安全问题
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
|
||||
@ConditionalOnBean(XssCleaner.class)
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Xss 清理者
|
||||
*
|
||||
* @return XssCleaner
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(XssCleaner.class)
|
||||
public XssCleaner xssCleaner() {
|
||||
return new JsoupXssCleaner();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
|
||||
*
|
||||
* @return Jackson2ObjectMapperBuilderCustomizer
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
|
||||
@ConditionalOnBean(ObjectMapper.class)
|
||||
@ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
|
||||
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
|
||||
return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
|
@ -0,0 +1,80 @@
|
||||
package cn.iocoder.yudao.framework.web.core.clean;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
/**
|
||||
* jsonp 过滤字符串
|
||||
*/
|
||||
public class JsoupXssCleaner implements XssCleaner {
|
||||
|
||||
private final Safelist safelist;
|
||||
|
||||
/**
|
||||
* 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
|
||||
*/
|
||||
private final String baseUri;
|
||||
|
||||
/**
|
||||
* 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
|
||||
*/
|
||||
public JsoupXssCleaner() {
|
||||
this.safelist = buildSafelist();
|
||||
this.baseUri = "";
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(Safelist safelist) {
|
||||
this.safelist = safelist;
|
||||
this.baseUri = "";
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(String baseUri) {
|
||||
this.safelist = buildSafelist();
|
||||
this.baseUri = baseUri;
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(Safelist safelist, String baseUri) {
|
||||
this.safelist = safelist;
|
||||
this.baseUri = baseUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个 Xss 清理的 Safelist 规则。
|
||||
* 基于 Safelist#relaxed() 的基础上:
|
||||
* 1. 扩展支持了 style 和 class 属性
|
||||
* 2. a 标签额外支持了 target 属性
|
||||
* 3. img 标签额外支持了 data 协议,便于支持 base64
|
||||
*
|
||||
* @return Safelist
|
||||
*/
|
||||
private Safelist buildSafelist() {
|
||||
// 使用 jsoup 提供的默认的
|
||||
Safelist relaxedSafelist = Safelist.relaxed();
|
||||
// 富文本编辑时一些样式是使用 style 来进行实现的
|
||||
// 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
|
||||
// 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
|
||||
relaxedSafelist.addAttributes(":all", "style", "class");
|
||||
// 保留 a 标签的 target 属性
|
||||
relaxedSafelist.addAttributes("a", "target");
|
||||
// 支持img 为base64
|
||||
relaxedSafelist.addProtocols("img", "src", "data");
|
||||
|
||||
// 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
|
||||
// WHITELIST.preserveRelativeLinks(false);
|
||||
|
||||
// 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
|
||||
// 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
|
||||
// WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
|
||||
// WHITELIST.removeProtocols("img", "src", "http", "https");
|
||||
|
||||
return relaxedSafelist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String clean(String html) {
|
||||
return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.framework.web.core.clean;
|
||||
|
||||
/**
|
||||
* 对 html 文本中的有 Xss 风险的数据进行清理
|
||||
*/
|
||||
public interface XssCleaner {
|
||||
|
||||
/**
|
||||
* 清理有 Xss 风险的文本
|
||||
*
|
||||
* @param html 原 html
|
||||
* @return 清理后的 html
|
||||
*/
|
||||
String clean(String html);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.framework.web.config.XssProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@ -13,7 +14,7 @@ import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Xss 过滤器
|
||||
*
|
||||
* <p>
|
||||
* 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
|
||||
*
|
||||
* @author 芋道源码
|
||||
@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
|
||||
*/
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new XssRequestWrapper(request), response);
|
||||
filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,21 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HTMLFilter;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
|
||||
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;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -24,113 +13,79 @@ import java.util.Map;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
/**
|
||||
* 基于线程级别的 HTMLFilter 对象,因为它线程非安全
|
||||
*/
|
||||
private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
|
||||
HTMLFilter htmlFilter = new HTMLFilter();
|
||||
// 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符
|
||||
ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
|
||||
return htmlFilter;
|
||||
});
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
|
||||
super(request);
|
||||
this.xssCleaner = xssCleaner;
|
||||
}
|
||||
|
||||
private static String filterXss(String content) {
|
||||
if (StrUtil.isEmpty(content)) {
|
||||
return content;
|
||||
// ============================ parameter ============================
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> map = new LinkedHashMap<>();
|
||||
Map<String, String[]> parameters = super.getParameterMap();
|
||||
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
map.put(entry.getKey(), values);
|
||||
}
|
||||
return HTML_FILTER.get().filter(content);
|
||||
}
|
||||
|
||||
// ========== IO 流相关 ==========
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 如果非 json 请求,不进行 Xss 处理
|
||||
if (!ServletUtils.isJsonRequest(this)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// 读取内容,并过滤
|
||||
String content = IoUtil.readUtf8(super.getInputStream());
|
||||
content = filterXss(content);
|
||||
final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return newInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Param 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return filterXss(value);
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (ArrayUtil.isEmpty(values)) {
|
||||
return values;
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
// 过滤处理
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
int count = values.length;
|
||||
String[] encodedValues = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
encodedValues[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
return values;
|
||||
return encodedValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> valueMap = super.getParameterMap();
|
||||
if (CollUtil.isEmpty(valueMap)) {
|
||||
return valueMap;
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
// 过滤处理
|
||||
for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ========== Header 相关 ==========
|
||||
// ============================ attribute ============================
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
Object value = super.getAttribute(name);
|
||||
if (value instanceof String) {
|
||||
xssCleaner.clean((String) value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ============================ header ============================
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return filterXss(value);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ============================ queryString ============================
|
||||
@Override
|
||||
public String getQueryString() {
|
||||
String value = super.getQueryString();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.framework.web.core.json;
|
||||
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS 过滤 jackson 反序列化器。
|
||||
* 在反序列化的过程中,会对字符串进行 XSS 过滤。
|
||||
*
|
||||
* @author Hccake
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class XssStringJsonDeserializer extends StringDeserializer {
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.hasToken(JsonToken.VALUE_STRING)) {
|
||||
return xssCleaner.clean(p.getText());
|
||||
}
|
||||
JsonToken t = p.currentToken();
|
||||
// [databind#381]
|
||||
if (t == JsonToken.START_ARRAY) {
|
||||
return _deserializeFromArray(p, ctxt);
|
||||
}
|
||||
// need to gracefully handle byte[] data, as base64
|
||||
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
|
||||
Object ob = p.getEmbeddedObject();
|
||||
if (ob == null) {
|
||||
return null;
|
||||
}
|
||||
if (ob instanceof byte[]) {
|
||||
return ctxt.getBase64Variant().encode((byte[]) ob, false);
|
||||
}
|
||||
// otherwise, try conversion using toString()...
|
||||
return ob.toString();
|
||||
}
|
||||
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
|
||||
if (t == JsonToken.START_OBJECT) {
|
||||
return ctxt.extractScalarFromObject(p, this, _valueClass);
|
||||
}
|
||||
|
||||
if (t.isScalarValue()) {
|
||||
String text = p.getValueAsString();
|
||||
return xssCleaner.clean(text);
|
||||
}
|
||||
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
|
||||
}
|
||||
}
|
||||
|
37
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal file
37
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?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>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-websocket</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>WebSocket</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,14 @@
|
||||
package cn.iocoder.yudao.framework.websocket.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||
public class WebSocketHandlerConfig {
|
||||
@Bean
|
||||
public HandshakeInterceptor handshakeInterceptor() {
|
||||
return new UserHandshakeInterceptor();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.framework.websocket.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* WebSocket 配置项
|
||||
*
|
||||
* @author xingyu4j
|
||||
*/
|
||||
@ConfigurationProperties("yudao.websocket")
|
||||
@Data
|
||||
@Validated
|
||||
public class WebSocketProperties {
|
||||
|
||||
/**
|
||||
* 路径
|
||||
*/
|
||||
private String path = "";
|
||||
/**
|
||||
* 默认最多允许同时在线用户数
|
||||
*/
|
||||
private int maxOnlineCount = 0;
|
||||
/**
|
||||
* 是否保存session
|
||||
*/
|
||||
private boolean sessionMap = true;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.framework.websocket.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WebSocket 自动配置
|
||||
*
|
||||
* @author xingyu4j
|
||||
*/
|
||||
@AutoConfiguration
|
||||
// 允许使用 yudao.websocket.enable=false 禁用websocket
|
||||
@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||
public class YudaoWebSocketAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
|
||||
WebSocketHandler webSocketHandler,
|
||||
WebSocketProperties webSocketProperties) {
|
||||
|
||||
return registry -> registry
|
||||
.addHandler(webSocketHandler, webSocketProperties.getPath())
|
||||
.addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class UserHandshakeInterceptor implements HandshakeInterceptor {
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebSocketKeyDefine {
|
||||
public static final String LOGIN_USER ="LOGIN_USER";
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class WebSocketMessageDO {
|
||||
/**
|
||||
* 接收消息的seesion
|
||||
*/
|
||||
private List<Object> seesionKeyList;
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
private String msgText;
|
||||
|
||||
public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
|
||||
return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class WebSocketSessionHandler {
|
||||
private WebSocketSessionHandler() {
|
||||
}
|
||||
|
||||
private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
public static void addSession(Object sessionKey, WebSocketSession session) {
|
||||
SESSION_MAP.put(sessionKey.toString(), session);
|
||||
}
|
||||
|
||||
public static void removeSession(Object sessionKey) {
|
||||
SESSION_MAP.remove(sessionKey.toString());
|
||||
}
|
||||
|
||||
public static WebSocketSession getSession(Object sessionKey) {
|
||||
return SESSION_MAP.get(sessionKey.toString());
|
||||
}
|
||||
|
||||
public static Collection<WebSocketSession> getSessions() {
|
||||
return SESSION_MAP.values();
|
||||
}
|
||||
|
||||
public static Set<String> getSessionKeys() {
|
||||
return SESSION_MAP.keySet();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
public class WebSocketUtils {
|
||||
public static boolean sendMessage(WebSocketSession seesion, String message) {
|
||||
if (seesion == null) {
|
||||
log.error("seesion 不存在");
|
||||
return false;
|
||||
}
|
||||
if (seesion.isOpen()) {
|
||||
try {
|
||||
seesion.sendMessage(new TextMessage(message));
|
||||
} catch (IOException e) {
|
||||
log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean sendMessage(Object sessionKey, String message) {
|
||||
WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
|
||||
return sendMessage(session, message);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
|
||||
|
||||
public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
|
||||
public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket 连接时执行的动作
|
||||
* @param session websocket session 对象
|
||||
* @throws Exception 异常对象
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
|
||||
Object sessionKey = sessionKeyGen(session);
|
||||
WebSocketSessionHandler.addSession(sessionKey, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket 关闭连接时执行的动作
|
||||
* @param session websocket session 对象
|
||||
* @param closeStatus 关闭状态对象
|
||||
* @throws Exception 异常对象
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||
Object sessionKey = sessionKeyGen(session);
|
||||
WebSocketSessionHandler.removeSession(sessionKey);
|
||||
}
|
||||
|
||||
public Object sessionKeyGen(WebSocketSession webSocketSession) {
|
||||
|
||||
Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
|
||||
|
||||
if (obj instanceof LoginUser) {
|
||||
LoginUser loginUser = (LoginUser) obj;
|
||||
// userId 作为唯一区分
|
||||
return String.valueOf(loginUser.getId());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.framework.websocket;
|
@ -0,0 +1 @@
|
||||
cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration
|
Reference in New Issue
Block a user