跨系统如何保持Session存活和Token共享问题

WEB端
问题描述

         WMS系统对监管仓进行访问(监管仓内嵌于WMS系统),但是需要登录监管仓系统才能看到引入WMS系统的界面,否则看不了监管仓。这里涉及到一个监管仓访问超时的问题:如果用户一直在WMS上操作,而对监管仓不闻不问,那么一般在30minutes 之后,再次点击监管仓页面就会发现打不开了;同理,如果用户一直停在监管仓操作,那么30minutes之后,回来再次访问WMS的页面的时候,WMS也会自动退出到登录界面。以上问题说明,两个系统使用的不是同一个session时,在同一个系统中操作时间越长,另一个系统session存活的几率就越小,不被用户操作的系统也最容易导致session死亡,大多数时候只能半开半闭式地访问,而不能灵活地游弋于多个系统。要解决session共享的问题才能实现多系统紧密协作。

处理方案

       可以将session用一个公用的数据存储区块存储起来,如:memcache。利用memcache的数据超时来处理session。同时还可以实现单点登录,如果用户通过验证,但是memcache已经存在此session那么就不再允许用户登录系统。

移动端
问题描述

         历史惊人的相似,在Android和IOS上分别访问WMS和监管仓的时候发现不能共享Token。因为此Token是WMS和监管仓都分别作了生成的,登录时也仅仅是对是否超时作了验证,所以在登录的过程中他们是没有问题的,但是访问其他系统页面后切换回来就是在用其他类系统的Token在访问自己的站点,所以导致Token通不过验证。我们并没有通知对方系统自己的Token已经变了,而是自以为是独立地工作。

处理方案

       方法一:同上;

       方法二:每个系统都提供交换Token的接口,改变了就通知对方系统,这样可以确保用户获取到的token永远都是最新的。

我觉得今天这个标题很有意思,值得大家深入去思考,上面的解决方案是余之鄙见,希冀更多处理方案!

Token实现代码
TOken实体

package com.wlyd.wms.service.api.data;

import java.io.Serializable;

public class UserToken implements Serializable
{
    private static final long serialVersionUID = -8766321739625153631L;

    private String signature;// 签名
    
    private String timestamp;// 时间戳
    
    private String random;// 随机数
    
    private String authorizationCode;//授权码
    
    public String getSignature()
    {
	return signature;
    }
    
    public void setSignature(String signature)
    {
	this.signature = signature;
    }
    
    public String getTimestamp()
    {
	return timestamp;
    }
    
    public void setTimestamp(String timestamp)
    {
	this.timestamp = timestamp;
    }
    
    public String getRandom()
    {
	return random;
    }
    
    public void setRandom(String random)
    {
	this.random = random;
    }
    
    
    public String getAuthorizationCode()
    {
        return authorizationCode;
    }

    public void setAuthorizationCode(String authorizationCode)
    {
        this.authorizationCode = authorizationCode;
    }

    @Override
    public String toString()
    {
	return "signature=" + signature + "×tamp=" + timestamp + "&random=" + random+ "&authorizationCode=" + authorizationCode;
    }
}
C#与Java DES加密工具

package com.wlyd.wms.util.api;

import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import com.wlyd.wms.service.api.data.UserToken;
import com.wlyd.wms.util.Log;
import Decoder.BASE64Decoder;
import Decoder.BASE64Encoder;

public class JCSharpDESUtil
{
    // DES加密的私钥,必须是8位长的字符串
    private static final byte[] DESkey = "11111111".getBytes();// 设置密钥
    
    private static final byte[] DESIV = "12345678".getBytes();// 设置向量
    
    static AlgorithmParameterSpec iv = null;// 加密算法的参数接口,IvParameterSpec是它的一个实现
    private static Key key = null;
    
    static
    {
	DESKeySpec keySpec;
	try
	{
	    keySpec = new DESKeySpec(DESkey);// 设置密钥参数
	    iv = new IvParameterSpec(DESIV);// 设置向量
	    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 获得密钥工厂
	    key = keyFactory.generateSecret(keySpec);// 得到密钥对象
	} catch (Exception e)
	{
	    Log.getLogger(JCSharpDESUtil.class).error("");
	}
	
    }
    
