亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

一、接口為什么要加密

接口加密傳輸,主要作用:

  • 敏感數(shù)據(jù)防止泄漏、
  • 保護(hù)隱私、
  • 防偽裝攻擊、
  • 防篡改攻擊、
  • 防重放攻擊
  • 等等…
  • 4個(gè)字概括:保護(hù)數(shù)據(jù)!

當(dāng)然不是說(shuō)接口加密后,就能完完全全的保護(hù)我們的數(shù)據(jù),但至少能防一部分人拿到我們的數(shù)據(jù)。
而且接口加密感覺(jué)逼格是不是高過(guò)一點(diǎn)!!!

二、加密思路

1、加密簡(jiǎn)介

加密算法有很多,在能加密又能解密的算法可分為:

  • 非對(duì)稱加密算法,常見(jiàn):RSA、DSA、ECC
    特點(diǎn):算法復(fù)雜,加解密速度慢,但安全性高,一般與對(duì)稱加密結(jié)合使用(對(duì)稱加密對(duì)內(nèi)容加密,非對(duì)稱對(duì)對(duì)稱所使用的密鑰加密)
  • 對(duì)稱加密算法,常見(jiàn):DES、3DES、AES、Blowfish、IDEA、RC5、RC6
    特點(diǎn):加密解密效率高,速度快,適合進(jìn)行大數(shù)據(jù)量的加解密

2、加密流程

思路:
假設(shè)現(xiàn)在客戶端是A,服務(wù)端是B,現(xiàn)在A要去B請(qǐng)求接口

  • 1、A要向B發(fā)送信息,A和B都要產(chǎn)生一對(duì)用于加密的非對(duì)稱加密公私鑰(AB各自生成自己的公私鑰)
  • 2、A的私鑰保密,A的公鑰告訴B;B的私鑰保密,B的公鑰告訴A。(AB互換公鑰)
  • 3、A要給B發(fā)送信息時(shí),A用B的公鑰加密信息,因?yàn)锳知道B的公鑰。(公鑰加密只有私鑰能解)
  • 4、A將這個(gè)消息發(fā)給B(已經(jīng)用B的公鑰加密消息)。
  • 5、B收到這個(gè)消息后,B用自己的私鑰解密A的消息。其他人收到這個(gè)報(bào)文都無(wú)法解密,因?yàn)橹挥蠦才有B的私鑰。

雖然這樣就實(shí)現(xiàn)了接口的加密方式,但是呢,非對(duì)稱加密的加解密速度相比對(duì)稱加密速度很慢,當(dāng)傳輸?shù)臄?shù)據(jù)很大時(shí)就更加明顯了。
所以我們對(duì)稱與非對(duì)稱一起用,理解上面的流程之后,我們?cè)谄浠A(chǔ)稍微改下:

  • 在A給B發(fā)信息的時(shí)候,隨機(jī)生成一個(gè)對(duì)稱加密的密鑰,然后用剛生成的密鑰加密信息,然后用B的公鑰加密剛生成的對(duì)稱密鑰。
  • A把加密的兩個(gè)信息發(fā)送給B。B收到數(shù)據(jù)之后,先用自己的私鑰解開(kāi)得到對(duì)稱密鑰,然后再用解開(kāi)的對(duì)稱密鑰解開(kāi)對(duì)稱加密的信息,最終得到A傳來(lái)的信息。

三、代碼實(shí)現(xiàn)

  • 在當(dāng)下JAVA還是SpringBoot為主流框架工作面試必備,今天還是以它來(lái)舉例。
  • 加解密代碼怎么寫(xiě),這個(gè)時(shí)候網(wǎng)上已經(jīng)有很多現(xiàn)成的庫(kù)了,不用我們操心,我們想的是如何在接口加解密的時(shí)候不影響我們自己的業(yè)務(wù),也就是不用更改我們已經(jīng)寫(xiě)好的代碼。
  • 很多人的第一反應(yīng)應(yīng)該就是AOP吧,對(duì)的沒(méi)錯(cuò)可以使用AOP進(jìn)行環(huán)繞增強(qiáng)。也可以使用@ControllerAdvice 對(duì)Controller進(jìn)行增強(qiáng)(本文以它來(lái)做為例子)。
  • Spring 提供兩個(gè)接口RequestBodyAdvice、ResponseBodyAdvice。實(shí)現(xiàn)它們,即可對(duì)Controller進(jìn)行增強(qiáng),第一個(gè)是在controller之前增強(qiáng),第二個(gè)就是對(duì)controller 的返回值進(jìn)行增強(qiáng)。
  • 在spring啟動(dòng)的時(shí)候會(huì)對(duì)RequestMAppingHandlerAdapter的initControllerAdviceCache()方法進(jìn)行初始化。會(huì)去把有@ControllerAdvice的類進(jìn)行注入。

