FIDO框架分析2(FIDO UAF服务器)

本文详细介绍了FIDO UAF服务器的实现,包括其基于Java的服务器框架、依赖库如GSON和Jersey,以及服务器包结构和功能。FidoUafResource作为连接FIDO演示服务器表面和内部处理的核心,管理用户注册、注销等操作。虽然此服务器为测试目的将RP服务器和FIDO服务器混合,实际应用中应将两者分离,并考虑与数据库集成。
摘要由CSDN通过智能技术生成

FIDO UAF服务器

 

fidouaf  github地址: https://github.com/eBay/UAF/tree/master/fidouaf

FIDO UAF演示服务器通过使用github上面分析的UAF Core的代码实现了可以实际运行和使用的服务器。它是用Java编写的,我使用SUN的Jersey服务器框架创建了一个服务器。依赖关系如下:除了使用GSON之外,演示服务器使用Jersey单独提供的JSON库来执行某些任务。此外,可以确认直接引用和使用UAF Core。

\fidouaf\pom.xml

<dependencies>
	<dependency>
		<groupId>com.sun.jersey</groupId>
		<artifactId>jersey-server</artifactId>
		<version>1.8</version>
	</dependency>
	<dependency>
		<groupId>com.sun.jersey</groupId>
		<artifactId>jersey-json</artifactId>
		<version>1.8</version>
	</dependency>
	<dependency>
		<groupId>org.ebayopensource</groupId>
		<artifactId>fido-uaf-core</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</dependency>
	<dependency>
		<groupId>com.google.code.gson</groupId>
		<artifactId>gson</artifactId>
		<version>2.3.1</version>
	</dependency>
</dependencies>

包的结构如下:驱动部分位于org.ebayopensource.fidouaf.res中的Hello和FidoUfResource中,Hello是一个简单的测试服务器,FidoUfResource提供实际的RP服务器和FIDO服务器页面。

以下是每个子包的说明。

  • “facets”包中包含为存储各种FacetID而创建的实体类。
  • “RPserver.msg”包中包含用于编写和接收各种消息的容器(Reg,Dereg,Auth),TokenType和Token是定义的.TokenType是枚举数据类型,如下所示,它包含登录类型。

TokenType.java:

package org.ebayopensource.fidouaf.RPserver.msg.enums;

public enum TokenType 
{
	HTTP_COOKIE,
	OAUTH,
	OAUTH2,
	SAML1_1,
	SAML2,
	JWT,
	OPENID_CONNECT
}
  • “stats”  包中有一个名为 Dash 的类,它存储多达100个已由命令(Auth,Reg,Dereg)处理的哈希映射,它是一个日志存储类。
  • “res”包可以被认为是真实的表面服务器实现,在“res.util”  包中,有一个Notary和Storage接口的实现类,并且存在一个处理响应消息或处理Dereg请求的类,当您包含AppID和AAID时,FetchRequest类充当中介,以帮助您执行特定请求。

 

FidoUafResource.java

@Path("/v1")
public class FidoUafResource {

    Gson gson = new GsonBuilder().disableHtmlEscaping().create();

    @GET
    @Path("/stats")
    @Produces(MediaType.APPLICATION_JSON)
    public String getStats() {
        return gson.toJson(Dash.getInstance().stats);
    }

    @GET
    @Path("/history")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Object> getHistory() {
        return Dash.getInstance().history;
    }

    @GET
    @Path("/registrations")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, RegistrationRecord> getDbDump() {
        return StorageImpl.getInstance().dbDump();
    }

    /**
     * @param username
     * @return RegistrationRequest[]
     * @desc 注册请求携带用户名
     * @method get
     */
    @GET
    @Path("/public/regRequest/{username}")
    @Produces(MediaType.APPLICATION_JSON)
    public RegistrationRequest[] getRegisReqPublic(@PathParam("username") String username) {
        RegistrationRequest[] regReq = new RegistrationRequest[1];
        regReq[0] = new FetchRequest(getAppId(), getAllowedAaids()).getRegistrationRequest(username);
        Dash.getInstance().stats.put(Dash.LAST_REG_REQ, regReq);
        Dash.getInstance().history.add(regReq);
        return regReq;
    }

