mirror of
https://gitee.com/dromara/RuoYi-Cloud-Plus.git
synced 2026-04-26 17:49:36 +08:00
update 修改包名为org.dromara
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package org.dromara.gateway;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
||||
|
||||
/**
|
||||
* 网关启动程序
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
|
||||
public class RuoYiGatewayApplication {
|
||||
public static void main(String[] args) {
|
||||
// 标记 sentinel 类型为 网关
|
||||
System.setProperty("csp.sentinel.app.type", "1");
|
||||
SpringApplication application = new SpringApplication(RuoYiGatewayApplication.class);
|
||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||
application.run(args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ 网关启动成功 ლ(´ڡ`ლ)゙ ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.dromara.gateway.captcha;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.math.Calculator;
|
||||
import cn.hutool.core.util.CharUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* 无符号计算生成器
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class UnsignedMathGenerator implements CodeGenerator {
|
||||
|
||||
private static final long serialVersionUID = -5514819971774091076L;
|
||||
|
||||
private static final String OPERATORS = "+-*";
|
||||
|
||||
/**
|
||||
* 参与计算数字最大长度
|
||||
*/
|
||||
private final int numberLength;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public UnsignedMathGenerator() {
|
||||
this(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param numberLength 参与计算最大数字位数
|
||||
*/
|
||||
public UnsignedMathGenerator(int numberLength) {
|
||||
this.numberLength = numberLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate() {
|
||||
final int limit = getLimit();
|
||||
int a = RandomUtil.randomInt(limit);
|
||||
int b = RandomUtil.randomInt(limit);
|
||||
String max = Integer.toString(Math.max(a,b));
|
||||
String min = Integer.toString(Math.min(a,b));
|
||||
max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
|
||||
min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
|
||||
|
||||
return max + RandomUtil.randomChar(OPERATORS) + min + '=';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String code, String userInputCode) {
|
||||
int result;
|
||||
try {
|
||||
result = Integer.parseInt(userInputCode);
|
||||
} catch (NumberFormatException e) {
|
||||
// 用户输入非数字
|
||||
return false;
|
||||
}
|
||||
|
||||
final int calculateResult = (int) Calculator.conversion(code);
|
||||
return result == calculateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码长度
|
||||
*
|
||||
* @return 验证码长度
|
||||
*/
|
||||
public int getLength() {
|
||||
return this.numberLength * 2 + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据长度获取参与计算数字最大值
|
||||
*
|
||||
* @return 最大值
|
||||
*/
|
||||
private int getLimit() {
|
||||
return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.dromara.gateway.config;
|
||||
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.CircleCaptcha;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.captcha.ShearCaptcha;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaConfig {
|
||||
|
||||
private static final int WIDTH = 160;
|
||||
private static final int HEIGHT = 60;
|
||||
private static final Color BACKGROUND = Color.PINK;
|
||||
private static final Font FONT = new Font("Arial", Font.BOLD, 48);
|
||||
|
||||
/**
|
||||
* 圆圈干扰验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public CircleCaptcha circleCaptcha() {
|
||||
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线段干扰的验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public LineCaptcha lineCaptcha() {
|
||||
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扭曲干扰验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public ShearCaptcha shearCaptcha() {
|
||||
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.dromara.gateway.config;
|
||||
|
||||
import org.dromara.gateway.handler.SentinelFallbackHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
/**
|
||||
* 网关限流配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
public class GatewayConfig {
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
|
||||
return new SentinelFallbackHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.dromara.gateway.config;
|
||||
|
||||
import org.dromara.gateway.handler.ValidateCodeHandler;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
|
||||
/**
|
||||
* 路由配置信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
public class RouterFunctionConfiguration {
|
||||
@Autowired
|
||||
private ValidateCodeHandler validateCodeHandler;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Bean
|
||||
public RouterFunction routerFunction() {
|
||||
return RouterFunctions.route(
|
||||
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
|
||||
validateCodeHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.gateway.config.properties;
|
||||
|
||||
import org.dromara.gateway.enums.CaptchaCategory;
|
||||
import org.dromara.gateway.enums.CaptchaType;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "security.captcha")
|
||||
public class CaptchaProperties {
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
private CaptchaType type;
|
||||
|
||||
/**
|
||||
* 验证码类别
|
||||
*/
|
||||
private CaptchaCategory category;
|
||||
|
||||
/**
|
||||
* 数字验证码位数
|
||||
*/
|
||||
private Integer numberLength;
|
||||
|
||||
/**
|
||||
* 字符验证码长度
|
||||
*/
|
||||
private Integer charLength;
|
||||
|
||||
/**
|
||||
* 验证码开关
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.dromara.gateway.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 自定义gateway参数配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "spring.cloud.gateway")
|
||||
public class CustomGatewayProperties {
|
||||
|
||||
/**
|
||||
* 请求日志
|
||||
*/
|
||||
private Boolean requestLog;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.dromara.gateway.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 放行白名单配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Configuration
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "security.ignore")
|
||||
public class IgnoreWhiteProperties {
|
||||
/**
|
||||
* 放行白名单配置,网关不校验此处的白名单
|
||||
*/
|
||||
private List<String> whites = new ArrayList<>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.dromara.gateway.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* XSS跨站脚本配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "security.xss")
|
||||
public class XssProperties {
|
||||
/**
|
||||
* Xss开关
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 排除路径
|
||||
*/
|
||||
private List<String> excludeUrls = new ArrayList<>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.dromara.gateway.enums;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CircleCaptcha;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.captcha.ShearCaptcha;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码类别
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CaptchaCategory {
|
||||
|
||||
/**
|
||||
* 线段干扰
|
||||
*/
|
||||
LINE(LineCaptcha.class),
|
||||
|
||||
/**
|
||||
* 圆圈干扰
|
||||
*/
|
||||
CIRCLE(CircleCaptcha.class),
|
||||
|
||||
/**
|
||||
* 扭曲干扰
|
||||
*/
|
||||
SHEAR(ShearCaptcha.class);
|
||||
|
||||
private final Class<? extends AbstractCaptcha> clazz;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.dromara.gateway.enums;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import org.dromara.gateway.captcha.UnsignedMathGenerator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CaptchaType {
|
||||
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
MATH(UnsignedMathGenerator.class),
|
||||
|
||||
/**
|
||||
* 字符
|
||||
*/
|
||||
CHAR(RandomGenerator.class);
|
||||
|
||||
private final Class<? extends CodeGenerator> clazz;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.gateway.config.properties.IgnoreWhiteProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* [Sa-Token 权限认证] 拦截器
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Configuration
|
||||
public class AuthFilter {
|
||||
|
||||
/**
|
||||
* 注册 Sa-Token 全局过滤器
|
||||
*/
|
||||
@Bean
|
||||
public SaReactorFilter getSaReactorFilter(IgnoreWhiteProperties ignoreWhite) {
|
||||
return new SaReactorFilter()
|
||||
// 拦截地址
|
||||
.addInclude("/**")
|
||||
.addExclude("/favicon.ico", "/actuator/**")
|
||||
// 鉴权方法:每次访问进入
|
||||
.setAuth(obj -> {
|
||||
// 登录校验 -- 拦截所有路由
|
||||
SaRouter.match("/**")
|
||||
.notMatch(ignoreWhite.getWhites())
|
||||
.check(r -> {
|
||||
// 检查是否登录 是否有token
|
||||
StpUtil.checkLogin();
|
||||
|
||||
// 有效率影响 用于临时测试
|
||||
// if (log.isDebugEnabled()) {
|
||||
// log.debug("剩余有效时间: {}", StpUtil.getTokenTimeout());
|
||||
// log.debug("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
|
||||
// }
|
||||
});
|
||||
}).setError(e -> SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 黑名单过滤器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
return (exchange, chain) -> {
|
||||
|
||||
String url = exchange.getRequest().getURI().getPath();
|
||||
if (config.matchBlacklist(url)) {
|
||||
return WebFluxUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
|
||||
}
|
||||
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
|
||||
public BlackListUrlFilter() {
|
||||
super(Config.class);
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
private List<String> blacklistUrl;
|
||||
|
||||
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
|
||||
|
||||
public boolean matchBlacklist(String url) {
|
||||
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
|
||||
}
|
||||
|
||||
public List<String> getBlacklistUrl() {
|
||||
return blacklistUrl;
|
||||
}
|
||||
|
||||
public void setBlacklistUrl(List<String> blacklistUrl) {
|
||||
this.blacklistUrl = blacklistUrl;
|
||||
this.blacklistUrlPattern.clear();
|
||||
this.blacklistUrl.forEach(url -> {
|
||||
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.dev33.satoken.same.SaSameUtil;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 转发认证过滤器(内部服务外网隔离)
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Component
|
||||
public class ForwardAuthFilter implements GlobalFilter, Ordered {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ServerHttpRequest newRequest = exchange
|
||||
.getRequest()
|
||||
.mutate()
|
||||
// 为请求追加 Same-Token 参数
|
||||
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
|
||||
.build();
|
||||
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
|
||||
return chain.filter(newExchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return -100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 全局缓存获取body请求数据(解决流不能重复读取问题)
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Component
|
||||
public class GlobalCacheRequestFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
// 只缓存json类型请求
|
||||
if (!WebFluxUtils.isJsonRequest(exchange)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
|
||||
if (serverHttpRequest == exchange.getRequest()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.cors.reactive.CorsUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Component
|
||||
public class GlobalCorsFilter implements WebFilter, Ordered {
|
||||
|
||||
/**
|
||||
* 这里为支持的请求头,如果有自定义的header字段请自己添加
|
||||
*/
|
||||
private static final String ALLOWED_HEADERS = "X-Requested-With, Content-Language, Content-Type, Authorization, credential, X-XSRF-TOKEN, isToken, token, Admin-Token, App-Token";
|
||||
private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS,HEAD";
|
||||
private static final String ALLOWED_ORIGIN = "*";
|
||||
private static final String ALLOWED_EXPOSE = "*";
|
||||
private static final String MAX_AGE = "18000L";
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
if (CorsUtils.isCorsRequest(request)) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
|
||||
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
|
||||
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
|
||||
headers.add("Access-Control-Expose-Headers", ALLOWED_EXPOSE);
|
||||
headers.add("Access-Control-Max-Age", MAX_AGE);
|
||||
headers.add("Access-Control-Allow-Credentials", "true");
|
||||
if (request.getMethod() == HttpMethod.OPTIONS) {
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.i18n.SimpleLocaleContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 全局国际化处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GlobalI18nFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
String language = exchange.getRequest().getHeaders().getFirst("content-language");
|
||||
Locale locale = Locale.getDefault();
|
||||
if (language != null && language.length() > 0) {
|
||||
String[] split = language.split("_");
|
||||
locale = new Locale(split[0], split[1]);
|
||||
}
|
||||
LocaleContextHolder.setLocaleContext(new SimpleLocaleContext(locale), true);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import org.dromara.common.core.utils.JsonUtils;
|
||||
import org.dromara.gateway.config.properties.CustomGatewayProperties;
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 全局日志过滤器
|
||||
* <p>
|
||||
* 用于打印请求执行参数与响应时间等等
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GlobalLogFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Autowired
|
||||
private CustomGatewayProperties customGatewayProperties;
|
||||
|
||||
private static final String START_TIME = "startTime";
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
if (!customGatewayProperties.getRequestLog()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = WebFluxUtils.getOriginalRequestUrl(exchange);
|
||||
String url = request.getMethod().name() + " " + path;
|
||||
|
||||
// 打印请求参数
|
||||
if (WebFluxUtils.isJsonRequest(exchange)) {
|
||||
String jsonParam = WebFluxUtils.resolveBodyFromCacheRequest(exchange);
|
||||
log.debug("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
|
||||
} else {
|
||||
MultiValueMap<String, String> parameterMap = request.getQueryParams();
|
||||
if (MapUtil.isNotEmpty(parameterMap)) {
|
||||
String parameters = JsonUtils.toJsonString(parameterMap);
|
||||
log.debug("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
|
||||
} else {
|
||||
log.debug("[PLUS]开始请求 => URL[{}],无参数", url);
|
||||
}
|
||||
}
|
||||
|
||||
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
|
||||
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
|
||||
Long startTime = exchange.getAttribute(START_TIME);
|
||||
if (startTime != null) {
|
||||
long executeTime = (System.currentTimeMillis() - startTime);
|
||||
log.debug("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", url, executeTime);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import org.dromara.common.core.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.gateway.config.properties.CaptchaProperties;
|
||||
import org.dromara.gateway.service.ValidateCodeService;
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 验证码过滤器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> {
|
||||
private final static String[] VALIDATE_URL = new String[]{"/auth/login", "/auth/register", "/auth/smsLogin"};
|
||||
|
||||
@Autowired
|
||||
private ValidateCodeService validateCodeService;
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
private static final String CODE = "code";
|
||||
|
||||
private static final String UUID = "uuid";
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Object config) {
|
||||
return (exchange, chain) -> {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
|
||||
// 非登录/注册请求或验证码关闭,不处理
|
||||
if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
try {
|
||||
String rspStr = WebFluxUtils.resolveBodyFromCacheRequest(exchange);
|
||||
Dict obj = JsonUtils.parseMap(rspStr);
|
||||
validateCodeService.checkCaptcha(obj.getStr(CODE), obj.getStr(UUID));
|
||||
} catch (Exception e) {
|
||||
return WebFluxUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
|
||||
}
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.gateway.config.properties.XssProperties;
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.*;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 跨站脚本过滤器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
|
||||
public class XssFilter implements GlobalFilter, Ordered {
|
||||
// 跨站脚本的 xss 配置,nacos自行添加
|
||||
@Autowired
|
||||
private XssProperties xss;
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
// GET DELETE 不过滤
|
||||
HttpMethod method = request.getMethod();
|
||||
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
// 非json类型,不过滤
|
||||
if (!WebFluxUtils.isJsonRequest(exchange)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
// excludeUrls 不过滤
|
||||
String url = request.getURI().getPath();
|
||||
if (StringUtils.matches(url, xss.getExcludeUrls())) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
|
||||
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
|
||||
|
||||
}
|
||||
|
||||
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) {
|
||||
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
Flux<DataBuffer> body = super.getBody();
|
||||
return body.buffer().map(dataBuffers -> {
|
||||
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
|
||||
DataBuffer join = dataBufferFactory.join(dataBuffers);
|
||||
byte[] content = new byte[join.readableByteCount()];
|
||||
join.read(content);
|
||||
DataBufferUtils.release(join);
|
||||
String bodyStr = new String(content, StandardCharsets.UTF_8);
|
||||
// 防xss攻击过滤
|
||||
bodyStr = HtmlUtil.cleanHtmlTag(bodyStr);
|
||||
// 转成字节
|
||||
byte[] bytes = bodyStr.getBytes();
|
||||
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
|
||||
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
|
||||
buffer.write(bytes);
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.putAll(super.getHeaders());
|
||||
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
|
||||
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
|
||||
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
};
|
||||
return serverHttpRequestDecorator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.dromara.gateway.handler;
|
||||
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 网关统一异常处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(-1)
|
||||
@Configuration
|
||||
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
if (exchange.getResponse().isCommitted()) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
|
||||
String msg;
|
||||
|
||||
if (ex instanceof NotFoundException) {
|
||||
msg = "服务未找到";
|
||||
} else if (ex instanceof ResponseStatusException) {
|
||||
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
|
||||
msg = responseStatusException.getMessage();
|
||||
} else {
|
||||
msg = "内部服务器错误";
|
||||
}
|
||||
|
||||
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
|
||||
|
||||
return WebFluxUtils.webFluxResponseWriter(response, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.dromara.gateway.handler;
|
||||
|
||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebExceptionHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 自定义限流异常处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class SentinelFallbackHandler implements WebExceptionHandler {
|
||||
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
|
||||
return WebFluxUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍候再试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
if (exchange.getResponse().isCommitted()) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
if (!BlockException.isBlockException(ex)) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
|
||||
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.dromara.gateway.handler;
|
||||
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.CaptchaException;
|
||||
import org.dromara.gateway.service.ValidateCodeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码获取
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
public class ValidateCodeHandler implements HandlerFunction<ServerResponse> {
|
||||
@Autowired
|
||||
private ValidateCodeService validateCodeService;
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> handle(ServerRequest serverRequest) {
|
||||
R<Map<String, Object>> ajax;
|
||||
try {
|
||||
ajax = validateCodeService.createCaptcha();
|
||||
} catch (CaptchaException | IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.dromara.gateway.service;
|
||||
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.CaptchaException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface ValidateCodeService {
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
R<Map<String, Object>> createCaptcha() throws IOException, CaptchaException;
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*/
|
||||
void checkCaptcha(String key, String value) throws CaptchaException;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.dromara.gateway.service.impl;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.CaptchaException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.gateway.config.properties.CaptchaProperties;
|
||||
import org.dromara.gateway.enums.CaptchaType;
|
||||
import org.dromara.gateway.service.ValidateCodeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码实现处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class ValidateCodeServiceImpl implements ValidateCodeService {
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@Override
|
||||
public R<Map<String, Object>> createCaptcha() throws IOException, CaptchaException {
|
||||
Map<String, Object> ajax = new HashMap<>();
|
||||
boolean captchaEnabled = captchaProperties.getEnabled();
|
||||
ajax.put("captchaEnabled", captchaEnabled);
|
||||
if (!captchaEnabled) {
|
||||
return R.ok(ajax);
|
||||
}
|
||||
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
boolean isMath = CaptchaType.MATH == captchaType;
|
||||
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
String code = captcha.getCode();
|
||||
if (isMath) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
|
||||
code = exp.getValue(String.class);
|
||||
}
|
||||
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
ajax.put("uuid", uuid);
|
||||
ajax.put("img", captcha.getImageBase64());
|
||||
return R.ok(ajax);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*/
|
||||
@Override
|
||||
public void checkCaptcha(String code, String uuid) throws CaptchaException {
|
||||
if (StringUtils.isEmpty(code)) {
|
||||
throw new CaptchaException("user.jcaptcha.not.blank");
|
||||
}
|
||||
if (StringUtils.isEmpty(uuid)) {
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.dromara.gateway.utils;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.gateway.filter.GlobalCacheRequestFilter;
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
|
||||
|
||||
/**
|
||||
* WebFlux 工具类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class WebFluxUtils {
|
||||
|
||||
/**
|
||||
* 获取原请求路径
|
||||
*/
|
||||
public static String getOriginalRequestUrl(ServerWebExchange exchange) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
LinkedHashSet<URI> uris = exchange.getAttributeOrDefault(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, new LinkedHashSet<>());
|
||||
URI requestUri = uris.stream().findFirst().orElse(request.getURI());
|
||||
return UriComponentsBuilder.fromPath(requestUri.getRawPath()).build().toUriString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是Json请求
|
||||
*
|
||||
* @param exchange HTTP请求
|
||||
*/
|
||||
public static boolean isJsonRequest(ServerWebExchange exchange) {
|
||||
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
|
||||
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取request内的body
|
||||
*
|
||||
* 注意一个request只能读取一次 读取之后需要重新包装
|
||||
*/
|
||||
public static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
|
||||
// 获取请求体
|
||||
Flux<DataBuffer> body = serverHttpRequest.getBody();
|
||||
AtomicReference<String> bodyRef = new AtomicReference<>();
|
||||
body.subscribe(buffer -> {
|
||||
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
|
||||
DataBufferUtils.release(buffer);
|
||||
bodyRef.set(charBuffer.toString());
|
||||
});
|
||||
return bodyRef.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中读取request内的body
|
||||
*
|
||||
* 注意要求经过 {@link ServerWebExchangeUtils#cacheRequestBody(ServerWebExchange, Function)} 此方法创建缓存
|
||||
* 框架内已经使用 {@link GlobalCacheRequestFilter} 全局创建了body缓存
|
||||
*
|
||||
* @return body
|
||||
*/
|
||||
public static String resolveBodyFromCacheRequest(ServerWebExchange exchange) {
|
||||
Object obj = exchange.getAttributes().get(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
DataBuffer buffer = (DataBuffer) obj;
|
||||
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
|
||||
return charBuffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置webflux模型响应
|
||||
*
|
||||
* @param response ServerHttpResponse
|
||||
* @param value 响应内容
|
||||
* @return Mono<Void>
|
||||
*/
|
||||
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value) {
|
||||
return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置webflux模型响应
|
||||
*
|
||||
* @param response ServerHttpResponse
|
||||
* @param code 响应状态码
|
||||
* @param value 响应内容
|
||||
* @return Mono<Void>
|
||||
*/
|
||||
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value, int code) {
|
||||
return webFluxResponseWriter(response, HttpStatus.OK, value, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置webflux模型响应
|
||||
*
|
||||
* @param response ServerHttpResponse
|
||||
* @param status http状态码
|
||||
* @param code 响应状态码
|
||||
* @param value 响应内容
|
||||
* @return Mono<Void>
|
||||
*/
|
||||
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) {
|
||||
return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置webflux模型响应
|
||||
*
|
||||
* @param response ServerHttpResponse
|
||||
* @param contentType content-type
|
||||
* @param status http状态码
|
||||
* @param code 响应状态码
|
||||
* @param value 响应内容
|
||||
* @return Mono<Void>
|
||||
*/
|
||||
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) {
|
||||
response.setStatusCode(status);
|
||||
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
R<?> result = R.fail(code, value.toString());
|
||||
DataBuffer dataBuffer = response.bufferFactory().wrap(JsonUtils.toJsonString(result).getBytes());
|
||||
return response.writeWith(Mono.just(dataBuffer));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user