Spring Security:身份验证令牌Authentication介绍与Debug分析

Spring Security中,通过Authentication来封装用户的验证请求信息,Authentication可以是需要验证和已验证的用户请求信息封装。接下来,博主介绍Authentication接口及其实现类。

Authentication

Authentication接口源码(Authentication接口继承Principal接口,Principal接口表示主体的抽象概念,可用于表示任何实体):

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;

public interface Authentication extends Principal, Serializable {
	/**
	 * 由AuthenticationManager(用于验证Authentication请求)设置,用于指示已授予主体的权限
	 * 除非已由受信任的AuthenticationManager设置,否则实现类不应依赖此值作为有效值
	 * 实现应确保对返回的集合数组的修改不会影响Authentication对象的状态,或使用不可修改的实例
	 * 返回:授予主体的权限,如果令牌尚未经过身份验证,则为空集合
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 证明主体的凭据
	 * 通常是一个密码,但可以是与AuthenticationManager相关的任何内容
	 * 调用者应填充凭据
	 */
	Object getCredentials();

	/**
	 * 存储有关身份验证请求的其他详细信息
	 * 可能是IP地址、证书序列号等
	 */
	Object getDetails();

	/**
	 * 被认证的主体的身份
	 * 在使用用户名和密码的身份验证请求情况下,这将是用户名
	 * 调用者应填充身份验证请求的主体
	 * AuthenticationManager实现通常会返回一个包含更丰富信息的Authentication作为应用程序使用的主体
	 * 大多数身份验证提供程序将创建一个UserDetails对象作为主体
	 */
	Object getPrincipal();

	/**
	 * 用于向AbstractSecurityInterceptor指示它是否应该向AuthenticationManager提供身份验证令牌
	 * 通常,AuthenticationManager将在身份验证成功后返回一个不可变的身份验证令牌
	 * 在这种情况下,该令牌的此方法可以安全地返回true
	 * 返回true将提高性能,因为不再需要为每个请求调用AuthenticationManager
	 * 出于安全原因,这个接口的实现应该非常小心地从这个方法返回true
	 * 除非它们是不可变的,或者有某种方式确保属性自最初创建以来没有被更改
	 */
	boolean isAuthenticated();

	/**
	 * 实现应始终允许使用false参数调用此方法
	 * 可以使用它来指定不应信任的身份验证令牌
	 */
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication接口及其实现类如下图所示:
在这里插入图片描述

AbstractAuthenticationToken

它是Authentication接口的基类,使用此类的实现应该是不可变的(模板模式)。

public abstract class AbstractAuthenticationToken implements Authentication,
		CredentialsContainer {
		
 	// 权限列表	
	private final Collection<GrantedAuthority> authorities;
	// 存储有关身份验证请求的其他详细信息,可能是IP地址、证书序列号等
	private Object details;
	// 是否已验证,默认为false
	private boolean authenticated = false;

	/**
	 * 使用提供的权限列表创建一个身份验证令牌
	 */
	public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
	    // 权限列表为null,会将authorities属性设置为AuthorityUtils.NO_AUTHORITIES
	    // List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList()
	    // Collections.emptyList()会返回一个空列表,并且不可变
		if (authorities == null) {
			this.authorities = AuthorityUtils.NO_AUTHORITIES;
			return;
		}

		for (GrantedAuthority a : authorities) {
		    // 权限列表中存在权限为null,抛出异常
			if (a == null) {
				throw new IllegalArgumentException(
						"Authorities collection cannot contain any null elements");
			}
		}
		ArrayList<GrantedAuthority> temp = new ArrayList<>(
				authorities.size());
		temp.addAll(authorities);
        // 不可修改的权限列表
		this.authorities = Collections.unmodifiableList(temp);
	}

    // 返回主体的权限列表
	public Collection<GrantedAuthority> getAuthorities() {
		return authorities;
	}