    /**
     * @param username,appId
     * @return String
     * @desc 注册请求携带用户名和appId
     * @method get
     */
    @GET
    @Path("/public/regRequest/{username}/{appId}")
    @Produces(MediaType.APPLICATION_JSON)
    public String getRegReqForAppId(
            @PathParam("username") String username,
            @PathParam("appId") String appId) {
        RegistrationRequest[] regReq = getRegisReqPublic(username);
        setAppId(appId, regReq[0].header);
        return gson.toJson(regReq);
    }

    /**
     * @param username
     * @return RegistrationRequest[]
     * @desc 注册请求
     * @method get
     */
    @GET
    @Path("/public/regRequest")
    @Produces(MediaType.APPLICATION_JSON)
    public RegistrationRequest[] postRegisReqPublic(String username) {
        RegistrationRequest[] regReq = new RegistrationRequest[1];
        regReq[0] = new FetchRequest(getAppId(), getAllowedAaids()).getRegistrationRequest(username);
        Dash.getInstance().stats.put(Dash.LAST_REG_REQ, regReq);
        Dash.getInstance().history.add(regReq);
        return regReq;
    }

    private String[] getAllowedAaids() {
        String[] ret = {"EBA0#0001", "0015#0001", "0012#0002", "0010#0001",
                "4e4e#0001", "5143#0001", "0011#0701", "0013#0001",
                "0014#0000", "0014#0001", "53EC#C002", "DAB8#8001",
                "DAB8#0011", "DAB8#8011", "5143#0111", "5143#0120",
                "4746#F816", "53EC#3801"};
        return ret;
    }

    @GET
    @Path("/public/uaf/facets")
    @Produces("application/fido.trusted-apps+json")
    public Facets facets() {
        String timestamp = new Date().toString();
        Dash.getInstance().stats.put(Dash.LAST_REG_REQ, timestamp);
        String[] trustedIds = {"https://www.head2toes.org",
                "android:apk-key-hash:Df+2X53Z0UscvUu6obxC3rIfFyk",
                "android:apk-key-hash:bE0f1WtRJrZv/C0y9CM73bAUqiI",
                "https://openidconnect.ebay.com"};
        Facets facets = new Facets();
        facets.trustedFacets = new TrustedFacets[1];
        TrustedFacets trusted = new TrustedFacets();
        trusted.version = new Version(1, 0);
        trusted.ids = trustedIds;
        facets.trustedFacets[0] = trusted;
        return facets;
    }

    /**
     * 获取appId
     */
    private String getAppId() {
        return "https://www.head2toes.org/fidouaf/v1/public/uaf/facets";
    }

    /**
     * @param payload
     * @return RegistrationRecord[]
     * @desc 注册响应
     * @method post
     */
    @POST
    @Path("/public/regResponse")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public RegistrationRecord[] processRegResponse(String payload) {
        RegistrationRecord[] result = null;
        Gson gson = new Gson();

        RegistrationResponse[] fromJson = gson.fromJson(payload, RegistrationResponse[].class);
        Dash.getInstance().stats.put(Dash.LAST_REG_RES, fromJson);
        Dash.getInstance().history.add(fromJson);

        RegistrationResponse registrationResponse = fromJson[0];
        result = new ProcessResponse().processRegResponse(registrationResponse);
        if (result[0].status.equals("SUCCESS")) {
            try {
                StorageImpl.getInstance().store(result);
            } catch (DuplicateKeyException e) {
                result = new RegistrationRecord[1];
                result[0] = new RegistrationRecord();
                result[0].status = "Error: Duplicate Key";
            } catch (SystemErrorException e1) {
                result = new RegistrationRecord[1];
                result[0] = new RegistrationRecord();
                result[0].status = "Error: Data couldn't be stored in DB";
            }
        }
        return result;
    }

