把简单的事情做好

0%

使用 owncloud 搭建企业云盘系统

owncloud 是国外一款有比较有名气的开源云盘系统,旨在为企业提供文件安全存储、便捷访问、高效管理的数据使用方式。这篇文章会简单介绍一下 owncloud 的特性,如何快速搭建系统,备份文件,以及接入企业内部登录认证。

简介

owncloud 最基础的功能就是文件上传、同步、分享。用户首次登录会创建一个属于自己的目录,可以下载同步客户端于云目录实时同步。上传的文件可以通过用户组进行共享,共享的目录也可以通过客户端实时同步。同时,还可以创建公共链接,任何人都可以通过链接去访问分享的目录或者文件。除此之外,owncloud 还提供了很多其他特性,当然有些特性只有企业版才会支持,下面只列出了部分,更多请访问官网

  • 外置存储 可以使用挂载到 linux 的存储设备(nas,san等等)和云存储(FTP,Swift,S3,DropBox等)
  • 灵活的API owncloud 开放的架构,使得开发者很容易通过 REST API 扩展 owncloud 的功能,例如文件管理、用户管理、文件分享等
  • 访客模式 未登录的用户可以通过分享的公开链接访问文件
  • 密码策略 可以为公开链接设置访问密码和有效期,到期密码自动失效;管理员可以设置密码复杂度
  • 双因子验证 除了用户名/密码外,还可以通过额外的令牌来验证。例如,通过手机获取改令牌登录系统
  • 端到端加密 用户可以创建基于客户端加密的文件,系统管理员和第三方基础设施管理员都无法获取数据

搭建

本文实验选择的版本是最新的社区版 production 10.3.2 。官方推荐的系统环境是 LAMP(Linux + Apache + Mysql + PHP),截止到发文时间 PHP 支持的版本是 7.1、7.2、7.3。我们的服务器都是 LNMP 的架构,所以使用了 nginx 来作为 web server,nginx 的示例配置文件可以在官方文档中找到。具体的安装过程很简单,这里就不多做描述了,如果觉得搭建环境装扩展很繁琐,官方也提供了 docker 镜像。更多请访问官方安装教程

安装完以后,通过连接 http://your_server_name/owncloud 即可访问 owncloud 的配置界面,设置管理员密码、文件存储目录、数据配置,后台会通过 config.example.php 生成一份新的配置文件。

如果通过源码方式安装的,有一些设置也是必要的。例如,php.ini 及 nginx 的上传文件最大限制,PHP 环境变量的设置,STMP 服务器的设置等等

文件备份

由于硬件资源限制,文件备份并没有使用外置存储,而采用了 rsync + inotify 的方式来实时同步系统的存储目录。 rsync 是可以实现两台服务器之间增量同步文件的命令,inotify 可以监听的文件的变化,通过不断监听系统存储目录的变化,使用 rsync 命令将文件变化增量同步到另外一台服务上。并且把这个操作封装成一个服务,开机自动重启。假设两台服务器使用 root 账号,具体操作如下:

  1. 实现免密 ssh 登录,在 owncloud 服务器上执行 ssh-keygen -t rsa -f /root/rsync-key -N '' 生成密钥,将公钥拷贝到要备份服务器的用户目录下的 /root/.ssh/authorized_keys 中,可以试下是否可以免密登录成功
  2. 在备份服务器上创建一个同步目录,并分配权限 mkdir /home/owncloud
  3. 在 owncloud 服务新建一个 shell 脚本,假如存放在 /data/scripts/ 目录下,file-sync.sh,给文件加上可执行权限,可以执行一下看是否需成功
1
2
3
4
5
6
#!/bin/bash

while true; do
inotifywait -r -e modify,attrib,close_write,move,create,delete /data/owncloud
rsync -avz -e "ssh -i /root/rsync-key -o StrictHostKeyChecking=no" /data/owncloud/ root@backup_server_ip:/home/owncloud/ --delete
done

4.新建 service 文件,vi /etc/systemd/system/file-sync.service,贴上下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description = SyncService
After = network.target

