堡垒机系统

需求:
  所有的用户操作日志要保留在数据库中
  每个用户登录堡垒机后,只需要选择具体要访问的设置,就连接上了,不需要再输入目标机器的访问密码
  允许用户对不同的目标设备有不同的访问权限,例:
  对10.0.2.34 有mysql 用户的权限
  对192.168.3.22 有root用户的权限
  对172.33.24.55 没任何权限
  分组管理,即可以对设置进行分组,允许用户访问某组机器,但对组里的不同机器依然有不同的访问权限  
实现的需求:
  针对不同的用户使用有权限的账户进入到指定的服务器中,进行操作,并记录下操作的命令
  首先创建表数据
  python bin\start.py syncdb
  然后用yaml模块写入数据
       例如:python bin\start.py create_hosts -f share\example\new_hosts.yml
       数据导入完毕就开始连接远程系统
       python bin\start.py start_session
       输入堡垒机的用户名和密码:(alex,alex123)
       显示出分组信息
       选择需要进入的服务器信息(不用输入服务器的账号密码)
       进入系统

测试:在ubuntu16.04下 安装有mysql5.7.21 使用python3.5 远程系统centos6.5

目录结构

数据库数据构建

 

流程图

 

代码:

1 #_*_coding:utf-8_*_
2 import os,sys
3 
4 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 sys.path.append(BASE_DIR)
6 
7 from modules.actions import excute_from_command_line
8 if __name__=="__main__":
9     excute_from_command_line(sys.argv)
bin\start
 1 #_*_coding:utf-8_*_
 2 from modules import views
 3 
 4 actions = {
 5     "syncdb": views.syncdb,
 6     "create_hosts":views.create_hosts,
 7     "create_groups":views.create_groups,
 8     "create_remoteusers":views.create_remoteusers,
 9     "create_userprofiles":views.create_userprofiles,
10     "create_bindhosts":views.create_bindhosts,
11     "start_session":views.start_session,
12 }
conf\action_registers
1 import os,sys
2 
3 BASE_DIR =os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4 
5 
6 ConnParams="mysql+pymysql://root:alex1234@192.168.189.129/fortdb?charset=utf8"
conf\settings
  1 from sqlalchemy import Integer, ForeignKey, String, Column,Table,UniqueConstraint,DateTime
  2 from sqlalchemy.ext.declarative import declarative_base
  3 from sqlalchemy.orm import relationship
  4 from sqlalchemy_utils import ChoiceType,PasswordType
  5 
  6 
  7 
  8 
  9 Base=declarative_base() #创建数据表结构
 10 
 11 userprofile_m2m_bindhost = Table("userprofile_m2m_bindhost",Base.metadata,
 12                             Column("userprofile_id",Integer,ForeignKey("user_profile.id")),
 13                             Column("bindhost_id",Integer,ForeignKey("bind_host.id")),
 14                             )
 15 
 16 hostgroup_m2m_bindhost = Table("hostgroup_m2m_bindhost",Base.metadata,
 17                             Column("hostgroup_id",Integer,ForeignKey("host_group.id")),
 18                             Column("bindhost_id",Integer,ForeignKey("bind_host.id")),
 19                             )
 20 
 21 userprofile_m2m_hostgroup = Table("userprofile_m2m_hostgroup",Base.metadata,
 22                             Column("userprofile_id",Integer,ForeignKey("user_profile.id")),
 23                             Column("hostgroup_id",Integer,ForeignKey("host_group.id")),
 24                             )
 25 
 26 class Host(Base):#主机
 27     __tablename__="host"
 28     id =Column(Integer,primary_key=True)
 29     hostname=Column(String(64),unique=True)
 30     ip = Column(String(64),unique=True)
 31     port = Column(Integer,default=22)
 32 
 33 
 34     def __repr__(self):
 35         return self.hostname
 36 class HostGroup(Base):#主机组
 37     __tablename__="host_group"
 38     id =Column(Integer,primary_key=True)
 39     groupname=Column(String(64),unique=True)
 40 
 41     bindhosts=relationship("BindHost",secondary="hostgroup_m2m_bindhost",backref="host_groups")
 42 
 43     def __repr__(self):
 44         return self.groupname
 45 class RemoteUser(Base):#系统用户
 46     __tablename__ = "remote_user"
 47     __tableargs__ = UniqueConstraint("username","password","auth_type",name="user_passwd_uc")#联合唯一
 48     AuthTypes = [
 49         ('ssh-passwd', 'SSH/Password'),
 50         ('ssh-key', 'SSH/KEY'),
 51     ]
 52     id = Column(Integer, primary_key=True)
 53     username = Column(String(64))
 54     password = Column(String(128))
 55     auth_type=Column(ChoiceType(AuthTypes))
 56 
 57 
 58     def __repr__(self):
 59         return self.username
 60 class UserProfile(Base):#堡垒机用户
 61     __tablename__ = "user_profile"
 62     id = Column(Integer, primary_key=True)
 63     username = Column(String(64),unique=True)
 64     password = Column(String(128))
 65     bind_hosts = relationship("BindHost",secondary="userprofile_m2m_bindhost",backref="user_profiles")
 66     host_groups = relationship("HostGroup",secondary="userprofile_m2m_hostgroup",backref="user_profiles")
 67     audit_logs = relationship('AuditLog')
 68 
 69     def __repr__(self):
 70         return self.username
 71 class BindHost(Base):
 72     """
 73     192.168.1.0  web
 74     192.168.1.1  mysql
 75     """
 76     __tablename__ = "bind_host"
 77     __tableargs__=UniqueConstraint("host_id","remoteuser_id",name="host_remoteuser_uc")
 78     id = Column(Integer, primary_key=True)
 79     host_id = Column(Integer,ForeignKey("host.id"))
 80     # group_id = Column(Integer,ForeignKey("host_group.id"))
 81     remoteuser_id = Column(Integer,ForeignKey("remote_user.id"))
 82     host=relationship("Host",backref="bind_hosts")
 83     # group=relationship("HostGroup",backref="bind_hosts")
 84     remoteuser=relationship("RemoteUser",backref="bind_hosts")
 85     audit_logs = relationship('AuditLog')
 86 
 87     def __repr__(self):
 88         return "<%s --- %s>"%(self.host.ip,self.remoteuser.username)
 89 class AuditLog(Base):
 90     __tablename__ = 'audit_log'
 91     id = Column(Integer,primary_key=True)
 92     user_id = Column(Integer,ForeignKey('user_profile.id'))
 93     bind_host_id = Column(Integer,ForeignKey('bind_host.id'))
 94     action_choices= [
 95         ('cmd','CMD'),
 96         ('login','Login'),
 97         ('logout','Logout'),
 98     ]
 99     action_type = Column(ChoiceType(action_choices))
