CAS 5.3.x 单点登录实现集群搭建,快速入手(三)!!!!

3 篇文章 0 订阅
3 篇文章 1 订阅

前言:

该篇教程描述如何搭建CAS5.3.x集群操作,由于官方文档并没有贴出集群搭建方案,所以本博主根据源码剖析解决该问题。

进入下面小程序可以体验效果:

 

文档并没有深入浅出说明原理,直接贴代码用于快速上手学习。

学习CAS5.x 推荐看 以下两个博主文章

此博主文章:https://blog.csdn.net/u010475041/article/category/7156505

此博主文章:狂飙的yellowcong的博客_CSDN博客-centos,单点登录,Docker学习领域博主

全部基于springboot开发,请存在此基础再学习!

该教程由本博主(Garc)首发,所以转载请说明原处,谢谢!!

该教程分为两个部分,衔接第二篇

       1.解决 flowExecutionKey CAS点击按钮后异常跳回原页面

PS:请按顺序一步一步来

如果发现CAS不能打印info日志,增加一个AsyncLogger指定自己的工程package就好了

以下所有的 configuration 都需要配置spring ,使用spring aop 配置

配置文件目录:src/main/resources/META-INF/spring.factories 增加如下内容:

com.hpay.sso.support.auth.config.CasWebflowContextConfiguration

解决该异常说明:

由于登录的时候在Nginx分发节点时,将webflow的其中一个节点的flowExecutionKey下发到另一个节点中,然后另一个节点不能正常解密(原因未找到,加密算法我没看懂)。导致出现CAS点击按钮后异常跳回原页面的问题,由于是nginx分发,所以会随机出现。

一、解决 flowExecutionKey 异常跳回原页面

maven依赖包

                <dependency>
                    <groupId>org.cryptacular</groupId>
                    <artifactId>cryptacular</artifactId>
                    <version>1.2.2</version>
                </dependency>

                <dependency>
                    <groupId>org.apereo.cas</groupId>
                    <artifactId>cas-server-support-throttle-core</artifactId>
                    <version>${cas.version}</version>
                </dependency>

                <dependency>
                    <groupId>org.apereo.cas</groupId>
                    <artifactId>cas-server-support-pm-webflow</artifactId>
                    <version>${cas.version}</version>
                </dependency>

application.properties 配置

因为需要替换掉框架内的源码,所以必须要使用该配置排除框架的Bean配置管理使用自定义配置

spring.autoconfigure.exclude=org.apereo.cas.web.flow.config.CasWebflowContextConfiguration

EncryptedTranscoder 自定义加密解密算法

该代码是在原框架代码上加上了自定义加密解密算法,本人使用AES加密解密