    /**
     * @param payload
     * @return String
     * @desc 注销请求
     * @method post
     */
    @POST
    @Path("/public/deregRequest")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public String deregRequestPublic(String payload) {
        return new DeregRequestProcessor().process(payload);
    }

    /**
     * @param
     * @return String
     * @desc 授权请求
     * @method get
     */
    @GET
    @Path("/public/authRequest")
    @Produces(MediaType.APPLICATION_JSON)
    public String getAuthReq() {
        return gson.toJson(getAuthReqObj());
    }

    /**
     * @param appId
     * @return String
     * @desc 授权请求携带appId
     * @method get
     */
    @GET
    @Path("/public/authRequest/{appId}")
    @Produces(MediaType.APPLICATION_JSON)
    public String getAuthForAppIdReq(@PathParam("appId") String appId) {
        AuthenticationRequest[] authReqObj = getAuthReqObj();
        setAppId(appId, authReqObj[0].header);
        return gson.toJson(authReqObj);
    }

    private void setAppId(String appId, OperationHeader header) {
        if (appId == null || appId.isEmpty()) {
            return;
        }
        String decodedAppId = new String(Base64.decodeBase64(appId));
        Facets facets = facets();
        if (facets == null || facets.trustedFacets == null || facets.trustedFacets.length == 0
                || facets.trustedFacets[0] == null || facets.trustedFacets[0].ids == null) {
            return;
        }
        String[] ids = facets.trustedFacets[0].ids;
        for (int i = 0; i < ids.length; i++) {

            if (decodedAppId.equals(ids[i])) {
                header.appID = decodedAppId;
                break;
            }
        }
    }

    /**
     * @param appId,trxContent
     * @return String
     * @desc 授权请求携带appId和trxContent
     * @method get
     */
    @GET
    @Path("/public/authRequest/{appId}/{trxContent}")
    @Produces(MediaType.APPLICATION_JSON)
    public String getAuthTrxReq(@PathParam("appId") String appId, @PathParam("trxContent") String trxContent) {
        AuthenticationRequest[] authReqObj = getAuthReqObj();
        setAppId(appId, authReqObj[0].header);
        setTransaction(trxContent, authReqObj);
        return gson.toJson(authReqObj);
    }

    private void setTransaction(String trxContent, AuthenticationRequest[] authReqObj) {
        authReqObj[0].transaction = new Transaction[1];
        Transaction t = new Transaction();
        t.content = trxContent;
        t.contentType = MediaType.TEXT_PLAIN;
        authReqObj[0].transaction[0] = t;
    }

    public AuthenticationRequest[] getAuthReqObj() {
        AuthenticationRequest[] ret = new AuthenticationRequest[1];
        ret[0] = new FetchRequest(getAppId(), getAllowedAaids()).getAuthenticationRequest();
        Dash.getInstance().stats.put(Dash.LAST_AUTH_REQ, ret);
        Dash.getInstance().history.add(ret);
        return ret;
    }

    /**
     * @param payload
     * @return AuthenticatorRecord[]
     * @desc 授权响应
     * @method post
     */
    @POST
    @Path("/public/authResponse")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public AuthenticatorRecord[] processAuthResponse(String payload) {
        Dash.getInstance().stats.put(Dash.LAST_AUTH_RES, payload);
        Gson gson = new Gson();
        AuthenticationResponse[] authResp = gson.fromJson(payload, AuthenticationResponse[].class);
        Dash.getInstance().stats.put(Dash.LAST_AUTH_RES, authResp);
        Dash.getInstance().history.add(authResp);
        AuthenticatorRecord[] result = new ProcessResponse().processAuthResponse(authResp[0]);
        return result;
    }