    public static String encrypt(String data)
    {
	try
	{
	    Cipher enCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");// 得到加密对象Cipher
	    enCipher.init(Cipher.ENCRYPT_MODE, key, iv);// 设置工作模式为加密模式,给出密钥和向量
	    byte[] pasByte = enCipher.doFinal(data.getBytes("utf-8"));
	    BASE64Encoder base64Encoder = new BASE64Encoder();
	    return base64Encoder.encode(pasByte).replace("\n", "").replace("\r", "");// 去掉换行回车符
	} catch (Exception e)
	{
	    Log.getLogger(JCSharpDESUtil.class).error("加密异常:" + e.getMessage());
	}
	return "";
    }
    
    public static String decrypt(String data)
    {
	try
	{
	    Cipher deCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
	    deCipher.init(Cipher.DECRYPT_MODE, key, iv);
	    BASE64Decoder base64Decoder = new BASE64Decoder();
	    byte[] pasByte = deCipher.doFinal(base64Decoder.decodeBuffer(data));
	    return new String(pasByte, "UTF-8");
	} catch (Exception e)
	{
	    Log.getLogger(JCSharpDESUtil.class).error("解密异常:" + e.getMessage());
	}
	return "";
    }
    
    public static void main(String[] args) throws Exception
    {
	String data = "/YG1K/6/mh86b5EbjYdZwrNm+MGzGw7DXXzj1mBM3ssuw2BS2Xan9Yk1mBijRLoujGtD9Wo692KvVUoIvZZY9NdpJvgv76Rn";
	
	System.out.println("加密:" + JCSharpDESUtil.encrypt("boonya"));
	
	System.out.println("解密:" + JCSharpDESUtil.decrypt(JCSharpDESUtil.encrypt("boonya")));
	
	System.out.println("CShap data Length " + (data.getBytes().length % 8));
	System.out.println("CShap解密:" + JCSharpDESUtil.decrypt(data));
	
	UserToken encryptedToken = new UserToken();
	encryptedToken.setSignature("wmsadmin");
	encryptedToken.setTimestamp(Calendar.getInstance().getTimeInMillis() + "");
	encryptedToken.setRandom(new Random().nextInt(999999999) + "");
	String text = encryptedToken.toString();
	System.out.println("明文:" + text);
	String encryptData = JCSharpDESUtil.encrypt(text);
	System.out.println("加密:" + encryptData);
	
	System.out.println("解密:" + JCSharpDESUtil.decrypt(JCSharpDESUtil.encrypt(text)));
	
	long time = new Date().getTime();
	System.out.println(Calendar.getInstance().getTimeInMillis());
	System.out.println(new Date(Calendar.getInstance().getTimeInMillis()));
	System.out.println(time);
	
    }
    
}

TOKEN工具类

package com.wlyd.wms.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

import com.wlyd.wms.service.api.data.UserToken;
import com.wlyd.wms.util.api.JCSharpDESUtil;

public class TokenUtil
{
    private static ConcurrentHashMap<String, UserToken> map = new ConcurrentHashMap<String, UserToken>();
    
    /**
     * 生成加密Token
     * 
     * @param username
     * @return
     */
    public static UserToken generateToken(String username)
    {
	UserToken userToken = new UserToken();
	// 设置签名
	userToken.setSignature(username);
	// 设置时间戳
	userToken.setTimestamp(getTimeStamp());
	// 设置随机数
	userToken.setRandom(getRandom());
	// 设置授权码
	userToken.setAuthorizationCode(getAuthorizationCode(username));
	
	return userToken;
    }
    
    /**
     * 判定是否已经登录
     * 
     * @param signature
     * @return
     */
    public static boolean whetherUserHasSignedIn(String signature)
    {
	if(map.containsKey(signature))
	{
	    return true;
	}
	return false;
    }
    
