SpringBoot + Vue 实现 AES 加密和 AES 工具类总结

目录

一、加密知识回顾 

AES加密模式

二、Java 自定义 AES 工具类 

三、SpringBoot 实现 AES 加密登陆

controller 层

server 层

四、Vue 实现 AES 加密登陆

五、前端AES工具类

六、实现结果


一、加密知识回顾 

        密钥是AES算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥。
AES支持三种长度的密钥:  128位,192位,256位 
平时大家所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用。从安全性来看,AES256 安全性最高。从性能来看,AES128 性能最高。本质原因是它们的加密处理轮数不同。

对称加密与非对称加密有什么区别,敏感数据怎么加解密和传输

AES加密模式

1 : ECB(Electronic Code Book电子密码本)模式
ECB模式是最早采用和最简单的模式,它将加密的数据分成若干组,每组的大小跟加密密钥长度相同,然后每组都用相同的密钥进行加密。


优点:1、简单;2:有利于并行计算;3、误差不会被传送; 
缺点:1、不能隐藏明文的模式;2、可能对明文进行主动攻击;
因此,此模式适于加密小消息。

2 : CBC(Cipher Block Chaining,加密块链)模式
优点:1、不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
缺点:1、不利于并行计算;2、误差传递;3、需要初始化向量IV

3 : CFB(Cipher FeedBack Mode,加密反馈)模式
优点:1、隐藏了明文模式;2、分组密码转化为流模式;3、可以及时加密传送小于分组的数据;
缺点:1、不利于并行计算;2、误差传送:一个明文单元损坏影响多个单元;3、唯一的IV;

4 : OFB(Output FeedBack,输出反馈)模式
优点:1、隐藏了明文模式;2、分组密码转化为流模式;3、可以及时加密传送小于分组的数据;
缺点:1、不利于并行计算;2、对明文的主动攻击是可能的;3、误差传送:一个明文单元损坏影响多个单元;

        ECB 不够安全,只适合于短数据的加密,而 CBC 和 CFB 相较于 ECB 更加安全,因为前一个密文块会影响当前明文块,使攻击者难以预测密文的结构。总的来说,选择 AES 的算法模式需要根据加密需要的安全性和速度来进行选择,通常推荐使用CBC 或 CFB 模式,而不是 ECB 模式。

二、Java 自定义 AES 工具类 

AES 算法在 Java 的 javax.crypto 包里有很好的封装,我们来看一下调用的方法:

public class AesSingleUtil {
    private static volatile AesSingleUtil aESUtil;
    private static String encodeRules = "9JLsB1ukY3o4mdTuuE90+Q==";

    private AesSingleUtil() {
    }

    public static AesSingleUtil getSingleton() {
        if (aESUtil == null) {
            Class var0 = AesSingleUtil.class;
            synchronized(AesSingleUtil.class) {
                if (aESUtil == null) {
                    aESUtil = new AesSingleUtil();
                }
            }
        }

        return aESUtil;
    }

    public String AESEncode(String content) throws Exception {
        KeyGenerator keygen = KeyGenerator.getInstance("AES");
        keygen.init(128, new SecureRandom(encodeRules.getBytes()));
        SecretKey original_key = keygen.generateKey();
        byte[] raw = original_key.getEncoded();
        SecretKey key = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(1, key);
        byte[] byte_encode = content.getBytes("utf-8");
        byte[] byte_AES = cipher.doFinal(byte_encode);
        String AES_encode = new String(Base64.getEncoder().encode(byte_AES));
        return AES_encode;
    }

    public String AESDecode(String content) throws Exception {
        KeyGenerator keygen = KeyGenerator.getInstance("AES");
        keygen.init(128, new SecureRandom(encodeRules.getBytes()));
        SecretKey original_key = keygen.generateKey();
        byte[] raw = original_key.getEncoded();
        SecretKey key = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(2, key);
        byte[] byte_content = Base64.getDecoder().decode(content);
        byte[] byte_decode = cipher.doFinal(byte_content);
        String AES_decode = new String(byte_decode, "utf-8");
        return AES_decode;
    }
}

 AesSingleUtil 类:这是一个单例类,使用了双重检查锁(double-checked locking)实现按需延迟初始化。在 getSingleton 方法中,通过检查静态变量 aESUtil 是否为 null,来确定是否需要创建新的实例。通过单例模式确保了整个应用程序中只有一个实例存在,节省了资源并避免了不必要的重复创建对象。

encodeRules 变量:存储着用于生成 AES 密钥的编码规则。

