diff --git a/pom.xml b/pom.xml index 691e8bf..e416929 100644 --- a/pom.xml +++ b/pom.xml @@ -20,10 +20,10 @@ org.springframework.cloud spring-cloud-starter-gateway - + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + org.projectlombok lombok @@ -34,12 +34,42 @@ org.springframework spring-webmvc + + + org.springframework.boot + spring-boot-starter-data-redis + + + io.lettuce + lettuce-core + + + + + redis.clients + jedis + com.squareup.okhttp3 okhttp 4.11.0 + + io.jsonwebtoken + jjwt + 0.9.0 + + + org.apache.commons + commons-lang3 + + + + com.alibaba + fastjson + + diff --git a/src/main/java/com/yxt/ss/gateway/api/AuthFilter.java b/src/main/java/com/yxt/ss/gateway/api/AuthFilter.java new file mode 100644 index 0000000..2432bda --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/AuthFilter.java @@ -0,0 +1,223 @@ +package com.yxt.ss.gateway.api; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yxt.ss.gateway.api.authutils.*; +import com.yxt.ss.gateway.api.utils.AppKeyConfig; +import com.yxt.ss.gateway.api.utils.ResultBean; +import com.yxt.ss.gateway.api.utils.SignatureUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +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; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * @author dimengzhe + * @description 网关鉴权 + */ +@Component +public class AuthFilter implements GlobalFilter, Ordered { + + private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); + + @Autowired + private IgnoreWhiteProperties ignoreWhite; + @Autowired + private AppKeyConfig appKeyConfig; + + public String getSecret(String appKey) { + return appKeyConfig.getKeys().get(appKey); + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + String url = exchange.getRequest().getURI().getPath(); + // 1. URI 白名单过滤:如果请求路径在白名单中,直接放行 + if (isWhitelisted(url)) { + return chain.filter(exchange); + } + + // 2. 提取请求参数并进行验证 + return extractParameters(exchange) + .flatMap(parameters -> { + // 校验请求参数 + ResultBean validationResult = validate(parameters); + // 校验失败,返回 401 Unauthorized 错误响应 + if (!validationResult.getSuccess()) { + return setUnauthorizedResponse(exchange, validationResult.getMsg()); + } + + // 3. 如果需要,可以从参数中提取信息并添加到请求头 + ServerHttpRequest mutableReq = exchange.getRequest().mutate() + .header(CacheConstants._APP, parameters.get("_app")) + .build(); + ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build(); + // 4. 继续执行后续过滤器链 + return chain.filter(mutableExchange); + }); + } + + // 提取请求参数方法:根据请求类型 (GET 或 POST) 提取参数 + private Mono> extractParameters(ServerWebExchange exchange) { + HttpMethod method = exchange.getRequest().getMethod(); + // 1. 如果是 GET 请求,从 URL 查询参数中提取 + if (method == HttpMethod.GET) { + return Mono.just(exchange.getRequest().getQueryParams().toSingleValueMap()); + } + // 如果是 POST、PUT 或 DELETE 请求,从请求体中提取参数 + if (exchange.getRequest().getMethod() == HttpMethod.POST || + exchange.getRequest().getMethod() == HttpMethod.PUT || + exchange.getRequest().getMethod() == HttpMethod.DELETE) { + + String contentType = exchange.getRequest().getHeaders().getContentType().toString(); + if (contentType.contains("application/json")) { + return exchange.getRequest().getBody() + .collectList() + .map(dataBuffers -> { + // 将 DataBuffer 转换为字符串 + StringBuilder bodyBuilder = new StringBuilder(); + dataBuffers.forEach(dataBuffer -> { + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()); + bodyBuilder.append(charBuffer); + }); + + // 解析 JSON 请求体 + try { + return parseJsonBody(bodyBuilder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + return new HashMap(); // 如果解析失败,返回空 Map + }); + } else if (contentType.contains("application/x-www-form-urlencoded")) { + // 处理 x-www-form-urlencoded 格式 + return exchange.getRequest().getBody() + .collectList() + .map(dataBuffers -> { + StringBuilder bodyBuilder = new StringBuilder(); + dataBuffers.forEach(dataBuffer -> { + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()); + bodyBuilder.append(charBuffer); + }); + try { + return parseFormBody(bodyBuilder.toString()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return new HashMap(); + }); + } + } + + // 其他情况返回空 Map + return Mono.just(new HashMap<>()); + } + + //解析 JSON 请求体的方法 + private Map parseJsonBody(String body) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(body, Map.class); + } + + // 辅助方法:解析 form-data 格式字符串 + private Map parseFormBody(String body) throws UnsupportedEncodingException { + Map parameters = new HashMap<>(); + // 按照 & 分割每一对键值 + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + // 解码键和值 + parameters.put(URLDecoder.decode(keyValue[0], "UTF-8"), + URLDecoder.decode(keyValue[1], "UTF-8")); + } + } + return parameters; + } + + // 设置未授权响应方法 + private Mono setUnauthorizedResponse(ServerWebExchange exchange, String message) { + // 设置响应状态码为 401 Unauthorized + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + // 设置响应内容类型为 JSON + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + // 创建返回的错误信息 + DataBuffer buffer = exchange.getResponse().bufferFactory() + .wrap(("{\"error\":\"" + message + "\"}").getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } + + private boolean isWhitelisted(String url) { + return StringUtils.matches(url, ignoreWhite.getWhites()) || + StringUtils.matchesTwo(url, ignoreWhite.getWhitesTwo()); + } + + @Override + public int getOrder() { + return 0; + } + + ResultBean validate(Map data) { + ResultBean rb = ResultBean.fireFail(); + // 解析参数 + String app = data.get("_app"); + if (org.springframework.util.StringUtils.isEmpty(app)) { + return rb.setMsg("_app参数缺失或无效"); + } + // 获取 secret 值 + String secret = getSecret(app); + if (org.springframework.util.StringUtils.isEmpty(secret)) { + return rb.setMsg("_app参数不正确"); + } + // 校验时间戳 _t 参数 + String timestampStr = data.get("_t"); + if (org.springframework.util.StringUtils.isEmpty(timestampStr)) { + return rb.setMsg("_t参数缺失"); + } + long timestamp; + try { + timestamp = Long.parseLong(timestampStr); + } catch (NumberFormatException e) { + return rb.setMsg("_t参数格式不正确"); + } + // 时间范围校验 + long currentTimestamp = Instant.now().getEpochSecond(); + long timeDifference = Math.abs(currentTimestamp - timestamp); + final int ALLOWED_TIME_DIFF = 300; // 最大允许时间偏差(秒) + if (timeDifference > ALLOWED_TIME_DIFF) { + return rb.setMsg("时间已超过5分钟,时间失效"); + } + + // 签名验证 + ResultBean resultBean = SignatureUtil.validateSignature(data, secret); + if (!resultBean.getSuccess()) { + return rb.setMsg(resultBean.getMsg()); + } + + return rb.success(); + + + } + +} diff --git a/src/main/java/com/yxt/ss/gateway/api/GatewayApiApplication.java b/src/main/java/com/yxt/ss/gateway/api/GatewayApiApplication.java index 86b8252..d9f1dd9 100644 --- a/src/main/java/com/yxt/ss/gateway/api/GatewayApiApplication.java +++ b/src/main/java/com/yxt/ss/gateway/api/GatewayApiApplication.java @@ -10,11 +10,12 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient; * @author: dimengzhe * @date: 2024/12/6 **/ -//@EnableDiscoveryClient +@EnableDiscoveryClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class GatewayApiApplication { public static void main(String[] args) { SpringApplication.run(GatewayApiApplication.class, args); } + } diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/CacheConstants.java b/src/main/java/com/yxt/ss/gateway/api/authutils/CacheConstants.java new file mode 100644 index 0000000..6cfa56c --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/CacheConstants.java @@ -0,0 +1,25 @@ +package com.yxt.ss.gateway.api.authutils; + +/** + * @author dimengzhe + * @description 缓存的key 常量 + */ + +public class CacheConstants { + + /** + * 令牌自定义标识 + */ + public static final String HEADER = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 用户名字段 + */ + public static final String DETAILS_USERNAME = "userName"; + public static final String _APP = "_app"; +} diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/CharsetKit.java b/src/main/java/com/yxt/ss/gateway/api/authutils/CharsetKit.java new file mode 100644 index 0000000..a9855c7 --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/CharsetKit.java @@ -0,0 +1,90 @@ +package com.yxt.ss.gateway.api.authutils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author dimengzhe + * @description + */ + +public class CharsetKit { + + /** + * ISO-8859-1 + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** + * UTF-8 + */ + public static final String UTF_8 = "UTF-8"; + /** + * GBK + */ + public static final String GBK = "GBK"; + + /** + * ISO-8859-1 + */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** + * UTF-8 + */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** + * GBK + */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) { + if (null == srcCharset) { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() { + return Charset.defaultCharset().name(); + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/Convert.java b/src/main/java/com/yxt/ss/gateway/api/authutils/Convert.java new file mode 100644 index 0000000..79a86fc --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/Convert.java @@ -0,0 +1,849 @@ +package com.yxt.ss.gateway.api.authutils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; + +/** + * @author dimengzhe + * @description + */ + +public class Convert { + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof String) { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof Character) { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Byte) { + return (Byte) value; + } + if (value instanceof Number) { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Byte.parseByte(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Short) { + return (Short) value; + } + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Short.parseShort(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Number) { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return NumberFormat.getInstance().parse(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Integer.parseInt(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Integer[]{}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Long[]{}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Float) { + return (Float) value; + } + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Float.parseFloat(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) { + case "true": + return true; + case "false": + return false; + case "yes": + return true; + case "ok": + return true; + case "no": + return false; + case "1": + return true; + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) { + if (value == null) { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Enum.valueOf(clazz, valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (value instanceof Long) { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigInteger(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Long) { + return new BigDecimal((Long) value); + } + if (value instanceof Double) { + return new BigDecimal((Double) value); + } + if (value instanceof Integer) { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigDecimal(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) { + if (null == obj) { + return null; + } + + if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof byte[] || obj instanceof Byte[]) { + return str((Byte[]) obj, charset); + } else if (obj instanceof ByteBuffer) { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) { + if (data == null) { + return null; + } + + if (null == charset) { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) { + if (data == null) { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) { + if (null == charset) { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) { + char c[] = input.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') { + c[i] = '\u3000'; + } else if (c[i] < '\177') { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) { + char c[] = text.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') { + c[i] = ' '; + } else if (c[i] > '\uFF00' && c[i] < '\uFF5F') { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) { + String[] fraction = {"角", "分"}; + String[] digit = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; + String[][] unit = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}}; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) { + s += (digit[(int) (Math.floor(n * 10 * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/IgnoreWhiteProperties.java b/src/main/java/com/yxt/ss/gateway/api/authutils/IgnoreWhiteProperties.java new file mode 100644 index 0000000..d92ec22 --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/IgnoreWhiteProperties.java @@ -0,0 +1,41 @@ +package com.yxt.ss.gateway.api.authutils; + +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 dimengzhe + * @description 放行白名单配置 + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "ignore") +public class IgnoreWhiteProperties { + + /** + * 放行白名单配置,网关不校验此处的白名单 + */ + private List whites = new ArrayList<>(); + + public List getWhites() { + return whites; + } + + public void setWhites(List whites) { + this.whites = whites; + } + + private List whitesTwo = new ArrayList<>(); + + public List getWhitesTwo() { + return whitesTwo; + } + + public void setWhitesTwo(List whitesTwo) { + this.whitesTwo = whitesTwo; + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/StrFormatter.java b/src/main/java/com/yxt/ss/gateway/api/authutils/StrFormatter.java new file mode 100644 index 0000000..f734801 --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/StrFormatter.java @@ -0,0 +1,75 @@ +package com.yxt.ss.gateway.api.authutils; + +/** + * @author dimengzhe + * @description + */ + +public class StrFormatter { + + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) { + if (handledPosition == 0) { + return strPattern; + } else { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } else { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } else { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/authutils/StringUtils.java b/src/main/java/com/yxt/ss/gateway/api/authutils/StringUtils.java new file mode 100644 index 0000000..933c795 --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/authutils/StringUtils.java @@ -0,0 +1,525 @@ +package com.yxt.ss.gateway.api.authutils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author dimengzhe + * @description + */ + +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + /** + * 空字符串 + */ + private static final String NULLSTR = ""; + + /** + * 下划线 + */ + private static final char SEPARATOR = '_'; + + /** + * 星号 + */ + private static final String START = "*"; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + * * @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULLSTR; + } + + if (start < 0) { + start = str.length() + start; + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULLSTR; + } + + if (end < 0) { + end = str.length() + end; + } + if (start < 0) { + start = str.length() + start; + } + + if (end > str.length()) { + end = str.length(); + } + + if (start > end) { + return NULLSTR; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) { + if (isEmpty(params) || isEmpty(template)) { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 下划线转驼峰命名 + */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) { + if (str != null && strs != null) { + for (String s : strs) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matchesTwo(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String testStr : strs) { + if (matchesTwo(str, testStr)) { + return true; + } + } + return false; + } + + public static boolean matches(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String testStr : strs) { + if (matches(str, testStr)) { + return true; + } + } + return false; + } + + /** + * 查找指定字符串是否匹配指定字符串数组中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, String... strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String testStr : strs) { + if (matches(str, testStr)) { + return true; + } + } + return false; + } + + public static boolean matches(String str, String pattern) { + if (isEmpty(pattern) || isEmpty(str)) { + return false; + } + + pattern = pattern.replaceAll("\\s*", ""); // 替换空格 + int beginOffset = 0; // pattern截取开始位置 + int formerStarOffset = -1; // 前星号的偏移位置 + int latterStarOffset = -1; // 后星号的偏移位置 + + String remainingURI = str; + String prefixPattern = ""; + String suffixPattern = ""; + + boolean result = false; + do { + formerStarOffset = indexOf(pattern, START, beginOffset); + prefixPattern = substring(pattern, beginOffset, formerStarOffset > -1 ? formerStarOffset : pattern.length()); + + // 匹配前缀Pattern + result = remainingURI.equals(prefixPattern); + // 已经没有星号,直接返回 + if (formerStarOffset == -1) { + return result; + } + + // 匹配失败,直接返回 + if (!result) { + return false; + } + if (!isEmpty(prefixPattern)) { + remainingURI = substringAfter(str, prefixPattern); + } + + // 匹配后缀Pattern + latterStarOffset = indexOf(pattern, START, formerStarOffset + 1); + suffixPattern = substring(pattern, formerStarOffset + 1, latterStarOffset > -1 ? latterStarOffset : pattern.length()); + + result = remainingURI.equals(suffixPattern); + // 匹配失败,直接返回 + if (!result) { + return false; + } + if (!isEmpty(suffixPattern)) { + remainingURI = substringAfter(str, suffixPattern); + } + + // 移动指针 + beginOffset = latterStarOffset + 1; + + } + while (!isEmpty(suffixPattern) && !isEmpty(remainingURI)); + + return true; + } + + /** + * 查找指定字符串是否匹配 + * + * @param str 指定字符串 + * @param pattern 需要检查的字符串 + * @return 是否匹配 + */ + public static boolean matchesTwo(String str, String pattern) { + if (isEmpty(pattern) || isEmpty(str)) { + return false; + } + + pattern = pattern.replaceAll("\\s*", ""); // 替换空格 + int beginOffset = 0; // pattern截取开始位置 + int formerStarOffset = -1; // 前星号的偏移位置 + int latterStarOffset = -1; // 后星号的偏移位置 + + String remainingURI = str; + String prefixPattern = ""; + String suffixPattern = ""; + + boolean result = false; + do { + formerStarOffset = indexOf(pattern, START, beginOffset); + prefixPattern = substring(pattern, beginOffset, formerStarOffset > -1 ? formerStarOffset : pattern.length()); + + // 匹配前缀Pattern + result = remainingURI.contains(prefixPattern); + // 已经没有星号,直接返回 + if (formerStarOffset == -1) { + return result; + } + + // 匹配失败,直接返回 + if (!result) { + return false; + } + if (!isEmpty(prefixPattern)) { + remainingURI = substringAfter(str, prefixPattern); + } + + // 匹配后缀Pattern + latterStarOffset = indexOf(pattern, START, formerStarOffset + 1); + suffixPattern = substring(pattern, formerStarOffset + 1, latterStarOffset > -1 ? latterStarOffset : pattern.length()); + + result = remainingURI.contains(suffixPattern); + // 匹配失败,直接返回 + if (!result) { + return false; + } + if (!isEmpty(suffixPattern)) { + remainingURI = substringAfter(str, suffixPattern); + } + + // 移动指针 + beginOffset = latterStarOffset + 1; + + } + while (!isEmpty(suffixPattern) && !isEmpty(remainingURI)); + + return true; + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) { + return (T) obj; + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/rest/ApiTestRest.java b/src/main/java/com/yxt/ss/gateway/api/rest/ApiTestRest.java index e69de29..1c965d7 100644 --- a/src/main/java/com/yxt/ss/gateway/api/rest/ApiTestRest.java +++ b/src/main/java/com/yxt/ss/gateway/api/rest/ApiTestRest.java @@ -0,0 +1,128 @@ +package com.yxt.ss.gateway.api.rest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yxt.ss.gateway.api.authutils.StringUtils; +import com.yxt.ss.gateway.api.service.ClientService; +import com.yxt.ss.gateway.api.utils.AppKeyConfig; +import com.yxt.ss.gateway.api.utils.ResultBean; +import com.yxt.ss.gateway.api.utils.SignatureQuery; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; + +/** + * @description: + * @author: dimengzhe + * @date: 2024/12/10 + **/ +@RestController +@RequestMapping("/ApiTestRest") +public class ApiTestRest { + + @Autowired + private ClientService clientService; + + + //appkey + static final String APPKEY = "appKey4"; + static final String SECRET = "secret"; + + //开发端,生成签名并调用服务器端验证签名、appKey等值。 + @PostMapping("/getSign") + ResultBean getSign(SignatureQuery query) { + ResultBean rb = ResultBean.fireFail(); + try { + Map formData = query.getParameters(); + //使用treeMap排序 + Map tree = new TreeMap<>(formData); + tree.put("_app", APPKEY); + tree.put("_t", String.valueOf(System.currentTimeMillis() / 1000)); + tree.put("_s", ""); + // 生成签名 + String sign = clientService.generateSignature(tree, SECRET); + //添加签名值map + tree.put("_sign", sign); + //发起请求 + ResultBean resultBean = client(tree); + if (!resultBean.getSuccess()) { + return rb.setMsg(resultBean.getMsg()); + } + //通过验证继续调用接口 + + + return rb.success(); + } catch (UnsupportedEncodingException e) { + return rb.setMsg("Unsupported encoding: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + return rb.setMsg("Algorithm not found: " + e.getMessage()); + } + } + + //发起请求验证签名等 + public ResultBean client(Map data) { + ResultBean rb = ResultBean.fireFail(); + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + + try { + // 构建URL + String endPoint = "http://127.0.0.1:9999"; + String path = "/signature/validate"; + + // 创建FormData + FormBody.Builder formBuilder = new FormBody.Builder(); + for (Map.Entry entry : data.entrySet()) { + formBuilder.add(entry.getKey(), entry.getValue()); + } + RequestBody formBody = formBuilder.build(); + + // 构建POST请求 + String url = endPoint + path; + System.out.println("Request URL: " + url); + System.out.println("Request Data: " + data); + + Request request = new Request.Builder() + .url(url) + .post(formBody) + .build(); + + // 发送请求 + try (Response response = client.newCall(request).execute()) { + String responseBody = response.body().string(); + // 使用 Jackson 解析 JSON 响应 + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(responseBody); + String success = jsonNode.path("success").asText(); + String msg = jsonNode.path("msg").asText(); + if ("false".equals(success)) { + return rb.setMsg(msg); + } + if (response.isSuccessful()) { + System.out.println("Response: " + response.body().string()); + } else { + System.err.println("Request failed: " + response.message()); + } + + + } + } catch (IOException e) { + System.err.println("Network error: " + e.getMessage()); + } catch (Exception e) { + System.err.println("Unexpected error: " + e.getMessage()); + } + return rb.success(); + } +} diff --git a/src/main/java/com/yxt/ss/gateway/api/rest/ClientRest.java b/src/main/java/com/yxt/ss/gateway/api/rest/ClientRest.java index a184e1e..3fd30e1 100644 --- a/src/main/java/com/yxt/ss/gateway/api/rest/ClientRest.java +++ b/src/main/java/com/yxt/ss/gateway/api/rest/ClientRest.java @@ -1,3 +1,4 @@ +/* package com.yxt.ss.gateway.api.rest; import com.fasterxml.jackson.databind.JsonNode; @@ -18,11 +19,13 @@ import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeUnit; +*/ /** * @description: * @author: dimengzhe * @date: 2024/12/10 - **/ + **//* + @RestController @RequestMapping("/client") public class ClientRest { @@ -123,3 +126,4 @@ public class ClientRest { return rb.success(); } } +*/ diff --git a/src/main/java/com/yxt/ss/gateway/api/service/ClientService.java b/src/main/java/com/yxt/ss/gateway/api/service/ClientService.java index 2157c21..f2e3959 100644 --- a/src/main/java/com/yxt/ss/gateway/api/service/ClientService.java +++ b/src/main/java/com/yxt/ss/gateway/api/service/ClientService.java @@ -9,6 +9,7 @@ import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; +import java.util.TreeMap; /** * @description: @@ -31,13 +32,12 @@ public class ClientService { * @param parameters 请求参数 * @return 签名 */ - public String generateSignature(Map parameters) throws UnsupportedEncodingException, NoSuchAlgorithmException { - + public String generateSignature(Map parameters, String secret) throws UnsupportedEncodingException, NoSuchAlgorithmException { + //1.对参数进行排序 + Map treeMap = new TreeMap<>(parameters); // 2. 拼接参数字符串 String content = joinParameters(parameters); - // 3. 将密钥加在参数字符串的前后 - String secret = getSecret(parameters.get("_app")); content = secret + content + secret; // 4. 计算签名 (MD5) diff --git a/src/main/java/com/yxt/ss/gateway/api/service/Signature.java b/src/main/java/com/yxt/ss/gateway/api/service/Signature.java index 82bc134..779ef76 100644 --- a/src/main/java/com/yxt/ss/gateway/api/service/Signature.java +++ b/src/main/java/com/yxt/ss/gateway/api/service/Signature.java @@ -34,62 +34,41 @@ public class Signature { @PostMapping("/validate") ResultBean validate(Map data) { ResultBean rb = ResultBean.fireFail(); - Map parameters = data; - //1、解析参数,校验_app是否正确,_t是否在5分钟内。 - - //检验_app 参数,1参数中是否存在2该参数是否为空3_app参数值是否在数据库中存在 - if (!parameters.containsKey("_app") || - parameters.get("_app") == null || - parameters.get("_app").trim().isEmpty()) { + // 解析参数:_app是否存在、_app参数值是否在数据库中存在 + String app = data.get("_app"); + if (StringUtils.isEmpty(app)) { return rb.setMsg("_app参数缺失或无效"); } - //2、根据_app参数获取对应的secret值。后续考虑从数据库中获取。 - String secret = getSecret(parameters.get("_app")); + // 获取 secret 值:根据_app参数获取对应的secret值。后续考虑从数据库中获取。 + String secret = getSecret(app); if (StringUtils.isEmpty(secret)) { return rb.setMsg("_app参数不正确"); } - if (parameters.containsKey("_t") || - parameters.get("_t") == null || - parameters.get("_t").trim().isEmpty()) { + // 校验时间戳 _t 参数 + String timestampStr = data.get("_t"); + if (StringUtils.isEmpty(timestampStr)) { return rb.setMsg("_t参数缺失"); } - - String _t = parameters.get("_t"); - // 获取当前的秒级时间戳 + long timestamp; + try { + timestamp = Long.parseLong(timestampStr); + } catch (NumberFormatException e) { + return rb.setMsg("_t参数格式不正确"); + } + // 时间范围校验 long currentTimestamp = Instant.now().getEpochSecond(); - // 将字符串转换为 long 类型 - long timestamp = Long.parseLong(_t); - // 计算时间差,允许最大偏差 5 分钟(300秒) long timeDifference = Math.abs(currentTimestamp - timestamp); - - if (timeDifference > 300) { + final int ALLOWED_TIME_DIFF = 300; // 最大允许时间偏差(秒) + if (timeDifference > ALLOWED_TIME_DIFF) { return rb.setMsg("时间已超过5分钟,时间失效"); } - String _sign = parameters.get("_sign"); - //3、检验签名,成功则继续调用接口,失败返回失败信息。 - parameters.remove("_sign"); - try { - // 3.1. 重新生成签名 - String calculatedSignature = SignatureUtil.generateSignature(parameters, secret); - // 3.2. 使用固定时间比较方式验证签名 - boolean valid = MessageDigest.isEqual( - calculatedSignature.getBytes("UTF-8"), - _sign.getBytes("UTF-8") - ); - if (!valid) { - return rb.setMsg("签名不正确"); - } - - } catch (UnsupportedEncodingException e) { - return rb.setMsg("Encoding error: " + e.getMessage()); - } catch (NoSuchAlgorithmException e) { - return rb.setMsg("Algorithm error: " + e.getMessage()); - } catch (Exception e) { - return rb.setMsg("Unexpected error: " + e.getMessage()); + // 签名验证 + ResultBean resultBean = SignatureUtil.validateSignature(data, secret); + if (!resultBean.getSuccess()) { + return rb.setMsg(resultBean.getMsg()); } - return rb.success(); diff --git a/src/main/java/com/yxt/ss/gateway/api/utils/AppKeyConfig.java b/src/main/java/com/yxt/ss/gateway/api/utils/AppKeyConfig.java index bb8a6b5..19370db 100644 --- a/src/main/java/com/yxt/ss/gateway/api/utils/AppKeyConfig.java +++ b/src/main/java/com/yxt/ss/gateway/api/utils/AppKeyConfig.java @@ -3,6 +3,8 @@ package com.yxt.ss.gateway.api.utils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; +import java.util.HashMap; import java.util.Map; /** @@ -11,7 +13,7 @@ import java.util.Map; * @date: 2024/12/9 **/ @Component -@ConfigurationProperties(prefix = "app.keys") +@ConfigurationProperties(prefix = "appkey") public class AppKeyConfig { private Map keys; diff --git a/src/main/java/com/yxt/ss/gateway/api/utils/SignatureUtil.java b/src/main/java/com/yxt/ss/gateway/api/utils/SignatureUtil.java index 11a55d0..9c33289 100644 --- a/src/main/java/com/yxt/ss/gateway/api/utils/SignatureUtil.java +++ b/src/main/java/com/yxt/ss/gateway/api/utils/SignatureUtil.java @@ -1,5 +1,8 @@ package com.yxt.ss.gateway.api.utils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.MessageDigest; @@ -68,4 +71,41 @@ public class SignatureUtil { } return sb.toString(); } + + /** + * 验证签名是否正确 + * + * @param parameters 请求参数 + * @param secret 密钥 + * @return 是否验证通过 + */ + public static ResultBean validateSignature(Map parameters, String secret) { + ResultBean rb = ResultBean.fireFail(); + boolean valid = false; + //原签名 + String _sign = parameters.get("_sign"); + //3、检验签名,成功则继续调用接口,失败返回失败信息。 + parameters.remove("_sign"); + try { + // 3.1. 重新生成签名 + String calculatedSignature = SignatureUtil.generateSignature(parameters, secret); + + // 3.2. 使用固定时间比较方式验证签名 + valid = MessageDigest.isEqual( + calculatedSignature.getBytes("UTF-8"), + _sign.getBytes("UTF-8") + ); + if (!valid) { + return rb.setMsg("签名不正确"); + } + + } catch (UnsupportedEncodingException e) { + return rb.setMsg("Encoding error: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + return rb.setMsg("Algorithm error: " + e.getMessage()); + } catch (Exception e) { + return rb.setMsg("Unexpected error: " + e.getMessage()); + } + return rb.success().setData(valid); + } } diff --git a/src/main/java/com/yxt/ss/gateway/api/utils/WebFluxLoggingConfig.java b/src/main/java/com/yxt/ss/gateway/api/utils/WebFluxLoggingConfig.java new file mode 100644 index 0000000..dacef81 --- /dev/null +++ b/src/main/java/com/yxt/ss/gateway/api/utils/WebFluxLoggingConfig.java @@ -0,0 +1,29 @@ +package com.yxt.ss.gateway.api.utils; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.server.WebFilter; + +/** + * @description: + * @author: dimengzhe + * @date: 2024/12/11 + **/ +@Configuration +public class WebFluxLoggingConfig { + + @Bean + public WebFilter loggingFilter() { + return (exchange, chain) -> { + // 打印请求信息 + System.out.println("Request: " + exchange.getRequest().getURI()); + + // 继续处理请求 + return chain.filter(exchange) + .doOnTerminate(() -> { + // 打印响应信息 + System.out.println("Response: " + exchange.getResponse().getStatusCode()); + }); + }; + } +} diff --git a/src/main/resources/application-devv.yml b/src/main/resources/application-devv.yml index f79e9c6..b1f2978 100644 --- a/src/main/resources/application-devv.yml +++ b/src/main/resources/application-devv.yml @@ -1,4 +1,4 @@ -app: +appkey: keys: appKey1: secret1 appKey2: secret2 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a08d670..5141f5e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,10 +5,25 @@ spring: name: ss-gateway-api profiles: active: devv + cloud: + gateway: + httpclient: + response-timeout: 10s + connect-timeout: 30 + routes: + - id: ss-gateway-api + predicates: + - Path= /ssa/** + uri: lb://ss-gateway-api + filters: + - StripPrefix=1 + +ignore: + whites: + + whitesTwo: #包含所有 logging: level: - org: - springframework: - context: DEBUG + org.springframework.web.filter.RequestLoggingFilter: DEBUG