1、自定義類

下面就來(lái)實(shí)現(xiàn)上面的兩個(gè)接口實(shí)現(xiàn)類代碼

EncryptRequestAdvice.java

  • 這個(gè)類的功能就是在請(qǐng)求到controller之前就把前端傳上來(lái)的數(shù)據(jù)解密好
  • 我們還要校驗(yàn)是否有必要解密
@ControllerAdvice(basePackages = {"top.lrshuai.encrypt.controller"})
public class EncryptRequestAdvice implements RequestBodyAdvice {

    @Autowired
    private KeyConfig keyConfig;

    /**
     * 是否需要解碼
     */
    private boolean isDecode;

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // 方法或類上有注解
        if (Utils.hasMethodAnnotation(methodParameter,new Class[]{Encrypt.class,Decode.class})) {
            isDecode=true;
            // 這里返回true 才支持
            return true;
        }
        return false;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        if(isDecode){
            return new DecodeInputMessage(httpInputMessage, keyConfig);
        }
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // 這里就是已經(jīng)讀取到body了,obj就是
        return obj;
    }

    @Override
    public Object handleEmptyBody(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // body 為空的時(shí)候調(diào)用
        return obj;
    }

}
  • 在上面實(shí)現(xiàn)類中需要重寫(xiě):supports()、beforeBodyRead()、afterBodyRead()、handleEmptyBody() 方法
  • 只有在supports() 返回true 后面的方法才會(huì)支持執(zhí)行。在RequestResponseBodyAdviceChain有判斷
  • 我們可以在beforeBodyRead()這個(gè)方法進(jìn)行解密處理。
  • 在上面的代碼中,我加了自定義注解,因?yàn)榭赡苄枨笫沁@樣的,有些接口加密有些接口不加密,用自定義注解比較方便。
  • 然后DecodeInputMessage 這個(gè)類是自定義實(shí)現(xiàn)了HttpInputMessage接口,解碼邏輯都在里面。如下:

DecodeInputMessage.java

這個(gè)類就是具體的解碼邏輯了

public class DecodeInputMessage implements HttpInputMessage {

    private HttpHeaders headers;

    private InputStream body;

