SpringCloud Gateway API接口安全设计(加密 、签名、安全)

SpringCloud Gateway API接口安全设计(加密 、签名、安全)

防止数据抓包窃取

风险简述

简述:当用户登录时,恶意攻击者可以用抓包工具可以拿到用户提交的表单信息,可以获取用户的账号密码,进而可以恶意访问网站。

RSA 非对称加密

RSA简介

RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。

假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。

1983年麻省理工学院在美国为RSA算法申请了专利。这个专利2000年9月21日失效。由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。

RSA应用过程

非对称算法的在应用的过程如下:

  • 接收方生成公钥和私钥,公钥公开,私钥保留;
  • 发送方将要发送的消息采用公钥加密,得到密文,然后将密文发送给接收方;
  • 接收方收到密文后,用自己的私钥进行解密,获得明文。

RSA工具类

package com.demo.utils;import java.util.Map;@Slf4jpublic class RSAUtils { public static final String PUBLIC_KEY = “public_key”; public static final String PRIVATE_KEY = “private_key”; public static Map generateRasKey() { Map rs = new HashMap(); try { // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 KeyPairGenerator keyPairGen = null; keyPairGen = KeyPairGenerator.getInstance(“RSA”); keyPairGen.initialize(1024, new SecureRandom()); // 生成一个密钥对,保存在keyPair中 KeyPair keyPair = keyPairGen.generateKeyPair(); // 得到私钥 公钥 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded())); // 得到私钥字符串 String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded()))); // 将公钥和私钥保存到Map rs.put(PUBLIC_KEY, publicKeyString); rs.put(PRIVATE_KEY, privateKeyString); } catch (Exception e) { log.error(“RsaUtils invoke genKeyPair failed.”, e); throw new RsaException(“RsaUtils invoke genKeyPair failed.”); } return rs; } public static String encrypt(String str, String publicKey) { try { //base64编码的公钥 byte[] decoded = Base64.decodeBase64(publicKey); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(“RSA”).generatePublic(new X509EncodedKeySpec(decoded)); //RSA加密 Cipher cipher = Cipher.getInstance(“RSA”); cipher.init(Cipher.ENCRYPT_MODE, pubKey); return Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { log.error(“RsaUtils invoke encrypt failed.”, e); throw new RsaException(“RsaUtils invoke encrypt failed.”); } } public static String decrypt(String str, String privateKey) { try { //64位解码加密后的字符串 byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8)); //base64编码的私钥 byte[] decoded = Base64.decodeBase64(privateKey); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(“RSA”).generatePrivate(new PKCS8EncodedKeySpec(decoded)); //RSA解密 Cipher cipher = Cipher.getInstance(“RSA”); cipher.init(Cipher.DECRYPT_MODE, priKey); return new String(cipher.doFinal(inputByte)); } catch (Exception e) { log.error(“RsaUtils invoke decrypt failed.”, e); throw new RsaException(“RsaUtils invoke decrypt failed.”); } }}

RsaException: 是自定义异常

@Getterpublic class RsaException extends RuntimeException { private final String message; public RsaException(String message) { this.message = message; }}

1.2.4 UT

package com.rosh;public class RsaTest { /** * 用测试生成的公钥,私钥赋值 */ private static final String PUBLIC_KEY = “MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB”; private static final String PRIVATE_KEY = “MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAIW1OUvrUiogZ359EtSEnQyOyoVcUmzAKiEnjmbnVa9vEM9E/eCWVbRYKGTvgPkkQ6kwNJajgLtF+gaUFE1buRDqpga6RhnmOBinOhPT6Cneif3p9BcTJAnKy/3GJM8h2ZJddVWPUcA4nDb1FvPEhUpRLPM9e8S1dFO0ILX7CQAlAgMBAAECgYBC4amtbiKFa/wY61tV7pfYRjzLhKi+OUlZmD3E/4Z+4KGZ7DrJ8qkgMtDR3HO5LAikQrare1HTW2d7juqw32ascu+uDObf4yrYNKin+ZDLUYvIDfLhThPxnZJwQ/trdtfxO3VM//XbwZacmwYbAsYW/3QPUXwwOPAgbC2oth8kqQJBANKLyXcdjZx4cwJVl7xNeC847su8y6bPpcBASsaQloCIPiNBIg1h76dpfEGIQBYWJWbBsxtHe/MhOmz7fNFDS2sCQQCiktYZR0dZNH4eNX329LoRuBiltpr9tf36rVOlKr1GSHkLYEHF2qtyXV2mdrY8ZWpvuo3qm1oSLaqmop2rN9avAkBHk85B+IIUF77BpGeZVJzvMOO9z8lMRHuNCE5jgvQnbinxwkrZUdovh+T+QlvHJnBApslFFOBGn51FP5oHamFRAkEAmwZmPsinkrrpoKjlqz6GyCrC5hKRDWoj/IyXfKKaxpCJTH3HeoIghvfdO8Vr1X/n1Q8SESt+4mLFngznSMQAZQJBAJx07bCFYbA2IocfFV5LTEYTIiUeKdue2NP2yWqZ/+tB5H7jNwQTJmX1mn0W/sZm4+nJM7SjfETpNZhH49+rV6U=”; /** * 生成公钥私钥 */ @Test public void generateRsaKey() { Map map = RSAUtils.generateRasKey(); System.out.println(“随机生成的公钥为:” + map.get(RSAUtils.PUBLIC_KEY)); System.out.println(“随机生成的私钥为:” + map.get(RSAUtils.PRIVATE_KEY)); } /** * 加密: Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo= */ @Test public void testEncrypt() { JSONObject jsonObject = new JSONObject(); jsonObject.put(“username”, “rosh”); jsonObject.put(“password”, “123456”); String str = jsonObject.toJSONString(); String encrypt = RSAUtils.encrypt(str, PUBLIC_KEY); System.out.println(encrypt); } @Test public void testDecrypt() { String decrypt = RSAUtils.decrypt(“Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo=”, PRIVATE_KEY); System.out.println(decrypt); }}

案例

SpringCloud Gateway + SpringBoot + Nacos+redis

前端登录代码

后端把公钥跟前端约定好:

登录页面

登录

账号: 密码:

前端查询代码

设定公钥、token,token是登录成功后返回的值

查询测试id:

GatewayFilterConfig

解密前端传来的参数并修改传参

package com.demo.gateway.config;public class GatewayFilterConfig implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1 如果是登录不校验Token String requestUrl = exchange.getRequest().getPath().value(); AntPathMatcher pathMatcher = new AntPathMatcher(); if (!pathMatcher.match(“/user/login”, requestUrl)) { String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN); Claims claim = TokenUtils.getClaim(token); if (StringUtils.isBlank(token) || claim == null) { return FilterUtils.invalidToken(exchange); } } //2 修改请求参数,并获取请求参数 try { updateRequestParam(exchange); } catch (Exception e) { return FilterUtils.invalidUrl(exchange); } //3 获取请求体,修改请求体 ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders()); Mono modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> { String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY); return Mono.just(encrypt); }); //创建BodyInserter修改请求体 BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); headers.remove(HttpHeaders.CONTENT_LENGTH); //创建CachedBodyOutputMessage并且把请求param加入 CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux getBody() { return outputMessage.getBody(); } }; return chain.filter(exchange.mutate().request(decorator).build()); })); } /** * 修改前端传的参数 */ private void updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException { ServerHttpRequest request = exchange.getRequest(); URI uri = request.getURI(); String query = uri.getQuery(); if (StringUtils.isNotBlank(query) && query.contains(“param”)) { String[] split = query.split(“=”); String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY); Field targetQuery = uri.getClass().getDeclaredField(“query”); targetQuery.setAccessible(true); targetQuery.set(uri, param); } } @Override public int getOrder() { return 80; }}

GateWay 统一异常

public abstract class AbstractExceptionHandler { protected JSONObject buildErrorMap(Throwable ex) { JSONObject json = new JSONObject(); if (ex instanceof RSAException || ex instanceof IllegalArgumentException) { json.put(“code”, HttpStatus.BAD_REQUEST.value()); if (StringUtils.isNotBlank(ex.getMessage())){ json.put(“msg”, ex.getMessage()); }else { json.put(“msg”, “无效的请求”); } } else { json.put(“code”, HttpStatus.BAD_REQUEST.value()); json.put(“msg”, “未知错误联系管理员”); } return json; }}@Configurationpublic class GatewayExceptionConfig { @Primary @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { GatewayExceptionHandler gatewayExceptionHandler = new GatewayExceptionHandler(); gatewayExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList)); gatewayExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); gatewayExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); return gatewayExceptionHandler; }}package com.demo.gateway.exception;import com.alibaba.fastjson.JSONObject;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.codec.HttpMessageReader;import org.springframework.http.codec.HttpMessageWriter;import org.springframework.util.Assert;import org.springframework.web.reactive.function.BodyInserters;import org.springframework.web.reactive.function.server.RequestPredicates;import org.springframework.web.reactive.function.server.RouterFunctions;import org.springframework.web.reactive.function.server.ServerRequest;import org.springframework.web.reactive.function.server.ServerResponse;import org.springframework.web.reactive.result.view.ViewResolver;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.Collections;import java.util.List;import java.util.Map;@Slf4jpublic class GatewayExceptionHandler extends AbstractExceptionHandler implements ErrorWebExceptionHandler { private List messageReaders = Collections.emptyList(); private List messageWriters = Collections.emptyList(); private List viewResolvers = Collections.emptyList(); private ThreadLocal exceptionHandlerResult = new ThreadLocal(); public void setMessageReaders(List messageReaders) { Assert.notNull(messageReaders, “‘messageReaders’ must not be null”); this.messageReaders = messageReaders; } public void setViewResolvers(List viewResolvers) { this.viewResolvers = viewResolvers; } public void setMessageWriters(List messageWriters) { Assert.notNull(messageWriters, “‘messageWriters’ must not be null”); this.messageWriters = messageWriters; } @Override public Mono handle(ServerWebExchange exchange, Throwable ex) { JSONObject errorInfo = super.buildErrorMap(ex); if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } exceptionHandlerResult.set(errorInfo); ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders); return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest) .switchIfEmpty(Mono.error(ex)) .flatMap(handler -> handler.handle(newRequest)) .flatMap(response -> write(exchange, response)); } protected Mono renderErrorResponse(ServerRequest request) { Map result = exceptionHandlerResult.get(); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } private Mono write(ServerWebExchange exchange, ServerResponse response) { exchange.getResponse().getHeaders().setContentType(response.headers().getContentType()); return response.writeTo(exchange, new ResponseContext()); } private class ResponseContext implements ServerResponse.Context { @Override public List messageWriters() { return GatewayExceptionHandler.this.messageWriters; } @Override public List viewResolvers() { return GatewayExceptionHandler.this.viewResolvers; } }}