AESEncode 方法:接受一个字符串作为参数,使用该字符串作为密钥的种子,生成 AES 密钥,并对输入的内容进行 AES 加密,最终返回 Base64 编码的加密结果。

AESDecode 方法:与 AESEncode 方法相对应,接受一个经过 Base64 编码的加密字符串作为参数,使用同样的密钥规则生成 AES 密钥,并对传入的加密字符串进行 AES 解密,返回解密后的原始字符串。

import com.ss.check.NotNullCheck;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AesWebUtil {
    private static Logger log = LoggerFactory.getLogger(AesWebUtil.class);
    private static String KEY = "kl9o56u98gvjhw9i";
    private static String IV = "grjei564389tj843";

    public AesWebUtil() {
    }

    public String encrypt(String data, String key, String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            int blockSize = cipher.getBlockSize();
            byte[] dataBytes = data.getBytes("UTF-8");
            int plaintextLength = dataBytes.length;
            if (plaintextLength % blockSize != 0) {
                plaintextLength += blockSize - plaintextLength % blockSize;
            }

            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
            cipher.init(1, keyspec, ivspec);
            byte[] encrypted = cipher.doFinal(plaintext);
            return (new Base64()).encodeToString(encrypted);
        } catch (Exception var12) {
            return null;
        }
    }

    public String desEncrypt(String data, String key, String iv) {
        try {
            byte[] encrypted1 = (new Base64()).decode(data.getBytes("UTF-8"));
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
            cipher.init(2, keyspec, ivspec);
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString;
        } catch (Exception var10) {
            return null;
        }
    }

    public static String jiaMi(String data) {
        String mm = "";

        try {
            if (NotNullCheck.str(data)) {
                mm = (new AesWebUtil()).encrypt(data, KEY, IV).trim();
            }
        } catch (Exception var3) {
        }

        return mm;
    }

    public static String jieMi(String data) {
        String mm = "";

        try {
            if (NotNullCheck.str(data)) {
                mm = (new AesWebUtil()).desEncrypt(data, KEY, IV).trim();
            }
        } catch (Exception var3) {
        }

        return mm;
    }

    public static void main(String[] args) throws Exception {
        String jiemi = (new AesWebUtil()).desEncrypt("oLlc7AJnsgl93vaAEYMgGd2/G8zLFX8HrCN9HaBwS9rLJYFT8+RmlReuNHHdX+NacpP6MAByLIdxSeMBQ/9a5HA1oikanQY5I1kTCxpaN639WvA/Rj8I74PicVYemYqiMr/W6dDwpyJt7H5jL7Sofw6bnNmQAtsF9UNQthDS73ddxo19ThLpxOajVE5LF+Mu", KEY, IV).trim();
        System.out.println("解密:" + jiemi);
        new AesWebUtil();
        String jiami = jiaMi("123456");
        System.out.println("加密:" + jiami);
    }
}

三、SpringBoot 实现 AES 加密登陆

controller 层

    @PostMapping("login")
    public String userLogin(@RequestBody String user, HttpServletRequest request) {
        return userService.userLogin(user, request);
    }

server 层

    public String userLogin(String user, HttpServletRequest request) {
        String jiemiUserStr = null;
        try {
            jiemiUserStr = AesWebUtil.jieMi(user);
        } catch (Exception e) {
            throw new SSError(GlobalCodeEnum.SystemLogicError, "非法登录!");
        }
        UserAll userAll = GsonUtil.jsonToObject(jiemiUserStr, UserAll.class);
        if (userAll != null && userAll.getLoginType() != null) {
            UserAll loginBack = null;
            if (userAll.getLoginType() == 1 && NotNullCheck.str(userAll.getUserLoginName())
                    && NotNullCheck.str(userAll.getUserPassword())) {
                loginBack = userMapper.getUserByLoginName(userAll.getUserLoginName());
                if(loginBack==null){
                    throw new SSError(GlobalCodeEnum.SystemLogicError, "用户不存在");
                }else if(loginBack!=null&&!AesWebUtil.jiaMi(userAll.getUserPassword()).equals(loginBack.getUserPassword())){
                    throw new SSError(GlobalCodeEnum.SystemLogicError, "密码不正确");
                }
            } else if (userAll.getLoginType() == 2 && NotNullCheck.str(userAll.getUserCard())) {
                loginBack = userMapper.getUserByUserCard(userAll);
            }
            if (loginBack == null) {
                throw new SSError(GlobalCodeEnum.SystemLogicError, "登录信息错误");
            } else {
                checkUserRightState(loginBack);
                try {
                    loginBack.setUserPassword(JwtUtil.generateToken());
                } catch (Exception e) {
                    e.printStackTrace();
                }
//                log.info("****************登录成功:" + loginBack);
                //埋点登录
                return AesWebUtil.jiaMi(GsonUtil.objectToJson(loginBack));
            }
        }
        throw new SSError(GlobalCodeEnum.SystemLogicError, "登录信息不完整");
    }

