视频点播提供 Referer 黑白名单、IP 黑白名单、Origin 黑白名单等功能对访客进行过滤。但是在某些情况下,Referer, IP 和 Origin 可以被伪造,容易造成站点资源被恶意盗用。如果您对于站点内容的安全性有很高的要求,可以采用 URL 鉴权。本文为您介绍如何在视频点播控制台开启和配置 URL 鉴权。
下图展示了视频点播 URL 鉴权的整体流程:
具体步骤说明如下:
在这个流程中,应用客户端负责发起请求,应用服务端负责生成鉴权 URL,CDN 节点负责接收请求并进行鉴权信息的校验。鉴权信息的生成和校验依赖于共享的密钥和鉴权算法,确保了请求的合法性和安全性。通过这种方式,您可以有效地保护 CDN 资源免受未经授权的访问。
URL 鉴权适用于点播加速域名和自定义源站加速域名。其中:
在完成 URL 鉴权的配置后,您可以通过鉴权计算器实现以下功能:
按照以下步骤使用鉴权计算器:
说明
新增点播加速域名后,默认开启 A 类型 URL 鉴权,签名有效时长为 3,600 秒(60 分钟)。您可以在域名配置的访问控制 > URL 鉴权区域修改配置。
按照以下步骤修改点播加速域名的 URL 鉴权配置:
说明
配置点播加速域名的 URL 鉴权前,请先在域名列表关闭点播调度。
表示对哪些文件的请求需要被鉴权。您至少需要添加一个规则,最多可以添加 10 个。
详细的参数信息如下表所示。
参数 | 说明 |
|---|---|
类型 | A 类型 |
规则类型 | 支持以下选项:
|
是否匹配 | 可设为匹配或不匹配。 |
规则内容 | 根据规则类型,输入规则的内容。长度不能超过 1,024 个字符。不能包含以下字符:连续斜杠(
|
参数 | 说明 |
|---|---|
主 Key | 主密钥。支持自定义签名 key。仅支持可见 ASCII 码,长度为 1-120 个字符。 |
备 Key | 备密钥。支持自定义签名 key。仅支持可见 ASCII 码,长度限制为1-120 个字符。 说明 对线上业务域名变更 URL 鉴权时,建议在过渡期使用备 Key,避免业务中断。 |
签名参数名 | 固定为 auth_key。 |
签名有效时间 | 签名的有效时长,单位为秒。该参数与客户端请求中的 |
时间戳进制 | 固定为十进制(Unix 时间戳)。 |
鉴权算法 | 表示签名计算使用的算法,支持的算法有:
说明 SHA-256 算法的安全性比 MD5 高,生成的哈希值出现碰撞的几率比 MD5 低,但是加密和解密过程的耗时比 MD5 长。 |
M3U8 改写 | 表示在响应
说明
|
按照以下步骤为自定义源站加速域名开启并配置 URL 鉴权:
为方便您快速进行签名计算,视频点播提供了多种语言的签名 Demo,演示如何根据给定的密钥和其他参数,生成不同类型的带签名的 URL。
return (<Tabs> <Tabs.TabPane title="Go" key="DWoqrJ42R7"><RenderMd content={`\`\`\`Go package main import ( "crypto/md5" "encoding/hex" "fmt" "math/rand" "regexp" "strconv" "time" ) func splitUrl(url string) []string { reg := regexp.MustCompile("^(http://|https://)?([^/?]+)(/[^?]*)?(\\\\?.*)?$") return reg.FindStringSubmatch(url) } func getMd5(text string) string { hashByte := md5.Sum([]byte(text)) return hex.EncodeToString(hashByte[:]) } func getRandomString(length int) string { b := make([]byte, length) rand.Read(b) return fmt.Sprintf("%x", b)[:length] } func GenTypeAUrl(url string, key string, signName string, uid string, ts int64) string { params := splitUrl(url) scheme, host, path, args := params[1], params[2], params[3], params[4] randstr := getRandomString(10) text := fmt.Sprintf("%s-%d-%s-%s-%s", path, ts, randstr, uid, key) hash := getMd5(text) authArg := fmt.Sprintf("%s=%d-%s-%s-%s", signName, ts, randstr, uid, hash) if args == "" { return fmt.Sprintf("%s%s%s?%s", scheme, host, path, authArg) } else { return fmt.Sprintf("%s%s%s%s&%s", scheme, host, path, args, authArg) } } func GenTypeBUrl(url string, key string, ts int64) string { params := splitUrl(url) scheme, host, path, args := params[1], params[2], params[3], params[4] tsStr := time.Unix(ts, 0).Format("200601021504") text := fmt.Sprintf("%s%s%s", key, tsStr, path) hash := getMd5(text) return fmt.Sprintf("%s%s/%s/%s%s%s", scheme, host, tsStr, hash, path, args) } func GenTypeCUrl(url string, key string, ts int64) string { params := splitUrl(url) scheme, host, path, args := params[1], params[2], params[3], params[4] tsStr := strconv.FormatInt(ts, 16) text := fmt.Sprintf("%s%s%s", key, path, tsStr) hash := getMd5(text) return fmt.Sprintf("%s%s/%s/%s%s%s", scheme, host, hash, tsStr, path, args) } func GenTypeDUrl(url, key, signName, timeName string, ts int64, base int) string { params := splitUrl(url) scheme, host, path, args := params[1], params[2], params[3], params[4] tsStr := strconv.FormatInt(ts, base) text := fmt.Sprintf("%s%s%s", key, path, tsStr) hash := getMd5(text) authArg := fmt.Sprintf("%s=%s&%s=%s", signName, hash, timeName, tsStr) if args == "" { return fmt.Sprintf("%s%s%s?%s", scheme, host, path, authArg) } else { return fmt.Sprintf("%s%s%s%s&%s", scheme, host, path, args, authArg) } } // GenTypeEUrl Genrate signed url by custom rule(eg.:key+domain+uri+timestamp) func GenTypeEUrl(url, key, signName, tsName string, ts int64, base int) string { params := splitUrl(url) scheme, domain, uri, args := params[1], params[2], params[3], params[4] tsStr := strconv.FormatInt(ts, base) text := fmt.Sprintf("%s%s%s%s", key, domain, uri, tsStr) hash := getMd5(text) authArg := fmt.Sprintf("%s=%s&%s=%s", signName, hash, tsName, tsStr) if args == "" { return fmt.Sprintf("%s%s%s?%s", scheme, domain, uri, authArg) } else { return fmt.Sprintf("%s%s%s%s&%s", scheme, domain, uri, args, authArg) } } func init() { rand.Seed(time.Now().UnixNano()) } func main() { var url = "http://www.test.com/a.txt?a=b&c=d" var primaryKey = "primary123456" var signName = "auth_key" var timeName = "t" var uid = "0" var ts = time.Now().Unix() + 3600 //默认一小时过期 typeAUrl := GenTypeAUrl(url, primaryKey, signName, uid, ts) typeBUrl := GenTypeBUrl(url, primaryKey, ts) typeCUrl := GenTypeCUrl(url, primaryKey, ts) typeDUrl := GenTypeDUrl(url, primaryKey, signName, timeName, ts, 10) typeEUrl := GenTypeEUrl(url, primaryKey, signName, timeName, ts, 10) fmt.Println("OriginUrl: ", url) fmt.Println("TypeA: ", typeAUrl) fmt.Println("TypeB: ", typeBUrl) fmt.Println("TypeC: ", typeCUrl) fmt.Println("TypeD: ", typeDUrl) fmt.Println("TypeE: ", typeEUrl) } \`\`\` `}> </RenderMd></Tabs.TabPane> <Tabs.TabPane title="Java" key="SAaS43zrKE"><RenderMd content={`\`\`\`Java import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.regex.*; import java.util.*; public class url_auth { private static String getMd5(String text) { MessageDigest hash = null; try { hash = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } hash.update(StandardCharsets.UTF_8.encode(text)); return String.format("%032x", new BigInteger(1, hash.digest())); } private static String getRandomString(int length) { int leftLimit = 48; // numeral '0' int rightLimit = 122; // letter 'z' Random random = new Random(); String str = random.ints(leftLimit, rightLimit + 1) .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) .limit(length) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString(); return str; } private static Map<String,String> splitUrl(String url) { String pattern = "^(http://|https://)?([^/?]+)(/[^?]*)?(\\\\?.*)?$"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(url); Map<String,String> result = new HashMap<>(); String scheme = "", domain = "", uri = "", args = ""; if (m.find()) { scheme = m.group(1) == null ? "http://" : m.group(1); domain = m.group(2) == null ? "" : m.group(2); uri = m.group(3) == null ? "/" : m.group(3); args = m.group(4) == null ? "" : m.group(4); } result.put("scheme", scheme); result.put("domain", domain); result.put("uri", uri); result.put("args", args); return result; } private static String genTypeAUrl(String url, String key, String signName, String uid, long ts) { Map<String,String> uriInfo = splitUrl(url); String scheme = uriInfo.get("scheme"), domain = uriInfo.get("domain"), uri = uriInfo.get("uri"), args = uriInfo.get("args"); String rand = getRandomString(10); String text = String.format("%s-%d-%s-%s-%s", uri, ts, rand, uid, key); String hash = getMd5(text); String authArg = String.format("%s=%d-%s-%s-%s", signName, ts, rand, uid, hash); if (args == "") return String.format("%s%s%s?%s", scheme, domain, uri, authArg); else return String.format("%s%s%s%s&%s", scheme, domain, uri, args, authArg); } private static String genTypeBUrl(String url, String key, long ts) { Map<String,String> uriInfo = splitUrl(url); String scheme = uriInfo.get("scheme"), domain = uriInfo.get("domain"), uri = uriInfo.get("uri"), args = uriInfo.get("args"); String tsStr = new SimpleDateFormat("yyyyMMddHHmm").format(ts*1000); String text = String.format("%s%s%s", key, tsStr, uri); String hash = getMd5(text); return String.format("%s%s/%s/%s%s%s", scheme, domain, tsStr, hash, uri, args); } private static String genTypeCUrl(String url, String key, long ts) { Map<String,String> uriInfo = splitUrl(url); String scheme = uriInfo.get("scheme"), domain = uriInfo.get("domain"), uri = uriInfo.get("uri"), args = uriInfo.get("args"); String hexts = Long.toHexString(ts); String text = String.format("%s%s%s", key, uri, hexts); String hash = getMd5(text); return String.format("%s%s/%s/%s%s%s", scheme, domain, hash, hexts, uri, args); } private static String genTypeDUrl(String url, String key, String signName, String timeName, long ts, int base) { Map<String,String> uriInfo = splitUrl(url); String scheme = uriInfo.get("scheme"), domain = uriInfo.get("domain"), uri = uriInfo.get("uri"), args = uriInfo.get("args"); String tsStr = String.format("%d", ts); if (base==16) tsStr = Long.toHexString(ts); String text = String.format("%s%s%s", key, uri, tsStr); String hash = getMd5(text); String authArg = String.format("%s=%s&%s=%s", signName, hash, timeName, tsStr); if (args == "") return String.format("%s%s%s?%s", scheme, domain, uri, authArg); else return String.format("%s%s%s%s&%s", scheme, domain, uri, args, authArg); } // GenTypeEUrl Genrate signed url by custom rule(eg.:key+domain+uri+timestamp) private static String genTypeEUrl(String url, String key, String signName, String timeName, long ts, int base) { Map<String,String> uriInfo = splitUrl(url); String scheme = uriInfo.get("scheme"), domain = uriInfo.get("domain"), uri = uriInfo.get("uri"), args = uriInfo.get("args"); String tsStr = String.format("%d", ts); if (base==16) tsStr = Long.toHexString(ts); String text = String.format("%s%s%s%s", key, domain, uri, tsStr); String hash = getMd5(text); String authArg = String.format("%s=%s&%s=%s", signName, hash, timeName, tsStr); if (args == "") return String.format("%s%s%s?%s", scheme, domain, uri, authArg); else return String.format("%s%s%s%s&%s", scheme, domain, uri, args, authArg); } public static void main(String[] args) { String url = "http://www.test.com/a.txt?a=b&c=d"; String primaryKey = "primary123456"; String signName = "auth_key"; String tName = "t"; String uid = "0"; long ts = System.currentTimeMillis() / 1000 + 3600;//默认一小时过期 System.out.println("OriginUrl:" + url); System.out.println("TypeA:" + genTypeAUrl(url, primaryKey, signName, uid, ts)); System.out.println("TypeB:" + genTypeBUrl(url, primaryKey, ts)); System.out.println("TypeC:" + genTypeCUrl(url, primaryKey, ts)); System.out.println("TypeD:" + genTypeDUrl(url, primaryKey, signName, tName, ts, 10)); System.out.println("TypeE:" + genTypeEUrl(url, primaryKey, signName, tName, ts, 10)); } } \`\`\` `}> </RenderMd></Tabs.TabPane> <Tabs.TabPane title="Python" key="a5UNrLXFDM"><RenderMd content={`\`\`\`Python import re import time import random import string import hashlib def getMd5(text): hash = hashlib.md5() hash.update(text.encode("utf8")) return hash.hexdigest() def splitUrl(url): p = re.compile(r"^(http://|https://)?([^/?]+)(/[^?]*)?(\?.*)?$") if not p: return None m = p.match(url) if not m: return "", "", "", "" scheme, domain, uri, args = m.groups() if not scheme: scheme = "http://" if not uri: uri = "/" if not args: args = "" return scheme, domain, uri, args def getRandomString(n): x = ''.join(random.sample(string.ascii_letters + string.digits, n)) return x def genTypeAUrl(url, key, signName, uid, ts): scheme, domain, uri, args = splitUrl(url) rand = getRandomString(10) text = "%s-%d-%s-%s-%s" %(uri, ts, rand, uid, key) hash = getMd5(text) authArg = "%s=%d-%s-%s-%s" %(signName, ts, rand, uid, hash) if args: return "%s%s%s%s&%s" %(scheme, domain, uri, args, authArg) else: return "%s%s%s?%s" %(scheme, domain, uri, authArg) def genTypeBUrl(url, key, ts): scheme, domain, uri, args = splitUrl(url) ts_str = time.strftime('%Y%m%d%H%M', time.localtime(ts)) text = "%s%s%s" % (key, ts_str, uri) hash = getMd5(text) return "%s%s/%s/%s%s%s" %(scheme, domain, ts_str, hash, uri, args) def genTypeCUrl(url, key, ts): scheme, domain, uri, args = splitUrl(url) ts = "%x" %(int(ts)) text = "%s%s%s" %(key, uri, ts) hash = getMd5(text) return "%s%s/%s/%s%s%s" %(scheme, domain, hash, ts, uri, args) def genTypeDUrl(url, key, signName, tName, ts, ts_base): scheme, domain, uri, args = splitUrl(url) ts_str = "%d" %(int(ts)) if ts_base == 16: ts_str = "%x" %(int(ts)) text = "%s%s%s" %(key, uri, ts_str) hash = getMd5(text) authArg = "%s=%s&%s=%s" %(signName, hash, tName, ts_str) if args: return "%s%s%s%s&%s" %(scheme, domain, uri, args, authArg) else: return "%s%s%s?%s" %(scheme, domain, uri, authArg) def genTypeEUrl(url, key, signName, timeName, ts, ts_base): scheme, domain, uri, args = splitUrl(url) ts_str = "%d" %(int(ts)) if ts_base == 16: ts_str = "%x" %(int(ts)) text = "%s%s%s%s" %(key, domain, uri, ts_str) hash = getMd5(text) authArg = "%s=%s&%s=%s" %(signName, hash, timeName, ts_str) if args: return "%s%s%s%s&%s" %(scheme, domain, uri, args, authArg) else: return "%s%s%s?%s" %(scheme, domain, uri, authArg) def test(): url = 'http://www.test.com/a.txt?a=b&c=d' primaryKey = 'primary123456' signName = "auth_key" timeName = "t" uid = "0" ts = time.time() + 3600 #默认一小时过期 print("OriginUrl:{}".format(url)) print("TypeA:{}".format(genTypeAUrl(url, primaryKey, signName, uid, ts))) print("TypeB:{}".format(genTypeBUrl(url, primaryKey, ts))) print("TypeC:{}".format(genTypeCUrl(url, primaryKey, ts))) print("TypeD:{}".format(genTypeDUrl(url, primaryKey, signName, timeName, ts, 10))) print("TypeE:{}".format(genTypeEUrl(url, primaryKey, signName, timeName, ts, 10))) if __name__ == "__main__": test() \`\`\` `}> </RenderMd></Tabs.TabPane> <Tabs.TabPane title="PHP" key="yoOMKjM8dJ"><RenderMd content={`\`\`\`PHP <?php date_default_timezone_set('Asia/Shanghai'); function getRandomString($length = 10) { return substr(str_shuffle(str_repeat($x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length/strlen($x)) )),1,$length); } function genTypeAUrl($url, $key, $sign, $uid, $ts) { $rand = getRandomString(); $arr = parse_url($url); $text = $arr["path"]."-".$ts."-".$rand."-".$uid."-".$key; $hash = md5($text); $authArg = "{$sign}={$ts}-{$rand}-{$uid}-{$hash}"; $result = "{$arr["scheme"]}://{$arr["host"]}{$arr["path"]}"; if (array_key_exists("query", $arr) ) { $result = $result."?{$arr["query"]}&{$authArg}"; } else { $result = $result."?{$authArg}"; } return $result; } function genTypeBurl($url, $key, $ts) { $arr = parse_url($url); $tnow = date('YmdHi', $ts); $text = "{$key}{$tnow}{$arr["path"]}"; $hash = md5($text); $result = "{$arr["scheme"]}://{$arr["host"]}/{$tnow}/{$hash}{$arr["path"]}"; if (array_key_exists("query", $arr) ) { $result = $result."?{$arr["query"]}"; } return $result; } function genTypeCUrl($url, $key, $ts) { $ts = dechex($ts); $arr = parse_url($url); $text = "{$key}{$arr["path"]}{$ts}"; $hash = md5($text); $result = "{$arr["scheme"]}://{$arr["host"]}/{$hash}/{$ts}{$arr["path"]}"; if (array_key_exists("query", $arr) ) { $result = $result."?{$arr["query"]}"; } return $result; } function genTypeDUrl($url, $key, $signName, $timeName, $ts, $ts_base) { $arr = parse_url($url); $ts_str = "$ts"; if ($ts_base==16) { $ts_str = dechex($ts); } $text = "{$key}{$arr["path"]}{$ts_str}"; $hash = md5($text); $authArg = "{$signName}={$hash}&{$timeName}={$ts_str}"; $result = "{$arr["scheme"]}://{$arr["host"]}{$arr["path"]}"; if (array_key_exists("query", $arr) ) { $result = $result."?{$arr["query"]}&{$authArg}"; } else { $result = $result."?{$authArg}"; } return $result; } function genTypeEUrl($url, $key, $signName, $timeName, $ts, $ts_base) { $arr = parse_url($url); $ts_str = "$ts"; if ($ts_base==16) { $ts_str = dechex($ts); } $text = "{$key}{$arr["host"]}{$arr["path"]}{$ts_str}"; $hash = md5($text); $authArg = "{$signName}={$hash}&{$timeName}={$ts_str}"; $result = "{$arr["scheme"]}://{$arr["host"]}{$arr["path"]}"; if (array_key_exists("query", $arr) ) { $result = $result."?{$arr["query"]}&{$authArg}"; } else { $result = $result."?{$authArg}"; } return $result; } function test() { $url = "http://www.test.com/a.txt?a=b&c=d"; $primaryKey = "primary123456"; $signName = "auth_key"; $timeName = "t"; $uid = "0"; $ts = time() + 3600;//默认一小时过期 echo("OriginUrl: $url "); echo("TypeA: ".genTypeAUrl($url, $primaryKey, $signName, $uid, $ts)." "); echo("TypeB: ".genTypeBUrl($url, $primaryKey, $ts)." "); echo("TypeC: ".genTypeCUrl($url, $primaryKey, $ts)." "); echo("TypeD: ".genTypeDUrl($url, $primaryKey, $signName, $timeName, $ts, 10)." "); echo("TypeE: ".genTypeEUrl($url, $primaryKey, $signName, $timeName, $ts, 10)." "); } test(); ?> \`\`\` `}> </RenderMd></Tabs.TabPane> <Tabs.TabPane title="JavaScript" key="p7lnAvJGcn"><RenderMd content={`\`\`\`JavaScript import crypto from "crypto"; // 使用正则表达式匹配链接的各个部分 function splitUrl(url) { const parsed = new URL(url); return [url, parsed.protocol + "//", parsed.host, parsed.pathname, parsed.search]; } // 计算字符串的 md5 值 function getMd5(str) { return crypto.createHash("md5").update(str).digest("hex"); } // 生成随机字符串 function getRandomString(length) { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; for (let i = 0; i < length; i++) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; } function genTimeString(ts) { const date = new Date(ts); return \`\${date.getFullYear()}\${date.getMonth()}\${date.getDate()}\${date.getHours()}\${date.getMinutes()}\` } // 生成鉴权链接 function GenTypeAUrl(url, key, signName, uid, ts) { const params = splitUrl(url); const [, scheme, host, path, args] = params; const randStr = getRandomString(10); const text = \`\${path}-\${ts}-\${randStr}-\${uid}-\${key}\`; const hash = getMd5(text); const authArg = \`\${signName}=\${ts}-\${randStr}-\${uid}-\${hash}\`; if (!args) { return \`\${scheme}\${host}\${path}?\${authArg}\`; } else { return \`\${scheme}\${host}\${path}\${args}&\${authArg}\`; } } function GenTypeBUrl(url, key, ts) { const params = splitUrl(url); const [, scheme, host, path, args] = params; const tsStr = genTimeString(ts*1000); const text = \`\${key}\${tsStr}\${path}\`; const hash = getMd5(text); return \`\${scheme}\${host}/\${tsStr}/\${hash}\${path}\${args}\`; } function GenTypeCUrl(url, key, ts) { const params = splitUrl(url); const [, scheme, host, path, args] = params; const hexts = Number(ts).toString(16); const text = \`\${key}\${path}\${hexts}\`; const hash = getMd5(text); return \`\${scheme}\${host}/\${hash}/\${hexts}\${path}\${args}\`; } function GenTypeDUrl(url, key, signName, timeName, ts, base) { const params = splitUrl(url); const [, scheme, host, path, args] = params; let tsStr = Number(ts).toString();; if (base===16){ tsStr = Number(ts).toString(16); } const text = \`${key}${path}${tsStr}\`; const hash = getMd5(text); const authArg = \`${signName}=${hash}&${timeName}=${tsStr}\`; if (args === ""){ return \`${scheme}${host}${path}?${authArg}\`; }else { return \`${scheme}${host}${path}${args}&${authArg}\`; } } function GenTypeEUrl(url, key, signName, timeName, ts, base) { const params = splitUrl(url); const [, scheme, host, path, args] = params; let tsStr = Number(ts).toString();; if (base===16){ tsStr = Number(ts).toString(16); } const text = \`${key}${host}${path}${tsStr}\`; const hash = getMd5(text); const authArg = \`${signName}=${hash}&${timeName}=${tsStr}\`; if (args === ""){ return \`${scheme}${host}${path}?${authArg}\`; }else { return \`${scheme}${host}${path}${args}&${authArg}\`; } } function main() { const url = "http://www.test.com/a.txt?a=b&c=d"; const primaryKey = "primary123456"; const signName = "auth_key"; const timeName = "t" const uid = "0"; const ts = Math.floor(Date.now() / 1000) + 3600; // 10 位时间戳 console.log("OriginUrl:", url); console.log("TypeA: ", GenTypeAUrl(url, primaryKey, signName, uid, ts)); console.log("TypeB: ", GenTypeBUrl(url, primaryKey, ts)); console.log("TypeC: ", GenTypeCUrl(url, primaryKey, ts)); console.log("TypeD: ", GenTypeDUrl(url, primaryKey, signName, timeName, ts, 10)); console.log("TypeE: ", GenTypeEUrl(url, primaryKey, signName, timeName, ts, 10)); } // 执行 main() \`\`\` `}> </RenderMd></Tabs.TabPane> </Tabs>);