    public DecodeInputMessage(HttpInputMessage httpInputMessage, KeyConfig keyConfig) {
        // 這里是body 讀取之前的處理
        this.headers = httpInputMessage.getHeaders();
        String encodeAesKey = "";
        List<String> keys = this.headers.get(Result.KEY);
        if (keys != null && keys.size() > 0) {
            encodeAesKey = keys.get(0);
        }
        try {
            // 1、解碼得到aes 密鑰
            String decodeAesKey = RsaUtils.decodeBase64ByPrivate(keyConfig.getRsaPrivateKey(), encodeAesKey);
            // 2、從inputStreamReader 得到aes 加密的內(nèi)容
            String encodeAesContent = new BufferedReader(new InputStreamReader(httpInputMessage.getBody())).lines().collect(Collectors.joining(System.lineSeparator()));
            // 3、AES通過(guò)密鑰CBC解碼
            String aesDecode = AesUtils.decodeBase64(encodeAesContent, decodeAesKey, keyConfig.getAesIv().getBytes(), AesUtils.CIPHER_MODE_CBC_PKCS5PADDING);
            if (!StringUtils.isEmpty(aesDecode)) {
                // 4、重新寫(xiě)入到controller
                this.body = new ByteArrayInputStream(aesDecode.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return headers;
    }
}
  • 上面的代碼注釋我覺(jué)得都寫(xiě)的清楚了,不多介紹。

EncryptResponseAdvice.java

  • 這個(gè)類的主要功能就是對(duì)返回值進(jìn)行加密操作
  • 直接在beforeBodyWrite()里面執(zhí)行具體的加密操作即可
  • supports()方法也是需要返回true,在RequestResponseBodyAdviceChain.processBody()中有個(gè)判斷只有supports()返回true才會(huì)執(zhí)行beforeBodyWrite()
@Slf4j
@ControllerAdvice(basePackages = {"top.lrshuai.encrypt.controller"})
public class EncryptResponseAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private KeyConfig keyConfig;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // return true 有效
        return true;
    }

    /**
     * 返回結(jié)果加密
     * @param obj 接口返回的對(duì)象
     * @param methodParameter method
     * @param mediaType  mediaType
     * @param aClass HttpMessageConverter class
     * @param serverHttpRequest request
     * @param serverHttpResponse response
     * @return obj
     */
    @Override
    public Object beforeBodyWrite(Object obj, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 方法或類上有注解
        if (Utils.hasMethodAnnotation(methodParameter,new Class[]{Encrypt.class, Encode.class})) {
            // 這里假設(shè)已經(jīng)定義好返回的model就是Result
            if (obj instanceof Result) {
                try {
                    // 1、隨機(jī)aes密鑰
                    String randomAesKey = AesUtils.generateSecret(256);
                    // 2、數(shù)據(jù)體
                    Object data = ((Result) obj).getData();
                    // 3、轉(zhuǎn)json字符串
                    String jsonString = JSON.toJSONString(data);
                    // 4、aes加密數(shù)據(jù)體
                    String aesEncode = AesUtils.encodeBase64(jsonString, randomAesKey,keyConfig.getAesIv().getBytes(),AesUtils.CIPHER_MODE_CBC_PKCS5PADDING);
                    // 5、重新設(shè)置數(shù)據(jù)體
                    ((Result) obj).put(Result.DATA,aesEncode);
                    // 6、使用前端的rsa公鑰加密 aes密鑰 返回給前端
                    ((Result) obj).put(Result.KEY,RsaUtils.encodeBase64PublicKey(keyConfig.getFrontRsaPublicKey(),randomAesKey));
                    // 7、返回
                    return obj;
                } catch (Exception e) {
                   log.error("加密失敗:",e);
                }
            }
        }
        return obj;
    }
}

看代碼注釋,不說(shuō)了。

2、加密工具類

加密工具類,我在網(wǎng)上收集整理了一下,搞了個(gè)jar。直接在pom.xml 引入即可。如下:

<dependency>
	<groupId>top.lrshuai.encryption</groupId>
	<artifactId>encryption-tools</artifactId>
	<version>1.0.3</version>
</dependency>

自此核心代碼都講完了,這里只是給出了個(gè)demo,可以參考一下(代碼寫(xiě)的也不是很好,很多地方也沒(méi)有封裝),加密方式多種多樣,都是可以自由更改,這種加密方式不喜歡就改。
差點(diǎn)忘記了,前端代碼呢。

3、前端代碼

前端也是在Github分別找了兩個(gè)庫(kù):

  • jsencrypt
    這個(gè)是RSA加密庫(kù),這個(gè)是在原版的jsencrypt進(jìn)行增強(qiáng)修改,原版的我用過(guò)太長(zhǎng)數(shù)據(jù)加密失敗,多此加密解密失敗,所以就用了這個(gè)庫(kù)。
  • CryptoJS
    AES加密庫(kù),這個(gè)庫(kù)是google開(kāi)源的,有AES、MD5、SHA 等加密方法

然后我使用的是Vue寫(xiě)的簡(jiǎn)單頁(yè)面(業(yè)余前端)

html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>請(qǐng)求</title>

    <style>
        #app {
            width: 500px;
            height: 500px;
            margin: 100px auto;
        }

        .mytable {
            border: 1px solid #A6C1E4;
            font-family: Arial;
            border-collapse: collapse;
        }

        table th {
            border: 1px solid black;
            background-color: #71c1fb;
            width: 100px;
            height: 20px;
            font-size: 15px;
        }

        table td {
            border: 1px solid #A6C1E4;
            text-align: center;
            height: 15px;
            padding-top: 5px;
            font-size: 12px;
        }

        .double {
            background-color: #c7dff6;
        }

        input {
            width: 95%;
            padding-left: 10px;
        }
    </style>