    /**
     * 创建用户Token
     * 
     * @param username
     * @return
     */
    public static String createToken(String username)
    {
	UserToken userToken = generateToken(username);
	// 保存用户Token
	String token=encryptUserToken(userToken);
	
	map.put(encryptSignature(userToken.getSignature()), userToken);
	
	return token;
    }
    
    /**
     * 设置自定义的Token
     * 
     * @param username
     * @param userToken
     */
    public static void setSelfToken(String username,UserToken userToken)
    {
	map.put(username, userToken);
    }
    
    /**
     * 【重新生成】更新Token
     * 
     * @param token
     * @return
     */
    public static String reCreateToken(String token)
    {
	UserToken userToken=TokenUtil.analyseEncryptedUserToken(token);
	return createToken(userToken.getSignature());
    }
    
    /**
     * 加密签名
     * 
     * @param username
     * @return
     */
    public static String encryptSignature(String username)
    {
	return JCSharpDESUtil.encrypt(username);
    }
    
    /**
     * 解密签名
     * 
     * @param signature
     * @return
     */
    public static String decryptSignature(String signature)
    {
	return JCSharpDESUtil.decrypt(signature);
    }
    
    /**
     * 生成时间戳
     * @return
     */
    public static String getTimeStamp()
    {
	return Calendar.getInstance().getTimeInMillis() + "";
    }
    
    /**
     * 生成随机数
     * 
     * @return
     */
    public static String getRandom()
    {
	return new Random().nextInt(999999999) + "";
    }
    
    /**
     * 加密用户Token
     * 
     * @param userToken
     * @return
     */
    public static String encryptUserToken(UserToken userToken)
    {
	return JCSharpDESUtil.encrypt(userToken.toString());
    }
    
    /**
     * 解析加密用户Token
     * 
     * @param token
     * @return
     */
    public static UserToken decryptUserToken(String token)
    {
	String decryptToken=JCSharpDESUtil.decrypt(token);
	String[] params = decryptToken.split("&");
	// 分析用户提交过来的Token
	UserToken userToken = new UserToken();
	for (int i = 0, j = params.length; i < j; i++)
	{
	    String[] currentParams = params[i].split("=");
	    String param = currentParams[0];
	    switch (param)
	    {
		case "signature":
		    userToken.setSignature(currentParams[1]);
		    break;
		case "timestamp":
		    userToken.setTimestamp(currentParams[1]);
		    break;
		case "random":
		    userToken.setRandom(currentParams[1]);
		    break;
		case "authorizationCode":
		    userToken.setAuthorizationCode(currentParams[1]);
		    break;
		default:
		    break;
	    }
	}
	return userToken;
    }
    
    /**
     * 【解密用户Token】分析用户Token
     * 
     * @param token
     * @return
     */
    public static UserToken analyseEncryptedUserToken(String token)
    {
	UserToken userToken=decryptUserToken( token);
	return userToken;
    }
    
    /**
     * 验证用户Token
     * 
     * @param token
     * @param userToken
     * @return
     */
    public static boolean verifyUserToken(String token)
    {
	// 分析用户需要验证的Token
	UserToken userToken=decryptUserToken(token);
	// 给用户加密签名
	String encryptSignature=encryptSignature(userToken.getSignature());
	// 判定是否包含此Token
	if(!map.containsKey(encryptSignature))
	{
	    return false;
	}
	// 获取登录的用户Token
	UserToken userTokenIner=(UserToken) map.get(encryptSignature);
	// 验证是否存在此用户登录的Token
	if (userTokenIner!=null&&(userToken.toString()).equals(userTokenIner.toString()))
	{
	    // 判定时间戳是否过期
	    long currentTime = Calendar.getInstance().getTimeInMillis();
	    long timestamp = Long.valueOf(userToken.getTimestamp());
	    // Token有效时间为30分钟
	    long verifyTime = 30 * 60 * 1000;
	    if (currentTime - timestamp > verifyTime)
	    {
		// 移除过期的Token
		map.remove(encryptSignature);
		return false;
	    }
	    return true;
	}
	 return false;
    }
    
