JEEPLUS集成CAS的方案

一、文件jeeplus.properties,添加配置
内容如下:

cas.server.serverurlprefix.url=http://10.10.128.90:7001/cas
cas.project.service=http://10.10.128.90:7002/portal
shiro.service=${cas.project.service}/cas/login
shiro.failureUrl=/webpage/error/error_cas.jsp
cas.server.login.url=${cas.server.serverurlprefix.url}?service=${shiro.service}
cas.server.logout.url=${cas.server.serverurlprefix.url}/logout?service=${cas.project.service}

注意:只有门户在退出时去cas服务注销token,各个子系统在注销时只注销自己的session就可以了

在各个子系统中配置:

cas.server.serverurlprefix.url=http://10.10.128.90:7001/cas
cas.project.service=http://10.10.128.90:7003/zxt
cas.project.service2=http://10.10.128.90:7003/portal
shiro.service=${cas.project.service}/cas/login
shiro.failureUrl=/webpage/error/error_cas.jsp
cas.server.login.url=${cas.server.serverurlprefix.url}?service=${shiro.service}
cas.server.logout.url=${cas.project.service2}/logout

二、修改文件 spring-context-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.0.xsd"
	   default-lazy-init="true">

	<description>Shiro Configuration</description>

	<!-- 加载配置属性文件 -->
	<context:property-placeholder ignore-unresolvable="true" location="classpath:/config/jeeplus.properties" />
	<!-- Shiro权限过滤过滤器定义 -->
	<bean name="shiroFilterChainDefinitions" class="java.lang.String">
		<constructor-arg>
			<value>
				/static/** = anon
				/userfiles/** = anon
				${adminPath}/sys/user/infoCareStatus = anon
				${adminPath}/sys/user/validateLoginName = anon
				${adminPath}/sys/user/validateMobile = anon
				${adminPath}/sys/user/validateMobileExist = anon
				${adminPath}/sys/user/resetPassword = anon
				${adminPath}/sys/register = anon
				${adminPath}/sys/register/registerUser = anon
				${adminPath}/sys/register/getRegisterCode = anon
				${adminPath}/sys/register/validateMobileCode = anon
				${adminPath}/soft/sysVersion/getAndroidVer = anon
				${adminPath}/soft/sysVersion/getIosVer = anon
				/cas/login = casFilter
				${adminPath}/login = user
				${adminPath}/logout = logoutFilter
				${adminPath}/** = user
				/act/rest/service/editor/** = perms[act:model:edit]
				/act/rest/service/model/** = perms[act:model:edit]
				/act/rest/service/** = user
				/ReportServer/** = user
			</value>
		</constructor-arg>
	</bean>

	<!-- 安全认证过滤器 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager" />
		<property name="loginUrl" value="${cas.server.login.url}" />
		<property name="filters">
			<map>
				<entry key="casFilter" value-ref="casFilter" />
				<entry key="logoutFilter" value-ref="logoutFilter" />
			</map>
		</property>
		<property name="filterChainDefinitions">
			<ref bean="shiroFilterChainDefinitions"/>
		</property>
	</bean>


	<!-- CAS认证过滤器 -->
	<bean id="casFilter" class="com.jeeplus.common.security.shiro.UserCasFilter">
		<property name="failureUrl" value="${shiro.failureUrl}"/>
	</bean>

	<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
		<property name="redirectUrl" value="${cas.server.logout.url}" />
	</bean>

	<bean id="casRealm" class="com.jeeplus.common.security.shiro.UserRealm">
		<!-- 认证通过后的默认角色 -->
		<property name="defaultRoles" value="ROLE_USER" />
		<!-- cas服务端地址前缀 -->
		<property name="casServerUrlPrefix" value="${cas.server.serverurlprefix.url}" />
		<!-- 应用服务地址,用来接收cas服务端票据 -->
		<property name="casService" value="${shiro.service}" />
	</bean>

	<!-- 定义Shiro安全管理配置 -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="casRealm" />
		<property name="sessionManager" ref="sessionManager" />
		<property name="cacheManager" ref="shiroCacheManager" />
	</bean>
	<!-- 自定义会话管理配置 -->
	<bean id="sessionManager" class="com.jeeplus.common.security.shiro.session.SessionManager">
		<property name="sessionDAO" ref="sessionDAO"/>

		<!-- 会话超时时间,单位:毫秒  -->
		<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>

		<!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话   -->
		<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
		<!--  		<property name="sessionValidationSchedulerEnabled" value="false"/> -->
		<property name="sessionValidationSchedulerEnabled" value="true"/>

		<property name="sessionIdCookie" ref="sessionIdCookie"/>
		<property name="sessionIdCookieEnabled" value="true"/>
	</bean>

	<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
		当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
	<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
		<constructor-arg name="name" value="jeeplus.session.id"/>
	</bean>

	<!-- 自定义Session存储容器 -->

	<bean id="sessionDAO" class="com.jeeplus.common.security.shiro.session.CacheSessionDAO">
		<property name="sessionIdGenerator" ref="idGen" />
		<property name="activeSessionsCacheName" value="activeSessionsCache" />
		<property name="cacheManager" ref="shiroCacheManager" />
	</bean>

	<!-- 定义授权缓存管理器 -->
	<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
		<property name="cacheManager" ref="cacheManager" />
	</bean>

	<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

	<!-- AOP式方法级权限检查  -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
		<property name="proxyTargetClass" value="true" />
	</bean>
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
		<property name="securityManager" ref="securityManager"/>
	</bean>

</beans>

三、新建文件UserRealm.java

package com.jeeplus.common.security.shiro;

import com.jeeplus.common.config.Global;
import com.jeeplus.common.utils.Encodes;
import com.jeeplus.common.utils.SpringContextHolder;
import com.jeeplus.common.utils.StringUtils;
import com.jeeplus.common.web.Servlets;
import com.jeeplus.modules.sys.entity.Menu;
import com.jeeplus.modules.sys.entity.Role;
import com.jeeplus.modules.sys.entity.User;
import com.jeeplus.modules.sys.security.SystemAuthorizingRealm;
import com.jeeplus.modules.sys.service.SystemService;
import com.jeeplus.modules.sys.utils.LogUtils;
import com.jeeplus.modules.sys.utils.UserUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;

/**
 * 系统安全认证实现类
 * @author jeeplus
 * @version 2017-7-5
 */
