学习堡垒机之前先来学习一个核心模块paramiko,常见的Fabric和ansible等批量管理服务器工具都是基于paramiko模块来实现,或者说都是将paramiko模块封装后再进行相关改进。常用的功能有SSHClient和SFTPClient两个类及其对应的方法:
先介绍SSHClient,它是paramiko模块下的一个类,用于创建一个ssh连接对象,ssh可以通过基于密码的认证登录也可以基于秘钥的登录认证,接下来分两种情况介绍:
①基于密码的登录认证
就创建了一个ssh对象
ssh = paramiko.SSHClient()
允许不在本地主机kown_hosts文件中的主机进行连接
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
创建一个基于ssh协议的连接主机的ssh连接
ssh.connect(hostname='10.0.0.13',port=22,username='opuser',password='123456')
执行远程命令
stdin,stdout,stderr = ssh.exec_command('df')
获取执行结果
result = stdout.read()
print result
关闭ssh连接
ssh.close()
范例1:
#!/usr/bin/env python
# -*- coding:utf8 -*-
# 导入paramiko模块
import paramiko
# 通过类SSHClient实例化一个对象ssh
ssh = paramiko.SSHClient()
# 允许本地主机kown_hosts不存在的主机登录
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 开启一个ssh连接
ssh.connect(hostname='10.0.0.13',port=22,username='opuser',password='123456')
# 执行shell命令df -Th
stdin,stdout,stderr = ssh.exec_command('df -Th')
# 读取返回结果并打印到屏幕
print stdout.read()
# 关闭ssh连接
ssh.close()
②公钥的登录认证
定义指定的公钥
private_key = paramiko.RSAKey.from_private_key_file('/home/opuser/.ssh/id_rsa')
就创建了一个ssh对象
ssh = paramiko.SSHClient()
允许不在本地主机kown_hosts文件中的主机进行连接
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
创建一个基于ssh协议的连接主机的ssh连接
ssh.connect(hostname='10.0.0.13',port=22,username='opuser',pkey=private_key)
执行远程命令
stdin,stdout,stderr = ssh.exec_command('df')
获取执行结果
result = stdout.read()
print result
关闭ssh连接
ssh.close()
【注:基于公钥的登录认证实现要将本地的公钥拷贝到远程主机的对应用户家目录下的.ssh/下,然后才可以实现免密码登录】
范例2:
# -*- coding:utf8 -*-
import paramiko #导入paramiko模块
# 定义指定的公钥
private_key = paramiko.RSAKey.from_private_key_file('/home/opuser/.ssh/id_rsa')
# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='10.0.0.13', port=22, username='opuser',pkey=private_key)
# 执行命令
stdin, stdout, stderr = ssh.exec_command('df -Th')
# 读取命令结果
result = stdout.read()
# 将获取的结果输出到屏幕
print result
由上面的介绍可以发现SSHClient类用于登录远程主机,那么SFTPClient类用来做啥呢?从字面意思不难发现跟sftp有关,在Linux下sftp是一个ftp程序,相当于windows下的ftp,所以SFTPClinet也应该可以实现对应的ftp功能,而ftp功能有哪些,我们平时用的最多的就是文件的上传和下载,所以接下来介绍SFTPClinet的上传和下载功能。跟SSHClinet类似,SFTPClinet也有两种登录认证:密码和秘钥
①基于密码认证
# 导入paramiko模块
import paramiko
# 实例化创建一个tranport对象通道(注意括号中的参数是一个元组)
transport = paramiko.Transport(('10.0.0.13', 22))
# 在transport通道中传入用户名和密码创建一个ssh连接
transport.connect(username='opuser', password=‘123456’)
#创建一个sfp连接
sftp = paramiko.SFTPClient.from_transport(transport)
#定义本地文件及路径
localfile='/lcsourece/localtion.py'
#定义远端文件存放路径
refile='/tmp/refile.py'
#将本地的文件localtion.py上传到远端/tmp/refile.py
sftp.put(localfile,refile)
#将远端文件下载到本地
sftp.get('remoute_path','local_path')
#关闭ssh连接
transport.close()
②基于秘钥的认证
#导入paramiko模块
import paramiko
#定义私钥地址
private_key = paramiko.RSAKey.from_private_key_file('/home/opuser/.ssh/id_rsa')
# 实例化创建一个tranport对象通道(注意括号中的参数是一个元组)
transport = paramiko.Transport(('10.0.0.13', 22))
# 在transport通道中传入用户名和秘钥创建一个ssh连接
transport.connect(username='opuser', pkey=private_key)
#建立一个sftp连接
sftp=paramiko.SFTPClient.from_transport(transport)
# 将location.py 上传至服务器 /tmp/test.py
sftp.put('/tmp/location.py', '/tmp/test.py')
# 将remove_path 下载到本地 local_path
sftp.get('remove_path', 'local_path')
#关闭sftp
transport.close()
面向对象封装多个远程操作
#看上面的paramiko执行命令的时候有两种方法,传输文件的时候有一种方法!并且这里在执行命令的时候连接下然后关闭,传输文件的时候传输完后关闭,这样不是很好!那么我们可以连接上,然后把执行命令和上传文件写成两个方法操作。另外在远程执行命令的时候其实是很快的但是他们消耗的时间基本上都在建立连接上了,所以我们要写成连接上一次执行命令或上传文件全部都完事之后关闭。
#!/usr/bin/env python
# -*- coding:utf8 -*-
import paramiko
import uuid
class Haproxy(): #------->建立类Haproxy
def __init__(self):
self.host='10.0.0.13'
self.port=22
self.username='opuser'
self.password='123456'
def create_file(self): #创建文件方法
file_name=str(uuid.uuid4())#读取uuid方法uuid4()生成的文件名
with open(file_name,'w') as f:
f.write('sb')
return file_name
def run(self):
self.connect()
self.upload()
self.rename()
self.close()
def connect(self):#设置连接方法
transport = paramiko.Transport((self.host,self.port))#创建一个连接对象
transport.connect(username=self.username,password=self.password)#调用transport对象中的连接方法
self.__transport = transport#把transport赋值给__transport
def close(self):#关闭连接
self.__transport.close()
def upload(self):#上传文件方法
file_name = self.create_file()
sftp = paramiko.SFTPClient.from_transport(self.__transport)
sftp.put(file_name,'/home/opuser/ttttttt.py')
def rename(self):#执行修改名字方法
ssh = paramiko.SSHClient()#建立ssh对象
ssh._transport = self.__transport#替换ssh_transport字段为self.__transport
stdin,stdout,stderr = ssh.exec_command('mv /home/opuser/ttttttt.py /home/opuser/oooooo.py')#执行shell命令mv修改文件名
print stdout.read()#将执行的命令的结果读出来并返回输出到屏幕
ha=Haproxy()
ha.run()
堡垒机的实现
堡垒机执行流程:
-
管理员为用户在服务器上创建账号(将公钥放置服务器,或者使用用户名密码)
-
用户登陆堡垒机,输入堡垒机用户名密码,现实当前用户管理的服务器列表
-
用户选择服务器,并自动登陆
-
执行操作并同时将用户操作记录
-
【注意:要想实现登录就执行脚本,可以在配置文件.bashrc中定义登录后自动执行脚本:如:/usr/bin/python /home/opuser/menu.py】
①实现用户登录
1
2
3
4
5
6
7
|
import
getpass
user
=
raw_input
(
'username:'
)
pwd
=
getpass.getpass(
'password'
)
if
user
=
=
'alex'
and
pwd
=
=
'123'
:
print
'登陆成功'
else
:
print
'登陆失败'
|
②根据用户获取相关服务器列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
dic
=
{
'alex'
: [
'10.0.0.10'
,
'10.0.0.11'
,
'10.0.0.12'
,
],
'eric'
: [
'10.0.0.14'
,
]
}
host_list
=
dic[
'alex'
]
print
'please select:'
for
index, item
in
enumerate
(host_list,
1
):
print
index, item
inp
=
raw_input
(
'your select (No):'
)
inp
=
int
(inp)
hostname
=
host_list[inp
-
1
]
port
=
22
|
③根据用户名、私钥登陆服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
tran
=
paramiko.Transport((hostname, port,))
tran.start_client()
default_path
=
os.path.join(os.environ[
'HOME'
],
'.ssh'
,
'id_rsa'
)
key
=
paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey(
'opuser'
, key)
# 打开一个通道
chan
=
tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell()
#########
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
#########
chan.close()
tran.close()
|
获取一个Linux终端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import
paramiko
import
os
import
sys
import
select
import
socket
tran
=
paramiko.Transport((
'10.0.0.13'
,
22
,))
tran.start_client()
'''
#使用密钥认证
default_path = os.path.join(os.environ['opuser'], '.ssh', 'id_rsa')
key = paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey('opuser', key)
'''
tran.auth_password(
'opuser'
,
'123456!'
)
#通过密码认证
chan
=
tran.open_session()
# 打开一个通道
chan.get_pty()
# 获取一个终端
chan.invoke_shell()
# 激活器
'''
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
'''
while
True
:
# 监视用户输入和服务器返回数据
# sys.stdin 处理用户输入
# chan 是之前创建的通道,用于接收服务器返回信息
readable, writeable, error
=
select.select([chan, sys.stdin, ],[],[],
1
)
#坚挺chen和终端
#只要发生变化,chan或者stdin或者都变化
if
chan
in
readable:
#远端有变化后捕获到
try
:
x
=
chan.recv(
1024
)
#ssh连接后他发送接收数据也是通过socket来做的
if
len
(x)
=
=
0
:
print
'\r\n*** EOF\r\n'
,
break
sys.stdout.write(x)
#把内容输入到终端上
sys.stdout.flush()
except
socket.timeout:
pass
if
sys.stdin
in
readable:
#当终端有输入捕获到之后
inp
=
sys.stdin.readline()
#把用户的那一行输入
chan.sendall(inp)
#发送命令至远端
chan.close()
tran.close()
|
#上面的例子中在捕获输出的时候我们输入的一行命令(字符串)回车之后,sys.stdin才捕获到,这个是默认的终端是这样的,我们就可以打开一个文件记录用户的所有命令操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import
paramiko
import
os
import
sys
import
select
import
socket
tran
=
paramiko.Transport((
'10.0.0.13'
,
22
,))
tran.start_client()
'''
#使用密钥认证
default_path = os.path.join(os.environ['opuser'], '.ssh', 'id_rsa')
key = paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey('opuser', key)
'''
tran.auth_password(
'opuser'
,
'123456!'
)
#通过密码认证
chan
=
tran.open_session()
# 打开一个通道
chan.get_pty()
# 获取一个终端
chan.invoke_shell()
# 激活器
'''
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
'''
log
=
open
(
'record'
,
'ab'
)
#打开一个文件记录用户的输入
while
True
:
# 监视用户输入和服务器返回数据
# sys.stdin 处理用户输入
# chan 是之前创建的通道,用于接收服务器返回信息
readable, writeable, error
=
select.select([chan, sys.stdin, ],[],[],
1
)
#坚挺chen和终端
#只要发生变化,chan或者stdin或者都变化
if
chan
in
readable:
#远端有变化后捕获到
try
:
x
=
chan.recv(
1024
)
#ssh连接后他发送接收数据也是通过socket来做的
if
len
(x)
=
=
0
:
log.close()
#关闭文件
print
'\r\n************************ EOF ************************\r\n'
,
break
sys.stdout.write(x)
#把内容输入到终端上
sys.stdout.flush()
except
socket.timeout:
pass
if
sys.stdin
in
readable:
#当终端有输入捕获到之后
inp
=
sys.stdin.readline()
#把用户的那一行输入
log.write(inp)
#记录命令
chan.sendall(inp)
#发送命令至远端
chan.close()
tran.close()
|
#还有个例子是我们在终端输入命令的时候,经常忘记命令全部的字符。
#默认换行,对于特殊字符特殊处理,比如Ctrl+c
#改变终端默认由行+回车-->stdin,改为一个字符--> stdin
首先我们要做的就是修改终端模式:把原来的默认由“回车”换行和特殊字符特殊处理,改为输入一个字符就捕获到并且记录到对应的日志文件,但是不记录tab键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import
paramiko
import
os
import
sys
import
select
import
socket
import
termios
import
tty
tran
=
paramiko.Transport((
'10.0.0.13'
,
22
,))
tran.start_client()
'''
#使用密钥认证
default_path = os.path.join(os.environ['opuser'], '.ssh', 'id_rsa')
key = paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey('opuser', key)
'''
tran.auth_password(
'opuser'
,
'123456!'
)
#通过密码认证
chan
=
tran.open_session()
# 打开一个通道
chan.get_pty()
# 获取一个终端
chan.invoke_shell()
# 激活器
'''
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
'''
# 获取原tty属性
oldtty
=
termios.tcgetattr(sys.stdin)
try
:
# 为tty设置新属性
# 默认当前tty设备属性:
# 输入一行回车,执行
# CTRL+C 进程退出,遇到特殊字符,特殊处理。
# 这是为原始模式,不认识所有特殊符号
# 放置特殊字符应用在当前终端,如此设置,将所有的用户输入均发送到远程服务器
tty.setraw(sys.stdin.fileno())
#把远端更换为LINUX原始模式
chan.settimeout(
0.0
)
while
True
:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e
=
select.select([chan, sys.stdin], [], [],
1
)
if
chan
in
r:
try
:
x
=
chan.recv(
1024
)
if
len
(x)
=
=
0
:
print
'\r\n*** EOF\r\n'
,
break
sys.stdout.write(x)
sys.stdout.flush()
except
socket.timeout:
pass
if
sys.stdin
in
r:
x
=
sys.stdin.read(
1
)
if
len
(x)
=
=
0
:
break
chan.send(x)
finally
:
# 重新设置终端属性
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
chan.close()
tran.close()
|
最终修改版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import
paramiko
import
os
import
sys
import
select
import
socket
import
termios
import
tty
tran
=
paramiko.Transport((
'10.0.0.13'
,
22
,))
tran.start_client()
'''
#使用密钥认证
default_path = os.path.join(os.environ['opuser'], '.ssh', 'id_rsa')
key = paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey('opuser', key)
'''
tran.auth_password(
'opuser'
,
'123456!'
)
#通过密码认证
chan
=
tran.open_session()
# 打开一个通道
chan.get_pty()
# 获取一个终端
chan.invoke_shell()
# 激活器
'''
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
'''
# 获取原tty属性
oldtty
=
termios.tcgetattr(sys.stdin)
#打开文件
try
:
# 为tty设置新属性
# 默认当前tty设备属性:
# 输入一行回车,执行
# CTRL+C 进程退出,遇到特殊字符,特殊处理。
# 这是为原始模式,不认识所有特殊符号
# 放置特殊字符应用在当前终端,如此设置,将所有的用户输入均发送到远程服务器
tty.setraw(sys.stdin.fileno())
#把远端更换为LINUX原始模式
chan.settimeout(
0.0
)
user_log
=
open
(
'terminalnew_log'
,
'ab'
)
while
True
:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e
=
select.select([chan, sys.stdin], [], [],
1
)
if
chan
in
r:
try
:
x
=
chan.recv(
1024
)
if
len
(x)
=
=
0
:
user_log.close()
print
'\r\n*** EOF\r\n'
,
break
sys.stdout.write(x)
sys.stdout.flush()
except
socket.timeout:
pass
if
sys.stdin
in
r:
x
=
sys.stdin.read(
1
)
if
len
(x)
=
=
0
:
break
if
x
=
=
'\t'
:
#判断用户的是否为tab如果为tab将不记录
pass
else
:
user_log.write(x)
#如果用户输入的命令保存至日志
chan.send(x)
finally
:
# 重新设置终端属性
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
chan.close()
tran.close()
|
Windows下打开终端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import
paramiko
import
sys
import
threading
tran
=
paramiko.Transport((
'10.0.0.13'
,
22
,))
tran.start_client()
'''
#使用密钥认证
default_path = os.path.join(os.environ['opuser'], '.ssh', 'id_rsa')
key = paramiko.RSAKey.from_private_key_file(default_path)
tran.auth_publickey('opuser', key)
'''
tran.auth_password(
'opuser'
,
'123456!'
)
#通过密码认证
chan
=
tran.open_session()
# 打开一个通道
chan.get_pty()
# 获取一个终端
chan.invoke_shell()
# 激活器
'''
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
'''
sys.stdout.write(
"Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n"
)
def
writeall(sock):
while
True
:
data
=
sock.recv(
256
)
'''
SSH发送数据的也是通过socket进行发送数据的,那么我们就可以使用socket来获取远程机器发送回来的数据。
while循环一直接收数据,sock.recv(256)是阻塞的只有数据过来的时候才会继续走。
'''
if
not
data:
sys.stdout.write(
'\r\n*** EOF ***\r\n\r\n'
)
sys.stdout.flush()
break
sys.stdout.write(data)
sys.stdout.flush()
writer
=
threading.Thread(target
=
writeall, args
=
(chan,))
#创建了一个线程,去执行writeall方法,参数为chan(建立的SSH连接)
writer.start()
try
:
while
True
:
#主线程循环
d
=
sys.stdin.read(
1
)
#一直监听用户的输入,输入一个发送一个
if
not
d:
break
chan.send(d)
except
EOFError:
# user hit ^Z or F6
pass
chan.close()
tran.close()
|
数据库基本操作
Python操作MySQL模块的安装
linux:
yum install MySQL-python
windows:
http://files.cnblogs.com/files/wupeiqi/py-mysql-win.zip
SQL基本使用
1、数据库操作
show databases;
use[databasename];
create database [name];
2、数据库的表操作
show tables;
create table students
(
id int not null auto_increment primary key,
name char(8) not null,
sex char(4) not null,
age tinyint unsigned not null,
tel char(13) null default "-"
);
示例代码:
CREATE TABLE `wb_blog` (
`id` smallint(8) unsigned NOT NULL,
`catid` smallint(5) unsigned NOT NULL DEFAULT '0',
`title` varchar(80) NOT NULL DEFAULT '',
`content` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `catename` (`catid`)
) ;
3、数据操作
-
insert into students(name,sex,age,tel) values('alex','man',18,'151515151')
-
delete from students where id =2;
-
update students set name = 'sb' where id =1;
-
select * from students
4、其他
主键
外键
左右连接
Python MySQL API
一、插入数据
import MySQLdb
conn = MySQLdb.connect(host='127.0.0.1',user='root',passwd='1234',db='mydb')
cur = conn.cursor()
reCount = cur.execute('insert into UserInfo(Name,Address) values(%s,%s)',('alex','usa'))
# reCount = cur.execute('insert into UserInfo(Name,Address) values(%(id)s, %(name)s)',{'id':12345,'name':'opuser'})
conn.commit()
cur.close()
conn.close()
print reCount
批量插入
import MySQLdb
conn = MySQLdb.connect(host='127.0.0.1',user='root',passwd='1234',db='mydb')
cur = conn.cursor()
li =[
('alex','usa'),
('sb','usa'),
]
reCount = cur.executemany('insert into UserInfo(Name,Address) values(%s,%s)',li)
conn.commit()
cur.close()
conn.close()
print reCount
注意:cur.lastrowid
二、删除数据
import MySQLdb
conn = MySQLdb.connect(host='127.0.0.1',user='root',passwd='1234',db='mydb')
cur = conn.cursor()
reCount = cur.execute('delete from UserInfo')
conn.commit()
cur.close()
conn.close()
print reCount
四、查数据
# ############################## fetchone/fetchmany(num) ##############################
import MySQLdb
conn = MySQLdb.connect(host='127.0.0.1',user='root',passwd='1234',db='mydb')
cur = conn.cursor()
reCount = cur.execute('select * from UserInfo')
print cur.fetchone()
print cur.fetchone()
cur.scroll(-1,mode='relative')
print cur.fetchone()
print cur.fetchone()
cur.scroll(0,mode='absolute')
print cur.fetchone()
print cur.fetchone()
cur.close()
conn.close()
print reCount
# ############################## fetchall ##############################
import MySQLdb
conn = MySQLdb.connect(host='127.0.0.1',user='root',passwd='1234',db='mydb')
#cur = conn.cursor(cursorclass = MySQLdb.cursors.DictCursor)
cur = conn.cursor()
reCount = cur.execute('select Name,Address from UserInfo')
nRet = cur.fetchall()
cur.close()
conn.close()
print reCount
print nRet
for i in nRet:
print i[0],i[1]
本文转自027ryan 51CTO博客,原文链接:http://blog.51cto.com/ucode/1768298,如需转载请自行联系原作者