    /**
     * @param payload
     * @return ReturnUAFRegistrationRequest
     * @desc uaf注册请求
     * @method post
     */
    @POST
    @Path("/public/uafRegRequest")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ReturnUAFRegistrationRequest GetUAFRegistrationRequest(String payload) {
        RegistrationRequest[] result = getRegisReqPublic("iafuser01");
        ReturnUAFRegistrationRequest uafReq = null;
        if (result != null) {
            uafReq = new ReturnUAFRegistrationRequest();
            uafReq.statusCode = 1200;
            uafReq.uafRequest = result;
            uafReq.op = Operation.Reg;
            uafReq.lifetimeMillis = 5 * 60 * 1000;
        }
        return uafReq;
    }

    /**
     * @param payload
     * @return ReturnUAFAuthenticationRequest
     * @desc uaf授权请求
     * @method post
     */
    @POST
    @Path("/public/uafAuthRequest")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ReturnUAFAuthenticationRequest GetUAFAuthenticationRequest(String payload) {
        AuthenticationRequest[] result = getAuthReqObj();
        ReturnUAFAuthenticationRequest uafReq = null;
        if (result != null) {
            uafReq = new ReturnUAFAuthenticationRequest();
            uafReq.statusCode = 1200;
            uafReq.uafRequest = result;
            uafReq.op = Operation.Auth;
            uafReq.lifetimeMillis = 5 * 60 * 1000;
        }
        return uafReq;
    }

    /**
     * @param payload
     * @return ReturnUAFDeregistrationRequest
     * @desc uaf注销请求
     * @method post
     */
    @POST
    @Path("/public/uafDeregRequest")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ReturnUAFDeregistrationRequest GetUAFDeregistrationRequest(String payload) {
        String result = deregRequestPublic(payload);
        ReturnUAFDeregistrationRequest uafReq = new ReturnUAFDeregistrationRequest();
        if (result.equalsIgnoreCase("Success")) {
            uafReq.statusCode = 1200;
        } else if (result.equalsIgnoreCase("Failure: Problem in deleting record from local DB")) {
            uafReq.statusCode = 1404;
        } else if (result.equalsIgnoreCase("Failure: problem processing deregistration request")) {
            uafReq.statusCode = 1491;
        } else {
            uafReq.statusCode = 1500;

        }
        uafReq.uafRequest = null;
        uafReq.op = Operation.Dereg;
        uafReq.lifetimeMillis = 0;
        return uafReq;
    }

    /**
     * @param payload
     * @return ServerResponse
     * @desc uaf授权响应
     * @method post
     */
    @POST
    @Path("/public/uafAuthResponse")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ServerResponse UAFAuthResponse(String payload) {
        String findOp = payload;
        findOp = findOp.substring(findOp.indexOf("op") + 6,
                findOp.indexOf(",", findOp.indexOf("op")) - 1);
        System.out.println("findOp=" + findOp);

        AuthenticatorRecord[] result = processAuthResponse(payload);
        ServerResponse servResp = new ServerResponse();
        if (result[0].status.equals("SUCCESS")) {
            servResp.statusCode = 1200;
            servResp.Description = "OK. Operation completed";
        } else if (result[0].status.equals("FAILED_SIGNATURE_NOT_VALID")
                || result[0].status.equals("FAILED_SIGNATURE_VERIFICATION")
                || result[0].status.equals("FAILED_ASSERTION_VERIFICATION")) {
            servResp.statusCode = 1496;
            servResp.Description = result[0].status;
        } else if (result[0].status.equals("INVALID_SERVER_DATA_EXPIRED")
                || result[0].status.equals("INVALID_SERVER_DATA_SIGNATURE_NO_MATCH")
                || result[0].status.equals("INVALID_SERVER_DATA_CHECK_FAILED")) {
            servResp.statusCode = 1491;
            servResp.Description = result[0].status;
        } else {
            servResp.statusCode = 1500;
            servResp.Description = result[0].status;
        }

        return servResp;
    }

