aneasystone@little-stone:~$ sudo mkdir -p /git/repo
aneasystone@little-stone:~$ sudo git init --bare /git/repo/test.git
已初始化空的 Git 仓库于 /git/repo/test.git/
我们在 /git/repo 目录通过 git init --bare 命令创建一个裸仓库(bare repository,即一个不包含当前工作目录的仓库),只要这一步,我们就可以开始使用了。接着我们在工作目录 clone 这个版本库:
aneasystone@little-stone:~$ cd ~/working/
aneasystone@little-stone:~/working$ git clone /git/repo/test.git
正克隆到 'test'...
warning: 您似乎克隆了一个空仓库。
完成。
然后我们可以使用 pull、push 就像操作其他的版本库一样。
aneasystone@little-stone:~/working$ cd test/
aneasystone@little-stone:~/working/test$ touch 1
aneasystone@little-stone:~/working/test$ touch 2
aneasystone@little-stone:~/working/test$ git add .
aneasystone@little-stone:~/working/test$ git commit -m 'first commit'
[master (根提交) 4983f84] first commit
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1
create mode 100644 2
aneasystone@little-stone:~/working/test$ sudo git push
[sudo] aneasystone 的密码:
对象计数中: 3, 完成.
Delta compression using up to 8 threads.
压缩对象中: 100% (2/2), 完成.
写入对象中: 100% (3/3), 205 bytes | 205.00 KiB/s, 完成.
Total 3 (delta 0), reused 0 (delta 0)
To /git/repo/test.git
* [new branch] master -> master
本地协议不仅在做 Git 实验时很有用,如果你的团队有一个共享文件系统,可以在这个共享文件系统上创建一个远程版本库,团队成员把这个共享文件系统挂在本地,就可以直接使用本地协议进行协作开发,完全不需要搭建一台专门的 Git 服务器。二、SSH 协议
root@myserver:~# mkdir -p /git/repo
root@myserver:~# git init --bare /git/repo/test.git
已初始化空的 Git 仓库于 /git/repo/test.git/
然后在本地 clone 这个版本库:
aneasystone@little-stone:~/working$ git clone ssh://root@myserver/git/repo/test.git
正克隆到 'test'...
root@myserver's password:
warning: 您似乎克隆了一个空仓库。
可以看到和使用本地协议几乎一样,不同的地方在于,在 clone 的时候需要在 URL 前面加上 ssh://root@myserver,你也可以使用 scp 式的写法:
$ git clone root@myserver:/git/repo/test.git
另外一点不同的地方是,每次 pull、push 的时候都需要输入远程服务器的 root 密码。很显然,让每个 Git 用户都使用 root 来访问服务器是一种很不安全的做法,有几种方法可以解决这个问题:
最显而易见的方法是为每个 Git 用户创建一个独立的账号,并分别为他们分配对仓库的读写权限,这种方法行的通,但是对账号的管理非常麻烦,在团队人员不是很多的时候可以尝试,但是并不推荐;
另一种方法是配置 SSH 服务器使用某个已有的认证系统来管理用户,比如 LDAP,这在很多企业中是很常见的,这样可以省去用 adduser 手工管理服务器账号的麻烦;
还有一种方法是只创建一个账号,比如叫做 Git,他对仓库具有读写权限,大家都使用这个账号来访问仓库。这种方法的好处是用户管理起来比较简单,而且可以使用后面介绍的 authorized_keys 文件对用户的公钥进行管理。
root@myserver:~# adduser git
Adding user `git' ...
Adding new group `git' (1000) ...
Adding new user `git' (1000) with group `git' ...
Creating home directory `/home/git' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for git
Enter the new value, or press ENTER for the default
Full Name []: git
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] Y
再设置一下 Git 仓库的权限(默认情况下,Git 仓库的权限为 rwxr-xr-x,只有创建者 root 有写的权限,也就意味着使用 Git 账号只能 clone pull,不能 push):
# chmod a+w -R /git/repo/test.git
我们这里非常粗暴的使用 chmod a+w 将 Git 仓库设置为对所有人可写,这里可以想一想,如果我们希望设置某些用户对仓库具有只读的权限,该怎么做呢?然后就可以在本地愉快的进行 Git 操作了:
$ git clone git@myserver:/git/repo/test.git
到这里似乎一切都很正常,但是几次实操之后你就会发现,每次 Git 操作都要输入一次密码,这也太麻烦了,能不能“免密提交代码”呢?首先我们要知道,只要能通过 SSH 登陆到服务器,我们就能操作 Git,所以如果 SSH 能支持免密登陆,我们就可以“免密提交代码”。还好,SSH 支持公钥认证,这种认证方式无需密码登陆。在 Linux 操作系统中,每个用户都可以拥有自己的一个或多个密钥对(公钥和私钥成对出现),这些密钥一般情况会保存在 ~/.ssh 目录下,在开始之前,我们先确认下自己是否已经生成过公钥了,可以看下这个目录下是否有 iddsa.pub 或 idrsa.pub 这样的文件,如果没有,我们通过 ssh-keygen 来生成:
aneasystone@little-stone:~/.ssh$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/aneasystone/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/aneasystone/.ssh/id_rsa.
Your public key has been saved in /home/aneasystone/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:4Ulpufuhs/AgDMb0VXnqMUTw6bD/HrAOI2z9c1cod9I aneasystone@little-stone
The key's randomart image is:
+---[RSA 2048]----+
| .oo. |
| oo+. |
| . o.Oo |
| o . . B++ |
| + . ..So o |
| . + . ..+. + E |
| * * + oo + |
| . o Oo+.o. |
| **+. |
+----[SHA256]-----+
这样我们在 ~/.ssh 目录生成了两个文件,idrsa 是你的私钥,idrsa.pub 是你的公钥。关于私钥和公钥的原理以及 RSA 加密算法等内容可以参考我之前写过的一篇介绍 HTTPS 和证书[3]的文章。我们假设你的 Git 服务器是由专门的服务器管理员负责维护和管理,当你生成你的公钥之后,就可以给服务器管理员发送一封申请 Git 服务的邮件,并附上你的公钥。服务器管理员在收到你的申请之后,如果同意了,就可以进行下面的操作:首先将公钥文件拷贝到服务器上:
# scp id_rsa.pub root@myserver:/home/git
将公钥文件的内容追加到 Git 账户的 authorizedkeys 文件中(要注意的是,如果是第一次操作,/home/git 目录下是没有 .ssh 目录的,需要手工创建 .ssh 目录和 authorizedkeys 文件):
root@myserver:/home/git# cat id_rsa.pub >> /home/git/.ssh/authorized_keys
后续如果有其他的用户申请 Git 服务,都可以按照这个步骤操作。一旦完成这个操作,服务器管理员将会回复你的邮件,通知你 Git 服务已经开通,这个时候你再进行 Git 操作就可以不用输入密码了。关于 SSH 的使用,更详细的步骤可以参考 GitHub 上的这篇指南:Connecting to GitHub with SSH[4]。作为服务器管理员,关于 SSH 还有一点需要考虑,那就是 SSH 的安全问题。在上面介绍本地协议时,我们说这种方式无法控制用户对 Git 仓库的操作,无法防止用户有意或无意的损坏 Git 仓库,使用 SSH 协议一样存在这样的问题,用户能通过 SSH 拉取和提交代码,也就意味着用户可以通过 SSH 连接到服务器,对 Git 仓库进行任何操作,这是一件很让人担心的事情。因此,我们还需要对 Git 账号做一些限制。默认情况下,我们新建账号的登陆 shell 是 /bin/bash,这个配置在 /etc/passwd 文件中:
git:x:1000:1000:git,,,:/home/git:/bin/bash
可以使用 chsh 命令修改用户的登陆 shell,让他不能通过 SSH 访问服务器,怎么修改呢?我们可以看一下 /etc/shells 文件,这里定义了所有可以使用的登陆 shell,你可以将 /bin/bash 改成这里的任何一个:
root@myserver:~# cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
很显然,这些 shell 并不是我们想要的,有没有一个 shell 只允许用户进行 Git 操作,而不允许其他操作呢?还好,Git 的软件包提供了一个名叫 git-shell 的登陆 shell,我们可以把他加进去,一般情况下位于 /usr/bin/git-shell。我们使用 chsh 修改 Git 的登陆 shell:
root@myserver:~# chsh git
Changing the login shell for git
Enter the new value, or press ENTER for the default
Login Shell [/bin/bash]: /usr/bin/git-shell
这样当用户 Git 通过 SSH 连接服务器时,就会直接被拒绝了。三、Git 协议
使用 g+w 设置 Git 仓库的权限,让仓库创建者所在的用户组具有写权限,而不是所有人都有写权限(这一步通常也可以在 git init 的时候加上 --shared 参数);
然后将 Git 账号加到仓库创建者的用户组;
再创建一个 git_ro 账户,这个账户对仓库只有只读权限;
最后为 gitro 账户创建一个密钥对,把 gitro 的私钥公开出来供所有人使用。
root@myserver:~# git daemon --reuseaddr --base-path=/git/repo/ /git/repo/
上面的各个参数可以 参考 git-daemon[5] 的文档。git-daemon 会监听 9418 端口,如果你的服务器有防火墙,需要将该端口添加到白名单,如果你使用的是阿里云服务器,需要像下面这样添加一个安全组规则:
![100b74a151ff3929bdf5d3d7ff4f52d8.png](https://i-blog.csdnimg.cn/blog_migrate/e179acc6495fcbb5ef39767fffcea92a.jpeg)
root@myserver:~# cd /git/repo/test.git/
root@myserver:/git/repo/test.git/# touch git-daemon-export-ok
至此,所有人都可以通过 Git 协议来克隆或拉取项目源码了(注意上面指定了 base-path 参数为 /git/repo/,所以 URL 可以直接写 git://myserver/test.git):
aneasystone@little-stone:~/working$ git clone git://myserver/test.git
一般情况下,服务器管理员还会做一些其他的配置,譬如在服务器重启时让 Git 守护进程自动启动,这有很多种方式可以实现,可以参考《Pro Git[6]》 Git 守护进程这一节的内容。四、HTTP(S) 协议
# apt-get install -y git-core nginx fcgiwrap apache2-utils
其中,Nginx 作为 Web 服务器,本身是不能执行外部 CGI 脚本的,需要通过 FcgiWrap来中转,就像使用 php-fpm 来执行 PHP 脚本一样。apache2-utils 是 Apache 提供的一个 Web 服务器的工具集,包含了一些有用的小工具,譬如下面我们会用到的 htpasswd 可以生成 Basic 认证文件。启动 Nginx 和 FcgiWrap,并访问 http://myserver 测试 Web 服务器是否能正常访问:
# service nginx start
# service fcgiwrap start
然后我们打开并编辑 Nginx 的配置文件(/etc/nginx/sites-available/default):
location / {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
这里通过 fastcgi_param 设置了一堆的 FastCGI 参数,如下:
SCRIPT_FILENAME:指定 CGI 脚本 git-http-backend 的位置,表示每次 HTTP 请求会被转发到该 CGI 脚本;
GITHTTPEXPORTALL:git-http-backend 默认只能访问目录下有 git-daemon-export-ok 文件的 Git 仓库,和上面介绍的 Git 协议是一样的,如果指定了 GITHTTPEXPORTALL,表示允许访问所有仓库;
GITPROJECTROOT:Git 仓库的根目录;
REMOTE_USER:如果有认证,将认证的用户信息传到 CGI 脚本;
aneasystone@little-stone:~/working$ git clone http://myserver/test.git
4.1 开启身份认证到这里一切 OK,但是当我们 push 代码的时候,却会报下面的 403 错误:
aneasystone@little-stone:~/working/test$ git push origin master
fatal: unable to access 'http://myserver/test.git/': The requested URL returned error: 403
为了解决这个错误,我们可以在 git-http-backend 的官网文档上找到这样的一段描述:By default, only the upload-pack service is enabled, which serves git fetch-pack and git ls-remote clients, which are invoked from git fetch, git pull, and git clone. If the client is authenticated, the receive-pack service is enabled, which serves git send-pack clients, which is invoked from git push.第一次读这段话可能会有些不知所云,这是因为我们对这里提到的 upload-pack、fetch-pack、receive-pack、send-pack 这几个概念还没有什么认识。但是我们大抵可以猜出来,默认情况下,只有认证的用户才可以 push 代码,如果某个 Git 仓库希望所有用户都有权限 push 代码,可以为相应的仓库设置 http.receivepack:
root@myserver:/# cd /git/repo/test.git/
root@myserver:/git/repo/test.git# git config http.receivepack true
当然最好的做法还是对 push 操作开启认证,官网文档上有一个 lighttpd 的配置我们可以借鉴:
$HTTP["querystring"] =~ "service=git-receive-pack" {
include "git-auth.conf"
}
$HTTP["url"] =~ "^/git/.*/git-receive-pack$" {
include "git-auth.conf"
}
这个配置看上去非常简单,但是想要理解为什么这样配置,就必须去了解下 Git 的内部原理。正如上面 git-http-backend 文档上的那段描述,当 Git 客户端执行 git fetch,git pull,and git clone 时,会调用 upload-pack 服务,当执行 git push 时,会调用 receive-pack 服务,为了更清楚的说明这一点,我们来看看 Nginx 的访问日志。执行 git clone:
[27/Nov/2018:22:18:00] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:18:00] "POST /test.git/git-upload-pack HTTP/1.1" 200 306 "-" "git/1.9.1"
执行 git pull:
[27/Nov/2018:22:20:25] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:20:25] "POST /test.git/git-upload-pack HTTP/1.1" 200 551 "-" "git/1.9.1"
执行 git push:
[27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 401 204 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 200 193 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "POST /test.git/git-receive-pack HTTP/1.1" 200 63 "-" "git/1.9.1"
可以看到执行 clone 和 pull 请求的接口是一样的,先请求 /info/refs?service=git-upload-pack,然后再请求 /git-upload-pack;而 push 是先请求 /info/refs?service=git-receive-pack,然后再请求 /git-receive-pack,所以在上面的 lighttpd 的配置中我们看到了两条记录,如果要对 push 做访问控制,那么对这两个请求都要限制。关于 Git 传输的原理可以参考 《Pro Git》的 Git 内部原理 - 传输协议这一节。我们依葫芦画瓢,Nginx 配置文件如下:
location @auth {
auth_basic "Git Server";
auth_basic_user_file /etc/nginx/passwd;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
location / {
error_page 418 = @auth;
if ( $query_string = "service=git-receive-pack" ) { return 418; }
if ( $uri ~ "git-receive-pack$" ) { return 418; }
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
fastcgi_param GIT_HTTP_EXPORT_ALL "";
fastcgi_param GIT_PROJECT_ROOT /git/repo;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
其中相同的配置我们也可以用 include 指令放在一个共用的配置文件里,这样我们就实现了在 push 的时候需要填写用户名和密码了。我们通过 Nginx 的 authbasicuser_file 指令来做身份认证,用户名和密码保存在 /etc/nginx/passwd 文件中,这个文件可以使用上面提到的 apache2-utils 包里的 htpasswd 来生成:
root@myserver:/# htpasswd -cb /etc/nginx/passwd admin 123456
另外,在 push 的时候,有时候可能会遇到 unpack failed: unable to create temporary object directory 这样的提示错误:
aneasystone@little-stone:~/working/test$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 193 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
error: unpack failed: unable to create temporary object directory
To http://myserver/test.git
! [remote rejected] master -> master (unpacker error)
error: failed to push some refs to 'http://myserver/test.git'
这一般情况下都是由于 Git 仓库目录的权限问题导致的,在这里 Git 仓库的根目录 /git/repo 是 root 创建的,而运行 nginx 和 fcgiwrap 的用户都是 www-data,我们可以把 Git 仓库目录设置成对所有人可读可写,也可以像下面这样将它的拥有者设置成 www-data 用户:
root@myserver:/# chown -R www-data:www-data /git/repo
4.2 凭证管理上面我们站在管理员的角度解决了用户身份认证的问题,但是站在用户的角度,每次提交代码都要输入用户名和密码是一件很痛苦的事情。在上面介绍 SSH 协议时,我们可以使用 SSH 协议自带的公钥认证机制来省去输入密码的麻烦,那么在 HTTP 协议中是否存在类似的方法呢?答案是肯定的,那就是 Git 的凭证存储工具:credential.helper。譬如像下面这样,将用户名和密码信息保存在缓存中:
$ git config --global credential.helper cache
这种方式默认只保留 15 分钟,如果要改变保留的时间,可以通过 --timeout 参数设置,或者像下面这样,将密码保存在文件中:
$ git config --global credential.helper store
这种方式虽然可以保证密码不过期,但是要记住的是,这种方式密码是以明文的方式保存在你的 home 目录下的。可以借鉴操作系统自带的凭证管理工具来解决这个问题, 比如 OSX Keychain 或者 Git Credential Manager for Windows。更多的内容可以参考《Pro Git》凭证存储这一节。除此之外,还有一种更简单粗暴的方式:
aneasystone@little-stone:~/working$ git clone http://admin:123456@myserver/test.git
五、综合对比
优点:架设简单,不依赖外部服务,直接使用现有文件和网络权限,常用于共享文件系统
缺点:共享文件系统的配置和使用不方便,且无法保护仓库被意外损坏,传输性能较低
优点:架设简单,所有数据经过授权加密,数据传输很安全,传输性能很高
缺点:不支持匿名访问,配置 SSH 的密钥对小白用户有一定的门槛
优点:对开放的项目很适用,无需授权,传输性能最高
缺点:缺乏授权机制,架设较麻烦,企业一般不会默认开放 9418 端口需要另行添加
优点:同时支持授权访问和无授权访问,传输性能较高,配合 HTTPS 也可以实现数据安全
缺点:架设 HTTP 服务较麻烦,认证凭证不好管理