前提是你已经在Linux服务器上安装了git
SSH
(2019.01.31补)
SSH是一种协议标准,其目的是实现安全远程登录以及其它安全网络服务.
SSH(Secure Shell)
仅仅是一 协议标准 ,其具体的实现有很多,既有开源实现的OpenSSH,也有商业实现方案.使用范围最广泛的当然是开源实现OpenSSH.
工作原理
SSH的定义中可以看出,SSH和telnet、ftp等协议主要的区别在于安全性.这就引出下一个问题:如何实现数据的安全呢?首先想到的实现方案肯定是对数据进行加密.加密的方式主要有两种:
- 对称加密(也称为秘钥加密)
- 非对称加密(也称公钥加密)
所谓对称加密,指加密解密使用同一套秘钥.如下图所示:
对称加密的加密强度高,很难破解.但是在实际应用过程中不得不面临一个棘手的问题:如何安全的保存密钥呢?尤其是考虑到数量庞大的Client端,很难保证密钥不被泄露.一旦一个Client端的密钥被窃据,那么整个系统的安全性也就不复存在.为了解决这个问题,非对称加密应运而生.非对称加密有两个密钥:“公钥”和“私钥”.
两个密钥的特性:公钥加密后的密文,只能通过对应的私钥进行解密.而通过公钥推理出私钥的可能性微乎其微.
- 远程Server收到Client端用户TopGun的登录请求,Server把自己的公钥发给用户.
- Client使用这个公钥,将密码进行加密.
- Client将加密的密码发送给Server端.
- 远程Server用自己的私钥,解密登录密码,然后验证其合法性.
- 若验证结果,给Client相应的响应.
私钥是Server端独有,这就保证了Client的登录信息即使在网络传输过程中被窃据,也没有私钥进行解密,保证了数据的安全性,这充分利用了非对称加密的特性.
** 这样就一定安全了吗?**
上述流程会有一个问题:Client端如何保证接受到的公钥就是目标Server端的?,如果一个攻击者中途拦截Client的登录请求,向其发送自己的公钥,Client端用攻击者的公钥进行数据加密.攻击者接收到加密信息后再用自己的私钥进行解密,不就窃取了Client的登录信息了吗?这就是所谓的中间人攻击
** SSH中是如何解决这个问题的?**
一.基于口令的认证
从上面的描述可以看出,问题就在于如何对Server的公钥进行认证?在https中可以通过CA来进行公证,可是SSH的publish key和private key都是自己生成的,没法公证.只能通过Client端自己对公钥进行确认.通常在第一次登录的时候,系统会出现下面提示信息:
The authenticity of host 'ssh-server.example.com (12.18.429.21)' can't be established.
RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
Are you sure you want to continue connecting (yes/no)?
上面的信息说的是:无法确认主机ssh-server.example.com(12.18.429.21)的真实性,不过知道它的公钥指纹,是否继续连接?
之所以用fingerprint代替key,主要是key过于长(RSA算法生成的公钥有1024位),很难直接比较.所以,对公钥进行hash生成一个128位的指纹,这样就方便比较了.
如果输入yes后,会出现下面信息:
Warning: Permanently added 'ssh-server.example.com,12.18.429.21' (RSA) to the list of known hosts.
Password: (enter password)
该host已被client确认,并被追加到client端的known_hosts文件中,然后就需要输入密码,之后的流程就按照图1-3进行.
二.基于公钥认证(重要)
在上面介绍的登录流程中可以发现,每次登录都需要输入密码,很麻烦.SSH提供了另外一种可以免去输入密码过程的登录方式:公钥登录.流程如下:
-
Client
将自己的公钥存放在Server上,Server
将其追加在文件authorized_keys
中. - Server端接收到Client的连接请求后,会在authorized_keys中匹配到Client的公钥pubKey,并生成随机数R,用Client的公钥对该随机数进行加密得到pubKey(R),然后将加密后信息发送给Client.
- Client端通过私钥进行解密得到随机数R,然后对随机数R和本次会话的SessionKey利用MD5生成摘要Digest1,发送给Server端.
- Server端会也会对R和SessionKey利用同样摘要算法生成Digest2.
- Server端会最后比较Digest1和Digest2是否相同,完成认证过程.
在步骤1中,Client将自己的公钥存放在Server上.需要用户手动将公钥copy到server上.这就是在配置ssh的时候进程进行的操作.下图是GitHub上SSH keys设置视图:
ssh实践
生成密钥操作
$ ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 0600 ~/.ssh/authorized_keys
ssh-keygen
是用于生产密钥的工具.
-t:指定生成密钥类型(rsa、dsa、ecdsa等)
-P:指定passphrase,用于确保私钥的安全(可为空)
-C: 值身份标识,很多时候你会看到让你输入邮箱,其实可以输入任何字符串,或者空.(见下文git中实践ssh登陆)
-f:指定存放密钥的文件(公钥文件默认和私钥同目录下,不同的是,存放公钥的文件名需要加上后缀.pub)
首先看下面~/.ssh中的四个文件:
- id_rsa:保存私钥
- id_rsa.pub:保存公钥
- authorized_keys:保存已授权的客户端公钥
- known_hosts:保存已认证的远程主机ID
需要注意的是:一台主机可能既是Client,也是Server.所以会同时拥有
authorized_keys
和known_hosts
.
关于known_hosts
known_hosts中存储是已认证的远程主机host key,每个SSH Server都有一个secret, unique ID, called a host key.
当我们第一次通过SSH登录远程主机的时候,Client端会有如下提示:
Host key not found from the list of known hosts.
Are you sure you want to continue connecting (yes/no)?
此时,如果我们选择 yes
,那么该 host key
就会被加入到Client
的 known_hosts
中,格式如下:
# domain name+encryption algorithm+host key
example.hostname.com ssh-rsa AAAAB4NzaC1yc2EAAAABIwAAAQEA...
最后探讨下为什么需要 known_hosts
,这个文件主要是通过Client和Server的双向认证,从而避免中间人(man-in-the-middle attack)攻击,每次Client向Server发起连接的时候,不仅仅Server要验证Client的合法性,Client同样也需要验证Server的身份,SSH client就是通过known_hosts中的host key来验证Server的身份的.
这中方案足够安全吗?当然不,比如第一次连接一个未知Server的时候,known_hosts还没有该Server的host key,这不也可能遭到中间人攻击吗?这可能只是安全性和可操作性之间的折中吧.
登陆操作
# 以用户名user,登录远程主机host
$ ssh user@host
# 本地用户和远程用户相同,则用户名可省去
$ ssh host
# SSH默认端口22,可以用参数p修改端口
$ ssh -p 2017 user@host
以上转载自图解ssh
最后总结
私钥和公钥其实有两方面作用:一是加密数据,二是签名验证(即验证身份,免密登陆).
私钥可生成公钥,但是公钥无法倒推私钥,所以这种方式即能证明交易成功,又能保证私钥的安全性.既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密(即服务器把公钥给客户端,客户端先把要传输的数据用公钥加密,传给服务器后,服务器利用私钥解密).
同理,既然是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证.(客户端把在自己机器上生产的公钥给服务端,客户端要链接服务端时发出自己的私钥,服务端利用公钥验证私钥的身份.具体过程见上文基于公钥认证.)
git中实践ssh登陆
公钥与私钥
我们平时在window电脑下执行git clone操作都是使用的https协议,虽然也可以通过全局设置避免了每次都输入账户与密码的问题.但我们在linux服务端是不能写死账号密码的,因为有可能有多个人要使用linux上的git操作.(纯属个人猜想,未验证).
我们可以使用git协议,然后使用ssh密钥.这样也可以去执行git clone 和 git四步操作,同时省去每次都输密码.
公钥获取
进入linux服务器
# 获取公钥/私钥对(注意这里的-C只是标识符,any comment can be here),并不需要你输入你的码云邮箱,任何字符串都可以
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
# 告诉你已经生成了公交和私钥
$ Generating public/private rsa key pair.
$ # 密钥对存储位置,不用管,直接回车(其实是存储,默认叫id_rsa,如果修改了名字记得写config配置文件)
$ Enter file in which to save the key (/root/.ssh/id_rsa):
# 输入一次密码(这是一个口令,可以不填,如果填了每次链接时都要输入,强烈建议不填)
Enter passphrase (empty for no passphrase):
# 再输入一次密码(口令,建议不填)
Enter same passphrase again:
# 你的私钥已经保存在 /root/.ssh/id_rsa.
Your identification has been saved in /root/.ssh/id_rsa.
# 你的公钥已经保存在 /root/.ssh/id_rsa.pub.
Your public key has been saved in /root/.ssh/id_rsa.pub.
添加SSH Keys到github账户中
- 我用ftp找到刚才生成的id_rsa.pub文件,打开并复制里面所有的内容.当然你也可以使用linux命令达到复制内容目的.
- 进入码云,鼠标放头像上会有下拉菜单,点击设置进入.选择 SSH 公钥,将上一步复制的公钥粘贴到公钥输入框,此时标题输入框会自动填入你刚才输入的邮箱,这里可以修改为任何标题.点击确定时会要求输入密码一次.
服务端的使用
现在你已经可以在linux上使用git命令了
git clone git@gitee.com:shuaiutopia/Project-oss.git //注意这是git协议了
但是我们会发现会要求我们输入密码:(ssh生产密钥对时设置的密码,所以生成密钥对时不要设置密码)
//要求输入密码(这就是你设置密码(口令)的后果)
Enter passphrase for /root/.ssh/id_rsa:
输入密码后确实可以做克隆操作了.
注意: 如果克隆时报错 Permission denied (publickey).
,说明上传到码云或则github的公钥与本地的私钥不匹配,可以输入 ssh -v git@gitee.com
或则 ssh -v git@github.com
来查看连接时用到的 private key
和 public key
的名字和位置,默认位置是 /root/.ssh/identity
和 /root/.ssh/id_rsa
,因此我们在生成密钥对时最好不要改名字和位置.如果你当时修改了名字不是叫 id_rsa
,就需要写 config
配置文件,方法见下文.
实现码云代码同步到服务器
原理就很简单了,利用码云的 webHook
自动执行一段脚本,这个脚本就是执行相关 git
操作.
注意:webHook
只能执行 clone
和 pull
操作.
ssh
在 git
上的使用---进阶篇(2019.04.03)
我们使用 ssh
免密登陆 git/github
服务器, 如果是单账户,很方便,默认拿 id_rsa
与你的 git/github
服务器的公钥对比;
但我们现在有一个需求,就是将码云代码同步到自己服务器.假如我的码云账号下设置了公钥,并通过 webhook
和 nodejs
实现了代码同步,那么在接下来的项目中,必须把我加为项目开发者才可以实现同步.加入我想让团队中的所有成员都在自己的账号下设置下公钥从而实现代码同步可以吗?
第一个否决方案
最先想到的方案就是把在自己服务器上生成的公钥,配置到所有团队成员的码云账号上,结果证明公钥只能被一个人使用,不允许配置到多个码云账号上.
自己服务器生成多个公钥/密钥对并配置多个git用户使用
生成多个密钥对
生成密钥方式相同,在保存位置或则 -f ~/.ssh/id_rsa
参数时分别指定即可.假如我们现在在 ~/.ssh
文件夹下生成了 id_rsa_user1
和 id_rsa_user2
两对密钥对.
将公钥添加到码云或github
账号的公钥内
分别将 id_rsa_user1.pub
和 id_rsa_user2.pub
两个公钥加入到两个不同的码云或则 github
账号下.
密钥添加到 SSH agent
中
因为默认只读取 id_rsa
,为了让 SSH
识别新的私钥,需将其添加到 SSH agent
中:
ssh-add ~/.ssh/id_rsa_user1
ssh-add ~/.ssh/id_rsa_user2
如果出现Could not open a connection to your authentication agent
的错误,就试着用以下命令:
ssh-agent bash
ssh-add ~/.ssh/id_rsa_user1
ssh-add ~/.ssh/id_rsa_user2
修改 config
文件
在 ~/.ssh
目录下找到 config
文件,如果没有就创建:
touch config
然后修改我的config配置如下:
# user1的码云登陆密钥对
Host gitee.com
HostName gitee.com
PreferredAuthentications publickey
# 指定特定的ssh私钥文件
IdentityFile ~/.ssh/id_rsa_user1
# user2的码云登陆密钥对
Host gitee2
HostName gitee.com
PreferredAuthentications publickey
# 指定特定的ssh私钥文件
IdentityFile ~/.ssh/id_rsa_user2
注意:这里配置的 Host
不同.
测试
注意测试地址中,测试账号 host
必须和 config
中的 Host
指定字段一致.
ssh -T git@gitee.com
# 返回 Hi utopia! You've successfully authenticated, but GitHub does not provide shell access.
ssh -T git@gitee2
# 返回 Hi chenshaosheng! You've successfully authenticated, but GitHub does not provide shell access.
在 git
命令中的区别
此时 user1
执行 git clone
命令和之前无差别,如
git clone git@gitee.com:shuaiutopia/project_dingsou.git ./
但 user2
就必须修改 host
git clone git@gitee2:shuaiutopia/project_dingsou.git ./
因此我么在自动同步代码的 nodejs
程序中必须要对克隆地址做动态修改.如果文件夹内已经有 .git
文件了,清空后再执行 git clone
命令.
自动同步到服务器端的 nodejs
代码
路由文件如下:
const gitR = require("koa-router")();
const fs = require("fs");
const fse = require("fs-extra");
const path = require("path");
const utils = require("../utils");
const shell = require("shelljs");
const util = require("util");
const glob = require("glob");
const shellExec = util.promisify(shell.exec);
gitR.prefix("/");
gitR.post("/gitee2", async (ctx, next) => {
let {password,tarDir,testDir,gitBranch,sshUrl,ref="",myhost="gitee.com",quickMode=0} = ctx.request.body;
let branchName = gitBranch;
if(password != "bluej1234"){
ctx.body = "密码错误" + new Date().toLocaleTimeString();
await next();
return;
}
// myhost的作用是为了给同一个服务器设置多个ssh公钥区分用(后续需要再配),详见蓝景电子书ssh章节
sshUrl = sshUrl || ctx.request.body.repository.git_ssh_url || "";
sshUrl = sshUrl.replace("gitee.com",myhost);
console.log("sshUrl", sshUrl);
if(!sshUrl || sshUrl.startsWith("https")){
ctx.body = "请使用ssh地址!";
await next();
return;
}
// 测试用
// tarDir = "/web/test/";
if(!tarDir){
ctx.body = "请设置tarDir目标路径参数!";
await next();
return;
}
tarDir = path.normalize(tarDir);
if(!path.isAbsolute(tarDir)){
ctx.body = "tarDir目标路径必须为绝对路径!";
await next();
return;
}
let testBranchName = "test";
if(branchName){
branchName = branchName == testBranchName? testBranchName:"master";
}else{
branchName = ref.endsWith(testBranchName)? testBranchName : "master";
}
console.log("分支名",branchName);
// 当分支名为 test时,会将test分支部署到测试文件夹下面,默认为tarDir的兄弟目录
let tarDirName = path.parse(tarDir).name;
testDir = testDir || path.join(tarDir,"..",tarDirName+"_test");
if(branchName == testBranchName){
tarDir = path.normalize(testDir);
}
fse.ensureDirSync(tarDir);
console.log("git 的目标目录", tarDir);
try {
// shell.cd 会改变 process.cwd()以及paht.resolve()的工作目录
let tempProject = path.join(__dirname,"../","public/tempProject/"); // 结尾"/"必须,文件移动时用
if(quickMode==1){
// 会新建一个文件夹来存储项目,而不是和别的项目公用tempProject
let uniqueDir = utils.md5(sshUrl);
tempProject = path.join(__dirname,"../","public",uniqueDir,"/");
}
fse.ensureDirSync(tempProject);
let gitDir = path.join(tempProject,".git");
let workTree = tempProject;
if((ctx.app.lastGit && ctx.app.lastGit == sshUrl) || ((quickMode==1) && fs.existsSync(gitDir))){
// 上次同步项目和这次是同一个项目,说明上次已经 git clone了
console.log("执行git fetch操作");
await shellExec(`git --git-dir=${gitDir} --work-tree=${workTree} fetch -q --tags origin ${branchName}`);
await shellExec(`git --git-dir=${gitDir} --work-tree=${workTree} reset -q --hard FETCH_HEAD`);
}else{
console.log("执行git clone操作");
ctx.app.lastGit = sshUrl; // 更新lastGit
await shellExec(`rm -fr ${tempProject}`);
console.log("sshUrl",sshUrl);
await shellExec(`git clone -q -b ${branchName} ${sshUrl} ${tempProject} --depth=1`);
// 为了避免使用shell.cd 就暂不用子模块拷贝功能
// shell.cd(tempProject);
// await shellExec(`git submodule update --init --recursive`)
}
/************要忽略的文件******************/
let excludeName = [".git",".vscode",".gitignore","ignoreMe.list"];
let excludeStr = "";
excludeName.forEach(cur=>{
excludeStr += ` --exclude=${cur}`;
})
// 忽略文件在项目根目录下,名字为ignoreMe.list
let ignoreFile = path.join(tempProject,"ignoreMe.list");
if(!fs.existsSync(ignoreFile)){
ignoreFile = ""
}
/****************************************/
// rsync -aqzO /web/project_autogit/public/tempProject/ /web/test/ --exclude-from=/web/project_autogit/public/tempProject/ignoreMe.list
// 故意不加 await 避免超时
shellExec(`rsync -aqzO ${tempProject} ${tarDir} ${excludeStr} --exclude-from=${ignoreFile}`);
} catch (e) {
console.log("错误", e);
ctx.body = "[错误原因:]" + e;
await next();
return;
}
ctx.body = `${branchName}分支部署完成,部署文件夹为${tarDir};`;
await next();
})
module.exports = gitR;