100     cmd = Column(String(255))
101     date = Column(DateTime)
102     user_profile = relationship("UserProfile")
103     bind_host = relationship("BindHost")
104 
105     # def __repr__(self):
106     #     return "<user=%s,host=%s,action=%s,cmd=%s,date=%s>" %(
107     #         self.user_profile.username,
108     #         self.bind_host.host.hostname,
109     #         self.action_type,
110     #         self.date)
models\models
 1 #_*_coding:utf-8_*_
 2 
 3 
 4 from conf import settings
 5 from conf import action_registers
 6 from modules import utils
 7 
 8 
 9 def help_msg():
10     print("\033[31;1mAvailable commands:\033[0m")
11     for key in action_registers.actions:
12         print("\t",key)
13 
14 
15 def excute_from_command_line(argvs):
16     if len(argvs) <2:
17         help_msg()
18         exit()
19     if argvs[1] not in action_registers.actions:
20         utils.print_err("Command [%s] does not exist!" % argvs[1], quit=True)
21     action_registers.actions[argvs[1]](argvs[1:])
modules\actions
 1 #_*_coding:utf-8_*_
 2 
 3 from sqlalchemy import create_engine,Table
 4 from sqlalchemy.orm import sessionmaker
 5 
 6 from conf import settings
 7 
 8 
 9 engine = create_engine(settings.ConnParams)