    // 返回主体的名称
	public String getName() {
	    // 如果主体是UserDetails实例,返回实例的用户名
		if (this.getPrincipal() instanceof UserDetails) {
			return ((UserDetails) this.getPrincipal()).getUsername();
		}
		// 如果主体是AuthenticatedPrincipal实例,返回实例的名称
		if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
			return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
		}
		// 如果主体是Principal实例,返回实例的名称
		if (this.getPrincipal() instanceof Principal) {
			return ((Principal) this.getPrincipal()).getName();
		}

        // 如果主体为null,则返回"",否则返回实例的toString()
		return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
	}

    // 返回主体是否已验证
	public boolean isAuthenticated() {
		return authenticated;
	}

    // 设置authenticated属性
	public void setAuthenticated(boolean authenticated) {
		this.authenticated = authenticated;
	}

    // 返回存储有关身份验证请求的其他详细信息
	public Object getDetails() {
		return details;
	}

    // 设置details属性
	public void setDetails(Object details) {
		this.details = details;
	}

	/**
	 * 检查credentials、principal和details属性
	 * 对任何CredentialsContainer实例调用eraseCredentials方法
	 */
	public void eraseCredentials() {
		eraseSecret(getCredentials());
		eraseSecret(getPrincipal());
		eraseSecret(details);
	}

    // 判断参数是否instanceof  CredentialsContainer
    // 如果是,则调用参数的eraseCredentials方法
	private void eraseSecret(Object secret) {
		if (secret instanceof CredentialsContainer) {
			((CredentialsContainer) secret).eraseCredentials();
		}
	}
	...
}
  • UserDetailsSpring Security使用UserDetails接口来抽象用户(Spring Security:用户UserDetails源码与Debug分析)。
  • AuthenticatedPrincipal:一旦Authentication请求已通过AuthenticationManager.authenticate(Authentication)方法成功验证,则表示经过身份验证的Principal(实体)。实现者通常提供他们自己的Principal表示,其中通常包含描述Principal实体的信息,例如名字、地址、电子邮件、电话以及ID等,此接口允许实现者公开其自定义的特定属性以通用方式表示Principal
  • Principal:该接口表示主体的抽象概念,可用于表示任何实体,是java.security包下的接口,并非由Spring Security提供。

UsernamePasswordAuthenticationToken

它是一种Authentication实现,继承AbstractAuthenticationToken抽象类,旨在简单地表示用户名和密码。principalcredentials属性应设置为通过其toString方法提供相应属性的Object,最简单的就是String类型。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;
	private Object credentials;

	/**
	 * 任何希望创建UsernamePasswordAuthenticationToken实例的代码都可以安全地使用此构造函数
	 * 因为isAuthenticated()将返回false
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/**
	 * 此构造函数只能由满足生成可信(即isAuthenticated() = true )身份验证令牌的AuthenticationManager或AuthenticationProvider实现使用
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // 必须使用super来设置
	}

    // 返回凭证(如密码)
	public Object getCredentials() {
		return this.credentials;
	}

    // 返回实体(如用户名)
	public Object getPrincipal() {
		return this.principal;
	}

    // 设置isAuthenticated属性,只能设置为false
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
	    // 无法将此令牌设置为受信任的令牌
	    // 需要使用有GrantedAuthority列表参数的构造函数
		if (isAuthenticated) {
			throw new IllegalArgumentException(
					"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		}

		super.setAuthenticated(false);
	}

    // 重写eraseCredentials方法
    // 将凭证直接设置为null
	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		credentials = null;
	}
}

TestingAuthenticationToken类是设计用于单元测试,对应的身份验证提供程序是TestingAuthenticationProvider,这里就不过多介绍它了。

RememberMeAuthenticationToken

它是一种Authentication实现,继承AbstractAuthenticationToken抽象类,表示需要记住的Authentication,需要记住的Authentication必须提供完全有效的Authentication ,包括适用的GrantedAuthority

public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	
	// 主体
	private final Object principal;
	// 识别此对象是否由授权客户生成的key的hashCode
	private final int keyHash;

	/**
	 * 构造函数
	 * 参数:
	 * key – 识别此对象是否由授权客户生成
	 * principal – 主体(通常是UserDetails)
	 * authorities — 授予主体的权限
	 */
	public RememberMeAuthenticationToken(String key, Object principal,
										Collection<? extends GrantedAuthority> authorities) {
		super(authorities);

		if ((key == null) || ("".equals(key)) || (principal == null)
				|| "".equals(principal)) {
			throw new IllegalArgumentException(
					"Cannot pass null or empty values to constructor");
		}

		this.keyHash = key.hashCode();
		this.principal = principal;
		setAuthenticated(true);
	}

	/**
	 * 帮助Jackson反序列化的私人构造函数
	 * 参数:
	 * keyHash – 上面给定key的hashCode
	 * principal – 主体(通常是UserDetails)
	 * authorities — 授予主体的权限
	 */
	private RememberMeAuthenticationToken(Integer keyHash, Object principal, Collection<? extends GrantedAuthority> authorities) {
		super(authorities);

		this.keyHash = keyHash;
		this.principal = principal;
		setAuthenticated(true);
	}

	/**
	 * 总是返回一个空String
	 */
	@Override
	public Object getCredentials() {
		return "";
	}

    // 返回keyHash 
	public int getKeyHash() {
		return this.keyHash;
	}

    // 返回主体
	@Override
	public Object getPrincipal() {
		return this.principal;
	}
}