@Service
//@DependsOn({"userMapper","roleMapper","menuMapper"})
public class UserRealm extends CasRealm {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private SystemService systemService;

    @Autowired
    HttpServletRequest request;

	/**
     * 认证回调函数, 登录时调用
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        CasToken casToken = (CasToken)token;
        if (token == null) {
             return null;
        } else {
            String ticket = (String) casToken.getCredentials();
            if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
                 return null;
            } else {
                //ticket检验器
                TicketValidator ticketValidator = ensureTicketValidator();

                int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
                try {
                    String casservice=getCasService();
                    // 去CAS服务端中验证ticket的合法性
                    Assertion casAssertion = ticketValidator.validate(ticket, casservice);
                    // 从CAS服务端中获取相关属性,包括用户名、是否设置RememberMe等
                    AttributePrincipal casPrincipal = casAssertion.getPrincipal();
                    String userId = casPrincipal.getName();
                    if (logger.isDebugEnabled()){
                        logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, userId);
                    }
                    // 校验用户名密码
                    User user = getSystemService().getUserByLoginName(userId);
                    if (user != null) {
                        if (Global.NO.equals(user.getLoginFlag())){
                            throw new AuthenticationException("msg:该已帐号禁止登录.");
                        }
                        byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
                        SystemAuthorizingRealm.Principal principals=new SystemAuthorizingRealm.Principal(user,false);
                        String name=getName();
                        ByteSource bytesource= ByteSource.Util.bytes(userId);
                        return new SimpleAuthenticationInfo(principals, ticket,bytesource , name);
                    } else {
                        return null;
                    }
                } catch (TicketValidationException e) {
                    throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
                }

            }
        }
    }


    /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Object _principal= principals.getPrimaryPrincipal();
        String username = getFieldValueByName("loginName",_principal);
        User user = getSystemService().getUserByLoginName(username);
        if (user != null) {
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            List<Menu> list = UserUtils.getMenuList();
            for (Menu menu : list){
                if (StringUtils.isNotBlank(menu.getPermission())){
                    // 添加基于Permission的权限信息
                    for (String permission : StringUtils.split(menu.getPermission(),",")){
                        info.addStringPermission(permission);
                    }
                }
            }
            // 添加用户权限
            info.addStringPermission("user");
            // 添加用户角色信息
            for (Role role : user.getRoleList()){
                info.addRole(role.getEnname());
            }
            // 更新登录IP和时间
            getSystemService().updateUserLoginInfo(user);
            // 记录登录日志
            LogUtils.saveLog(Servlets.getRequest(), "系统登录");
            return info;
        } else {
            return null;
        }
    }
    private String getFieldValueByName(String fieldName,  Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter, new Class[] {});
            Object value = method.invoke(o, new Object[] {});
            return (String)value;
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    protected void checkPermission(Permission permission, AuthorizationInfo info) {
        authorizationValidate(permission);
        super.checkPermission(permission, info);
    }

    @Override
    protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
        if (permissions != null && !permissions.isEmpty()) {
            for (Permission permission : permissions) {
                authorizationValidate(permission);
            }
        }
        return super.isPermitted(permissions, info);
    }

    @Override
    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        authorizationValidate(permission);
        return super.isPermitted(principals, permission);
    }

    @Override
    protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
        if (permissions != null && !permissions.isEmpty()) {
            for (Permission permission : permissions) {
                authorizationValidate(permission);
            }
        }
        return super.isPermittedAll(permissions, info);
    }

    /**
     * 授权验证方法
     * @param permission
     */
    private void authorizationValidate(Permission permission){
        // 模块授权预留接口
    }

