English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 8|回复: 0

基于Spring Boot的HTTP请求签名验证实现解析

[复制链接]
查看: 8|回复: 0

基于Spring Boot的HTTP请求签名验证实现解析

[复制链接]
查看: 8|回复: 0

249

主题

0

回帖

757

积分

高级会员

积分
757
CTwZVfsAII

249

主题

0

回帖

757

积分

高级会员

积分
757
2025-4-7 11:37:24 | 显示全部楼层 |阅读模式
概述

在分布式系统交互中,API接口的安全性至关重要。本文将深入解析基于Spring Boot实现的HTTP请求签名验证机制,该方案支持GET/POST等多种请求方式,提供时效性验证和数据完整性保障。以下是核心实现的技术要点解析。
<hr>功能特性


  • 多协议支持:完整覆盖GET、POST(JSON/Form-Data)等常见请求类型
  • 时效控制:5分钟有效期的请求时间窗口
  • 多重验证:公钥校验 + 签名验证 + 时间戳的三重防护机制
  • 安全过滤:自动排除签名参数参与验签计算
  • 编码兼容:自动处理URL特殊字符编码问题
核心实现解析

1. 主校验流程

public boolean verifySignature(HttpServletRequest request,                               HttpServletResponse response,                              GridAccountUser accountUser) {    // 获取公钥配置    StateGridAccount gridAccount = stateGridService        .getStateGridAccountById(accountUser.getAccountId());        // 请求类型路由    if(HttpMethod.POST.matches(request.getMethod())) {        // 处理JSON/Form-Data类型    } else if(HttpMethod.GET.matches(request.getMethod())) {        // 处理GET参数    }        // 统一返回校验结果}2. 关键技术点

2.1 时间戳验证

private Boolean verifyTimestamp(String timestampStr) {    long timestamp = Long.parseLong(timestampStr);    long currentTime = System.currentTimeMillis();    return Math.abs(currentTime - timestamp) <= 300_000; // 5分钟有效期}

  • 防止重放攻击
  • 要求客户端服务端时间同步
  • 误差窗口可配置化建议
2.2 请求体处理

JSON请求处理:
String requestStr = getRequestBody(request);JSONObject jsonObject = JSON.parseObject(requestStr);map.remove("sign"); // 过滤签名参数Form-Data处理:
Map<String, Object> formData = WebUtils.getParametersStartingWith(request, "");formData.remove("sign");GET请求处理:
Map<String, String[]> queryParams = request.getParameterMap();signature = signature.replaceAll(" ", "+"); // 处理URL编码2.3 签名验证

SignUtil.verifySignature(    uri,                  // 请求路径    data.toString(),      // 过滤后的参数    Long.parseLong(timestamp),     signature,    publicKey);优化建议


  • 参数序列化优化

    • 当前同时使用Fastjson和Hutool的JSONUtil,建议统一JSON处理库
    • 推荐使用Jackson进行标准化处理

  • 异常处理增强
try {    // 验签逻辑} catch (NumberFormatException e) {    log.error("时间戳格式异常: {}", timestampStr);    throw new InvalidTimestampException();} catch (SignatureException e) {    log.warn("签名验证失败: {}", e.getMessage());}

  • 性能优化

    • 添加公钥缓存机制(RedisCache)
    • 采用连接池管理数据库查询

  • 安全增强

    • 添加重放攻击计数器
    • 支持动态时间窗口配置
    • 增加黑名单IP机制

<hr>注意事项


  • 时间同步:确保NTP服务的时间同步
  • 密钥管理:建议采用密钥轮换机制
  • 空参数处理:需要明确空字符串和null的处理策略
  • 编码一致性:统一使用UTF-8字符集
  • 日志脱敏:敏感参数需要做日志过滤
<hr>总结

该实现方案为API接口安全提供了基础保障,在实际生产环境中可根据业务需求扩展以下功能:

  • 增加流量限频控制
  • 实现双向证书验证
  • 支持多种哈希算法
  • 添加OpenAPI规范支持
  • 集成API管理平台
通过持续优化验签流程和完善监控机制,可以有效构建安全可靠的API网关体系。
<hr>附完整代码
生产秘钥私钥工具类
package com.aspire.datasynchron.common.utils;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.NoSuchAlgorithmException;import java.util.Base64;import java.util.Map;public class KeyGenExample {    private static final String ALGORITHM = "RSA";    public static Map<String, String> generateKey() throws NoSuchAlgorithmException {        // 1. 选择算法(RSA/EC/DSA)        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);        keyGen.initialize(2048); // 密钥长度        // 2. 生成密钥对        KeyPair keyPair = keyGen.generateKeyPair();        // 3. 获取公私钥(Base64 编码打印)        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();        String privateKey = Base64.getEncoder().encodeToString(privateKeyBytes);        String publicKey = Base64.getEncoder().encodeToString(publicKeyBytes);        Map<String, String> map = ApiKeyGenerator.generateApiCredentials();        map.put("private_key", privateKey);        map.put("public_key", publicKey);        return map;    }    public static void main(String[] args) {        try {            Map<String, String> stringStringMap = generateKey();            System.out.println(stringStringMap);        } catch (NoSuchAlgorithmException e) {            e.printStackTrace();        }    }}生成签名和验签方法
package com.aspire.datasynchron.common.utils;import cn.hutool.core.bean.BeanUtil;import cn.hutool.json.JSONUtil;import org.apache.commons.lang3.ArrayUtils;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.security.*;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.Base64;import java.util.HashMap;import java.util.Map;/** * <b>System:</b>NCC<br/> * <b>Title:</b>SignUtil.java<br/> * <b>Description:</b>添加描述信息<br/> * <b>@author: </b>zhouxiaomin_a<br/> * <b>@date:</b>2018/6/21 17:00<br/> * <b>@version:</b> 1.0.0.0<br/> * <b>Copyright (c) 2017 ASPire Tech.</b> */public class SignUtil {    private static final String CHARSET = "UTF-8";    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";    private static final String KEY_ALGORITHM = "RSA";    // 通过传入私钥、数据和时间戳生成签名的方法    public static String generateSignature(String url, String data, long timestamp, String privateKeyStr) throws Exception {        // 将数据和时间戳合并成一个字符串        String dataWithTimestamp = url + "|" + data + "|" + timestamp;        // 通过传入的私钥字符串生成私钥        PrivateKey privateKey = getPrivateKeyFromString(privateKeyStr);        // 创建签名对象,使用 SHA256withRSA 算法        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);        signature.initSign(privateKey);        // 更新数据        signature.update(dataWithTimestamp.getBytes(StandardCharsets.UTF_8));        // 生成签名        byte[] signedData = signature.sign();        // 将签名转换为 Base64 编码的字符串        return Base64.getEncoder().encodeToString(signedData);    }    // 通过传入私钥字符串,生成私钥对象的方法    public static PrivateKey getPrivateKeyFromString(String privateKeyStr) throws Exception {        // 将 Base64 编码的私钥字符串解码为字节数组        byte[] decodedKey = Base64.getDecoder().decode(privateKeyStr);        // 使用 KeyFactory 生成私钥对象        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);        return keyFactory.generatePrivate(keySpec);    }    // 验证签名的方法    public static boolean verifySignature(String url, String data, Long timestamp, String signatureStr, String publicKeyStr) throws Exception {        if (StringUtils.isEmpty(url) || StringUtils.isEmpty(data) || null == timestamp                || StringUtils.isEmpty(signatureStr) || StringUtils.isEmpty(publicKeyStr)) {            return false;        }        // 将数据和时间戳合并成一个字符串        String dataWithTimestamp = url + "|" + data + "|" + timestamp;        // 通过传入的公钥字符串生成公钥        PublicKey publicKey = getPublicKeyFromString(publicKeyStr);        // 创建签名对象,使用 SHA256withRSA 算法        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);        signature.initVerify(publicKey);        // 更新数据        signature.update(dataWithTimestamp.getBytes(StandardCharsets.UTF_8));        // 将 Base64 编码的签名字符串转换为字节数组        byte[] signatureBytes = Base64.getDecoder().decode(signatureStr);        // 验证签名        return signature.verify(signatureBytes);    }    // 通过传入公钥字符串,生成公钥对象的方法    public static PublicKey getPublicKeyFromString(String publicKeyStr) throws Exception {        // 将 Base64 编码的公钥字符串解码为字节数组        byte[] decodedKey = Base64.getDecoder().decode(publicKeyStr);        // 使用 KeyFactory 生成公钥对象        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);        return keyFactory.generatePublic(keySpec);    }    // 测试代码    public static void main(String[] args) throws Exception {        // 获取公钥和私钥的 Base64 编码字符串        String privateKeyStr = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+3ZTH/VCeWaesY4muy7fOvqLLTX07QpfJVeG1KZKSCjeciih3VRQfMn1PsxL0elMUFIfRKGOtjS7o0+V3UqAwaJCozyxEMPw3OGKQ0bb2dCId/TCnjWkFpSOj1UKct/LdOT/CZ07Bu+8DCynjzHSxwATe2DYWZp8fHf9FAuYjPuXhkeFEkf+/TG5ObyDE4VqzHiTb4XmjkJCuhk98H5CLOCrWb+NiyHpbp/fPF865QzEG1n9/pyFKyMDsBgSxmx0iXDheR6ue/wotxnDvJgGqaG00kD88r/oU2M3xo581a9sF+v6fzNCldtF+MHJJVd4OhFcscahBySEhIUY9TZo/AgMBAAECggEAIh6TP7MBa+VEC5WZocUqHQvIJ0a5adQMNUIkgJGncXLhIRszg62SVMdeTlaJP2n0mwTWiKXLN9Wiup1SimObXjv7DCpI1AHbvHVYbWIH7oOxK6I8xd8KFKfCOMHhUAm0ISbgRnzYP9q8LdObj+zXOYVFeZ62AIgkzte6b9hGUqtWv4yGL3VTiki5TvzHpRPJoeEYHEk+zGhdCn9dr7GY+3mm3EfnNL69s78jWAdkib8fHQsgunOB4NiA4Gnn58Aphzw/SYio6mS5ieRXz4h9ng77UI4agkuU6QhYMdt+s+7YTs4Z6+iXnE3f0zHZJT/UA1GItcxnYuznl2X+vKWS3QKBgQD3L/lUfh5R2N40rB16cE4W75betT59TIjEW+/g5VY4WhZZXEmIi7HWcxoAYXeTp1/Ls2KrKWyAs5ys89lTmasJh2M8afS04mUTCtOO2/bsnUXc0g7P8M913X2fWME+uIVLXBbG2+0xfXGgLCtsgZro5IkDqrHO09SX8s02jOAbJQKBgQDFq5HkGsxJZzS7anFoaBnXxUK590bkIt4eIyOs5Z7AldppWIaT5uCR9TAq3N29rvxBPBwiRKIsghleTHtkvccImDECu6ztVubn1oaudvNnHPbUyi5rfxg+dAKUoLxX3nzniSYupyN94QDq+q/OPiTBopyAXxRmNfn8x3YGvOi0kwKBgQCv3D/E7x1fGa2tR66JR5EnHDn4JHZa6rJ7EPWuyTr4SI+R7+iY7toNOkKLdsx+DhxHbk6Ke6QoRKD5I1vA8JkQ5HOjrbZdYpyKWa99+dzJJnNn0UKcijTvJC+VyK1jlB+xJ8lEnX85MIhAbmxOfD7b5ovcQfrSrT6ZBDMf1kYyyQKBgHQq32NRyGr/B0N5S8rTGxTubceCphvezfCiMA4lKAYAS0qL5xM2pRXCJZubD4mxM7hWziXpdfF4R9ZeVkofKcBISM1VZExbPPpU3fPcHjGkGP93Do7IM4RIg1e7mtR9AaTEujbCrR4GRJbT2sv3Q3y0xwq+Veu3nwHKaveMv6mXAoGBAM0trI9b/k7oHS8z9FfETTeiXxac3puumRcGt3HmCISPmXxc4RWMVbMPUWSzm2KFdWtKEYMMxvSZcEG90rxM/Mh3Uhdj8Pdz2iwAnCghpwzAtp60N2yKxJyq80gRFywTlNp70VxDxYCFZ9Dugjzg7NT2JbhOdSjoZDP9FXg+konq";        String publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvt2Ux/1QnlmnrGOJrsu3zr6iy019O0KXyVXhtSmSkgo3nIood1UUHzJ9T7MS9HpTFBSH0ShjrY0u6NPld1KgMGiQqM8sRDD8NzhikNG29nQiHf0wp41pBaUjo9VCnLfy3Tk/wmdOwbvvAwsp48x0scAE3tg2FmafHx3/RQLmIz7l4ZHhRJH/v0xuTm8gxOFasx4k2+F5o5CQroZPfB+Qizgq1m/jYsh6W6f3zxfOuUMxBtZ/f6chSsjA7AYEsZsdIlw4Xkernv8KLcZw7yYBqmhtNJA/PK/6FNjN8aOfNWvbBfr+n8zQpXbRfjBySVXeDoRXLHGoQckhISFGPU2aPwIDAQAB";        String url = "/api/rest/demo/queryAlarmInfo";        // 要签名的数据和时间戳        String json = "{\"SPECIALTY\":8,\"NETWORKTYPE\":801,\"VENDORNAME\":\"华为\",\"NETYPE\":\"801\",\"ALARMTITLE\":\"\",\"ORG_EVENT_ID\":\"\"}";        Map map = JSONUtil.toBean(json, Map.class);        String data = map.toString();        System.out.println(data);        long timestamp = System.currentTimeMillis();  // 当前时间戳        System.out.println(timestamp);        // 生成签名        String signature = generateSignature(url, data, timestamp, privateKeyStr);        System.out.println("Generated Signature: " + signature);        // 验证签名        boolean isVerified = SignUtil.verifySignature(url, data, timestamp, signature, publicKeyStr);        System.out.println("Signature Verified: " + isVerified);        System.out.println("---------------------------------------------------------");        String url2 = "/api/rest/demo/getUser";        Map<String, Object> params = new HashMap<>();        params.put("idcard", "123456");        long timestamp1 = System.currentTimeMillis();  // 当前时间戳        System.out.println(timestamp1);        String data2 = params.toString();        System.out.println(data2);        // 生成签名        String signature1 = generateSignature(url2, data2, timestamp1, privateKeyStr);        System.out.println("Generated Signature1: " + signature1);        // 验证签名        boolean isVerified1 = SignUtil.verifySignature(url2, data2, timestamp1, signature1, publicKeyStr);        System.out.println("Signature isVerified1: " + isVerified1);        System.out.println("---------------------------------------------------------");        String url3 = "/api/rest/demo/getIdCard";        Map<String, Object> params1 = new HashMap<>();        params1.put("name", "李四");        params1.put("age", "27");        String data3 = params1.toString();        System.out.println(data3);        long timestamp2 = System.currentTimeMillis();  // 当前时间戳        System.out.println(timestamp2);        // 生成签名        String signature2 = generateSignature(url3, data3, timestamp2, privateKeyStr);        System.out.println("Generated signature2: " + signature2);        // 验证签名        boolean isVerified2 = SignUtil.verifySignature(url3, data3, timestamp2, signature2, publicKeyStr);        System.out.println("Signature isVerified2: " + isVerified2);    }}自定义拦截器
package com.aspire.datasynchron.framework.interceptor;import com.aspire.datasynchron.common.core.domain.model.GridAccountUser;import com.aspire.datasynchron.common.utils.StringUtils;import com.aspire.datasynchron.framework.web.service.TokenService;import com.aspire.datasynchron.framework.web.service.VerifySignatureService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.nio.charset.StandardCharsets;@Slf4j@Componentpublic class SignatureInterceptor implements HandlerInterceptor {    private final TokenService tokenService;    private final VerifySignatureService verifySignatureService;    @Autowired    public SignatureInterceptor(TokenService tokenService, VerifySignatureService verifySignatureService) {        this.tokenService = tokenService;        this.verifySignatureService = verifySignatureService;    }    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        String clientType = request.getHeader("clientType");        if (StringUtils.isNotEmpty(clientType)) {            GridAccountUser accountUser = tokenService.getAccountUser(request);            // 自定义验签逻辑            boolean verified = verifySignatureService.verifySignature(request, response, accountUser);            if (!verified) {                log.error("验签失败");                response.setStatus(HttpStatus.FORBIDDEN.value());                response.setContentType(MediaType.APPLICATION_JSON_VALUE);                response.setCharacterEncoding(StandardCharsets.UTF_8.name());                // 构建错误响应                String errorMessage = "{\"error\": \"签名不合法\", \"message\": \"Signature is invalid.\"}";                response.getWriter().write(errorMessage);                return false;            }        }        return true;    }}验签实现类
package com.aspire.datasynchron.framework.web.service;import cn.hutool.json.JSONUtil;import com.alibaba.fastjson2.JSON;import com.alibaba.fastjson2.JSONObject;import com.aspire.datasynchron.common.core.domain.model.GridAccountUser;import com.aspire.datasynchron.common.core.redis.RedisCache;import com.aspire.datasynchron.common.utils.SignUtil;import com.aspire.datasynchron.stateGrid.domain.StateGridAccount;import com.aspire.datasynchron.stateGrid.domain.vo.StateGridAccountDetailsVO;import com.aspire.datasynchron.stateGrid.service.IStateGridAccountService;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpMethod;import org.springframework.http.MediaType;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;import org.springframework.web.util.ContentCachingRequestWrapper;import org.springframework.web.util.ContentCachingResponseWrapper;import org.springframework.web.util.WebUtils;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.*;@Slf4j@Servicepublic class VerifySignatureService {    @Autowired    private IStateGridAccountService stateGridService;    public boolean verifySignature(HttpServletRequest request, HttpServletResponse response, GridAccountUser accountUser) {        //查询公钥        StateGridAccount gridAccount = stateGridService.getStateGridAccountById(accountUser.getAccountId());        if (gridAccount == null || StringUtils.isEmpty(gridAccount.getPublicKey())) {            log.error("账号[{}]公钥未配置", accountUser.getAccountId());            return false;        }        String publicKey = gridAccount.getPublicKey();        String uri = request.getRequestURI();        // 判断当前请求方式        if (HttpMethod.POST.name().equals(request.getMethod())) {            // 判断是否是 JSON 格式的请求            if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())) {                // 获取请求体的内容                String requestStr = getRequestBody(request);                log.info("请求体内容:{}", requestStr);                if (StringUtils.isEmpty(requestStr)) {                    return false;                }                JSONObject jsonObject = JSON.parseObject(requestStr);                Map map = JSONUtil.toBean(requestStr, Map.class);                map.remove("sign");                map.remove("timestamp");                String timestamp = jsonObject.getString("timestamp");                String signature = jsonObject.getString("sign");                // 验证时间戳                Boolean verifed = verifTimestamp(timestamp);                if (!verifed) {                    return false;                }                try {                    boolean isValid = SignUtil.verifySignature(uri, map.toString(), Long.parseLong(timestamp), signature, publicKey);                    return isValid;                } catch (Exception e) {                    log.error(e.getMessage());                    return false;                }            } else if (request.getContentType() != null && request.getContentType().startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) {                // 处理 form 表单数据格式                Map<String, Object> formData = WebUtils.getParametersStartingWith(request, "");                log.info("表单数据:{}", formData);                String timestamp = (String) formData.get("timestamp");                String signature = (String) formData.get("sign");                formData.remove("sign");                formData.remove("timestamp");                // 验证时间戳                Boolean verifed = verifTimestamp(timestamp);                if (!verifed) {                    return false;                }                try {                    boolean isValid = SignUtil.verifySignature(uri, formData.toString(), Long.parseLong(timestamp), signature, publicKey);                    return isValid;                } catch (Exception e) {                    throw new RuntimeException(e);                }            }        } else if (HttpMethod.GET.name().equals(request.getMethod())) {            // GET 请求的处理,获取查询参数进行验签            Map<String, String[]> queryParams = request.getParameterMap();            if (queryParams == null) {                return false;            }            Map<String, String> filteredParams = new HashMap<>();            queryParams.forEach((key, values) -> {                // 过滤掉 "sign" 和 "timestamp" 参数                if (!"sign".equals(key) && !"timestamp".equals(key)) {                    filteredParams.put(key, values.length > 0 ? values[0] : "");                }            });            String timestamp = request.getParameter("timestamp");            String signature = request.getParameter("sign");            if (timestamp == null || signature == null) {                return false;            }            // 验证时间戳            Boolean verifed = verifTimestamp(timestamp);            if (!verifed) {                return false;            }            //+ 符号通常会被转换为一个空格 (%20),这是因为在 URL 编码中,+ 符号表示空格            signature = signature.replaceAll(" ", "+");            boolean isValid = false;            try {                isValid = SignUtil.verifySignature(uri, filteredParams.toString(), Long.parseLong(timestamp), signature, publicKey);                if (!isValid) {                    return false;  // 返回 false 表示验签失败                }                return true;  // 返回 true 表示验签成功            } catch (Exception e) {                log.error(e.getMessage());                return false;            }        }        return false;  // 默认返回 false,如果请求方法不支持    }    /**     * @throws     * @MethodName verifTimestamp     * @author zhouzihao     * @param: timestampStr     * @DateTime 2025年3月25日, 0025 下午 06:13     * @return: java.lang.Boolean     * @description:检查时间戳:确保客户端和服务端时间同步,误差在5分钟内。     */    private Boolean verifTimestamp(String timestampStr) {        long timestamp = Long.parseLong(timestampStr);        long currentTime = System.currentTimeMillis();        if (Math.abs(currentTime - timestamp) > 300000) { // 5分钟            log.error("验签失败:时间戳已过期");            return false;        }        return true;    }    /**     * 读取请求体的内容     */    private static String getRequestBody(HttpServletRequest request) {        InputStream in = null;        StringBuffer sb = null;        try {            in = request.getInputStream();            BufferedReader br = new BufferedReader(new InputStreamReader(in,                    Charset.forName("UTF-8")));            sb = new StringBuffer("");            String temp;            while ((temp = br.readLine()) != null) {                sb.append(temp);            }            if (in != null) {                in.close();            }            if (br != null) {                br.close();            }        } catch (IOException e) {            throw new RuntimeException(e);        }        return sb.toString();    }}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

249

主题

0

回帖

757

积分

高级会员

积分
757

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-5-1 14:34 , Processed in 1.468028 second(s), 20 queries .

Powered by 智能设备

©2025