前言
一个使用 Go 实现文档分享链接功能的后端方案,包含生成分享链接、验证权限、访问控制等核心功能。
设计思路
为每个文档生成唯一的分享 token(UUID)
支持设置分享权限(只读 / 编辑)和有效期
通过中间件验证分享链接的有效性
提供创建、查询、撤销分享的接口
实现代码
数据模型定义
go
package main
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Document 文档模型
type Document struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:255"`
Content string `gorm:"type:text"`
OwnerID uint // 文档所有者ID
}
// Share 分享记录模型
type Share struct {
ID uint `gorm:"primaryKey"`
ShareID string `gorm:"size:36;uniqueIndex"` // 分享链接唯一标识(UUID)
DocumentID uint `gorm:"index"` // 关联的文档ID
Permission string `gorm:"size:20"` // 权限:read / edit
ExpiresAt *time.Time `gorm:"index"` // 过期时间(nil表示永久有效)
CreatedAt time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"` // 用于软删除(撤销分享)
}
// 生成新的分享记录
func NewShare(docID uint, permission string, expiresAt *time.Time) *Share {
return &Share{
ShareID: uuid.New().String(),
DocumentID: docID,
Permission: permission,
ExpiresAt: expiresAt,
}
}
数据库初始化
go
package main
import (
"gorm.io/driver/sqlite" // 示例使用SQLite,可替换为MySQL等
"gorm.io/gorm"
)
var db *gorm.DB
// 初始化数据库连接
func initDB() error {
var err error
db, err = gorm.Open(sqlite.Open("documents.db"), &gorm.Config{})
if err != nil {
return err
}
// 迁移数据表
return db.AutoMigrate(&Document{}, &Share{})
}
业务逻辑
go
package main
import (
"errors"
"time"
)
// 创建文档分享链接
func CreateShare(docID uint, permission string, expiresInHours int) (*Share, error) {
// 验证权限类型
if permission != "read" && permission != "edit" {
return nil, errors.New("invalid permission (must be 'read' or 'edit')")
}
// 计算过期时间(expiresInHours=0表示永久有效)
var expiresAt *time.Time
if expiresInHours > 0 {
t := time.Now().Add(time.Duration(expiresInHours) * time.Hour)
expiresAt = &t
}
// 创建分享记录
share := NewShare(docID, permission, expiresAt)
if err := db.Create(share).Error; err != nil {
return nil, err
}
return share, nil
}
// 验证分享链接有效性
func ValidateShare(shareID string) (*Share, error) {
var share Share
if err := db.Where("share_id = ?", shareID).First(&share).Error; err != nil {
return nil, errors.New("invalid or expired share link")
}
// 检查是否过期
if share.ExpiresAt != nil && time.Now().After(*share.ExpiresAt) {
return nil, errors.New("share link has expired")
}
return &share, nil
}
// 撤销分享链接(软删除)
func RevokeShare(shareID string, ownerID uint) error {
// 验证文档所有权
var doc Document
if err := db.Where("id = (SELECT document_id FROM shares WHERE share_id = ?)", shareID).First(&doc).Error; err != nil {
return errors.New("document not found")
}
if doc.OwnerID != ownerID {
return errors.New("no permission to revoke this share")
}
// 软删除分享记录
return db.Where("share_id = ?", shareID).Delete(&Share{}).Error
}
// 通过分享链接获取文档
func GetDocumentByShare(shareID string) (*Document, *Share, error) {
share, err := ValidateShare(shareID)
if err != nil {
return nil, nil, err
}
var doc Document
if err := db.Where("id = ?", share.DocumentID).First(&doc).Error; err != nil {
return nil, nil, errors.New("document not found")
}
return &doc, share, nil
}
API实现
go
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// 1. 创建分享链接
// 请求: POST /api/documents/:docID/share
// 参数: permission (read/edit), expiresInHours (0=永久)
func createShareHandler(c *gin.Context) {
docID, err := strconv.ParseUint(c.Param("docID"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document ID"})
return
}
var req struct {
Permission string `json:"permission" binding:"required,oneof=read edit"`
ExpiresInHours int `json:"expiresInHours" binding:"min=0"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 这里简化处理,实际应从认证中获取用户ID
ownerID := uint(1) // 示例:假设当前用户ID为1
// 验证文档所有权(实际项目需检查doc.OwnerID == ownerID)
var doc Document
if err := db.First(&doc, docID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "document not found"})
return
}
share, err := CreateShare(uint(docID), req.Permission, req.ExpiresInHours)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create share"})
return
}
// 生成完整分享链接
shareURL := "http://your-domain.com/shared/" + share.ShareID
c.JSON(http.StatusOK, gin.H{
"shareURL": shareURL,
"shareID": share.ShareID,
"permission": share.Permission,
"expiresAt": share.ExpiresAt,
})
}
// 2. 通过分享链接访问文档
// 请求: GET /api/shared/:shareID
func accessSharedDocumentHandler(c *gin.Context) {
shareID := c.Param("shareID")
doc, share, err := GetDocumentByShare(shareID)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"document": map[string]interface{}{
"id": doc.ID,
"title": doc.Title,
"content": doc.Content,
},
"permission": share.Permission,
})
}
// 3. 撤销分享链接
// 请求: DELETE /api/shares/:shareID
func revokeShareHandler(c *gin.Context) {
shareID := c.Param("shareID")
ownerID := uint(1) // 实际应从认证中获取
if err := RevokeShare(shareID, ownerID); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "share revoked successfully"})
}
主程序
go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 初始化数据库
if err := initDB(); err != nil {
panic("failed to initialize database: " + err.Error())
}
// 设置路由
r := gin.Default()
api := r.Group("/api")
{
// 文档分享相关接口
api.POST("/documents/:docID/share", createShareHandler) // 创建分享
api.GET("/shared/:shareID", accessSharedDocumentHandler) // 访问分享文档
api.DELETE("/shares/:shareID", revokeShareHandler) // 撤销分享
}
// 启动服务
r.Run(":8080")
}
3. 核心功能说明
- 分享链接生成:
使用 UUID 作为唯一标识,避免猜测
支持设置权限(只读 / 编辑)和有效期
生成的链接格式:http://your-domain.com/shared/{shareID} - 安全验证:
验证分享链接是否存在、未过期、未被撤销
检查访问者权限(只读 / 编辑)
只有文档所有者可撤销分享 - 扩展建议:
增加用户认证(如 JWT),替换示例中的ownerID硬编码
实现分享记录列表接口,方便用户管理所有分享
增加访问日志,记录分享链接的访问情况
对敏感文档可增加密码保护功能