JAVA业务代码

@RestController@RequestMapping(“/user”)public class UserController { @Autowired private UserService userService; @PostMapping(“/login”) public String login(@RequestBody UserForm userForm) { return userService.login(userForm); } @GetMapping(“/detail”) public JSONObject detail(@RequestParam(“id”) Long id) { return userService.detail(id); }}@Servicepublic class UserService { private static final String USERNAME = “admin”; private static final String PASSWORD = “123456”; private static final Long USER_ID = 1L; /** * 模拟 登录 username = admin, password =123456,user_id 1L 登录成功 返回token */ public String login(UserForm userForm) { String username = userForm.getUsername(); String password = userForm.getPassword(); if (USERNAME.equals(username) && PASSWORD.equals(password)) { JSONObject userInfo = new JSONObject(); userInfo.put(“username”, USERNAME); userInfo.put(“password”, PASSWORD); userInfo.put(“userId”, USER_ID); return TokenUtils.createToken(userInfo.toJSONString()); } return “账号密码不正确”; } public JSONObject detail(Long id) { JSONObject jsonObject = new JSONObject(); jsonObject.put(“id”, id); jsonObject.put(“name”, “admin”); return jsonObject; }}

测试

登录:返回token

查询:

设置URL有效时长

为了增强URL安全性,前端在header中添加时间戳。