    /**
     * 获取已经签入的用户Token
     * 
     * @return
     */
    public static UserToken getUserSignedToken(String signature)
    {
	if(map.containsKey(signature))
	{
	    return map.get(signature);
	}
	return null;
    }
    
    /**
     * 根据用户名获取MD5Token
     * 
     * @param username
     * @return
     */
    public static String getAuthorizationCode(String username)
    {
	SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
	String date = sdf.format(new Date());
	String token = Md5.getPassSalt("username=" + username + "&date=" + date, Md5.getMD5Str(username));
	return token;
    }
    
    public static void main(String[] args) 
    { 
	String	token=TokenUtil.createToken("boonya");
	System.out.println("token:"+token);
	
	System.out.println("Original token:"+TokenUtil.analyseEncryptedUserToken(token));
	
	boolean result=TokenUtil.verifyUserToken(token);
	System.out.println("verify reulst:"+result);
    }

    
    /**
     * 创建用户Token
     * 
     * @param username
     * @return
     */
    public static String createTokenIOS(String username){
		UserToken userToken = generateToken(username);
		map.put(encryptSignature(userToken.getSignature()), userToken);
		return userToken.toString();
    }
    
    
    public static UserToken decryptUserTokenIOS(String token)
    {
	String[] params = token.split("&");
	// 分析用户提交过来的Token
	UserToken userToken = new UserToken();
	for (int i = 0, j = params.length; i < j; i++)
	{
	    String[] currentParams = params[i].split("=");
	    String param = currentParams[0];
	    switch (param)
	    {
		case "signature":
		    userToken.setSignature(currentParams[1]);
		    break;
		case "timestamp":
		    userToken.setTimestamp(currentParams[1]);
		    break;
		case "random":
		    userToken.setRandom(currentParams[1]);
		    break;
		case "authorizationCode":
		    userToken.setAuthorizationCode(currentParams[1]);
		    break;
		default:
		    break;
	    }
	}
	return userToken;
    }
    
    /**
     * 解析token信息非加密
     * @MethodName: verifyUserTokenIOS 
     * @Description: 
     * @param token
     * @return
     * @throws
     */
    public static boolean verifyUserTokenIOS(String token)
    {
	// 分析用户需要验证的Token
	UserToken userToken=decryptUserTokenIOS(token);
	// 给用户加密签名
	String encryptSignature=encryptSignature(userToken.getSignature());
	// 判定是否包含此Token
	if(!map.containsKey(encryptSignature))
	{
	    return false;
	}
	// 获取登录的用户Token
	UserToken userTokenIner=(UserToken) map.get(encryptSignature);
	// 验证是否存在此用户登录的Token
	if (userTokenIner!=null&&(userToken.toString()).equals(userTokenIner.toString()))
	{
	    // 判定时间戳是否过期
	    long currentTime = Calendar.getInstance().getTimeInMillis();
	    long timestamp = Long.valueOf(userToken.getTimestamp());
	    // Token有效时间为30分钟
	    long verifyTime = 30 * 60 * 1000;
	    if (currentTime - timestamp > verifyTime)
	    {
		// 移除过期的Token
		map.remove(encryptSignature);
		return false;
	    }
	    return true;
	}
	 return false;
    }
    
    
}
交换公钥工具类

package com.wlyd.wms.util.api;

import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import com.wlyd.wms.service.api.data.UserToken;

public class UserSecurityManager
{
    private static ConcurrentHashMap<String, UserToken> TOKEN = new ConcurrentHashMap<String, UserToken>();
    
    /**
     * <k,v>:<sessionId,clientPublicKey>
     */
    private static ConcurrentHashMap<String, String> CLIENT_PUBLIC = new ConcurrentHashMap<String, String>();
    
    /**
     * <k,v>:<sessionId,serverPublicKey>
     */
    private static ConcurrentHashMap<String, String> SERVER_PUBLIC = new ConcurrentHashMap<String, String>();
    