    /**
     * @param payload
     * @return ServerResponse
     * @desc uaf注册响应
     * @method post
     */
    @POST
    @Path("/public/uafRegResponse")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ServerResponse UAFRegResponse(String payload) {
        String findOp = payload;
        findOp = findOp.substring(findOp.indexOf("op") + 6,
                findOp.indexOf(",", findOp.indexOf("op")) - 1);
        System.out.println("findOp=" + findOp);

        RegistrationRecord[] result = processRegResponse(payload);
        ServerResponse servResp = new ServerResponse();
        if (result[0].status.equals("SUCCESS")) {
            servResp.statusCode = 1200;
            servResp.Description = "OK. Operation completed";
        } else if (result[0].status.equals("ASSERTIONS_CHECK_FAILED")) {
            servResp.statusCode = 1496;
            servResp.Description = result[0].status;
        } else if (result[0].status.equals("INVALID_SERVER_DATA_EXPIRED")
                || result[0].status.equals("INVALID_SERVER_DATA_SIGNATURE_NO_MATCH")
                || result[0].status.equals("INVALID_SERVER_DATA_CHECK_FAILED")) {
            servResp.statusCode = 1491;
            servResp.Description = result[0].status;
        } else {
            servResp.statusCode = 1500;
            servResp.Description = result[0].status;
        }

        return servResp;
    }

    /**
     * @param payload(有效载荷)
     * @return String
     * @desc uaf请求
     * @method post
     */
    @POST
    @Path("/public/uafRequest")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public String GetUAFRequest(String payload) {
        String uafReq = null;
        Gson gson = new Gson();
        GetUAFRequest req = gson.fromJson(payload, GetUAFRequest.class);

        if (req.op.name().equals("Reg")) {
            RegistrationRequest[] result = getRegisReqPublic("iafuser01");
            ReturnUAFRegistrationRequest uafRegReq = null;
            if (result != null) {
                uafRegReq = new ReturnUAFRegistrationRequest();
                uafRegReq.statusCode = 1200;
                uafRegReq.uafRequest = result;
                uafRegReq.op = Operation.Reg;
                uafRegReq.lifetimeMillis = 5 * 60 * 1000;
            }
            uafReq = gson.toJson(uafRegReq);
        } else if (req.op.name().equals("Auth")) {
            AuthenticationRequest[] result = getAuthReqObj();
            ReturnUAFAuthenticationRequest uafAuthReq = null;
            if (result != null) {
                uafAuthReq = new ReturnUAFAuthenticationRequest();
                uafAuthReq.statusCode = 1200;
                uafAuthReq.uafRequest = result;
                uafAuthReq.op = Operation.Auth;
                uafAuthReq.lifetimeMillis = 5 * 60 * 1000;
            }
            uafReq = gson.toJson(uafAuthReq);
        } else if (req.op.name().equals("Dereg")) {
            String result = deregRequestPublic(payload);
            ReturnUAFDeregistrationRequest uafDeregReq = new ReturnUAFDeregistrationRequest();
            if (result.equalsIgnoreCase("Success")) {
                uafDeregReq.statusCode = 1200;
            } else if (result.equalsIgnoreCase("Failure: Problem in deleting record from local DB")) {
                uafDeregReq.statusCode = 1404;
            } else if (result.equalsIgnoreCase("Failure: problem processing deregistration request")) {
                uafDeregReq.statusCode = 1491;
            } else {
                uafDeregReq.statusCode = 1500;
            }
            uafDeregReq.uafRequest = null;
            uafDeregReq.op = Operation.Dereg;
            uafDeregReq.lifetimeMillis = 0;
            uafReq = gson.toJson(uafDeregReq);
        }
        return uafReq;
    }

