mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	feat: 新增mysql ssh代理连接方式
This commit is contained in:
		@@ -67,6 +67,28 @@
 | 
				
			|||||||
                    />
 | 
					                    />
 | 
				
			||||||
                    <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
 | 
					                    <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-form-item prop="enable_ssh" label="SSH:" v-if="form.type === 'mysql'">
 | 
				
			||||||
 | 
					                    <el-checkbox v-model="form.enable_ssh" :true-label=1 :false-label=0></el-checkbox>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="ssh_host" label="SSH Host:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
				
			||||||
 | 
					                    <el-input v-model.trim="form.ssh_host" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="ssh_user" label="SSH User:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
				
			||||||
 | 
					                    <el-input v-model.trim="form.ssh_user" placeholder="请输入用户名"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="ssh_pass" label="SSH Pass:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
				
			||||||
 | 
					                    <el-input
 | 
				
			||||||
 | 
					                        type="password"
 | 
				
			||||||
 | 
					                        show-password
 | 
				
			||||||
 | 
					                        v-model.trim="form.ssh_pass"
 | 
				
			||||||
 | 
					                        placeholder="请输入密码,修改操作可不填"
 | 
				
			||||||
 | 
					                        autocomplete="new-password"
 | 
				
			||||||
 | 
					                    ></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="ssh_port" label="SSH Port:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
				
			||||||
 | 
					                    <el-input type="number" v-model.number="form.ssh_port" placeholder="请输入端口"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
            </el-form>
 | 
					            </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template #footer>
 | 
					            <template #footer>
 | 
				
			||||||