    /**
     * <k,v>:<serverPublicKey,serverPrivateKey>
     */
    private static ConcurrentHashMap<String, String> SERVER_PUBLIC_PRIVATE = new ConcurrentHashMap<String, String>();
    
    
    public static String getSessionId(String clientPublicKey)
    {
	if(CLIENT_PUBLIC.containsValue(clientPublicKey))
	{
	    for (Entry<String, String > entry: CLIENT_PUBLIC.entrySet())
	    {
		if(entry.getValue().equals(clientPublicKey))
		{
		    return entry.getKey();
		}
	    }
	}
	return null;
    }
    
    public static ConcurrentHashMap<String, UserToken> getTOKEN()
    {
        return TOKEN;
    }

    public static void rememberUserToken(String key,UserToken token)
    {
	TOKEN.put(key, token);
    }
    
    public static void removeUserToken(String key)
    {
	if(TOKEN.containsKey(key))
	{
	    TOKEN.remove(key);
	}
    }
    
    public static void rememberClientPublicKey(String sessionId,String clientPublicKey)
    {
	CLIENT_PUBLIC.put(sessionId, clientPublicKey);
    }
    
    public static String getClientPublicKey(String sessionId)
    {
	if(CLIENT_PUBLIC.containsKey(sessionId))
	{
	   return CLIENT_PUBLIC.get(sessionId);
	}
	return null;
    }
    
    public static void removeClientPublicKey(String sessionId)
    {
	if(CLIENT_PUBLIC.containsKey(sessionId))
	{
	    CLIENT_PUBLIC.remove(sessionId);
	}
    }
    
    public static void rememberServerPublicKey(String sessionId,String serverPublicKey)
    {
	SERVER_PUBLIC.put(sessionId, serverPublicKey);
    }
    
    public static String getServerPublicKey(String sessionId)
    {
	if(SERVER_PUBLIC.containsKey(sessionId))
	{
	    return SERVER_PUBLIC.get(sessionId);
	}
	return null;
    }
    
    public static String getServerPrivateKey(String sessionId)
    {
	if(SERVER_PUBLIC.containsKey(sessionId))
	{
	    String serverPublicKey= SERVER_PUBLIC.get(sessionId);
	    if(SERVER_PUBLIC_PRIVATE.containsKey(serverPublicKey))
	    {
		SERVER_PUBLIC_PRIVATE.get(serverPublicKey);
	    }
	}
	return null;
    }
    
    public static void removeServerPublicKey(String sessionId)
    {
	if(SERVER_PUBLIC.containsKey(sessionId))
	{
	    SERVER_PUBLIC.remove(sessionId);
	}
    }
    
    public static void rememberServerPublicAndPrivateKey(String serverPublicKey,String serverPrivateKey)
    {
	SERVER_PUBLIC_PRIVATE.put(serverPublicKey, serverPrivateKey);
    }
    
