從通訊軟體到社群與短影片:物件儲存、推薦、mini app 與會員安全
這篇接著〈從裝置出發打造通訊軟體〉談社群和短影片,它們比通訊軟體多了公開內容、大量影片、推薦,還有跑在自家 App 裡的第三方程式。下面照這個順序寫,最後補上會員安全和法規。
公開內容讓伺服器回到中心
通訊軟體可以把資料放回裝置,是因為內容只給收發雙方看。社群貼文和短影片一發出去就是給陌生人看的,伺服器得讀得到內容,才能建索引、算推薦、處理檢舉。端對端加密保留給私訊,公開內容照一般服務設計。
想讓使用者帶得走資料,現有的路是聯邦協定:
- ActivityPub 在 2018 年 1 月 23 日成為 W3C Recommendation,每個帳號有 inbox 和 outbox,伺服器之間互相投遞。Mastodon 這一系都用它。
- Bluesky 的 AT Protocol 在 2024 年 2 月 22 日開放聯邦,任何人都能架自己的 PDS(Personal Data Server),一開始有規模上的限制。
- Threads 在 2025 年 6 月 17 日加上獨立的 fediverse 動態與跨站搜尋,當時還不能從那個動態直接回覆。
- Matrix 的房間在各 homeserver 之間複製,規格寫明沒有任何一台 homeserver 擁有某個房間,適合社群聊天室。
影片檔放哪:MinIO 之後的選擇
自架 S3 相容儲存,過去幾年最常見的答案是 MinIO,但社群版這一年多變化很大:
- 2025 年 5 月 24 日的版本把內建管理介面標為 deprecated,移到 object-browser,LDAP/OIDC 登入只留在商業版 AIStor。
- 2025 年 10 月起,README 寫明社群版只提供原始碼,不再提供預先編譯的執行檔。
- 2025 年 12 月,README 加上 maintenance mode,不再接受新變更,安全修正個案評估。
- GitHub 頁面顯示 repo 在 2026 年 4 月 25 日封存為唯讀。
授權一直是 AGPLv3,舊版還能用,但社群版已不再開發。新專案可以考慮這些:
| 選項 | 授權 | 語言 | 定位與備註 |
|---|---|---|---|
| SeaweedFS | Apache 2.0 | Go | 物件儲存、檔案系統與 Iceberg table,強調大量小檔案 |
| Garage | AGPL-3.0 | Rust | 小規模、跨地點的自架部署,可以跑在機房以外 |
| RustFS | Apache 2.0 | Rust | 2026 年 9 月 16 日發布 1.0 GA,官方稱可用於正式環境,2025 年 7 月才開源 |
| Ceph RGW | LGPL | C++ | 建在 Ceph 的 RADOS 之上,S3 API 相容,要連同整套 Ceph 叢集一起維運 |
| Apache Ozone | Apache 2.0 | Java | 偏向資料分析的分散式儲存,原生支援 S3 協定 |
不自架的話,Cloudflare R2 的定價頁寫明出站流量不收費,影片讀多寫少,出站流量這筆值得先算進成本。不管選哪個,程式裡只用 S3 API,不要用到某家獨有的功能,之後換掉的成本就小。
影片本身的流程大致是:上傳原始檔、轉檔成多種解析度、切成 HLS 片段(RFC 8216,2017 年)、放進物件儲存、前面掛 CDN。要壓低延遲可以用 Low-Latency HLS,把片段再切成更小的 partial segment(Apple 文件的例子約 200 毫秒),這部分規則在 HLS 第二版草案(draft-pantos-hls-rfc8216bis)裡,還沒成為 RFC。
推薦:先用 Qdrant 做最小版本
YouTube 2016 年的論文把推薦拆成兩階段:先從大量影片裡挑出幾百個候選,再用另一個模型排序。ByteDance 2022 年的 Monolith 論文處理的是另一個問題,即時訓練:embedding table 沒有雜湊碰撞、會過期淘汰,模型在線上持續訓練,使用者的互動近即時反映到推薦上。
起步不需要這麼複雜,把影片的標題和 hashtag 轉成向量放進 Qdrant,拿使用者看完的影片當正例、滑掉的當負例,就是一個能用的候選產生器:
from importlib.metadata import version
import tempfile
from fastembed import TextEmbedding
from qdrant_client import QdrantClient, models
# Each short video is described by its caption and hashtags.
VIDEOS = {
1: "十分鐘學會手沖咖啡 #咖啡 #教學",
2: "冷萃咖啡怎麼做才不苦 #咖啡",
3: "陽明山夜景散步路線 #登山 #台北",
4: "新手第一次爬百岳要準備什麼 #登山",
5: "貓咪第一次看到雪 #貓 #寵物",
6: "在家做義式濃縮的機器怎麼挑 #咖啡 #開箱",
7: "股票新手常犯的五個錯 #理財",
8: "合歡山日出要幾點出發 #登山 #日出",
}
MODEL = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
embedder = TextEmbedding(MODEL)
vectors = dict(zip(VIDEOS, embedder.embed(list(VIDEOS.values()))))
liked, skipped = [1, 3], [7] # watched to the end vs swiped away
seen = liked + skipped
with tempfile.TemporaryDirectory() as path:
client = QdrantClient(path=path)
client.create_collection(
"videos",
vectors_config=models.VectorParams(
size=len(vectors[1]), distance=models.Distance.COSINE
),
)
client.upsert(
"videos",
points=[
models.PointStruct(id=i, vector=v.tolist(), payload={"caption": VIDEOS[i]})
for i, v in vectors.items()
],
)
not_seen = models.Filter(must_not=[models.HasIdCondition(has_id=seen)])
for strategy in (
models.RecommendStrategy.AVERAGE_VECTOR,
models.RecommendStrategy.BEST_SCORE,
):
hits = client.query_points(
"videos",
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=liked, negative=skipped, strategy=strategy
)
),
query_filter=not_seen,
limit=3,
).points
print(strategy.value)
for h in hits:
print(f" {h.score:.3f} {h.payload['caption']}")
client.close()
print("qdrant-client", version("qdrant-client"), "fastembed", version("fastembed"))
輸出:
average_vector
0.548 合歡山日出要幾點出發 #登山 #日出
0.448 冷萃咖啡怎麼做才不苦 #咖啡
0.411 在家做義式濃縮的機器怎麼挑 #咖啡 #開箱
best_score
0.712 合歡山日出要幾點出發 #登山 #日出
0.707 冷萃咖啡怎麼做才不苦 #咖啡
0.704 在家做義式濃縮的機器怎麼挑 #咖啡 #開箱
qdrant-client 1.19.1 fastembed 0.8.1
貓的影片沒被推出來(理財那支是負例,本來就排除了),登山和咖啡各有入選。兩種策略的分數尺度不同,不能互相比較:average_vector 把正負例合成一個查詢向量,成本跟一般搜尋一樣;best_score 逐一比對每個例子,例子越多越慢,好處是使用者同時喜歡兩種差很多的內容時,查詢不會被平均到兩邊都不像。同樣是登山,「新手第一次爬百岳」沒有進前三名,可見只靠標題文字的向量很粗,實際上線還得加進觀看時長、互動和熱門度。
這個版本只看內容本身。想做到「看過這支的人也看了那支」,Qdrant 官方的 MovieLens 範例把每個使用者的評分存成 sparse vector,先找相似的使用者,再統計他們看過的影片。冷啟動和多樣性最好從第一版就處理:
- 冷啟動:TikTok 2020 年的說明是新使用者可以先選興趣類別,跳過的人看到一般熱門影片。
- 多樣性:同一份說明寫到,動態通常不會連續出現同一個音樂或同一個創作者的影片,偶爾也會放使用者興趣以外的內容。這種規則放在排序之後做就好,跟向量搜尋分開。
透過 WebView 加入第三方 mini app
讓第三方開發者在自家 App 裡放小程式,幾個平台是這樣做的:
- 微信小程式的渲染層用 WebView,邏輯層在獨立的 JsCore 執行緒跑,兩邊透過原生用戶端溝通,網路請求也由用戶端代發。
- 支付寶小程式的文件寫明框架不在瀏覽器裡執行,
document、window這些物件不能用。 - LINE MINI App 2023 年 9 月 27 日在台灣開放申請,本質是 LIFF 網頁跑在 LINE 的 WebView 裡;2025 年 10 月 1 日起所有 MINI App 都能在一般瀏覽器開啟。
- Telegram Mini Apps 也是 WebView 載入網頁,啟動時由 Telegram 帶入簽過章的使用者資料。
上架 iOS 還要看 App Store 審核指南 4.7。2024 年 1 月 25 日的修訂把 mini app、mini game、chatbot 和 plug-in 都納入,宿主 App 要為這些軟體負責;4.7.2 規定沒有 Apple 事先同意,不能把原生 API 開放給這些程式,4.7.3 規定分享資料或隱私權限要每次都取得使用者明確同意。
技術上最常出事的是 JavaScript bridge。Android 4.2(API 17)之前,addJavascriptInterface 讓網頁可以透過反射呼叫任意 Java 方法(CVE-2012-6636);現在的官方文件仍提醒,注入的物件會出現在 WebView 裡的每個 frame,包括 iframe。建議改用 addWebMessageListener 加上 allowedOriginRules,只接受指定來源的訊息,也不要拿 WebView.getUrl() 當安全判斷。iOS 的 WKScriptMessage 可以從 frameInfo.securityOrigin 確認訊息來自哪個 frame。
另一個常出問題的是身分。mini app 的前端拿到的使用者資料,送到後端之前都可能被改過。LINE 的文件直接寫明,不要把 liff.getProfile() 拿到的資料送到伺服器,要送 ID token,由伺服器向 LINE 驗證。Telegram 的做法是對啟動資料簽章:用 "WebAppData" 當 key 對 bot token 做 HMAC-SHA256 得到 secret key,再對排序後的欄位算 HMAC,跟 hash 欄位比對。下面模擬平台簽章、伺服器驗證,再試竄改和過期兩種情況:
import hashlib
import hmac
import json
import time
from urllib.parse import parse_qsl, urlencode
class InitDataError(Exception):
pass
def _check_string(fields: dict[str, str]) -> str:
return "\n".join(f"{k}={v}" for k, v in sorted(fields.items()))
def _secret(bot_token: str) -> bytes:
return hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
def sign(fields: dict[str, str], bot_token: str) -> str:
# What the platform does before handing init data to the mini app.
digest = hmac.new(_secret(bot_token), _check_string(fields).encode(), hashlib.sha256)
return urlencode({**fields, "hash": digest.hexdigest()})
def verify(init_data: str, bot_token: str, max_age: int = 3600) -> dict[str, str]:
# What the mini app's own server must do before trusting anything.
fields = dict(parse_qsl(init_data, strict_parsing=True))
received = fields.pop("hash", "")
expected = hmac.new(
_secret(bot_token), _check_string(fields).encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(received, expected):
raise InitDataError("bad hash")
if time.time() - int(fields["auth_date"]) > max_age:
raise InitDataError("expired")
return fields
BOT_TOKEN = "123456:TEST-ONLY-TOKEN" # dummy value for the example
now = str(int(time.time()))
user = json.dumps({"id": 42, "first_name": "Demo"}, separators=(",", ":"))
good = sign({"auth_date": now, "query_id": "AAE", "user": user}, BOT_TOKEN)
print("valid:", json.loads(verify(good, BOT_TOKEN)["user"])["id"])
forged = good.replace("%22id%22%3A42", "%22id%22%3A1")
old = sign({"auth_date": str(int(now) - 7200), "user": user}, BOT_TOKEN)
for name, data in (("forged", forged), ("old", old)):
try:
verify(data, BOT_TOKEN)
except InitDataError as e:
print(f"{name}: rejected ({e})")
輸出:
valid: 42
forged: rejected (bad hash)
old: rejected (expired)
比對用 hmac.compare_digest,避免逐字元比較洩漏時間差;auth_date 要檢查,否則截到的舊資料可以一直重用。Telegram 在 2024 年 11 月 17 日的 Bot API 8.0 另外加了 Ed25519 簽章,讓不持有 bot token 的第三方也能驗證,自己做平台也可以比照:平台用私鑰簽,mini app 開發者拿公鑰驗。
金鑰放在前端有多常見,CCS 2023 有篇論文實際量過:掃了 3,450,586 個微信小程式,其中 40,880 個把 AppSecret 這種主金鑰放在前端程式碼裡,研究者示範了竄改手機號碼接管帳號、濫用優惠和盜用付費雲端服務。
會員系統夠不夠安全
用平台登入(LINE、Telegram、Apple、Google)只確認了使用者是誰,之後的 session、權限和資料保護都得自己做。可以對照兩份公開標準檢查:
- NIST SP 800-63B-4(2025 年 7 月 31 日定稿):只用密碼時至少 15 字元,搭配多因素時至少 8 字元;不能強制大小寫符號這類組成規則,不能要求定期改密碼;新密碼要比對常見與已外洩的密碼清單;AAL2 等級要至少提供一種抗釣魚的驗證方式,passkey 屬於這一類。
- OWASP ASVS 5.0.0(2025 年 5 月 30 日發布):逐項列出驗證、session、存取控制等檢查點,適合拿來當上線前的清單。
做 mini app 時,自家帳號和平台帳號的對應要放在伺服器,用驗證過的平台使用者 ID 當 key;前端送來的任何身分欄位都不能直接寫進資料庫。session token 要短命,敏感操作再驗一次。
法規:台灣與歐盟目前公布的內容
台灣:
- 個人資料保護法:2023 年 5 月 31 日公布的修正,增訂個人資料保護委員會為主管機關,非公務機關未採取安全措施時,不必先限期改正即可處罰,一般情況新臺幣 2 萬元至 200 萬元,情節重大 15 萬元至 1,500 萬元。2025 年 11 月 11 日公布的修正,規定非公務機關發生個資外洩要通知當事人並通報個資會,並給個資會行政檢查權,施行日期由行政院定。截至本文查詢時,個資會官網仍以籌備處名義運作。
- 詐欺犯罪危害防制條例:2024 年 7 月 31 日公布,規範網路廣告平臺、電商、第三方支付與網路連線遊戲業者。達一定規模的廣告平臺要驗證廣告主與出資者身分、標示廣告與 AI 生成的肖像,並在期限內下架詐騙廣告,相關子法定為 24 小時。數位發展部在 2024 年 9 月 16 日指定 Google、LY、Meta、TikTok 四家。
- 數位中介服務法草案:國家通訊傳播委員會在 2022 年 9 月把草案退回內部重新研議,沒有時間表;截至本文查詢時,沒有查到公布的新版本。
- 資通安全管理法:2025 年 9 月 24 日公布修正,民間部分的適用對象是特定非公務機關,主要是關鍵基礎設施提供者。
歐盟與其他地區:
- Digital Services Act 在 2024 年 2 月 17 日全面適用,歐盟月活躍使用者 4,500 萬以上的平台有額外義務。歐盟執委會在 2024 年 8 月 5 日接受 TikTok 永久撤回 TikTok Lite 獎勵計畫的承諾;2026 年 2 月 6 日初步認定 TikTok 的成癮設計(無限滑動、自動播放、推播、推薦)違反 DSA。
- 愛爾蘭資料保護委員會在 2025 年 5 月 2 日依 GDPR 對 TikTok 處以 5.3 億歐元罰款,理由是把歐洲使用者資料傳到中國的方式違法,以及透明度不足。
- 澳洲的 16 歲以下社群媒體年齡限制在 2025 年 12 月 10 日生效,涵蓋 Facebook、Instagram、Threads、TikTok、YouTube 等平台,最高罰款 4,950 萬澳幣。
補充筆記
- 公開內容要讓伺服器讀得到,端對端加密只留給私訊;想讓使用者帶得走資料,可以看 ActivityPub、AT Protocol。
- MinIO 社群版 2025 年起只剩原始碼、進入 maintenance mode,2026 年 4 月封存;替代品有 SeaweedFS、Garage、RustFS、Ceph RGW、Apache Ozone,程式裡只用 S3 API,之後最容易替換。
- 最小的推薦系統可以用 Qdrant 的 recommend 查詢,正例負例加上排除已看過的過濾條件;多樣性規則放在排序之後。
- mini app 的 bridge 只開給信任的來源,身分一律由伺服器驗證平台簽發的 token 或簽章,金鑰不放前端。
- 會員系統可以對照 NIST SP 800-63B-4 和 OWASP ASVS 5.0.0 檢查;台灣的詐騙廣告下架已入法,個資外洩通報義務已修法公布、施行日期未定。
延伸閱讀
- W3C〈ActivityPub〉:2018-01-23 成為 Recommendation 的規格本文。
- TechCrunch〈Bluesky opens up federation〉與〈Threads expands open social web integrations〉:聯邦開放的日期與功能,屬新聞報導。
- Matrix〈Matrix Specification〉:房間在 homeserver 之間複製的說明,官方規格。
- MinIO〈GitHub repository〉與〈RELEASE.2025-05-24T17-08-30Z〉:README 的 source only、maintenance mode 說明與封存狀態,官方 repo。
- It’s FOSS〈MinIO moves away from open source〉:MinIO 社群版變化的時間線整理,屬新聞報導。
- 各專案官方頁面:〈SeaweedFS〉、〈Garage〉、〈Announcing RustFS 1.0.0 GA〉、〈Ceph Object Gateway〉、〈Apache Ozone〉,屬廠商或專案自述。
- Cloudflare〈R2 pricing〉:出站流量不收費的說明,官方定價頁。
- IETF〈RFC 8216: HTTP Live Streaming〉、〈HLS 第二版草案〉與 Apple〈Enabling Low-Latency HLS〉:HLS 規格與低延遲擴充。
- Covington 等〈Deep Neural Networks for YouTube Recommendations〉:RecSys 2016,兩階段推薦架構。
- Liu 等〈Monolith: Real Time Recommendation System With Collisionless Embedding Table〉:ByteDance 2022,即時訓練與 embedding table 設計。
- TikTok〈How TikTok recommends videos #ForYou〉:2020-06-18,推薦訊號、冷啟動與多樣性的官方說明。
- Qdrant〈Explore the data〉與〈Recommendation system with OVHcloud〉:recommend 策略與 sparse vector 協同過濾範例,官方文件。
- 微信〈Framework〉與支付寶〈Framework overview〉:雙執行環境的官方說明。
- LINE〈Using user profile〉、〈LINE MINI App 台灣開放申請〉與〈MINI App browser access〉:伺服器驗證 token 的規定與開放時程,官方文件與公告。
- Telegram〈Telegram Mini Apps〉:啟動資料驗證與 Bot API 8.0 第三方驗證,官方文件。
- Apple〈App Store Review Guidelines〉與〈2024-01-25 更新公告〉:4.7 條文,官方。
- Android〈Insecure WebView native bridges〉與〈Native API access with JavaScript bridge〉:bridge 風險與
addWebMessageListener的建議,官方文件。 - NVD〈CVE-2012-6636〉:
addJavascriptInterface的漏洞說明。 - Zhang、Yang、Lin〈Don’t Leak Your Keys: Understanding, Measuring, and Exploiting the AppSecret Leaks in Mini-Programs〉:ACM CCS 2023,同儕審查的大規模量測。
- NIST〈SP 800-63B-4〉與 OWASP〈ASVS〉:驗證與應用程式安全標準。
- 全國法規資料庫〈個人資料保護法〉、〈詐欺犯罪危害防制條例〉:條文與修正沿革。
- 個人資料保護委員會籌備處〈2025 年修正常見問答〉:外洩通報與檢查權的官方說明。
- 數位發展部〈指定網路廣告平臺業者公告〉:2024-09-16 指定的四家業者。
- 中央社〈NCC 數位中介服務法草案退回重議〉:2022 年 9 月的決定,屬新聞報導。
- 中央社〈資通安全管理法修正三讀〉:2025 年修法內容,屬新聞報導;條文見全國法規資料庫〈資通安全管理法〉。
- European Commission〈TikTok Lite Rewards〉與〈TikTok addictive design〉:DSA 執法紀錄,官方。
- Irish Data Protection Commission〈TikTok €530 million fine〉:2025-05-02 決定,官方。
- Australian Government〈Social media minimum age〉:16 歲以下限制的官方說明。
想法與技術判斷出自 Sheng,和 Claude 一起起草 · 範例在 Python 3.13.12、qdrant-client 1.19.1、fastembed 0.8.1 實測。