@@ -127,6 +149,11 @@ export default defineComponent({
 | 
				
			|||||||
                projectId: null,
 | 
					                projectId: null,
 | 
				
			||||||
                envId: null,
 | 
					                envId: null,
 | 
				
			||||||
                env: null,
 | 
					                env: null,
 | 
				
			||||||
 | 
					                enable_ssh: null,
 | 
				
			||||||
 | 
					                ssh_host: null,
 | 
				
			||||||
 | 
					                ssh_user: null,
 | 
				
			||||||
 | 
					                ssh_pass: null,
 | 
				
			||||||
 | 
					                ssh_port: 22,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            btnLoading: false,
 | 
					            btnLoading: false,
 | 
				
			||||||
            rules: {
 | 
					            rules: {
 | 
				
			||||||
@@ -264,6 +291,7 @@ export default defineComponent({
 | 
				
			|||||||
                if (valid) {
 | 
					                if (valid) {
 | 
				
			||||||
                    const reqForm = { ...state.form };
 | 
					                    const reqForm = { ...state.form };
 | 
				
			||||||
                    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
					                    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
				
			||||||
 | 
					                    reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass);
 | 
				
			||||||
                    dbApi.saveDb.request(reqForm).then(() => {
 | 
					                    dbApi.saveDb.request(reqForm).then(() => {
 | 
				
			||||||
                        ElMessage.success('保存成功');
 | 
					                        ElMessage.success('保存成功');
 | 
				
			||||||
                        emit('val-change', state.form);
 | 
					                        emit('val-change', state.form);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,6 +55,15 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// 密码脱敏记录日志
 | 
						// 密码脱敏记录日志
 | 
				
			||||||
	form.Password = "****"
 | 
						form.Password = "****"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if form.Type == "mysql" && form.EnableSSH == 1 {
 | 
				
			||||||
 | 
							originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true)
 | 
				
			||||||
 | 
							biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
				
			||||||
 | 
							db.SSHPass = originSSHPwd
 | 
				
			||||||
 | 
							// 密码脱敏记录日志
 | 
				
			||||||
 | 
							form.SSHPass = "****"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rc.ReqParam = form
 | 
						rc.ReqParam = form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	db.SetBaseInfo(rc.LoginAccount)
 | 
						db.SetBaseInfo(rc.LoginAccount)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,12 @@ type DbForm struct {
 | 
				
			|||||||
	Project   string `json:"project"`
 | 
						Project   string `json:"project"`
 | 
				
			||||||
	Env       string `json:"env"`
 | 
						Env       string `json:"env"`
 | 
				
			||||||
	EnvId     uint64 `binding:"required" json:"envId"`
 | 
						EnvId     uint64 `binding:"required" json:"envId"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						EnableSSH int    `json:"enable_ssh"`
 | 
				
			||||||
 | 
						SSHHost   string `json:"ssh_host"`
 | 
				
			||||||
 | 
						SSHPort   int    `json:"ssh_port"`
 | 
				
			||||||
 | 
						SSHUser   string `json:"ssh_user"`
 | 
				
			||||||
 | 
						SSHPass   string `json:"ssh_pass"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DbSqlSaveForm struct {
 | 
					type DbSqlSaveForm struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,4 +19,9 @@ type SelectDataDbVO struct {
 | 
				
			|||||||
	CreateTime *time.Time `json:"createTime"`
 | 
						CreateTime *time.Time `json:"createTime"`
 | 
				
			||||||
	Creator    *string    `json:"creator"`
 | 
						Creator    *string    `json:"creator"`
 | 
				
			||||||
	CreatorId  *int64     `json:"creatorId"`
 | 
						CreatorId  *int64     `json:"creatorId"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						EnableSSH *int    `json:"enable_ssh"`
 | 
				
			||||||
 | 
						SSHHost   *string `json:"ssh_host"`
 | 
				
			||||||
 | 
						SSHPort   *int    `json:"ssh_port"`
 | 
				
			||||||
 | 
						SSHUser   *string `json:"ssh_user"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,12 +12,14 @@ import (
 | 
				
			|||||||
	"mayfly-go/pkg/global"
 | 
						"mayfly-go/pkg/global"
 | 
				
			||||||
	"mayfly-go/pkg/model"
 | 
						"mayfly-go/pkg/model"
 | 
				
			||||||
	"mayfly-go/pkg/utils"
 | 
						"mayfly-go/pkg/utils"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/go-sql-driver/mysql"
 | 
				
			||||||
	_ "github.com/lib/pq"
 | 
						_ "github.com/lib/pq"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,7 +77,12 @@ func (d *dbAppImpl) GetById(id uint64, cols ...string) *entity.Db {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
					func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
				
			||||||
	// 默认tcp连接
 | 
						// 默认tcp连接
 | 
				
			||||||
 | 
						if dbEntity.Type == "mysql" && dbEntity.EnableSSH == 1 {
 | 
				
			||||||
 | 
							dbEntity.Network = "mysql+ssh"
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
		dbEntity.Network = "tcp"
 | 
							dbEntity.Network = "tcp"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 测试连接
 | 
						// 测试连接
 | 
				
			||||||
	if dbEntity.Password != "" {
 | 
						if dbEntity.Password != "" {
 | 
				
			||||||
		TestConnection(*dbEntity)
 | 
							TestConnection(*dbEntity)
 | 
				
			||||||
@@ -155,6 +162,18 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
				
			|||||||
	biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
 | 
						biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
 | 
				
			||||||
	global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
 | 
						global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//SSH Conect
 | 
				
			||||||
 | 
						if d.Type == "mysql" && d.EnableSSH == 1 {
 | 
				
			||||||
 | 
							sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort)
 | 
				
			||||||
 | 
								panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error())))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
 | 
				
			||||||
 | 
								return sshClient.Dial("tcp", addr)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
 | 
						// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
 | 
				
			||||||
	d.Database = db
 | 
						d.Database = db
 | 
				
			||||||
	DB, err := sql.Open(d.Type, getDsn(d))
 | 
						DB, err := sql.Open(d.Type, getDsn(d))
 | 
				
			||||||
@@ -202,6 +221,18 @@ func GetDbInstanceByCache(id string) *DbInstance {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestConnection(d entity.Db) {
 | 
					func TestConnection(d entity.Db) {
 | 
				
			||||||
 | 
						//SSH Conect
 | 
				
			||||||
 | 
						if d.Type == "mysql" && d.EnableSSH == 1 {
 | 
				
			||||||
 | 
							sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort)
 | 
				
			||||||
 | 
								panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error())))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
 | 
				
			||||||
 | 
								return sshClient.Dial("tcp", addr)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 验证第一个库是否可以连接即可
 | 
						// 验证第一个库是否可以连接即可
 | 
				
			||||||
	d.Database = strings.Split(d.Database, " ")[0]
 | 
						d.Database = strings.Split(d.Database, " ")[0]
 | 
				
			||||||
	DB, err := sql.Open(d.Type, getDsn(&d))
 | 
						DB, err := sql.Open(d.Type, getDsn(&d))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,4 +20,10 @@ type Db struct {
 | 
				
			|||||||
	Project   string
 | 
						Project   string
 | 
				
			||||||
	EnvId     uint64
 | 
						EnvId     uint64
 | 
				
			||||||
	Env       string
 | 
						Env       string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						EnableSSH int    `orm:"column(enable_ssh)" json:"enable_ssh"`
 | 
				
			||||||
 | 
						SSHHost   string `orm:"column(ssh_host)" json:"ssh_host"`
 | 
				
			||||||
 | 
						SSHPort   int    `orm:"column(ssh_port)" json:"ssh_port"`
 | 
				
			||||||
 | 
						SSHUser   string `orm:"column(ssh_user)" json:"ssh_user"`
 | 
				
			||||||
 | 
						SSHPass   string `orm:"column(ssh_pass)" json:"-"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ CREATE TABLE `t_db` (
 | 
				
			|||||||
  `type` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '数据库实例类型(mysql...)',
 | 
					  `type` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '数据库实例类型(mysql...)',
 | 
				
			||||||
  `database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
 | 
					  `database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
 | 
				
			||||||
  `params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
 | 
					  `params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
 | 
				
			||||||
  `network` varchar(8) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `project_id` bigint(20) DEFAULT NULL,
 | 
					  `project_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
  `project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
 | 
					  `env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
 | 
				
			||||||
@@ -41,6 +41,11 @@ CREATE TABLE `t_db` (
 | 
				
			|||||||
  `update_time` datetime DEFAULT NULL,
 | 
					  `update_time` datetime DEFAULT NULL,
 | 
				
			||||||
  `modifier_id` bigint(20) DEFAULT NULL,
 | 
					  `modifier_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
  `modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
 | 
					  `enable_ssh` tinyint(1) unsigned NOT NULL DEFAULT '0',
 | 
				
			||||||
 | 
					  `ssh_host` varchar(50) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					  `ssh_port` int(8) NOT NULL,
 | 
				
			||||||
 | 
					  `ssh_user` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					  `ssh_pass` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  PRIMARY KEY (`id`)
 | 
					  PRIMARY KEY (`id`)
 | 
				
			||||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
 | 
					) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										58
									
								
								server/pkg/utils/ssh_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								server/pkg/utils/ssh_client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SSHConnect(user, password, host, key string, port int) (*ssh.Client, error) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							auth         []ssh.AuthMethod
 | 
				
			||||||
 | 
							addr         string
 | 
				
			||||||
 | 
							clientConfig *ssh.ClientConfig
 | 
				
			||||||
 | 
							client       *ssh.Client
 | 
				
			||||||
 | 
							config       ssh.Config
 | 
				
			||||||
 | 
							//session      *ssh.Session
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get auth method
 | 
				
			||||||
 | 
						auth = make([]ssh.AuthMethod, 0)
 | 
				
			||||||
 | 
						if key == "" {
 | 
				
			||||||
 | 
							auth = append(auth, ssh.Password(password))
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							pemBytes, err := ioutil.ReadFile(key)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var signer ssh.Signer
 | 
				
			||||||
 | 
							if password == "" {
 | 
				
			||||||
 | 
								signer, err = ssh.ParsePrivateKey(pemBytes)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							auth = append(auth, ssh.PublicKeys(signer))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						clientConfig = &ssh.ClientConfig{
 | 
				
			||||||
 | 
							User:    user,
 | 
				
			||||||
 | 
							Auth:    auth,
 | 
				
			||||||
 | 
							Timeout: 30 * time.Second,
 | 
				
			||||||
 | 
							Config:  config,
 | 
				
			||||||
 | 
							HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						addr = fmt.Sprintf("%s:%d", host, port)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client, err = ssh.Dial("tcp", addr, clientConfig)
 | 
				
			||||||
 | 
						return client, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user