    public static void removeServerPublicAndPrivateKey(String serverPublicKey)
    {
	if(serverPublicKey==null||serverPublicKey.equals("")) return;
	if(SERVER_PUBLIC_PRIVATE.containsKey(serverPublicKey))
	{
	    SERVER_PUBLIC_PRIVATE.remove(serverPublicKey);
	}
    }
    
    
}



  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: sessionid和token的区别在于: 1. sessionid是服务器用来标识用户会话的一种机制,通常存储在cookie中,用于在用户访问不同页面时保持用户的登录状态。而token是一种用于身份验证和授权的令牌,通常由服务器生成并返回给客户端,客户端在后续请求中携带该令牌以证明自己的身份。 2. sessionid通常是由服务器生成的唯一标识符,用于标识用户的会话信息,包括用户的登录状态、购物车信息等。而token通常是由服务器生成的一段随机字符串,用于验证用户的身份和授权访问某些资源。 3. sessionid通常存储在cookie中,可以在客户端和服务器之间传递,但存在cookie被篡改的风险。而token通常存储在客户端的本地存储或浏览器的cookie中,可以在客户端和服务器之间传递,但相对于sessionid更加安全,因为token可以使用加密算法进行加密和解密,从而防止被篡改。 总之,sessionid和token都是用于标识用户身份和会话信息的机制,但它们的实现方式和使用场景有所不同。在实际应用中,需要根据具体的需求选择合适的机制来保证用户的安全和隐私。 ### 回答2: SessionID和TokenWeb开发中经常用到的两个概念,它们都是用来识别用户身份和维护用户状态的方式,在很多场景中我们都可以使用其中的一个或者两者结合使用。下面我将会从SessionID和Token的定义、特点和应用场景等几个方面来探讨它们之间的区别。 一、定义 SessionID,指会话ID,是服务器为了追踪用户状态而创建的一种机制,通常是一串唯一的标识符,可以存在于cookie、URL、表单隐藏元素等中。 Token,指令牌,是指用户身份验证上下文中使用的一种凭据,通常是由服务器生成的一段随机字符,通过它能够有效防止信息被篡改或者伪造。 二、特点 SessionID: 1.每个会话都有一个唯一ID,可以用于标识该会话的状态。 2.SessionID通常存储在cookie中,并随着请求一起发送到服务器。 3.由于SessionID存储在cookie中,所以存在cookie被禁用的情况下无法使用。 4.可以记录用户的状态信息,如登录状态、购物车等。 5.用户退出登录后,SessionID也会失效,并被服务器删除。 Token: 1.服务器生成Token并返回给客户端,客户端需要保存起来。 2.Token通常存储在HTTP Header或请求参数中。 3.由于Token不依赖Cookie,所以可以避免被禁用的情况。 4.可以记录用户的权限信息和登录状态。 5.客户端可以手动销毁Token。 三、应用场景 SessionID: 1.用于维护用户登录状态,避免登录后重复输入账号密码。 2.用于存储用户信息和订单信息等。 3.多用于业务场景较为简单、单一的应用。 Token: 1.为API接口鉴权提供支持。 2.用于维护浏览器与服务端的状态,如OAuth的授权过程。 3.多用于业务场景较为复杂、多样的应用。 总体来说,SessionID和Token都是用来识别用户和维护状态的方式,它们各自有其特点和应用场景。SessionID适用于简单、单一的业务场景,而Token则更适合于复杂、多样化的应用。根据实际情况选择使用哪种方式或者两者结合使用都是可以的。 ### 回答3: Session ID和Token都是身份验证和安全管理中常用的概念。 Session ID(会话标识符)是在服务器端生成的一段随机字符串。当用户第一次访问服务器时,服务器会创建该用户的一个session,并为该session生成一个唯一的session ID,该session ID会被存储在用户设备本地的cookie当中,用于在用户的后续请求中进行验证。服务器端通过session ID可以获取到用户的身份信息,从而进行相关的处理。Session ID的安全性取决于服务器生成的随机字符串的随机性和长度。 Token(令牌)通常是指一段由服务器生成的字符串,用于标识某个用户或某个操作的合法性。具体用法根据不同的情况而异。在某些场景下,Token需要在服务器端进行多次的验证,并随着后续操作的完成而失效;在另一些场景下,Token只需要在一次验证之后就可以使用,并且在过期之前一直有效。Token的安全性取决于其生成算法的安全性和密钥的保护程度。 Session ID和Token的使用场景不同。Session ID常用于Web应用程序中,在用户登录后,Web服务器会为用户生成一个session ID,并将其返回给浏览器保存在本地cookie中。该session ID可以用于识别用户,验证用户的身份,并存储用户的相关信息,如购物车、浏览记录等。而Token通常用于API调用认证中,API调用需要通过在HTTP请求头中携带Token来证明请求的合法性。Token可以有效地防止CSRF攻击,还可以提高服务器端的性能和可伸缩性。 综上所述,Session ID和Token都是用于身份验证和安全管理的重要概念,其区别在于用途和使用方式的不同。Session ID通常用于Web应用程序中,而Token则用于API调用认证等场景中。除此之外,Session ID的安全性取决于随机字符串的随机性和长度,而Token的安全性取决于其生成算法的安全性和密钥的保护程度。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值