AESCoder 此处不提供,请自备!

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.hpay.sso.support.auth.Crypto.AESCoder;
import org.apereo.spring.webflow.plugin.Transcoder;
import org.cryptacular.bean.BufferedBlockCipherBean;
import org.cryptacular.bean.CipherBean;
import org.cryptacular.bean.KeyStoreFactoryBean;
import org.cryptacular.generator.sp80038a.RBGNonce;
import org.cryptacular.io.URLResource;
import org.cryptacular.spec.BufferedBlockCipherSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EncryptedTranscoder implements Transcoder {

    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedTranscoder.class);

    private CipherBean cipherBean;
    private boolean compression = true;

    public EncryptedTranscoder() throws IOException {
        BufferedBlockCipherBean bufferedBlockCipherBean = new BufferedBlockCipherBean();
        bufferedBlockCipherBean.setBlockCipherSpec(new BufferedBlockCipherSpec("AES", "CBC", "PKCS7"));
        bufferedBlockCipherBean.setKeyStore(this.createAndPrepareKeyStore());
        bufferedBlockCipherBean.setKeyAlias("aes128");
        bufferedBlockCipherBean.setKeyPassword("changeit");
        bufferedBlockCipherBean.setNonce(new RBGNonce());
        this.setCipherBean(bufferedBlockCipherBean);
    }

    public EncryptedTranscoder(CipherBean cipherBean) throws IOException {
        this.setCipherBean(cipherBean);
    }

    public void setCompression(boolean compression) {
        this.compression = compression;
    }

    protected void setCipherBean(CipherBean cipherBean) {
        this.cipherBean = cipherBean;
    }

    public byte[] encode(Object o) throws IOException {
        if (o == null) {
            return new byte[0];
        } else {
            ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
            ObjectOutputStream out = null;

            try {
                if (this.compression) {
                    out = new ObjectOutputStream(new GZIPOutputStream(outBuffer));
                } else {
                    out = new ObjectOutputStream(outBuffer);
                }

                out.writeObject(o);
            } finally {
                if (out != null) {
                    out.close();
                }

            }

            try {
                byte[] bytes = AESCoder.encryptToByteArray(outBuffer.toByteArray());
                if (Arrays.equals(bytes, outBuffer.toByteArray())){
                    bytes = this.cipherBean.encrypt(outBuffer.toByteArray());
                }
                return bytes;
            } catch (Exception var7) {
                throw new IOException("Encryption error", var7);
            }
        }
    }

    public Object decode(byte[] encoded) throws IOException {

        byte[] data;
        try {
            data = AESCoder.decryptToByteArray(encoded);
            if (Arrays.equals(data, encoded)){
                data = this.cipherBean.encrypt(encoded);
            }
        } catch (Exception var11) {
            throw new IOException("Decryption error", var11);
        }

        ByteArrayInputStream inBuffer = new ByteArrayInputStream(data);
        ObjectInputStream in = null;

        Object var5;
        try {
            if (this.compression) {
                in = new ObjectInputStream(new GZIPInputStream(inBuffer));
            } else {
                in = new ObjectInputStream(inBuffer);
            }

            var5 = in.readObject();
        } catch (ClassNotFoundException var10) {
            throw new IOException("Deserialization error", var10);
        } finally {
            if (in != null) {
                in.close();
            }

        }

        return var5;
    }

    protected KeyStore createAndPrepareKeyStore() {
        KeyStoreFactoryBean ksFactory = new KeyStoreFactoryBean();
        URL u = this.getClass().getResource("/etc/keystore.jceks");
        ksFactory.setResource(new URLResource(u));
        ksFactory.setType("JCEKS");
        ksFactory.setPassword("changeit");
        return ksFactory.newInstance();
    }
}

WebflowExecutorFactory 自定义工厂

定义此处工厂是为了 将获取加密算法的对象替换成自定义加密算法对象,完成加密算法的替换工作

import org.apereo.cas.CipherExecutor;
import org.apereo.cas.configuration.model.webapp.WebflowProperties;
import org.apereo.cas.configuration.model.webapp.WebflowSessionManagementProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.cas.web.flow.executor.WebflowCipherBean;
import org.apereo.spring.webflow.plugin.ClientFlowExecutionRepository;
import org.apereo.spring.webflow.plugin.Transcoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.conversation.impl.SessionBindingConversationManager;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.impl.FlowExecutionImplFactory;
import org.springframework.webflow.execution.FlowExecutionListener;
import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader;
import org.springframework.webflow.execution.repository.impl.DefaultFlowExecutionRepository;
import org.springframework.webflow.execution.repository.snapshot
  .SerializedFlowExecutionSnapshotFactory;
import org.springframework.webflow.executor.FlowExecutor;
import org.springframework.webflow.executor.FlowExecutorImpl;

import java.io.IOException;