</head>
<body>
<div id="app">
    <table class="mytable">
        <tr class="double">
            <th>字段:</th>
            <th>Value:</th>
        </tr>
        <tr class="double">
            <td>userId:</td>
            <td><input v-model="userInfo.userId"></td>
        </tr>
        <tr class="double">
            <td>userName:</td>
            <td><input v-model="userInfo.userName"></td>
        </tr>
        <tr class="double">
            <td>age:</td>
            <td><input v-model="userInfo.age"></td>
        </tr>
        <tr class="double">
            <td>info:</td>
            <td>
                <textarea v-model="userInfo.info" cols="50" rows="5" placeholder="隨便輸一點(diǎn)"></textarea>
            </td>
        </tr>
        <tr class="double">
            <td>AES密鑰:</td>
            <td>
                <textarea v-model="aes.key" cols="50" rows="2" placeholder="AES密鑰"></textarea>
            </td>
        </tr>
        <tr class="double">
            <td>AES向量:</td>
            <td>
                <textarea v-model="aes.iv" cols="50" rows="1" placeholder="向量的長(zhǎng)度為16位"></textarea>
            </td>
        </tr>
    </table>
    <button @click="testRequest">發(fā)送測(cè)試請(qǐng)求</button>
    <br>
    <div>
        <p>要發(fā)送的數(shù)據(jù):<span>{{parameter}}</span></p>
        <p>加密后的數(shù)據(jù):{{encodeContent}}</p>
        <br>
        <p>收到服務(wù)端的內(nèi)容:{{result}}</p>
        <p>解密服務(wù)端AES密鑰內(nèi)容:{{decodeAes}}</p>
        <p>最終拿到服務(wù)端的內(nèi)容:{{decodeContent}}</p>

    </div>