四、Vue 实现 AES 加密登陆

<template>
  <common-layout>
    <div class="top">
      <div class="header">
        <img alt="logo" class="logo" src="@/assets/img/logo.png" />
        <span class="title">{{ systemName }}</span>
      </div>
      <div class="desc"></div>
    </div>
    <div class="login">
      <a-form @submit.native="onSubmit" :form="form">
        <a-tabs
          size="large"
          :tabBarStyle="{ textAlign: 'center' }"
          style="padding: 0 2px"
          @change="userLoginTypeChange"
        >
          <a-tab-pane tab="账户密码登录" key="1">
            <a-alert
              type="error"
              :closable="true"
              v-show="error"
              :message="error"
              showIcon
              style="margin-bottom: 24px"
            />
            <a-form-item>
              <a-input
                ref="userNameRef"
                autocomplete="autocomplete"
                size="large"
                allowClear
                placeholder="登录用户名"
                v-decorator="[
                  'name',
                  {
                    rules: [
                      {
                        required: true,
                        message: '请输入账户名',
                        whitespace: true,
                      },
                    ],
                  },
                ]"
              >
                <a-icon slot="prefix" type="user" />
              </a-input>
            </a-form-item>
            <a-form-item>
              <a-input
                size="large"
                placeholder="登录密码"
                autocomplete="autocomplete"
                type="password"
                allowClear
                v-decorator="[
                  'password',
                  {
                    rules: [
                      {
                        required: true,
                        message: '请输入密码',
                        whitespace: true,
                      },
                    ],
                  },
                ]"
              >
                <a-icon slot="prefix" type="lock" />
              </a-input>
            </a-form-item>
          </a-tab-pane>
          <a-tab-pane tab="工号登录" key="2">
            <a-form-item>
              <a-input
                size="large"
                placeholder="员工工号"
                v-model="userCard"
                ref="userCardRef"
                allowClear
              >
                <a-icon slot="prefix" type="solution" />
              </a-input>
            </a-form-item>
            <!-- <a-form-item> -->

            <!-- <a-row :gutter="8" style="margin: 0 -4px">
                <a-col :span="16"> -->

            <!--   <a-input size="large" placeholder="captcha">
                <a-icon slot="prefix" type="lock" />
              </a-input> -->

            <!-- </a-col> -->

            <!-- <a-col :span="8" style="padding-left: 4px">
                  <a-button
                    style="width: 100%"
                    class="captcha-button"
                    size="large"
                    >获取验证码</a-button
                  >
                </a-col> -->

            <!-- </a-row> -->

            <!-- </a-form-item> -->
          </a-tab-pane>
        </a-tabs>
        <!-- <div>
          <a-checkbox :checked="true">自动登录</a-checkbox>
        </div> -->
        <a-form-item>
          <a-button
            :loading="logging"
            style="width: 100%; margin-top: 24px"
            size="large"
            htmlType="submit"
            type="primary"
            >登录</a-button
          >
        </a-form-item>
      </a-form>
    </div>
  </common-layout>
</template>

<script>
import CommonLayout from "@/layouts/CommonLayout";
// import { login, getRoutesConfig } from "@/services/user";
// import { setAuthorization } from "@/utils/request";
// import { loadRoutes } from "@/utils/routerUtil";

import { mapMutations } from "vuex";
import { userLogin } from "@/api/user";

import { aesJiami, aesJiemi } from "@/utils/aes";

const timeList = ["早上好", "上午好", "中午好", "下午好", "晚上好"];