public class WebflowExecutorFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebflowExecutorFactory.class);
    private final WebflowProperties webflowProperties;
    private final FlowDefinitionRegistry flowDefinitionRegistry;
    private final CipherExecutor webflowCipherExecutor;
    private final FlowExecutionListener[] executionListeners;

    public FlowExecutor build() {
        return this.webflowProperties.getSession().isStorage() ? this.buildFlowExecutorViaServerSessionBindingExecution() : this.buildFlowExecutorViaClientFlowExecution();
    }

    private FlowExecutor buildFlowExecutorViaServerSessionBindingExecution() {
        SessionBindingConversationManager conversationManager = new SessionBindingConversationManager();
        WebflowSessionManagementProperties session = this.webflowProperties.getSession();
        conversationManager.setLockTimeoutSeconds((int)Beans.newDuration(session.getLockTimeout()).getSeconds());
        conversationManager.setMaxConversations(session.getMaxConversations());
        FlowExecutionImplFactory executionFactory = new FlowExecutionImplFactory();
        executionFactory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(this.executionListeners));
        SerializedFlowExecutionSnapshotFactory flowExecutionSnapshotFactory = new SerializedFlowExecutionSnapshotFactory(executionFactory, this.flowDefinitionRegistry);
        flowExecutionSnapshotFactory.setCompress(session.isCompress());
        DefaultFlowExecutionRepository repository = new DefaultFlowExecutionRepository(conversationManager, flowExecutionSnapshotFactory);
        executionFactory.setExecutionKeyFactory(repository);
        return new FlowExecutorImpl(this.flowDefinitionRegistry, executionFactory, repository);
    }

    private FlowExecutor buildFlowExecutorViaClientFlowExecution() {
        ClientFlowExecutionRepository repository = new ClientFlowExecutionRepository();
        repository.setFlowDefinitionLocator(this.flowDefinitionRegistry);
        try {
            repository.setTranscoder(this.getWebflowStateTranscoder());
        } catch (IOException e) {
            LOGGER.error("异常",e);
        }
        FlowExecutionImplFactory factory = new FlowExecutionImplFactory();
        factory.setExecutionKeyFactory(repository);
        factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(new FlowExecutionListener[0]));
        repository.setFlowExecutionFactory(factory);
        factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(this.executionListeners));
        return new FlowExecutorImpl(this.flowDefinitionRegistry, factory, repository);
    }

    private Transcoder getWebflowStateTranscoder() throws IOException {
        try {
            WebflowCipherBean cipherBean = new WebflowCipherBean(this.webflowCipherExecutor);
            return new EncryptedTranscoder(cipherBean);
        } catch (Throwable var2) {
            throw var2;
        }
    }

    public WebflowExecutorFactory(WebflowProperties webflowProperties, FlowDefinitionRegistry flowDefinitionRegistry, CipherExecutor webflowCipherExecutor, FlowExecutionListener[] executionListeners) {
        this.webflowProperties = webflowProperties;
        this.flowDefinitionRegistry = flowDefinitionRegistry;
        this.webflowCipherExecutor = webflowCipherExecutor;
        this.executionListeners = executionListeners;
    }
}

替换原框架CasWebflowContextConfiguration配置

需要该配置生效必须要配置spring.autoconfigure.exclude 排除掉源码的配置

如果有使用登录界面自定义参数(验证码之类)的,再其他地方配置了 FlowDefinitionRegistry loginFlowRegistry

请将那个配置在CasWebflowContextConfiguration之前加载!

import com.hpay.sso.support.auth.util.cipher.WebflowExecutorFactory;
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.util.CollectionUtils;
import org.apereo.cas.web.flow.CasFlowHandlerAdapter;
import org.apereo.cas.web.flow.CasWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowExecutionPlan;
import org.apereo.cas.web.flow.CasWebflowExecutionPlanConfigurer;
import org.apereo.cas.web.flow.actions.CasDefaultFlowUrlHandler;
import org.apereo.cas.web.flow.actions.LogoutConversionService;
import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.DefaultLogoutWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.GroovyWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.plan.DefaultCasWebflowExecutionPlan;
import org.apereo.cas.web.support.AuthenticationThrottlingExecutionPlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.webflow.config.FlowBuilderServicesBuilder;
import org.springframework.webflow.config.FlowDefinitionRegistryBuilder;
import org.springframework.webflow.context.servlet.FlowUrlHandler;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.ViewFactoryCreator;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
import org.springframework.webflow.execution.FlowExecutionListener;
import org.springframework.webflow.executor.FlowExecutor;
import org.springframework.webflow.expression.spel.WebFlowSpringELExpressionParser;
import org.springframework.webflow.mvc.builder.MvcViewFactoryCreator;
import org.springframework.webflow.mvc.servlet.FlowHandlerAdapter;
import org.springframework.webflow.mvc.servlet.FlowHandlerMapping;

import java.util.ArrayList;
import java.util.List;