    /**
     * @param payload
     * @return ServerResponse
     * @desc uaf响应
     * @method post
     */
    @POST
    @Path("/public/uafResponse")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ServerResponse UAFResponse(String payload) {
        String findOp = payload;
        findOp = findOp.substring(findOp.indexOf("op") + 6,
                findOp.indexOf(",", findOp.indexOf("op")) - 1);
        System.out.println("findOp=" + findOp);

        ServerResponse servResp = new ServerResponse();

        if (findOp.equals("Reg")) {
            RegistrationRecord[] result = processRegResponse(payload);
            if (result[0].status.equals("SUCCESS")) {
                servResp.statusCode = 1200;
                servResp.Description = "OK. Operation completed";
            } else if (result[0].status.equals("ASSERTIONS_CHECK_FAILED")) {
                servResp.statusCode = 1496;
                servResp.Description = result[0].status;
            } else if (result[0].status.equals("INVALID_SERVER_DATA_EXPIRED")
                    || result[0].status.equals("INVALID_SERVER_DATA_SIGNATURE_NO_MATCH")
                    || result[0].status.equals("INVALID_SERVER_DATA_CHECK_FAILED")) {
                servResp.statusCode = 1491;
                servResp.Description = result[0].status;
            } else {
                servResp.statusCode = 1500;
                servResp.Description = result[0].status;
            }
        } else if (findOp.equals("Auth")) {
            AuthenticatorRecord[] result = processAuthResponse(payload);
            if (result[0].status.equals("SUCCESS")) {
                servResp.statusCode = 1200;
                servResp.Description = "OK. Operation completed";
            } else if (result[0].status.equals("FAILED_SIGNATURE_NOT_VALID")
                    || result[0].status.equals("FAILED_SIGNATURE_VERIFICATION")
                    || result[0].status.equals("FAILED_ASSERTION_VERIFICATION")) {
                servResp.statusCode = 1496;
                servResp.Description = result[0].status;
            } else if (result[0].status.equals("INVALID_SERVER_DATA_EXPIRED")
                    || result[0].status.equals("INVALID_SERVER_DATA_SIGNATURE_NO_MATCH")
                    || result[0].status.equals("INVALID_SERVER_DATA_CHECK_FAILED")) {
                servResp.statusCode = 1491;
                servResp.Description = result[0].status;
            } else {
                servResp.statusCode = 1500;
                servResp.Description = result[0].status;
            }
        }
        return servResp;
    }
}

 

FidoUfResource是一个实际连接FIDO演示服务器表面(例如URL可访问)和内部(UAF基于核心的处理程序)的类,以下是FIDO演示服务器的类图,您可以在其中看到FidoUfResource创建并使用其他类。

结构本身很简单,由于在Jersey中提供网页的方式类似于JSP,因此第一个看到的人很快就能理解它。

FidoUafResource.getDbDump()

@GET
@Path("/registrations")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, RegistrationRecord> getDbDump() {
	return StorageImpl.getInstance().dbDump();
}

以上是注册用户的接口

@GET
@Path("/public/regRequest/{username}")
@Produces(MediaType.APPLICATION_JSON)
public RegistrationRequest[] getRegisReqPublic(
		@PathParam("username") String username) {
	RegistrationRequest[] regReq = new RegistrationRequest[1];
	regReq[0] = new FetchRequest(getAppId(),getAllowedAaids()).getRegistrationRequest(username);
	Dash.getInstance().stats.put(Dash.LAST_REG_REQ, regReq);
	Dash.getInstance().history.add(regReq);
	return regReq;
}

以上是使用用户名注册的接口

@POST
@Path("/public/deregRequest")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String deregRequestPublic(String payload) {

	return new DeregRequestProcessor().process(payload);
}

以上是接收用户注销请求的接口

总而言之,FIDO演示服务器以与上述相同的方式提供各种页面和处理请求。下面是已定义函数和变量的列表:

 

此演示服务器最重要的功能是RP服务器和FIDO服务器是混合的,这是因为它是出于测试目的而制作的,但一般来说,将RP服务器和FIDO服务器分开是一个规则。此外,由于未使用数据库引擎,因此所有appId和aaid都在类中进行了硬编码。可以理解为,仅在服务器运行时才在哈希映射中管理用户信息。

如果要基于此库构建自己的服务器,则需要单独与DB一起使用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值