10 
11 
12 Session = sessionmaker(bind=engine) #创建与数据库的会话session class ,注意,这里返回给session的是个class,不是实例
13 session = Session()
modules\db_conn
  1 import socket
  2 import sys
  3 from paramiko.py3compat import u
  4 from  models import models
  5 import datetime
  6 
  7 # windows does not have termios...
  8 try:
  9     import termios
 10     import tty
 11     has_termios = True
 12 except ImportError:
 13     has_termios = False
 14 
 15 
 16 def interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording):
 17     if has_termios:
 18         posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)
 19     else:
 20         windows_shell(chan)
 21 
 22 
 23 def posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording):
 24     import select
 25     
 26     oldtty = termios.tcgetattr(sys.stdin)
 27     try:
 28         tty.setraw(sys.stdin.fileno())
 29         tty.setcbreak(sys.stdin.fileno())
 30         chan.settimeout(0.0)
 31         cmd = ''
 32 
 33         tab_key = False
 34         while True:
 35             r, w, e = select.select([chan, sys.stdin], [], [])
 36             if chan in r:
 37                 try:
 38                     x = u(chan.recv(1024))
 39                     if tab_key:
 40                         if x not in ('\x07' , '\r\n'):
 41                             #print('tab:',x)
 42                             cmd += x
 43                         tab_key = False
 44                     if len(x) == 0:
 45                         sys.stdout.write('\r\n*** EOF\r\n')
 46                         break
 47                     sys.stdout.write(x)
 48                     sys.stdout.flush()
 49                 except socket.timeout:
 50                     pass
 51             if sys.stdin in r:
 52                 x = sys.stdin.read(1)
 53                 if '\r' != x:
 54                     cmd +=x
 55                 else:
 56 
 57                     print('cmd->:',cmd)
 58                     log_item = models.AuditLog(user_id=user_obj.id,
 59                                           bind_host_id=bind_host_obj.id,
 60                                           action_type='cmd',
 61                                           cmd=cmd ,
 62                                           date=datetime.datetime.now()
 63                                           )
 64                     cmd_caches.append(log_item)
 65                     cmd = ''
 66 
 67                     if len(cmd_caches)>=10:
 68                         log_recording(user_obj,bind_host_obj,cmd_caches)
 69                         cmd_caches = []
 70                 if '\t' == x:
 71                     tab_key = True
 72                 if len(x) == 0:
 73                     break
 74                 chan.send(x)
 75 
 76     finally:
 77         termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
 78 
 79     
 80 # thanks to Mike Looijmans for this code
 81 def windows_shell(chan):
 82     import threading
 83 
 84     sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")
 85         
 86     def writeall(sock):
 87         while True:
 88             data = sock.recv(256)
 89             if not data:
 90                 sys.stdout.write('\r\n*** EOF ***\r\n\r\n')
 91                 sys.stdout.flush()
 92                 break
 93             sys.stdout.write(data.decode())
 94             sys.stdout.flush()
 95         
 96     writer = threading.Thread(target=writeall, args=(chan,))
 97     writer.start()
 98         
 99     try:
100         while True:
101             d = sys.stdin.read(1)
102             if not d:
103                 break
104             chan.send(d)
105     except EOFError:
106         # user hit ^Z or F6
107         pass
modules\interactive
 1 #_*_coding:utf-8_*_
 2 
 3 
 4 
 5 import sys
 6 import traceback
 7 from  models import models
 8 import datetime
 9 