    /**
     * 设定密码校验的Hash算法与迭代次数
     */
    //@PostConstruct
    public void initCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(SystemService.HASH_ALGORITHM);
        matcher.setHashIterations(SystemService.HASH_INTERATIONS);
        setCredentialsMatcher(matcher);
    }

//	/**
//	 * 清空用户关联权限认证,待下次使用时重新加载
//	 */
	public void clearCachedAuthorizationInfo(Principal principal) {
		SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
		clearCachedAuthorizationInfo(principals);
	}

    /**
     * 清空所有关联认证
     * @Deprecated 不需要清空,授权缓存保存到session中
     */
    @Deprecated
    public void clearAllCachedAuthorizationInfo() {
//		Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
//		if (cache != null) {
//			for (Object key : cache.keys()) {
//				cache.remove(key);
//			}
//		}
    }

    /**
     * 获取系统业务对象
     */
    public SystemService getSystemService() {
        if (systemService == null){
            systemService = SpringContextHolder.getBean(SystemService.class);
        }
        return systemService;
    }

    /**
     * 授权用户信息
     */
    public static class Principal implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id; // 编号
        private String loginName; // 登录名
        private String name; // 姓名
        private boolean mobileLogin; // 是否手机登录

//		private Map<String, Object> cacheMap;

        public Principal(User user, boolean mobileLogin) {
            this.id = user.getId();
            this.loginName = user.getLoginName();
            this.name = user.getName();
            this.mobileLogin = mobileLogin;
        }

        public String getId() {
            return id;
        }

        public String getLoginName() {
            return loginName;
        }

        public String getName() {
            return name;
        }

        public boolean isMobileLogin() {
            return mobileLogin;
        }

        /**
         * 获取SESSIONID
         */
        public String getSessionid() {
            try{
                return (String) UserUtils.getSession().getId();
            }catch (Exception e) {
                return "";
            }
        }

        @Override
        public String toString() {
            return id;
        }
    }
}

四、单点登录认证异常

创建单点登录认证类UserCasFilter

package com.jeeplus.common.security.shiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;

public class UserCasFilter extends CasFilter {
    private String failureUrl;
    private static Logger logger = LoggerFactory.getLogger(CasFilter.class);

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, ServletResponse response) {
        try {
            request.setAttribute("exception", ae);
            request.getRequestDispatcher(this.failureUrl).forward(request, response);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("权限认证失败", e);
        }
        return false;
     }

    public void setFailureUrl(String failureUrl) {
        this.failureUrl = failureUrl;
    }
}

 

创建error/error_cas.jsp文件

<%
    response.setStatus(500);

// 获取异常类
    Throwable ex = Exceptions.getThrowable(request);
    if (ex != null) {
        LoggerFactory.getLogger("500.jsp").error(ex.getMessage(), ex);
    }

// 编译错误信息
    StringBuilder sb = new StringBuilder("错误信息:\n");
    if (ex != null) {
        sb.append(Exceptions.getStackTraceAsString(ex));
    } else {
        sb.append("未知错误.\n\n");
    }

// 如果是异步请求或是手机端,则直接返回信息
    if (Servlets.isAjaxRequest(request)) {
        out.print(sb);
    }

