極驗驗證目錄
- 一、樣例
- 二、注冊賬號
- 三、獲取ID
- 四、極驗官方檔案(參考)
- 五、SpringBoot集成極驗
- 5.1、maven依賴(可能有些需要自己去導,個人的包依賴太多不好全部放上來,核心就這兩個)
- 5.2、yml組態檔
- 5.3、util類(讀本地ip)
- 5.4、讀取配置類
- 5.5、獲取極驗的第一次資料包
- 5.5.1、在資源服務器里面放行路徑
- 5.5.2、添加控制器 GeetestController
- 5.5.3、在gateway網關里面添加 CorsConfig
一、樣例

二、注冊賬號
極驗驗證官網

在表單里面填寫真實資訊,客服會在 24h 聯系你進行審核,審核通過后,會發送這樣的郵件到你的郵箱,
點擊郵箱里面提供的注冊地址即可完成注冊, 注冊成功后,進行登錄:
三、獲取ID


填寫基本資訊:

創建成功后:

點擊 ID:

點擊查看部署指引:

都勾選成功后,點擊確認:

獲取到了 ID 和 KEY:

記錄 ID 和 KEY:
ID 上面獲得到的id
KEY 上面獲取到的value
四、極驗官方檔案(參考)
檔案地址:
https://docs.geetest.com/sensebot/start/
服務端:
https://docs.geetest.com/sensebot/deploy/server/java#%E9%85%8D%E7%BD %AE%E5%AF%86%E9%92%A5
Web 端:
https://docs.geetest.com/sensebot/deploy/client/web
五、SpringBoot集成極驗
5.1、maven依賴(可能有些需要自己去導,個人的包依賴太多不好全部放上來,核心就這兩個)
<!--json的依賴-->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20171018</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
5.2、yml組態檔
spring:
redis:
host: redis-server #自己redis的ip,此處我做了ip映射
port: 6380
password: 123456
geetest:
geetest-id: 上面獲得到的id
geetest-key: 上面獲取到的value
5.3、util類(讀本地ip)
package com.zhz.util;
import javax.servlet.http.HttpServletRequest;
/**
* 獲取本地ip地址
*/
public class IpUtil {
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CONNECTING_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} else if (ip.length() > 15) {
String[] ips = ip.split(",");
for (int index = 0; index < ips.length; index++) {
String strIp = (String) ips[index];
if (!("unknown".equalsIgnoreCase(strIp))) {
ip = strIp;
break;
}
}
}
return ip;
}
}
/**
* @(#)Result.JAVA, 2020年05月22日.
* <p>
* Copyright 2020 GEETEST, Inc. All rights reserved.
* GEETEST PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.zhz.geetest;
import org.json.JSONObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
/**
* sdk lib包,核心邏輯,
*/
public class GeetestLib {
/**
* 公鑰
*/
private String geetest_id;
/**
* 私鑰
*/
private String geetest_key;
/**
* 回傳資料的封裝物件
*/
private GeetestLibResult libResult;
/**
* 除錯開關,是否輸出除錯日志
*/
private static final boolean IS_DEBUG = true;
private static final String API_URL = "http://api.geetest.com";
private static final String REGISTER_URL = "/register.php";
private static final String VALIDATE_URL = "/validate.php";
private static final String JSON_FORMAT = "1";
private static final boolean NEW_CAPTCHA = true;
private static final int HTTP_TIMEOUT_DEFAULT = 5000; // 單位:毫秒
public static final String VERSION = "jave-servlet:3.1.0";
/**
* 極驗二次驗證表單傳參欄位 chllenge
*/
public static final String GEETEST_CHALLENGE = "geetest_challenge";
/**
* 極驗二次驗證表單傳參欄位 validate
*/
public static final String GEETEST_VALIDATE = "geetest_validate";
/**
* 極驗二次驗證表單傳參欄位 seccode
*/
public static final String GEETEST_SECCODE = "geetest_seccode";
/**
* 極驗驗證API服務狀態Session Key
*/
public static final String GEETEST_SERVER_STATUS_SESSION_KEY = "gt_server_status";
/**
* 極驗證里面的user
*/
public static final String GEETEST_SERVER_USER_KEY = "gt_server_user";
public GeetestLib(String geetest_id, String geetest_key) {
this.geetest_id = geetest_id;
this.geetest_key = geetest_key;
this.libResult = new GeetestLibResult();
}
public void gtlog(String message) {
if (this.IS_DEBUG) {
System.out.println("gtlog: " + message);
}
}
/**
* 驗證初始化
*/
public GeetestLibResult register(String digestmod, Map<String, String> paramMap) {
this.gtlog(String.format("register(): 開始驗證初始化, digestmod=%s.", digestmod));
String origin_challenge = this.requestRegister(paramMap);
this.buildRegisterResult(origin_challenge, digestmod);
this.gtlog(String.format("register(): 驗證初始化, lib包回傳資訊=%s.", this.libResult));
return this.libResult;
}
/**
* 向極驗發送驗證初始化的請求,GET方式
*/
private String requestRegister(Map<String, String> paramMap) {
paramMap.put("gt", this.geetest_id);
paramMap.put("json_format", this.JSON_FORMAT);
paramMap.put("sdk", this.VERSION);
String register_url = this.API_URL + this.REGISTER_URL;
this.gtlog(String.format("requestRegister(): 驗證初始化, 向極驗發送請求, url=%s, params=%s.", register_url, paramMap));
String origin_challenge = null;
try {
String resBody = this.httpGet(register_url, paramMap);
this.gtlog(String.format("requestRegister(): 驗證初始化, 與極驗網路互動正常, 回傳body=%s.", resBody));
JSONObject jsonObject = new JSONObject(resBody);
origin_challenge = jsonObject.getString("challenge");
} catch (Exception e) {
this.gtlog("requestRegister(): 驗證初始化, 請求例外,后續流程走宕機模式, " + e.toString());
origin_challenge = "";
}
return origin_challenge;
}
/**
* 構建驗證初始化介面回傳資料
*/
private void buildRegisterResult(String origin_challenge, String digestmod) {
// origin_challenge為慷訓者值為0代表失敗
if (origin_challenge == null || origin_challenge.isEmpty() || "0".equals(origin_challenge)) {
// 本地隨機生成32位字串
String challenge = UUID.randomUUID().toString().replaceAll("-", "");
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", 0);
jsonObject.put("gt", this.geetest_id);
jsonObject.put("challenge", challenge);
jsonObject.put("new_captcha", this.NEW_CAPTCHA);
this.libResult.setAll(0, jsonObject.toString(), "請求極驗register介面失敗,后續流程走宕機模式");
} else {
String challenge = null;
if ("md5".equals(digestmod)) {
challenge = this.md5_encode(origin_challenge + this.geetest_key);
} else if ("sha256".equals(digestmod)) {
challenge = this.sha256_encode(origin_challenge + this.geetest_key);
} else if ("hmac-sha256".equals(digestmod)) {
challenge = this.hmac_sha256_encode(origin_challenge, this.geetest_key);
} else {
challenge = this.md5_encode(origin_challenge + this.geetest_key);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", 1);
jsonObject.put("gt", this.geetest_id);
jsonObject.put("challenge", challenge);
jsonObject.put("new_captcha", this.NEW_CAPTCHA);
this.libResult.setAll(1, jsonObject.toString(), "");
}
}
/**
* 正常流程下(即驗證初始化成功),二次驗證
*/
public GeetestLibResult successValidate(String challenge, String validate, String seccode, Map<String, String> paramMap) {
this.gtlog(String.format("successValidate(): 開始二次驗證 正常模式, challenge=%s, validate=%s, seccode=%s.", challenge, validate, seccode));
if (!this.checkParam(challenge, validate, seccode)) {
this.libResult.setAll(0, "", "正常模式,本地校驗,引數challenge、validate、seccode不可為空");
} else {
String response_seccode = this.requestValidate(challenge, validate, seccode, paramMap);
if (response_seccode == null || response_seccode.isEmpty()) {
this.libResult.setAll(0, "", "請求極驗validate介面失敗");
} else if ("false".equals(response_seccode)) {
this.libResult.setAll(0, "", "極驗二次驗證不通過");
} else {
this.libResult.setAll(1, "", "");
}
}
this.gtlog(String.format("successValidate(): 二次驗證 正常模式, lib包回傳資訊=%s.", this.libResult));
return this.libResult;
}
/**
* 例外流程下(即驗證初始化失敗,宕機模式),二次驗證
* 注意:由于是宕機模式,初衷是保證驗證業務不會中斷正常業務,所以此處只作簡單的引數校驗,可自行設計邏輯,
*/
public GeetestLibResult failValidate(String challenge, String validate, String seccode) {
this.gtlog(String.format("failValidate(): 開始二次驗證 宕機模式, challenge=%s, validate=%s, seccode=%s.", challenge, validate, seccode));
if (!this.checkParam(challenge, validate, seccode)) {
this.libResult.setAll(0, "", "宕機模式,本地校驗,引數challenge、validate、seccode不可為空.");
} else {
this.libResult.setAll(1, "", "");
}
this.gtlog(String.format("failValidate(): 二次驗證 宕機模式, lib包回傳資訊=%s.", this.libResult));
return this.libResult;
}
/**
* 向極驗發送二次驗證的請求,POST方式
*/
private String requestValidate(String challenge, String validate, String seccode, Map<String, String> paramMap) {
paramMap.put("seccode", seccode);
paramMap.put("json_format", this.JSON_FORMAT);
paramMap.put("challenge", challenge);
paramMap.put("sdk", this.VERSION);
paramMap.put("captchaid", this.geetest_id);
String validate_url = this.API_URL + this.VALIDATE_URL;
this.gtlog(String.format("requestValidate(): 二次驗證 正常模式, 向極驗發送請求, url=%s, params=%s.", validate_url, paramMap));
String response_seccode = null;
try {
String resBody = this.httpPost(validate_url, paramMap);
this.gtlog(String.format("requestValidate(): 二次驗證 正常模式, 與極驗網路互動正常, 回傳body=%s.", resBody));
JSONObject jsonObject = new JSONObject(resBody);
response_seccode = jsonObject.getString("seccode");
} catch (Exception e) {
this.gtlog("requestValidate(): 二次驗證 正常模式, 請求例外, " + e.toString());
response_seccode = "";
}
return response_seccode;
}
/**
* 校驗二次驗證的三個引數,校驗通過回傳true,校驗失敗回傳false
*/
private boolean checkParam(String challenge, String validate, String seccode) {
return !(challenge == null || challenge.trim().isEmpty() || validate == null || validate.trim().isEmpty() || seccode == null || seccode.trim().isEmpty());
}
/**
* 發送GET請求,獲取服務器回傳結果
*/
private String httpGet(String url, Map<String, String> paramMap) throws Exception {
HttpURLConnection connection = null;
InputStream inStream = null;
try {
Iterator<String> it = paramMap.keySet().iterator();
StringBuilder paramStr = new StringBuilder();
while (it.hasNext()) {
String key = it.next();
if (key == null || key.isEmpty() || paramMap.get(key) == null || paramMap.get(key).isEmpty()) {
continue;
}
paramStr.append("&").append(URLEncoder.encode(key, "utf-8")).append("=").append(URLEncoder.encode(paramMap.get(key), "utf-8"));
}
if (paramStr.length() != 0) {
paramStr.replace(0, 1, "?");
}
url += paramStr.toString();
URL getUrl = new URL(url);
connection = (HttpURLConnection) getUrl.openConnection();
connection.setConnectTimeout(this.HTTP_TIMEOUT_DEFAULT); // 設定連接主機超時(單位:毫秒)
connection.setReadTimeout(this.HTTP_TIMEOUT_DEFAULT); // 設定從主機讀取資料超時(單位:毫秒)
connection.connect();
if (connection.getResponseCode() == 200) {
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
inStream = connection.getInputStream();
for (int n; (n = inStream.read(buf)) != -1; ) {
sb.append(new String(buf, 0, n, "UTF-8"));
}
return sb.toString();
}
return "";
} catch (Exception e) {
throw e;
} finally {
if (inStream != null) {
inStream.close();
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 發送POST請求,獲取服務器回傳結果
*/
private String httpPost(String url, Map<String, String> paramMap) throws Exception {
HttpURLConnection connection = null;
InputStream inStream = null;
try {
Iterator<String> it = paramMap.keySet().iterator();
StringBuilder paramStr = new StringBuilder();
while (it.hasNext()) {
String key = it.next();
if (key == null || key.isEmpty() || paramMap.get(key) == null || paramMap.get(key).isEmpty()) {
continue;
}
paramStr.append("&").append(URLEncoder.encode(key, "utf-8")).append("=").append(URLEncoder.encode(paramMap.get(key), "utf-8"));
}
if (paramStr.length() != 0) {
paramStr.replace(0, 1, "");
}
URL postUrl = new URL(url);
connection = (HttpURLConnection) postUrl.openConnection();
connection.setConnectTimeout(this.HTTP_TIMEOUT_DEFAULT);// 設定連接主機超時(單位:毫秒)
connection.setReadTimeout(this.HTTP_TIMEOUT_DEFAULT);// 設定從主機讀取資料超時(單位:毫秒)
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.connect();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(connection.getOutputStream(), "utf-8");
outputStreamWriter.write(paramStr.toString());
outputStreamWriter.flush();
outputStreamWriter.close();
if (connection.getResponseCode() == 200) {
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
inStream = connection.getInputStream();
for (int n; (n = inStream.read(buf)) != -1; ) {
sb.append(new String(buf, 0, n, "UTF-8"));
}
inStream.close();
connection.disconnect();
return sb.toString();
}
return "";
} catch (Exception e) {
throw e;
} finally {
if (inStream != null) {
inStream.close();
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* md5 加密
*/
private String md5_encode(String value) {
String re_md5 = new String();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(value.getBytes());
byte b[] = md.digest();
int i;
StringBuilder sb = new StringBuilder("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
sb.append("0");
sb.append(Integer.toHexString(i));
}
re_md5 = sb.toString();
} catch (Exception e) {
this.gtlog("md5_encode(): 發生例外, " + e.toString());
}
return re_md5;
}
/**
* sha256加密
*/
public String sha256_encode(String value) {
MessageDigest messageDigest;
String encodeStr = new String();
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(value.getBytes("UTF-8"));
encodeStr = byte2Hex(messageDigest.digest());
} catch (Exception e) {
this.gtlog("sha256_encode(): 發生例外, " + e.toString());
}
return encodeStr;
}
/**
* 將byte轉為16進制
*/
private static String byte2Hex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
// 得到一位的進行補0操作
sb.append("0");
}
sb.append(temp);
}
return sb.toString();
}
/**
* hmac-sha256 加密
*/
private String hmac_sha256_encode(String value, String key) {
String encodeStr = new String();
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(value.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
encodeStr = sb.toString();
} catch (Exception e) {
this.gtlog("hmac_sha256_encode(): 發生例外, " + e.toString());
}
return encodeStr;
}
}
5.4、讀取配置類
package com.zhz.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author :zhz
* @date :Created in 2021/01/02
* @version: V1.0
* @slogan: 天下風云出我輩,一入代碼歲月催
* @description: 極驗驗證的ID和Key配置
**/
@Data
@ConfigurationProperties(prefix = "geetest")
public class GeetestProperties {
/**
* 極驗的 ID
*/
private String geetestId;
/**
* 極驗的 key
*/
private String geetestKey;
}
package com.zhz.config;
import com.zhz.geetest.GeetestLib;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author :zhz
* @date :Created in 2021/01/02
* @version: V1.0
* @slogan: 天下風云出我輩,一入代碼歲月催
* @description: 極驗驗證的自動配置
**/
@Configuration
@EnableConfigurationProperties(GeetestProperties.class)
public class GeetestAutoConfiguration {
private GeetestProperties geetestProperties ;
public GeetestAutoConfiguration(GeetestProperties geetestProperties){
this.geetestProperties = geetestProperties ;
}
@Bean
public GeetestLib geetestLib(){
GeetestLib geetestLib = new GeetestLib(geetestProperties.getGeetestId(),
geetestProperties.getGeetestKey());
return geetestLib ;
}
}
/**
* @(#)Result.JAVA, 2020年04月02日.
* <p>
* Copyright 2020 GEETEST, Inc. All rights reserved.
* GEETEST PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.zhz.geetest;
/**
* sdk lib包的回傳結果資訊,
*/
public class GeetestLibResult {
/**
* 成功失敗的標識碼,1表示成功,0表示失敗
*/
private int status = 0;
/**
* 回傳資料,json格式
*/
private String data = "";
/**
* 備注資訊,如例外資訊等
*/
private String msg = "";
public void setStatus(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setAll(int status, String data, String msg) {
this.setStatus(status);
this.setData(data);
this.setMsg(msg);
}
@Override
public String toString() {
return String.format("GeetestLibResult{status=%s, data=%s, msg=%s}", this.status, this.data, this.msg);
}
}
5.5、獲取極驗的第一次資料包
5.5.1、在資源服務器里面放行路徑
我用的jwt+auth2.0,你們自己去放行路徑--》"/gt/register"
5.5.2、添加控制器 GeetestController
package com.zhz.controller;
import com.zhz.geetest.GeetestLib;
import com.zhz.geetest.GeetestLibResult;
import com.zhz.model.R;
import com.zhz.util.IpUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author :zhz
* @date :Created in 2021/01/02
* @version: V1.0
* @slogan: 天下風云出我輩,一入代碼歲月催
* @description:
**/
@RestController
@RequestMapping("/gt")
public class GeetestController {
@Autowired
private GeetestLib geetestLib;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/register")
public R<String> register(String uuid) {
String digestmod = "md5";
Map<String, String> paramMap = new HashMap<String, String>();
paramMap.put("digestmod", digestmod);
paramMap.put("user_id", uuid);
paramMap.put("client_type", "web");
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
paramMap.put("ip_address", IpUtil.getIpAddr(servletRequestAttributes.getRequest()));//ip地址
GeetestLibResult result = geetestLib.register(digestmod, paramMap); // 極驗服務器互動
// 將結果狀態寫到session中,此處register介面存入session,后續validate介面會取出使用
// 注意,此demo應用的session是單機模式,格外注意分布式環境下session的應用
redisTemplate.opsForValue().set(GeetestLib.GEETEST_SERVER_STATUS_SESSION_KEY, result.getStatus(), 180, TimeUnit.SECONDS);
// request.getSession().setAttribute( result.getStatus());
redisTemplate.opsForValue().set(GeetestLib.GEETEST_SERVER_USER_KEY + ":" + uuid, uuid, 180, TimeUnit.SECONDS);
// request.getSession().setAttribute("userId", userId);
// 注意,不要更改回傳的結構和值型別
return R.ok(result.getData());
}
}
5.5.3、在gateway網關里面添加 CorsConfig
package com.zhz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
/**
* @author :zhz
* @date :Created in 2020/12/20
* @version: V1.0
* @slogan: 天下風云出我輩,一入代碼歲月催
* @description: 解決跨域問題
**/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration corsConfiguration=new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource=new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(urlBasedCorsConfigurationSource);
}
}
具體代碼:看本人gitee上:https://gitee.com/zhouzhz/coin-exchange/tree/master/coin-member/member-services 上
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/243825.html
標籤:java
上一篇:JVM快速入門(上)
下一篇:爬蟲京東評論+可視化