export default {
  name: "Login",
  components: { CommonLayout },
  data() {
    return {
      logging: false,
      error: "",
      form: this.$form.createForm(this),
      userLgoinType: 1,
      userCard: "",
    };
  },
  computed: {
    systemName() {
      return this.$store.state.setting.systemName;
    },
  },
  mounted() {
    // 禁用浏览器返回键
    history.pushState(null, null, document.URL);
    window.addEventListener("popstate", this.disableBrowserBack);

    if (this.$refs.userNameRef) {
      this.$refs.userNameRef.focus();
    }
  },

  destroyed() {
    // 清除popstate事件 否则会影响到其他页面

    window.removeEventListener("popstate", this.disableBrowserBack, false);
  },
  methods: {
    ...mapMutations("account", ["setUser", "setPermissions", "setRoles"]),

    disableBrowserBack() {
      history.pushState(null, null, document.URL);
    },
    userLoginTypeChange(key) {
      setTimeout(() => {
        if (key === "1") {
          this.$refs.userNameRef.focus();
        } else if (key === "2") {
          this.$refs.userCardRef.focus();
        }
      }, 100);

      this.userLgoinType = key;
    },
    onSubmit(e) {
      e.preventDefault();
      let checkUserLoginInput = false;
      if (this.userLgoinType === 1) {
        this.form.validateFields((err) => {
          if (!err) {
            checkUserLoginInput = true;
          }
        });
      } else {
        if (
          this.userCard &&
          this.userCard.replace(/↵/g, "").replace(/[/n/r]/g, "")
        ) {
          checkUserLoginInput = true;
        }
      }

      if (!checkUserLoginInput) {
        return;
      }
      this.logging = true;
      const name = this.form.getFieldValue("name");
      const password = this.form.getFieldValue("password");

      // login(name, password).then(this.afterLogin);
      const userLoginInfo = {
        userLoginName: name,
        userPassword: password,
        userCard: this.userCard,
        loginType: this.userLgoinType,
      };
      userLogin(aesJiami(JSON.stringify(userLoginInfo)))
        .then((res) => {
          if (!res) {
            return;
          }
          try {
            res = JSON.parse(aesJiemi(res));
          } catch (error) {
            this.$message.error("登录失败,服务器校验不通过");
            return;
          }
          let permissions = null;
          if (res) {
            permissions = [{ id: res.userAuth, operation: ["add", "edit"] }];
            if (!permissions) {
              this.$message.error("用户权限校验失败");
              return;
            }
            this.setPermissions(aesJiami(JSON.stringify(permissions)));

            this.$router.push({ name: "主页" });
            const time = new Date();
            const hour = time.getHours();
            const welcomeTime =
              hour < 9
                ? timeList[0]
                : hour <= 11
                ? timeList[1]
                : hour <= 13
                ? timeList[2]
                : hour <= 17
                ? timeList[3]
                : timeList[4];
            this.$message.success(welcomeTime + "," + res.userName);

            const roles = [
              { id: res.userAuth, operation: ["add", "edit", "delete"] },
            ];

            res.name = res.userName;

            this.setRoles(aesJiami(JSON.stringify(roles)));
            this.setUser(aesJiami(JSON.stringify(res)));
          }
        })

        .finally(() => {
          this.logging = false;
        });
    },
    /*  afterLogin(res) {
      this.logging = false;
      const loginRes = res.data;
      if (loginRes.code >= 0) {
        const { user, permissions, roles } = loginRes.data;
        this.setUser(user);
        this.setPermissions(permissions);
        this.setRoles(roles);
        setAuthorization({
          token: loginRes.data.token,
          expireAt: new Date(loginRes.data.expireAt),
        });
        // 获取路由配置
        getRoutesConfig().then((result) => {
          const routesConfig = result.data.data;
          loadRoutes(routesConfig);
          this.$router.push("/dashboard/workplace");
          this.$message.success(loginRes.message, 3);
        });
      } else {
        this.error = loginRes.message;
      }
    }, */
  },
};
</script>

<style lang="less" scoped>
.common-layout {
  .top {
    text-align: center;
    .header {
      height: 44px;
      line-height: 44px;
      a {
        text-decoration: none;
      }
      .logo {
        height: 44px;
        vertical-align: top;
        margin-right: 16px;
      }
      .title {
        font-size: 33px;
        color: @title-color;
        font-family: "Myriad Pro", "Helvetica Neue", Arial, Helvetica,
          sans-serif;
        font-weight: 600;
        position: relative;
        top: 2px;
      }
    }
    .desc {
      font-size: 14px;
      color: @text-color-second;
      margin-top: 12px;
      margin-bottom: 40px;
    }
  }
  .login {
    width: 368px;
    margin: 0 auto;
    @media screen and (max-width: 576px) {
      width: 95%;
    }
    @media screen and (max-width: 320px) {
      .captcha-button {
        font-size: 14px;
      }
    }
    .icon {
      font-size: 24px;
      color: @text-color-second;
      margin-left: 16px;
      vertical-align: middle;
      cursor: pointer;
      transition: color 0.3s;

      &:hover {
        color: @primary-color;
      }
    }
  }
}
</style>

五、前端AES工具类

import CryptoJS from "crypto-js";

const KEY = CryptoJS.enc.Utf8.parse("kl9o56u98gvjhw9i");
const IV = CryptoJS.enc.Utf8.parse("grjei564389tj843");