PreAuthenticatedAuthenticationToken

它是一种Authentication实现,继承AbstractAuthenticationToken抽象类,用于预认证身份验证。有些情况下,希望使用Spring Security进行授权,但是在访问应用程序之前,用户已经被某个外部系统可靠地验证过了,将这种情况称为预认证场景,比如CSDN可以使用其他平台的账号进行登陆,如下图所示:
在这里插入图片描述

public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 主体
	private final Object principal;
	// 凭证
	private final Object credentials;

	/**
	 * 用于身份验证请求的构造函数
	 * isAuthenticated()将返回false 
	 */
	public PreAuthenticatedAuthenticationToken(Object aPrincipal, Object aCredentials) {
		super(null);
		this.principal = aPrincipal;
		this.credentials = aCredentials;
	}

	/**
	 * 用于身份验证响应的构造函数
	 * isAuthenticated()将返回true
	 */
	public PreAuthenticatedAuthenticationToken(Object aPrincipal, Object aCredentials,
			Collection<? extends GrantedAuthority> anAuthorities) {
		super(anAuthorities);
		this.principal = aPrincipal;
		this.credentials = aCredentials;
		setAuthenticated(true);
	}

	/**
	 * 返回凭证
	 */
	public Object getCredentials() {
		return this.credentials;
	}

	/**
	 * 返回主体
	 */
	public Object getPrincipal() {
		return this.principal;
	}
}

AnonymousAuthenticationToken

它是一种Authentication实现,继承AbstractAuthenticationToken抽象类,表示匿名Authentication

public class AnonymousAuthenticationToken extends AbstractAuthenticationToken implements
		Serializable {

	private static final long serialVersionUID = 1L;
	// 主体
	private final Object principal;
	// 识别此对象是否由授权客户生成的key的hashCode 
	private final int keyHash;

	/**
	 * 构造函数
	 * 参数:
	 * key – 识别此对象是否由授权客户生成
	 * principal – 主体(通常是UserDetails)
	 * authorities — 授予主体的权限
	 */
	public AnonymousAuthenticationToken(String key, Object principal,
										Collection<? extends GrantedAuthority> authorities) {
		this(extractKeyHash(key), principal, authorities);
	}

	/**
	 * 该构造函数有助于Jackson反序列化
	 * 参数:
	 * keyHash – 提供的Key的hashCode,由上面的构造函数提供
	 * principal – 主体(通常是UserDetails)
	 * authorities — 授予主体的权限
	 */
	private AnonymousAuthenticationToken(Integer keyHash, Object principal,
										Collection<? extends GrantedAuthority> authorities) {
		super(authorities);

		if (principal == null || "".equals(principal)) {
			throw new IllegalArgumentException("principal cannot be null or empty");
		}
		Assert.notEmpty(authorities, "authorities cannot be null or empty");

		this.keyHash = keyHash;
		this.principal = principal;
		setAuthenticated(true);
	}

    // 返回参数key的hashCode
	private static Integer extractKeyHash(String key) {
		Assert.hasLength(key, "key cannot be empty or null");
		return key.hashCode();
	}
	
	/**
	 * 总是返回一个空String
	 */
	@Override
	public Object getCredentials() {
		return "";
	}

    // 返回keyHash
	public int getKeyHash() {
		return this.keyHash;
	}

    // 返回主体
	@Override
	public Object getPrincipal() {
		return this.principal;
	}
}