前端代码

在header中添加时间戳

2.2 后端验证时间戳

private Long getDateTimestamp(HttpHeaders httpHeaders) { List list = httpHeaders.get(“timestamp”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(“拒绝服务”); } long timestamp = Long.parseLong(list.get(0)); long currentTimeMillis = System.currentTimeMillis(); //有效时长为5分钟 if (currentTimeMillis – timestamp > 1000 * 60 * 5) { throw new IllegalArgumentException(“拒绝服务”); } return timestamp; }

测试不传时间戳

确保URL唯一性

确保URL唯一性,前端请求中增加UUID,后端存入redis,有效时长为5分钟,5分钟重复提交拒绝服务

修改前端请求参数

3.2 后端增加验证RequestId

private String getRequestId(HttpHeaders headers) { List list = headers.get(“requestId”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(ERROR_MESSAGE); } String requestId = list.get(0); //如果requestId存在redis中直接返回 String temp = redisTemplate.opsForValue().get(requestId); if (StringUtils.isNotBlank(temp)) { throw new IllegalArgumentException(ERROR_MESSAGE); } redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES); return requestId; }

增加签名

最后一步,添加签名

前端增加签名

跟前端约定好,json数据按照ASCII升序排序。

登录页面:

登录页面

登录

账号: 密码:

增强读取Body类

/** * @Description: * @Author: Rosh * @Date: 2021/10/27 11:03 */public class MyCachedBodyOutputMessage extends CachedBodyOutputMessage { private Map paramMap; private Long dateTimestamp; private String requestId; private String sign; public MyCachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) { super(exchange, httpHeaders); } public void initial(Map paramMap, String requestId, String sign, Long dateTimestamp) { this.paramMap = paramMap; this.requestId = requestId; this.sign = sign; this.dateTimestamp = dateTimestamp; } public Map getParamMap() { return paramMap; } public Long getDateTimestamp() { return dateTimestamp; } public String getRequestId() { return requestId; } public String getSign() { return sign; }}

4.3 修改GatewayFilterConfig

package com.demo.gateway.config;public class GatewayFilterConfig implements GlobalFilter, Ordered { @Autowired private RedisTemplate redisTemplate; private static final String ERROR_MESSAGE = “拒绝服务”; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1 获取时间戳 Long dateTimestamp = getDateTimestamp(exchange.getRequest().getHeaders()); //2 获取RequestId String requestId = getRequestId(exchange.getRequest().getHeaders()); //3 获取签名 String sign = getSign(exchange.getRequest().getHeaders()); //4 如果是登录不校验Token String requestUrl = exchange.getRequest().getPath().value(); AntPathMatcher pathMatcher = new AntPathMatcher(); if (!pathMatcher.match(“/user/login”, requestUrl)) { String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN); Claims claim = TokenUtils.getClaim(token); if (StringUtils.isBlank(token) || claim == null) { return FilterUtils.invalidToken(exchange); } } //5 修改请求参数,并获取请求参数 Map paramMap; try { paramMap = updateRequestParam(exchange); } catch (Exception e) { return FilterUtils.invalidUrl(exchange); } //6 获取请求体,修改请求体 ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders()); Mono modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> { String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY); JSONObject jsonObject = JSON.parseObject(encrypt); for (Map.Entry entry : jsonObject.entrySet()) { paramMap.put(entry.getKey(), entry.getValue()); } checkSign(sign, dateTimestamp, requestId, paramMap); return Mono.just(encrypt); }); //创建BodyInserter修改请求体 BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); headers.remove(HttpHeaders.CONTENT_LENGTH); //创建CachedBodyOutputMessage并且把请求param加入,初始化校验信息 MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers); outputMessage.initial(paramMap, requestId, sign, dateTimestamp); return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux getBody() { Flux body = outputMessage.getBody(); if (body.equals(Flux.empty())) { //验证签名 checkSign(outputMessage.getSign(), outputMessage.getDateTimestamp(), outputMessage.getRequestId(), outputMessage.getParamMap()); } return outputMessage.getBody(); } }; return chain.filter(exchange.mutate().request(decorator).build()); })); } public void checkSign(String sign, Long dateTimestamp, String requestId, Map paramMap) { String str = JSON.toJSONString(paramMap) + requestId + dateTimestamp; String tempSign = Md5Utils.getMD5(str.getBytes()); if (!tempSign.equals(sign)) { throw new IllegalArgumentException(ERROR_MESSAGE); } } /** * 修改前端传的参数 */ private Map updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException { ServerHttpRequest request = exchange.getRequest(); URI uri = request.getURI(); String query = uri.getQuery(); if (StringUtils.isNotBlank(query) && query.contains(“param”)) { String[] split = query.split(“=”); String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY); Field targetQuery = uri.getClass().getDeclaredField(“query”); targetQuery.setAccessible(true); targetQuery.set(uri, param); return getParamMap(param); } return new TreeMap(); } private Map getParamMap(String param) { Map map = new TreeMap(); String[] split = param.split(“&”); for (String str : split) { String[] params = str.split(“=”); map.put(params[0], params[1]); } return map; } private String getSign(HttpHeaders headers) { List list = headers.get(“sign”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(ERROR_MESSAGE); } return list.get(0); } private Long getDateTimestamp(HttpHeaders httpHeaders) { List list = httpHeaders.get(“timestamp”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(ERROR_MESSAGE); } long timestamp = Long.parseLong(list.get(0)); long currentTimeMillis = System.currentTimeMillis(); //有效时长为5分钟 if (currentTimeMillis – timestamp > 1000 * 60 * 5) { throw new IllegalArgumentException(ERROR_MESSAGE); } return timestamp; } private String getRequestId(HttpHeaders headers) { List list = headers.get(“requestId”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(ERROR_MESSAGE); } String requestId = list.get(0); //如果requestId存在redis中直接返回 String temp = redisTemplate.opsForValue().get(requestId); if (StringUtils.isNotBlank(temp)) { throw new IllegalArgumentException(ERROR_MESSAGE); } redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES); return requestId; } @Override public int getOrder() { return 80; }}

测试登录

发现验签成功

测试查询

验签成功

地址

https://gitee.com/zhurongsheng/springcloud-gateway-rsa

郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
(0)
用户投稿
上一篇 2022年6月21日
下一篇 2022年6月21日

相关推荐

联系我们

联系邮箱:admin#wlmqw.com
工作时间:周一至周五,10:30-18:30,节假日休息