[Service]
PIDFile = /run/syncservice/syncservice.pid
User = root
Group = root
ExecStartPre = /bin/mkdir /run/syncservice
ExecStartPre = /bin/chown -R root:root /run/syncservice
ExecStart = /bin/bash /data/scripts/file-sync.sh
ExecReload = /bin/kill -s HUP $MAINPID
ExecStop = /bin/kill -s TERM $MAINPID
ExecStopPost = /bin/rm -rf /run/syncservice
PrivateTmp = true

[Install]
WantedBy = multi-user.target

5.启动服务,systemctl deamon-reload 加载新的服务,systemctl enable file-sync.servcie 设置开启启动,systemctl start file-sync.servcie 启动文件同步服务

自定义登录

综合考虑了一下,owncloud 支持 oauth2 的三方认证,但是内部并不支持这个协议。为了省去给每个人都创建账号的麻烦同时尽量减少对系统的改动,决定采用内部的统一登录认证方式,认证成功之后自动创建一个新的用户。通过一两天的源码研究,决定改动一下源码来实现这种登录方式,现在看来也是实现成本最低的一种方式。改动如下:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//core/Controllers/LoginController.php

public function tryLogin($user, $password, $redirect_url, $timezone = null) {
$originalUser = $user;
// TODO: Add all the insane error handling
$loginResult = $this->userSession->login($user, $password);

if ($loginResult !== true) {
$users = $this->userManager->getByEmail($user);
// we only allow login by email if unique
if (\count($users) === 1) {
$user = $users[0]->getUID();
$loginResult = $this->userSession->login($user, $password);
}
}
//从这个地方开始,添加内部登录验证
if ($loginResult !== true) {
if (strstr($user, '@')) {
$email = $user;
$user = substr($user, 0, strpos($user, '@'));
} else {
$email = $user . '@yourcompany.com';
}

if (!$this->userManager->userExists($user)) {
$loginResult = $this->auth($email, $password); //内部登录验证
if ($loginResult) {
try {
$this->userManager->createUser($user, $password); // 创建用户,并设置密码
$newUser = $this->userManager->get($user);
$newUser->setEMailAddress($email); // 设置邮箱
$loginResult = $this->userSession->login($user, $password); // 新用户登录
} catch (\Exception $e) {
$loginResult = false;
}
}
}
}
//结束
if ($loginResult !== true) {
$this->session->set('loginMessages', [
['invalidpassword'], []
]);
$args = [];
// Read current user and append if possible - we need to return the unmodified user otherwise we will leak the login name
if ($user !== null) {
$args['user'] = $originalUser;
}
// keep the redirect url
if (!empty($redirect_url)) {
$args['redirect_url'] = $redirect_url;
}
return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args));
}
/* @var $userObject IUser */
$userObject = $this->userSession->getUser();
// TODO: remove password checks from above and let the user session handle failures
// requires https://github.com/owncloud/core/pull/24616
$this->userSession->createSessionToken($this->request, $userObject->getUID(), $user, $password);

// User has successfully logged in, now remove the password reset link, when it is available
$this->config->deleteUserValue($userObject->getUID(), 'owncloud', 'lostpassword');

// Save the timezone
if ($timezone !== null) {
$this->config->setUserValue($userObject->getUID(), 'core', 'timezone', $timezone);
}

if ($this->twoFactorManager->isTwoFactorAuthenticated($userObject)) {
$this->twoFactorManager->prepareTwoFactorLogin($userObject);
if ($redirect_url !== null) {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', [
'redirect_url' => $redirect_url
]));
}
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}

if ($redirect_url !== null && $this->userSession->isLoggedIn()) {
$location = $this->urlGenerator->getAbsoluteURL(\urldecode($redirect_url));
// Deny the redirect if the URL contains a @
// This prevents unvalidated redirects like ?redirect_url=:user@domain.com
if (\strpos($location, '@') === false) {
return new RedirectResponse($location);
}
}
return new RedirectResponse($this->getDefaultUrl());
}
/**
* 内部认证
*
* @param $email
* @param $password
* @return bool
*/
private function auth($email, $password)
{
if ('auth failed') {
return false;
}
return true;
}