RunAsUserToken

它是一种Authentication实现,继承AbstractAuthenticationToken抽象类,用于支持RunAsManagerImplAuthentication实现。

RunAsManagerImpl类是RunAsManager接口的基本实现,如果发现ConfigAttribute.getAttribute()RUN_AS_为前缀,它会生成一个新的RunAsUserToken实例,包含与原始Authentication实例相同的主体、凭证和授予权限列表等。

RunAsManager接口仅为当前安全对象调用创建一个新的临时Authentication实例,此接口允许实现替换,仅适用于当前安全对象调用的Authentication对象。

这是为了建立具有两层对象的系统,一层是面向公众的,并且具有正常的安全方法,授予的权限预计由外部调用者持有。 另一层是私有的,只希望由面向公众的层中的对象调用,此私有层中的对象仍然需要安全性(否则它们将是公共方法),需要防止被外部调用者直接调用,并且私有层中的对象被授予的权限从不授予外部调用者。

RunAsManager接口提供了一种以上述方式提升安全性的机制。预计实现将提供相应的具体AuthenticationAuthenticationProvider以便可以对替换的Authentication对象进行身份验证。需要实施某种形式的安全性,以确保AuthenticationProvider仅接受由RunAsManager授权的具体实现创建的Authentication对象。

public class RunAsUserToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 原Authentication对象的类型
	private final Class<? extends Authentication> originalAuthentication;
	// 凭证
	private final Object credentials;
	// 主体
	private final Object principal;
	// 识别此对象是否由授权客户生成的key的hashCode 
	private final int keyHash;

    // 构造方法
	public RunAsUserToken(String key, Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities,
			Class<? extends Authentication> originalAuthentication) {
		super(authorities);
		this.keyHash = key.hashCode();
		this.principal = principal;
		this.credentials = credentials;
		this.originalAuthentication = originalAuthentication;
		setAuthenticated(true);
	}

    // 返回凭证
	@Override
	public Object getCredentials() {
		return this.credentials;
	}
    
    // 返回keyHash属性
	public int getKeyHash() {
		return this.keyHash;
	}

    // 返回原Authentication对象的类型
	public Class<? extends Authentication> getOriginalAuthentication() {
		return this.originalAuthentication;
	}

    // 返回主体
	@Override
	public Object getPrincipal() {
		return this.principal;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(super.toString());
		String className = this.originalAuthentication == null ? null
				: this.originalAuthentication.getName();
		sb.append("; Original Class: ").append(className);

		return sb.toString();
	}
}

JaasAuthenticationToken

UsernamePasswordAuthenticationToken的扩展,用来携带用户登录的Jaas LoginContext

public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
    // 用户登录的Jaas LoginContext
	private final transient LoginContext loginContext;

	// 构造函数
	public JaasAuthenticationToken(Object principal, Object credentials,
			LoginContext loginContext) {
		super(principal, credentials);
		this.loginContext = loginContext;
	}

    // 构造函数
	public JaasAuthenticationToken(Object principal, Object credentials,
			List<GrantedAuthority> authorities, LoginContext loginContext) {
		super(principal, credentials, authorities);
		this.loginContext = loginContext;
	}

	// 返回用户登录的Jaas LoginContext
	public LoginContext getLoginContext() {
		return loginContext;
	}
}

Debug分析