</div>
<script src="../static/js/vue.min.js" th:src="@{js/vue.min.js}"></script>
<script src="../static/js/rsa/jsencrypt.min.js" th:src="@{js/rsa/jsencrypt.min.js}"></script>
<script src="../static/js/aes/aes.js" th:src="@{js/aes/aes.js}"></script>
<script src ="https://unpkg.com/axIOS/dist/axios.min.js"></script>
<script>
    window.onload = function () {
        var vm = new Vue({
            el: '#app',
            data: {
                rsa: {
                    // 自己的rsa公私鑰
                    mePublicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCf0B4Al2wIuGK9Bj9Ao23siR2mMfkvdrxEGu2j0tNeA1LSyKOuw7FLmreRMYLCQMI4BTJNYsxUqvdS8IxFpD5hOx9mx6OqY2GQSIZq5a1lt3Rx4SpDiuuVGm7h5uuLN7bvMfaLBW3g4E5DAKapuZ/u5ULO+y2jczVXkaSb1IjNnwIDAQAB",
                    mePrivateKey:"MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ/QHgCXbAi4Yr0GP0CjbeyJHaYx+S92vEQa7aPS014DUtLIo67DsUuat5ExgsJAwjgFMk1izFSq91LwjEWkPmE7H2bHo6pjYZBIhmrlrWW3dHHhKkOK65UabuHm64s3tu8x9osFbeDgTkMApqm5n+7lQs77LaNzNVeRpJvUiM2fAgMBAAECgYAq4FxcTkPm5wleq4Fm5zIDxxnUUA4J5PJH122wiUy6KWwcL0ZzCf/UR/M+Gil50oQJIaPITVyCzsfCUdVgjdtKL7x8e1dQwlI3/DLEat02Njj4fl6KsMq9EqLyleq0UdgYtevZOOoi+ZKXlqZjkM3yOsbwyu9u0D+s77KfHihwuQJBAODhWKTLywJwSXPC6CvlSoyCjscWgUadk8IN+ELyLq591DYFCQllYQPyMj8Cy0dY5OC9GvwRLZurs9LGi6C9d0UCQQC17a76RNHqmmGKsEEGIx3XIzvDrjSRmE3v+NLMcf+JUaUJiKmedDZeWnJuxIXVmFbHi2bzCb2NXUYqhuXsuJ2TAkBHxSO5VKEx4gxPOcFHYSJtva07tN8FXn0tza+SDiD/54C2zNyZdxWDYOTQX1/pIWHKqA/YqtLXf/EgL+WYI1/RAkAk+RwRgsECo8NlEzLz01kyKtfvicznNgPI3FHC+PwM5UncKSkHqeiOvmT5O/lTEnW4cg1HIVijjSxAYlACDvb/AkBmwlv6+gLoKpCU6h7+J6OxB9GKM2Hjs3Mh5tgXgveCwMg2Knz+RPIj92jq7CLm20xs2654yYnyHc4V+kzr3Zu1",
                    // 后端的rsa公鑰
                    backPublicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJvCDW9GIBsiv9ma9r2btffIxQQHB98Pl1S2RV2PrQsK1O2yFSUf8P43l5EfAh+jiEn/k5egKEoeMRLdDZkt5afNgPYbNjiRFJP8NZTw4f3Yxp91+d04GGkeFcj59QIn/rqqHo2JLOESNae8IC1tKKQTqkwVIjLRwTIDcVmsq9NwIDAQAB"
                },
                aes: {
                    iv: "123456789abcdefh",
                    key:"",
                    encodeKey:""
                },
                userInfo: {
                    userId: "1",
                    userName: "rstyro",
                    age: 20,
                    info: "信息內(nèi)容......",
                },
                parameter: "",
                encodeContent: "",
                decodeContent: "",
                decodeAes: "",
                result: ""
            },
            http: {
                root: '/',
                headers: {
                    loginToken: "asdb",
                }
            },
            methods: {
                testRequest: function () {
                    // 隨機(jī)生成32位  aes 密鑰
                    this.aes.key=generateKey();
                    // 參數(shù)轉(zhuǎn)json 字符串
                    this.parameter = JSON.stringify(this.userInfo);
                    // aes 加密
                    this.encodeContent = aesEncode(this.parameter,this.aes.iv,this.aes.key);
                    // rsa 后端公鑰加密aes密鑰
                    let encodeAesKey = rsaEncode(this.rsa.backPublicKey,this.aes.key);
                    this.aes.encodeKey=encodeAesKey;
                    console.log("encodeAeskey:",this.aes.encodeKey);
                    axios.post('http://localhost:8800/test1', this.encodeContent,{
                        headers: {
                            "Content-Type": "application/json;charset=utf-8",
                            key:encodeAesKey
                        }
                    }).then(function (response) {
                        console.log("response:",response);
                        // 1、服務(wù)端返回的數(shù)據(jù)
                        vm.result=response.data;
                        // 2、rsa 解密拿到aes密鑰
                        vm.decodeAes = rsaDecode(vm.rsa.mePrivateKey,vm.result.key);
                        // 3、aes 解密
                        vm.decodeContent = aesDecode(vm.result.data,vm.aes.iv,vm.decodeAes);
                    }).catch(function (error) {
                        console.log("error:",error);
                    });
                }
            }
        });
    }

    // aes 加密
    function aesEncode(content,iv,aesKey){
        iv = CryptoJS.enc.Utf8.parse(iv);
        aesKey = CryptoJS.enc.Utf8.parse(aesKey);
        let encrypted = CryptoJS.AES.encrypt(content, aesKey, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return  encrypted.toString();
    }

    // aes 解密
    function aesDecode(encrypted,iv,aesKey){
        iv = CryptoJS.enc.Utf8.parse(iv);
        aesKey = CryptoJS.enc.Utf8.parse(aesKey);
        var decrypted = CryptoJS.AES.decrypt(encrypted, aesKey, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        // 轉(zhuǎn)換為 utf8 字符串
        return CryptoJS.enc.Utf8.stringify(decrypted);
    }

    // rsa 公鑰加密
    function rsaEncode(publicKey,content){
        // 加密+base64
        const encrypt = new JSEncrypt();
        // 設(shè)置公鑰
        encrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + publicKey + '-----END PUBLIC KEY-----');
        return encrypt.encryptLong(content);
    }

    // rsa 私鑰解密
    function rsaDecode(privateKey,content){
        // 加密+base64
        const encrypt = new JSEncrypt();
        encrypt.setPrivateKey(privateKey);
        return encrypt.decryptLong(content);
    }

    //隨機(jī)生成aes 密鑰
    function generateKey(){
        return CryptoJS.lib.wordArray.random(128/8).toString();
    }

</script>
</body>
</html>

主要看testRequest() 這個(gè)方法就行了,都有代碼注釋。

注意點(diǎn)

  • 后端需要注意的就是,controller參數(shù)需要用@RequestBody包起來(lái),如下:
  • @PostMapping("/test1")
    @ResponseBody
    public Object test1(@RequestBody(required = false) TestDto dto){
    System.out.println("dto="+dto);
    return Result.ok(dto);}
  • 而前端傳上來(lái)的時(shí)候header需要設(shè)置"Content-Type": "application/json;charset=utf-8"

最終效果

 


 

在上面的postman中

  • data:里面的數(shù)據(jù)就是aes加密后的數(shù)據(jù)
  • key:里面就是前端RSA公鑰加密后的AES密鑰(前端需要用私鑰解密得到aes密鑰,然后再用密鑰解開(kāi)data里面的數(shù)據(jù))
  • status:這個(gè)是狀態(tài)碼,如果報(bào)錯(cuò)了就不是200,不然報(bào)錯(cuò)了返回的數(shù)據(jù),前端解幾百年都解不開(kāi)。

4、源碼地址

https://gitee.com/rstyro/spring-boot/tree/master/Springboot2-api-encrypt
  • 作者:rstyro
  • 來(lái)源: https://rstyro.github.io/blog/2020/10/22/Springboot2接口加解密全過(guò)程詳解(含前端代碼)/

分享到:
標(biāo)簽:SpringBoot
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定