我们正在做一个项目,其中包括我们有一个 PC 应用程序和一个 Android 平板电脑上的应用程序。
PC应该能够更改/创建数据,平板电脑应该能够接收数据。我们决定使用 Realm 作为数据库,或者我们想这样做,但是:我们是学生,没有财力购买 Realm Cloud 来托管它,但是,我们确实有一个服务器,我们可以在其中自行托管它.
我们认为 Realm 的自托管已于 2018 年被废弃,但我们并不能 100% 确定,如果有人能花时间解释,我们将非常感激。 预先感谢您。
您可以自己运行服务器,但不能再免费了。这是运行您自己的领域服务器的文档:
https://docs.realm.io/server/manage
此外,他们的领域云还有一个学生/长期开发许可证,每月只需 10 美元。这不是太多,但我确实理解尝试找到免费版本。
领域数据库的自托管能力被剥夺,2018 年。
现在您只能托管其云服务的数据库。
最近,MongoDB 宣布他们停止对 Atlas Sync 的支持,开发人员将寻找替代方案。我认为写一篇关于我们如何自己托管 Realm 服务器的文章是个好主意。
在
realm-core
存储库中,位于同步服务器的源代码。我们需要添加一些包装器并启动服务器。启动是微不足道的。从这里构建realm-core
:https://github.com/molind/realm-core/tree/auth_check(允许使用我们的令牌进行授权的更改很小。您可以检查差异)。然后,在您的 server.cpp
中,大致写下以下内容:
#include <stdio.h>
#include <unistd.h>
#include <realm/sync/noinst/server/server.hpp>
#ifndef APP_VERSION
#define APP_VERSION "0.0.0"
#endif
int main(int argc, const char **argv)
{
const char *address = std::getenv("REALM_ADDRESS");
if (!address)
address = "127.0.0.1";
const char *port = std::getenv("REALM_PORT");
if (!port)
port = "9600";
const char *key = std::getenv("REALM_KEY");
if (!key)
key = "key.pem";
const char *path = std::getenv("REALM_PATH");
if (!path)
path = "realm_data";
fprintf(stderr, "realm (%s) %s:%s, %s, %s\n", APP_VERSION, address, port, key, path);
// Read hostname
char hostname[50];
gethostname(hostname, sizeof(hostname));
auto config = realm::sync::Server::Config();
config.id = hostname;
config.listen_address = address;
config.listen_port = port;
// see FIXME: in server.h
config.http_request_timeout = 100 * 60 * 1000;
config.http_response_timeout = 100 * 60 * 1000;
config.connection_reaper_timeout = 3 * 60 * 60 * 1000;
// config.connection_reaper_interval = default value
auto pkey = realm::sync::PKey::load_public(key);
auto server = realm::sync::Server(path, std::move(pkey), config);
server.start();
server.run();
return 0;
}
然后,构建它。
# export CC=clang
# export CXX=clang++
SRCS = server.cpp
OBJS := $(addsuffix .o,$(basename $(SRCS)))
DEPS := $(OBJS:.o=.d)
APP=server
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
CXXFLAGS=-O3 -g -std=c++17 -Irealm-core/src/ -Irealm-core/build/src/
LDFLAGS= -Wl,-Bstatic \
-Lrealm-core/build/src/realm/sync/noinst/server/ -lrealm-server \
-Lrealm-core/build/src/realm/sync/ -lrealm-sync \
-Lrealm-core/build/src/realm/ -lrealm \
-lssl -lcrypto -lz \
-Wl,-Bdynamic -lpthread -lc -ldl
endif
ifeq ($(UNAME_S),Darwin)
CXXFLAGS=-std=c++17 -Irealm-core/src/ -Irealm-core/build/src/ -g -O0
LDFLAGS= -g -Lrealm-core/build/src/realm/sync/noinst/server/ -lrealm-server-dbg \
-Lrealm-core/build/src/realm/sync/ -lrealm-sync-dbg \
-Lrealm-core/build/src/realm/ -lrealm-dbg \
-lz -lcompression \
-framework Foundation -framework CoreFoundation -framework Security
endif
# Check APP_VERSION env variable and pass it to CC
ifneq ($(APP_VERSION),)
CXXFLAGS += -DAPP_VERSION=\"$(APP_VERSION)\"
endif
.SUFFIXES: # Delete the default suffixes
.SUFFIXES: .cpp .o # Define our suffix list
all: $(APP)
.cpp.o:
${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@
$(APP): $(OBJS)
$(CXX) $(CPPFLAGS) -o $(APP) $(OBJS) $(LDFLAGS)
.PHONY: clean
clean:
rm -f $(OBJS) $(APP) $(DEPS)
-include $(DEPS)
MKDIR_P ?= mkdir -p
连接到同步服务器分几个阶段进行,您将需要实现多个 API 方法。
realmApi := dic.GetRealmAPI()
if realmApi != nil {
realm := apiGroup.Group("/client/v2.0")
realm.POST("/app/<realm_app_id>/auth/providers/custom-token/login", realmApi.Login)
realm.POST("/auth/session", realmApi.NewSession)
realm.DELETE("/auth/session", realmApi.DeleteSession)
realm.GET("/auth/profile", realmApi.Profile)
realm.GET("/app/<realm_app_id>/location", realmApi.Location) // returns the addresses of the authorization and synchronization servers
}
要获得
RealmUser
,您需要使用后端的令牌登录。授权方法也是在后端实现的,所以应该不难。验证令牌并返回以下响应。我会用 Go 复制我的后端部分,因为它更快。 :)
type AuthParams struct {
Token *string `json:"token"`
DeviceID *string `json:"device"`
}
func (r *Realm) Login(c *gin.Context) {
var auth AuthParams
err := c.ShouldBindJSON(&auth)
if err != nil || auth.Token == nil {
c.String(http.StatusBadRequest, "token is required")
return
}
claims, err := CheckUserToken(r.db, *auth.Token)
if err != nil {
c.String(http.StatusUnauthorized, "invalid token")
return
}
userID := claims["id"].(string)
var deviceID string
device, ok := claims["device"].(string)
if ok {
deviceID = device
} else {
deviceID = uuid.Nil.String()
}
response := gin.H{
"user_id": userID,
"device_id": deviceID,
"refresh_token": r.getRefreshToken(userID, deviceID),
"access_token": r.getAccessToken(userID, deviceID),
}
c.JSON(http.StatusOK, response)
}
func (r *Realm) getRefreshToken(userID, deviceID string) string {
claims := jwt.MapClaims{
"app_id": "<realm_app_id>",
"identity": userID,
"device": deviceID,
"access": []string{"refresh"},
"salt": rand.Uint64(),
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour * 24 * 365 * 10).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
str, _ := token.SignedString(common.Settings.AuthRealmKey)
return str
}
func (r *Realm) getAccessToken(userID, deviceID string) string {
iat := time.Now()
exp := iat.Add(time.Hour)
claims := jwt.MapClaims{
"app_id": "<realm_app_id>",
"identity": userID,
"device": deviceID,
"path": "/" + userID + "/db_name",
"access": []string{"download", "upload", "manage"},
"salt": rand.Uint64(),
"iat": iat.Unix(),
"exp": exp.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
str, _ := token.SignedString(common.Settings.AuthRealmKey)
return str
}
// Server addresses
func (r *Realm) Location(c *gin.Context) {
response := gin.H{
"app_id": "<realm_app_id>",
"deployment_model": "GLOBAL",
"location": "release",
"hostname": common.Settings.UserServerURL,
"ws_hostname": common.Settings.SyncServerURL,
}
c.JSON(http.StatusOK, response)
}
// User information
func (r *Realm) Profile(c *gin.Context) {
userID, deviceID, err := r.checkRealmAuth(c)
if err != nil {
common.ReportError(c, common.AuthError(err))
return
}
response := gin.H{
"identities": []gin.H{
{
"id": userID,
"provider_type": "custom-token",
},
},
"data": gin.H{},
}
if *deviceID == uuid.Nil.String() {
response["type"] = "browser"
}
c.JSON(http.StatusOK, response)
}
// Start session
func (r *Realm) NewSession(c *gin.Context) {
userID, deviceID, err := r.checkRealmAuth(c)
if err != nil {
common.ReportError(c, common.AuthError(err))
return
}
response := gin.H{
"access_token": r.getAccessToken(*userID, *deviceID),
}
c.JSON(http.StatusOK, response)
}
// End session
func (r *Realm) DeleteSession(c *gin.Context) {
_, _, err := r.checkRealmAuth(c)
if err != nil {
common.ReportError(c, common.AuthError(err))
return
}
c.Status(http.StatusOK)
}
我将离开智威汤逊工作作为家庭作业。
还有一些事情。客户端通过 WebSocket 连接,我们需要 Nginx 来验证 HTTPS 证书并将套接字转发到我们的 Realm 服务器。您也可以使用其他任何东西。
daemon off;
events {
worker_connections 128;
}
http {
server_tokens off;
# include mime.types;
charset utf-8;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream realm {
server localhost:9601;
}
server {
access_log /dev/stdout;
server_name server_name;
listen 443 ssl;
ssl_certificate certs/server_name.cert;
ssl_certificate_key certs/server_name.key;
ssl_session_timeout 10m;
location /api/client/v2.0/app/<realm-app-id>/ {
proxy_pass http://realm/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
server {
access_log /dev/stdout;
listen 9600;
location /api/client/v2.0/app/<realm-app-id>/ {
proxy_pass http://realm/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
}
然后,开始:
/opt/homebrew/opt/nginx/bin/nginx -c $PWD/nginx.conf
然后就可以开始测试了。
客户端访问您的后端,接收 JWT,并将其发送到 Realm 进行授权。 Realm 返回其访问密钥。客户端接收
realm-api
服务器的地址和同步服务器地址(api.server_name
和 sync.server_name
)。 API
是我们用 Go 编写的。 sync.server_name
是通过 WebSocket 连接的地方。
Realm 服务器将使用我们通过环境变量提供的公钥来验证授权令牌及其签名。