项目结构图:
在这里插入图片描述

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaven</groupId>
    <artifactId>security</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.6.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

application.yml

spring:
  security:
    user:
      name: kaven
      password: itkaven
logging:
  level:
    org:
      springframework:
        security: DEBUG

SecurityConfigSpring Security的配置类,不是必须的,因为会有默认的配置):

package com.kaven.security.config;

import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 任何请求都需要进行验证
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                 // 记住身份验证
                .rememberMe(Customizer.withDefaults())
                // 基于表单登陆的身份验证方式
                .formLogin(Customizer.withDefaults());
    }
}

MessageController(定义接口):

package com.kaven.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {
    @GetMapping("/message")
    public String getMessage() {
        return "hello spring security";
    }
}

启动类:

package com.kaven.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

Debug方式启动应用,访问http://localhost:8080/message。请求会被AnonymousAuthenticationFilter处理,该过滤器会创建Authentication实例。
在这里插入图片描述
创建的便是AnonymousAuthenticationToken实例。
在这里插入图片描述
在这里插入图片描述
创建完AnonymousAuthenticationToken实例之后,请求会继续被其他过滤器处理,这就是Spring Security提供的过滤器链。在访问接口前,Spring Security会检查该请求的客户端是否具有访问该接口的权限。很显然是没有权限的,因此访问被拒绝了。
在这里插入图片描述
并且请求会被重定向到登录页,填入用户名和密码(配置文件中定义的)。

在这里插入图片描述

登陆请求会被UsernamePasswordAuthenticationFilter处理,该过滤器会创建UsernamePasswordAuthenticationToken实例,该实例将用于验证。
在这里插入图片描述

在这里插入图片描述
如果该UsernamePasswordAuthenticationToken实例验证成功,将会创建一个新的UsernamePasswordAuthenticationToken实例,表示身份验证成功的令牌。

在这里插入图片描述
在这里插入图片描述
登陆请求验证成功后,又会进行重定向,重定向到原来想要访问的接口(资源),即/message
在这里插入图片描述
再次重定向的请求又会被过滤器链进行处理,最后会验证通过。
在这里插入图片描述
接口便访问成功了。

在这里插入图片描述
再来回味一下这段话,就很容易理解Authentication的作用了,在Spring Security中,通过Authentication来封装用户的验证请求信息,Authentication可以是需要验证和已验证的用户请求信息封装。

Debug分析中的整个流程搞明白,便很容易理解Authentication的作用,不同的Authentication实现用于不同的验证时机与场景。

身份验证令牌Authentication介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

  • 19
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Security AuthenticationSpring Security 框架中的一个关键概念。它提供了身份验证Authentication)的功能,用于验证用户的身份和凭据。 在 Spring Security 中,身份验证是通过 AuthenticationManager 进行的。AuthenticationManager 是一个接口,定义了身份验证的方法。它通常由 ProviderManager 实现,ProviderManager 可以配置多个 AuthenticationProvider,每个 Provider 都负责特定类型的身份验证。 在进行身份验证时,用户提供的凭据将被封装为一个 Authentication 对象,并传递给 AuthenticationManager 进行验证。AuthenticationManager 将根据配置的 AuthenticationProvider 逐个尝试进行验证,直到找到一个能够成功验证该凭据的 Provider。如果所有 Provider 都无法验证凭据,将抛出相应的异常。 一旦成功验证了用户的凭据,AuthenticationManager 将返回一个经过填充的 Authentication 对象,其中包含有关用户身份的信息和权限。这个经过验证的 Authentication 对象将被存储在 SecurityContext 中,以供后续的授权(Authorization)过程使用。 在 Spring Security 中,我们可以使用不同的凭据进行身份验证,比如用户名/密码、基于令牌身份验证等。同时,Spring Security 还提供了灵活的配置选项,可以根据需求自定义身份验证的流程和规则。 希望以上内容对你有所帮助!如果还有其他问题,请继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ITKaven

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

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

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

打赏作者

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

抵扣说明:

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

余额充值