10 import paramiko
11 try:
12     import interactive
13 except ImportError:
14     from . import interactive
15 
16 
17 def ssh_login(user_obj,bind_host_obj,mysql_engine,log_recording):
18     # now, connect and use paramiko Client to negotiate SSH2 across the connection
19     try:
20         client = paramiko.SSHClient()
21         client.load_system_host_keys()
22         client.set_missing_host_key_policy(paramiko.WarningPolicy())
23         print('*** Connecting...')
24         #client.connect(hostname, port, username, password)
25         print(bind_host_obj)
26         client.connect(bind_host_obj.host.ip,
27                        bind_host_obj.host.port,
28                        bind_host_obj.remoteuser.username,
29                        bind_host_obj.remoteuser.password,
30                        timeout=30)
31 
32         cmd_caches = []
33         chan = client.invoke_shell()
34         print(repr(client.get_transport()))
35         print('*** Here we go!\n')
36         cmd_caches.append(models.AuditLog(user_id=user_obj.id,
37                                           bind_host_id=bind_host_obj.id,
38                                           action_type='login',
39                                           date=datetime.datetime.now()
40                                           ))
41         log_recording(user_obj,bind_host_obj,cmd_caches)
42         interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)
43         chan.close()
44         client.close()
45 
46     except Exception as e:
47         print('*** Caught exception: %s: %s' % (e.__class__, e))
48         traceback.print_exc()
49         try:
50             client.close()
51         except:
52             pass
53         sys.exit(1)
modules\ssh_login
 1 #_*_coding:utf-8_*_
 2 
 3 from conf import settings
 4 import yaml
 5 try:
 6     from yaml import CLoader as Loader, CDumper as Dumper
 7 except ImportError:
 8     from yaml import Loader, Dumper
 9 
10 def print_err(msg,quit=False):
11     output = "\033[31;1mError: %s\033[0m" % msg
12     if quit:
13         exit(output)
14     else:
15         print(output)
16 
17 
18 def yaml_parser(yml_filename):
19     try:
20         yaml_file = open(yml_filename,'r')
21         data = yaml.load(yaml_file)
22         return data
23     except Exception as e:
24         print_err(e)
modules\utils
  1 #_*_coding:utf-8_*_
  2 
  3 from models import models
  4 from modules.db_conn import engine,session
  5 from modules.utils import print_err,yaml_parser
  6 # from modules import common_filters
  7 from modules import ssh_login
  8 
  9 
 10 def syncdb(argvs):
 11     print("Syncing DB....")
 12     models.Base.metadata.create_all(engine) #创建所有表结构
 13 
 14 def create_hosts(argvs):
 15     """
 16     创建主机数据host
 17     :param argvs:
 18     :return:
 19     """
 20     if "-f" in argvs:
 21         host_file = argvs[argvs.index("-f")+1]
 22     else:
 23         print_err("invalid usage, should be:\ncreate_hosts -f <the new hosts file>",quit=True)
 24     source =yaml_parser(host_file)
 25     if source:
 26         for key,val in source.items():
 27             print(key,val)
 28             obj=models.Host(hostname=key,ip=val.get("ip"),port=val.get("port") or 22)
 29             session.add(obj)
 30         session.commit()
 31 def create_groups(argvs):
 32     """
 33     创建主机组hostgroup
 34     :param argvs:
 35     :return:
 36     """
 37     if "-f" in argvs:
 38         hostgroups_file=argvs[argvs.index("-f")+1]
 39     else:
 40         print_err("invalid usage, should be:\ncreate_groups -f <the new groups file>",quit=True)
 41     source = yaml_parser(hostgroups_file)
 42     if source:
 43         for key,val in source.items():
 44             print(key,val)
 45             obj = models.HostGroup(groupname=key)
 46             session.add(obj)
 47         session.commit()
 48 def create_remoteusers(argvs):
 49     """
 50     创建系统用户remote_user
 51     :param argvs:
 52     :return:
 53     """
 54     if "-f" in argvs:
 55         remoteusers_file = argvs[argvs.index("-f")+1]
 56     else:
 57         print_err("invalid usage, should be:\ncreate_remoteusers -f <the new remoteusers file>",quit=True)
 58     source =yaml_parser(remoteusers_file)
 59     if source:
 60         for key,val in source.items():
 61             print(key,val)
 62             obj=models.RemoteUser(username=val.get("username"),password=val.get("password"),auth_type=val.get("auth_type"))
 63             session.add(obj)
 64         session.commit()
 65 def create_userprofiles(argvs):
 66     """
 67     创建堡垒机用户user_profile
 68     :param argvs:
 69     :return:
 70     """
 71     if "-f" in argvs:
 72         userprofile=argvs[argvs.index("-f")+1]
 73     else:
 74         print_err("invalid usage,should be:\n create_userprofiles -f <the new userprofiles>",quit=True)
 75     source = yaml_parser(userprofile)
 76     if source:
 77         for key,val in source.items():
 78             print(key,val)
 79             obj = models.UserProfile(username=key,password=val.get("password"))
 80             session.add(obj)
 81             if val.get("groups"):
 82                 groups= session.query(models.HostGroup).filter\
 83                     (models.HostGroup.groupname.in_(val.get("groups"))).all()
 84                 print(groups)
 85                 if not groups:
 86                     print_err("none of [%s] exist in groups tables"%val.get("groups"),quit=True)
 87                 obj.host_groups = groups
 88         session.commit()
 89 def create_bindhosts(argvs):
 90     '''
 91     create bind hosts
 92     :param argvs:
 93     :return:
 94     '''
 95     if '-f' in argvs:
 96         bindhosts_file  = argvs[argvs.index("-f") +1 ]
 97     else:
 98         print_err("invalid usage, should be:\ncreate_hosts -f <the new bindhosts file>",quit=True)
 99     source = yaml_parser(bindhosts_file)