export const aesJiami = (word, keyStr, ivStr) => {
  let key = KEY;
  let iv = IV;

  if (keyStr) {
    key = CryptoJS.enc.Utf8.parse(keyStr);
    iv = CryptoJS.enc.Utf8.parse(ivStr);
  }

  let srcs = CryptoJS.enc.Utf8.parse(word);
  var encrypted = CryptoJS.AES.encrypt(srcs, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.ZeroPadding,
  });
  // console.log("-=-=-=-", encrypted.ciphertext)
  return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
};

export const aesJiemi = (word, keyStr, ivStr) => {
  let key = KEY;
  let iv = IV;

  if (keyStr) {
    key = CryptoJS.enc.Utf8.parse(keyStr);
    iv = CryptoJS.enc.Utf8.parse(ivStr);
  }

  let base64 = CryptoJS.enc.Base64.parse(word);
  let src = CryptoJS.enc.Base64.stringify(base64);

  var decrypt = CryptoJS.AES.decrypt(src, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.ZeroPadding,
  });

  var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
  return decryptedStr.toString();
};

 这段前端代码的逻辑是:

  1. 导入 CryptoJS 库:通过 import CryptoJS from "crypto-js"; 这行代码导入了 CryptoJS 库,用于进行加密和解密操作。

  2. 定义加密所需的密钥和向量:

    KEY 和 IV 分别是用于加密和解密的密钥和初始化向量。在这段代码中,它们被硬编码为固定值。
  3. 定义 aesJiami 函数:

    aesJiami 函数用于对传入的字符串进行加密操作。如果有自定义的密钥和向量参数,则使用传入的参数,否则使用预设的 KEY 和 IV。将传入的字符串转换为 UTF-8 编码格式。使用 AES 加密算法对字符串进行加密,加密模式为 CBC,填充方式为 ZeroPadding。将加密后的结果转换为 Base64 字符串并返回。
  4. 定义 aesJiemi 函数:

    aesJiemi 函数用于对传入的 Base64 加密字符串进行解密操作。如果有自定义的密钥和向量参数,则使用传入的参数,否则使用预设的 KEY 和 IV。将传入的 Base64 加密字符串解析为 Base64 对象。使用 AES 解密算法对 Base64 对象进行解密,解密模式为 CBC,填充方式为 ZeroPadding。将解密后的结果转换为 UTF-8 编码格式的字符串并返回。

        总的来说,这段代码实现了对字符串的 AES 加密和解密操作,使用了 CryptoJS 库提供的加密算法和相关函数。通过传入不同的参数,可以实现自定义密钥和向量进行加密解密的功能

六、实现结果

{
    "reqID": 1,
    "method": "",
    "sender": "ss-smartdata-web-browser Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
    "token": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MTg5MzkzNzl9.FcPMd60PZQJ8TBrkRZLf3gL76Vr1w9paSfgZPYZlDKNT0h74_zF8Wa8DwrB0Gciw-zneaBtUc1CBAa6yF2RmRQ",
    "reqData": "cThk+vaRL/B41LkCUVrshuT0R8xfnKGh56D+NeqC6HC2PdfcWwFL5RU3gY/sjsc39e156rj29ryANn1LZ29pL5bfN6EIq2oVuqC34GQeu7g="
}

{
   result: 1, reqID: -1, respID: -1, sender: "", sendee: "", msg: "请求成功",…
}

msg : "请求成功"
reqID : -1
respData:"nWOD3Sw2iLf1xevNYnHuHGvPFr/TAJpviyh/M22qLYuiaHoqvxhCn9OF2WGitiKr+LxTbEJvuBsbCmetAPCo7b48WG0PzsHz3fQxlOsQKeh629Z18gyBKho4zEAayHHjhP4rrHjSNNTwoZzAD0n6Cj2HTx+COXH9KtL905HZC3y0+mn1n72BCN2nxVExAu+8cBP+N66MJWVQLyj+7ENWJ0Euq22v3xWWoX2fFKe2XZr8Y7taMRkSNEyfYpgyq3Tl1az7A3I6+eYdpuYndBlxe0m7K6qOgckjni4l9ApIN16P7947Y5LXPY3wHsKVs1t0NKERnNzUTE/aD5einOv/pE3HdZAujtigOHwmihwrhHbTeumSYTC02kbtKUHNlR3lw+GsKR/727sCvkNhYnqz3uXjfJHYmghJJjL20XmPzpr5GJaohr3f4ZcoABv5G3dy"
respID : -1
result : 1
sendee : ""
sender: ""

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值