@Configuration("casWebflowContextConfiguration")
@EnableConfigurationProperties({CasConfigurationProperties.class})
public class CasWebflowContextConfiguration {
    private static final Logger LOGGER = LoggerFactory.getLogger(CasWebflowContextConfiguration.class);
    private static final int LOGOUT_FLOW_HANDLER_ORDER = 3;
    private static final String BASE_CLASSPATH_WEBFLOW = "classpath*:/webflow";
    @Autowired
    private CasConfigurationProperties casProperties;
    @Autowired
    @Qualifier("authenticationThrottlingExecutionPlan")
    private ObjectProvider<AuthenticationThrottlingExecutionPlan> authenticationThrottlingExecutionPlan;
    @Autowired
    @Qualifier("registeredServiceViewResolver")
    private ObjectProvider<ViewResolver> registeredServiceViewResolver;
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    @Qualifier("webflowCipherExecutor")
    private CipherExecutor webflowCipherExecutor;

    public CasWebflowContextConfiguration() {
    }

    @Bean
    public ExpressionParser expressionParser() {
        return new WebFlowSpringELExpressionParser(new SpelExpressionParser(), this.logoutConversionService());
    }

    @Bean
    public ConversionService logoutConversionService() {
        return new LogoutConversionService();
    }

    @RefreshScope
    @Bean
    public ViewFactoryCreator viewFactoryCreator() {
        MvcViewFactoryCreator resolver = new MvcViewFactoryCreator();
        resolver.setViewResolvers(CollectionUtils.wrap((ViewResolver)this.registeredServiceViewResolver.getIfAvailable()));
        return resolver;
    }

    @Bean
    public FlowUrlHandler loginFlowUrlHandler() {
        return new CasDefaultFlowUrlHandler();
    }

    @Bean
    public FlowUrlHandler logoutFlowUrlHandler() {
        CasDefaultFlowUrlHandler handler = new CasDefaultFlowUrlHandler();
        handler.setFlowExecutionKeyParameter("RelayState");
        return handler;
    }

    @RefreshScope
    @Bean
    public HandlerAdapter logoutHandlerAdapter() {
        FlowHandlerAdapter handler = new CasFlowHandlerAdapter("logout");
        handler.setFlowExecutor(this.logoutFlowExecutor());
        handler.setFlowUrlHandler(this.logoutFlowUrlHandler());
        return handler;
    }

    @RefreshScope
    @Bean
    public FlowBuilderServices builder() {
        FlowBuilderServicesBuilder builder = new FlowBuilderServicesBuilder();
        builder.setViewFactoryCreator(this.viewFactoryCreator());
        builder.setExpressionParser(this.expressionParser());
        builder.setDevelopmentMode(this.casProperties.getWebflow().isRefresh());
        return builder.build();
    }

    @Bean
    public HandlerAdapter loginHandlerAdapter() {
        FlowHandlerAdapter handler = new CasFlowHandlerAdapter("login");
        handler.setFlowExecutor(this.loginFlowExecutor());
        handler.setFlowUrlHandler(this.loginFlowUrlHandler());
        return handler;
    }