100     if source:
101         for key,val in source.items():
102             #print(key,val)
103             host_obj = session.query(models.Host).filter(models.Host.hostname==val.get('hostname')).first()
104             assert host_obj
105             for item in val['remote_users']:
106                 print(item )
107                 assert item.get('auth_type')
108                 if item.get('auth_type') == 'ssh-passwd':
109                     remoteuser_obj = session.query(models.RemoteUser).filter(
110                                                         models.RemoteUser.username==item.get('username'),
111                                                         models.RemoteUser.password==item.get('password')
112                                                     ).first()
113                 else:
114                     remoteuser_obj = session.query(models.RemoteUser).filter(
115                                                         models.RemoteUser.username==item.get('username'),
116                                                         models.RemoteUser.auth_type==item.get('auth_type'),
117                                                     ).first()
118                 if not remoteuser_obj:
119                     print_err("RemoteUser obj %s does not exist." % item,quit=True )
120                 bindhost_obj = models.BindHost(host_id=host_obj.id,remoteuser_id=remoteuser_obj.id)
121                 session.add(bindhost_obj)
122                 #for groups this host binds to
123                 if source[key].get('groups'):
124                     group_objs = session.query(models.HostGroup).filter(models.HostGroup.groupname.in_(source[key].get('groups') )).all()
125                     assert group_objs
126                     print('groups:', group_objs)
127                     bindhost_obj.host_groups = group_objs
128                 #for user_profiles this host binds to
129                 if source[key].get('user_profiles'):
130                     userprofile_objs = session.query(models.UserProfile).filter(models.UserProfile.username.in_(
131                         source[key].get('user_profiles')
132                     )).all()
133                     assert userprofile_objs
134                     print("userprofiles:",userprofile_objs)
135                     bindhost_obj.user_profiles = userprofile_objs
136                 #print(bindhost_obj)
137         session.commit()
138 
139 def auth():
140     """user认证"""
141     count = 0
142     while count<3:
143         username = input("Username:").strip()
144         if len(username)==0: continue
145         password = input("Password:").strip()
146         if len(password) ==0:continue
147         user_obj = session.query(models.UserProfile).filter(models.UserProfile.username==username,
148                                                             models.UserProfile.password==password).first()
149         if user_obj:
150             return user_obj
151         else:
152             print("wrong  username or password,you have %s ore chances"%(3-count-1))
153             count+=1
154     else:
155         print_err("too many attempts")
156 def welcome(user):
157     WELCOME="""\033[32;1m
158     ------------- Welcome [%s] Fort Machine -------------
159     \033[0m"""%user.username
160     print(WELCOME)
161 
162 def log_recording(user_obj,bind_host_obj,logs):
163     '''
164     flush user operations on remote host into DB
165     :param user_obj:
166     :param bind_host_obj:
167     :param logs: list format [logItem1,logItem2,...]
168     :return:
169     '''
170     print("\033[41;1m--logs:\033[0m",logs)
171 
172     session.add_all(logs)
173     session.commit()
174 
175 def start_session(argvs):
176     print("going to start session")
177     user=auth()
178     if user:
179         welcome(user)
180         print(user.bind_hosts)
181         print(user.host_groups)
182         exit_flag = False
183         while not exit_flag:
184             if user.bind_hosts:
185                 print("ungroups host(%s)"%len(user.bind_hosts))
186             for index,groups in enumerate(user.host_groups):
187                 print("%s.\t%s(%s)"%(index,groups.groupname,len(groups.bindhosts)))
188             choice = input("[%s]:" % user.username).strip()
189             if len(choice) == 0:continue
190             if choice == "z":
191                 print("---Groups:ungroup hosts----")
192                 for index,bind_host in enumerate(user.bind_hosts):
193                     print("%s.\t%s@%s(%s)"%(index,
194                                             bind_host.remoteuser.username,
195                                             bind_host.host.hostname,
196                                             bind_host.host.ip))
197                 print("-----END-----")
198             elif choice.isdigit():
199                 choice = int(choice)
200                 if choice < len(user.host_groups):
201                     print("---Group:%s---"%user.host_groups[choice].groupname)
202                     for index,bind_host in enumerate(user.host_groups[choice].bindhosts):
203                         print("%s.\t%s@%s(%s)"%(index,
204                                             bind_host.remoteuser.username,
205                                             bind_host.host.hostname,
206                                             bind_host.host.ip))
207                     print("-----END-----")
208 
209                     while not exit_flag:
210                         user_option = input("[(b)back,(q)quit,select to host login]:").strip()
211                         if len(user_option) ==0:continue
212                         if user_option=="b":break
213                         if user_option =="q": exit_flag=True
214                         if user_option.isdigit():
215                             user_option=int(user_option)
216                             if user_option <len(user.host_groups[choice].bindhosts):
217                                 print('host:', user.host_groups[choice].bindhosts[user_option])
218                                 print('audit log:', user.host_groups[choice].bindhosts[user_option].audit_logs)
219                                 ssh_login.ssh_login(user,
220                                                     user.host_groups[choice].bindhosts[user_option],
221                                                     session,
222                                                     log_recording)
223                 else:
224                     print("no this option")
modules\views
 1 bind1:
 2   hostname: ubuntu-test
 3   remote_users:
 4     - user1:
 5       username: root
 6       auth_type: ssh-key
 7       #password: 123
 8     - user2:
 9       username: alex