// 输出异常信息页面
    else {
%>
<%@page import="org.slf4j.Logger,org.slf4j.LoggerFactory" %>
<%@page import="com.jeeplus.common.web.Servlets" %>
<%@page import="com.jeeplus.common.utils.Exceptions" %>
<%@page import="com.jeeplus.common.utils.StringUtils" %>
<%@page contentType="text/html;charset=UTF-8" isErrorPage="true" %>
<%@include file="/webpage/include/taglib.jsp" %>
<!DOCTYPE html>
<html>
<head>
    <title>权限认证失败</title>
    <link rel="stylesheet" href="${ctxStatic}/common/css/mainStyle.css"/>
    <%@include file="/webpage/include/head.jsp" %>
    <style type="text/css">
        .errorBox {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 600px;
            height: 300px;
            margin-left: -300px;
            margin-top: -200px;
        }

        .errorBox .errorImg {
            width: 100%;
            text-align: center;
        }

        .errorBox .errorTxt1 {
            width: 100%;
            padding-top: 20px;
            text-align: center;
            color: #333;
            font-size: 18px;
            line-height: 1;
        }

        .errorBox .errorTxt2 {
            width: 100%;
            padding-top: 10px;
            text-align: center;
            color: #666;
            font-size: 14px;
            line-height: 1
        }

        .errorBox .authorizeErrorTxt {
            width: 100%;
            margin-top: -35px;
            text-align: center;
            color: #666;
            font-size: 18px;
            line-height: 1;
        }

        .errorBox .authorizeErrorBtn {
            width: 100%;
            margin-top: 15px;
            text-align: center;
        }

        .errorBox .authorizeErrorBtn a {
            display: block;
            padding: 5px 15px;
            border-radius: 15px;
            border: 2px solid #00bd9c;
            color: #333;
            width: 116px;
            font-size: 14px;
            margin: 30px auto 0;
        }

        .errorBox .unknownErrorTxt {
            width: 100%;
            padding-top: 20px;
            text-align: center;
            color: #333;
            font-size: 18px;
            line-height: 1;
        }

        .errorBox .unknownErrorBtn {
            width: 100%;
            text-align: center;
        }

        .errorBox .unknownErrorBtn a {
            display: inline-block;
            padding: 5px 15px;
            border-radius: 15px;
            border: 2px solid #00bd9c;
            color: #333;
            width: 116px;
            font-size: 14px;
            margin: 30px auto 0;
        }
    </style>

    <script type="text/javascript">

        function showErrorMsg() {
            var temp = $(".errorMessage").clone();
            temp.removeClass('hide');
            top.layer.open({
                type: 1,
                skin: 'layerui-layer-rim',
                title: '错误详细信息',
                area: ['1000px', '600px'],
                content: temp.html(),
                btn: ['关闭']
            });
        }
    </script>
</head>
<body>
<div class="errorBox" style="height: 500px;margin-top:-300px;">
    <div class="errorImg">
        <img src="${ctxWebpage}/static/common/img/unknownError.png">
    </div>
    <div class="container-fluid">
        <div class="unknownErrorTxt">权限认证失败</div>
        <div class="unknownErrorBtn">
            错误信息:<%=ex == null ? "权限认证失败." : StringUtils.toHtml(ex.getMessage())%> <br/> <br/>
            请点击“查看详细信息”按钮,将详细错误信息发送给系统管理员,谢谢!<br/> <br/>
            <a href="javascript:" onclick="window.close();" class="btn">关闭当前页</a> &nbsp;
            <a href="javascript:" onclick="showErrorMsg()" class="btn">查看详细信息</a>
        </div>
        <div class="errorMessage hide">
            <%=StringUtils.toHtml(sb.toString())%> <br/>
        </div>
        <script>try {
            top.$.jBox.closeTip();
        } catch (e) {
        }</script>
    </div>
</div>
</body>
</html>
<%
    }
    out = pageContext.pushBody();
%>

五、未能够识别出目标 ***;票根

修改cas服务端相关配置文件: cas.properties

st.timeToKillInSeconds=10


原因,默认值时间是1.8秒,当登录成功,携带ST返回客户端,客户端带着server和ST去服务器验证,但此时服务器端的ST已经失效。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当然,我可以为您提供关于JeePlus的新手教程。JeePlus是一个基于Java的开源框架,用于快速开发企业级Java应用程序。它提供了许多便捷的功能和工具,可以简化开发过程,并提高开发效率。 以下是一个简单的JeePlus新手教程的大纲: 1. 环境设置: - 安装Java开发工具包(JDK)和集成开发环境(IDE),例如Eclipse或IntelliJ IDEA。 - 下载JeePlus框架,并导入到IDE中。 2. 创建新项目: - 在IDE中创建一个新的Java项目。 - 将JeePlus框架添加到项目的依赖中。 3. 数据库配置: - 配置数据库连接,包括数据库驱动程序和连接URL。 - 创建数据库表格或使用现有表格。 4. 实体类和数据访问对象(DAO): - 创建Java实体类,映射到数据库表格。 - 创建DAO类,用于对实体类进行数据库操作,例如增删改查。 5. 业务逻辑层: - 创建服务类,实现业务逻辑。 - 在服务类中调用DAO类来操作数据库。 6. 控制器层: - 创建控制器类,处理用户请求。 - 使用注解来映射URL和方法,处理HTTP请求和响应。 7. 视图层: - 创建JSP页面,用于显示数据和与用户交互。 - 使用JSTL(JavaServer Pages标准标签库)和EL(表达式语言)来处理页面逻辑和显示数据。 8. 测试和调试: - 编写单元测试用例,确保代码的正确性。 - 使用调试工具来调试代码并解决错误。 这只是一个简单的JeePlus新手教程大纲,涵盖了基本的项目搭建和开发流程。当然,JeePlus框架还有更多高级功能和特性,您可以根据需要深入学习和应用。希望对您有所帮助!如果您有任何具体的问题,可以随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值