    @RefreshScope
    @Bean
    @ConditionalOnMissingBean(
        name = {"localeChangeInterceptor"}
    )
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor bean = new LocaleChangeInterceptor();
        bean.setParamName(this.casProperties.getLocale().getParamName());
        return bean;
    }

    @Bean
    public HandlerMapping logoutFlowHandlerMapping() {
        FlowHandlerMapping handler = new FlowHandlerMapping();
        handler.setOrder(3);
        handler.setFlowRegistry(this.logoutFlowRegistry());
        Object[] interceptors = new Object[]{this.localeChangeInterceptor()};
        handler.setInterceptors(interceptors);
        return handler;
    }

    @Lazy
    @Bean
    public Object[] loginFlowHandlerMappingInterceptors() {
        List interceptors = new ArrayList();
        interceptors.add(this.localeChangeInterceptor());
        AuthenticationThrottlingExecutionPlan plan = (AuthenticationThrottlingExecutionPlan)this.authenticationThrottlingExecutionPlan.getIfAvailable();
        if (plan != null) {
            interceptors.addAll(plan.getAuthenticationThrottleInterceptors());
        }

        return interceptors.toArray();
    }

    @Bean
    public HandlerMapping loginFlowHandlerMapping() {
        FlowHandlerMapping handler = new FlowHandlerMapping();
        handler.setOrder(2);
        handler.setFlowRegistry(this.loginFlowRegistry());
        handler.setInterceptors(this.loginFlowHandlerMappingInterceptors());
        return handler;
    }

    @Bean
    public FlowDefinitionRegistry logoutFlowRegistry() {
        FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.builder());
        builder.setBasePath("classpath*:/webflow");
        builder.addFlowLocationPattern("/logout/*-webflow.xml");
        return builder.build();
    }

    @Bean
    public FlowDefinitionRegistry loginFlowRegistry() {
        FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.builder());
        builder.setBasePath("classpath*:/webflow");
        builder.addFlowLocationPattern("/login/*-webflow.xml");
        return builder.build();
    }

    @RefreshScope
    @Bean
    public FlowExecutor logoutFlowExecutor() {
        WebflowExecutorFactory factory = new WebflowExecutorFactory(this.casProperties.getWebflow(), this.logoutFlowRegistry(), this.webflowCipherExecutor, new FlowExecutionListener[0]);
        return factory.build();
    }

    @RefreshScope
    @Bean
    public FlowExecutor loginFlowExecutor() {
        WebflowExecutorFactory factory = new WebflowExecutorFactory(this.casProperties.getWebflow(), this.loginFlowRegistry(), this.webflowCipherExecutor, new FlowExecutionListener[0]);
        return factory.build();
    }

    @ConditionalOnMissingBean(
        name = {"defaultWebflowConfigurer"}
    )
    @Bean
    @Order(0)
    @RefreshScope
    public CasWebflowConfigurer defaultWebflowConfigurer() {
        DefaultLoginWebflowConfigurer c = new DefaultLoginWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
        c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
        c.setOrder(-2147483648);
        return c;
    }

    @ConditionalOnMissingBean(
        name = {"defaultLogoutWebflowConfigurer"}
    )
    @Bean
    @Order(0)
    @RefreshScope
    public CasWebflowConfigurer defaultLogoutWebflowConfigurer() {
        DefaultLogoutWebflowConfigurer c = new DefaultLogoutWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
        c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
        c.setOrder(-2147483648);
        return c;
    }

    @ConditionalOnMissingBean(
        name = {"groovyWebflowConfigurer"}
    )
    @Bean
    @DependsOn({"defaultWebflowConfigurer"})
    @RefreshScope
    public CasWebflowConfigurer groovyWebflowConfigurer() {
        GroovyWebflowConfigurer c = new GroovyWebflowConfigurer(this.builder(), this.loginFlowRegistry(), this.applicationContext, this.casProperties);
        c.setLogoutFlowDefinitionRegistry(this.logoutFlowRegistry());
        return c;
    }

    @Autowired
    @Bean
    public CasWebflowExecutionPlan casWebflowExecutionPlan(List<CasWebflowExecutionPlanConfigurer> configurers) {
        DefaultCasWebflowExecutionPlan plan = new DefaultCasWebflowExecutionPlan();
        configurers.forEach((c) -> {
            c.configureWebflowExecutionPlan(plan);
        });
        plan.execute();
        return plan;
    }

    @ConditionalOnMissingBean(
        name = {"casDefaultWebflowExecutionPlanConfigurer"}
    )
    @Bean
    public CasWebflowExecutionPlanConfigurer casDefaultWebflowExecutionPlanConfigurer() {
        return new CasWebflowExecutionPlanConfigurer() {
            public void configureWebflowExecutionPlan(CasWebflowExecutionPlan plan) {
                plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.defaultWebflowConfigurer());
                plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.defaultLogoutWebflowConfigurer());
                plan.registerWebflowConfigurer(CasWebflowContextConfiguration.this.groovyWebflowConfigurer());
            }
        };
    }
}

全部教程到此结束

第三篇教程内容到此已经结束了,只要根据此教程一步一步操作,就可以实现集群登录操作。

如果想了解更多,可以加QQ群 119170668

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Qensq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值