10       auth_type: ssh-passwd
11       password: alex3714
12   groups:
13     - beijing_group
14   user_profiles:
15     - alex
16 
17 bind2:
18   hostname: centos-mysql
19   remote_users:
20     - user1:
21       username: alex
22       auth_type: ssh-passwd
23       password: alex3714
24   groups:
25     - beijing_group
26     - shanghai_group
27 
28   user_profiles:
29     - rain
share\examples\new_bindhost
beijing_group:
#  bind_hosts:
#    - h1
#    - h2
  user_profiles:
    - alex

shanghai_group:
  user_profiles:
    - jack
    - alex
    - rain
share\examples\new_groups
 1 ubuntu-test:
 2   ip: 192.168.3.22
 3   port: 22
 4 
 5 redhat:
 6   ip: 172.33.24.55
 7   port: 3000
 8 
 9 centos-mysql:
10   ip: 10.0.2.34
share\examples\new_hosts
 1 user0:
 2   auth_type:  ssh-passwd
 3   username: root
 4   password: abc!23
 5 
 6 user1:
 7   auth_type:  ssh-passwd
 8   username: root
 9   password: alex!34321df
10 
11 user2:
12   auth_type:  ssh-key
13   username: root
14   #password: abc!23
15 
16 user3:
17   auth_type:  ssh-passwd
18   username: alex
19   password: alex3714
share\examples\new_remoteusers
 1 alex:
 2   password: alex123
 3   groups:
 4     - beijing_group
 5 
 6   #bind_hosts:
 7   #  - h1
 8   #  - h2
 9   #  - h3
10 
11 jack:
12   password: jack123
13   groups:
14     - shanghai_group
15 
16 rain:
17   password: rain123
18   #bind_hosts:
19   #  - h1
20   #  - h3
share\examples\new_userprofiles

 

转载于:https://www.cnblogs.com/garrett0220/articles/8532178.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值