package main import ( "embed" "encoding/base64" "encoding/json" "fmt" "io/fs" "log" "net/http" "os" "path/filepath" "sort" "strings" "time" ) // ============================================================================ // 嵌入前端文件 // ============================================================================ //go:embed index.html var content embed.FS // ============================================================================ // 数据结构定义 // ============================================================================ // UploadRequest 前端发送的签名上传请求结构 type UploadRequest struct { Name string `json:"name"` // 签署人姓名 Image string `json:"image"` // Base64 编码的签名图片 } // UploadResponse 服务器返回的响应结构 type UploadResponse struct { Success bool `json:"success"` // 是否成功 Message string `json:"message"` // 提示信息 Path string `json:"path"` // 文件保存路径 } // LoginRequest 登录请求结构 type LoginRequest struct { Account string `json:"account"` // 账号 Password string `json:"password"` // 密码 } // LoginResponse 登录响应结构 type LoginResponse struct { Success bool `json:"success"` // 是否成功 Token string `json:"token"` // 认证令牌 Message string `json:"message"` // 提示信息 } // SignatureItem 签名文件信息结构 type SignatureItem struct { Name string `json:"name"` // 签署人姓名 Path string `json:"path"` // 文件路径 Date string `json:"date"` // 签名日期 Size int64 `json:"size"` // 文件大小(字节) } // ============================================================================ // 全局变量 // ============================================================================ // 存储账号密码信息 var accountConfig struct { Account string `json:"account"` // 账号 Password string `json:"password"` // 密码 } // 存储有效的认证令牌,格式:令牌 -> 账号 var validTokens = make(map[string]string) // Token 过期时间(24小时) const tokenExpireDuration = 24 * time.Hour // 存储令牌的创建时间 var tokenCreateTimes = make(map[string]time.Time) // uploads 目录的绝对路径 var uploadsDir string // ============================================================================ // 主函数 // ============================================================================ func main() { // 获取可执行文件所在目录 execPath, err := os.Executable() if err != nil { log.Printf("获取可执行文件路径失败: %v,使用当前目录", err) execPath = filepath.Join(".", "placeholder") } execDir := filepath.Dir(execPath) // 初始化 uploads 目录的绝对路径 uploadsDir = filepath.Join(execDir, "uploads") // 加载账号密码配置文件 loadAccountConfig(execDir) // 注册路由 setupRoutes() // 启动服务器 port := ":10081" fmt.Println("========================================") fmt.Println(" 电子签名归档系统 v2.0") fmt.Println("========================================") fmt.Printf("监听端口: %s\n", port) fmt.Printf("签名存储: %s\n", uploadsDir) fmt.Printf("访问地址: http://localhost%s\n", port) fmt.Println("========================================") if err := http.ListenAndServe(port, nil); err != nil { log.Fatal("服务器启动失败:", err) } } // ============================================================================ // 路由设置 // ============================================================================ // setupRoutes 注册所有 HTTP 路由 func setupRoutes() { // 主页面 - 从嵌入的文件系统提供前端页面 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") data, err := content.ReadFile("index.html") if err != nil { http.Error(w, "系统错误: 页面文件丢失", 500) return } w.Write(data) }) // 签名上传接口(无需认证) http.HandleFunc("/upload", uploadHandler) // 登录接口 http.HandleFunc("/login", loginHandler) // 验证登录状态接口 http.HandleFunc("/api/check-auth", checkAuthHandler) // 获取签名列表接口(需要认证) http.HandleFunc("/api/signatures", signaturesHandler) // 下载签名文件接口(需要认证) http.HandleFunc("/download", downloadHandler) } // ============================================================================ // 认证相关函数 // ============================================================================ // loadAccountConfig 从 pwd.json 加载账号密码配置 // 优先从可执行文件目录加载,如果找不到则从当前工作目录加载 func loadAccountConfig(execDir string) { // 尝试从可执行文件目录加载 configPath := filepath.Join(execDir, "pwd.json") if err := loadConfigFromFile(configPath); err != nil { // 如果失败,尝试从当前工作目录加载 configPath = filepath.Join(".", "pwd.json") if err := loadConfigFromFile(configPath); err != nil { log.Printf("警告: 未找到 pwd.json 配置文件,登录功能将不可用") return } } log.Printf("已加载账号配置: %s", configPath) } // loadConfigFromFile 从指定路径加载配置文件 func loadConfigFromFile(path string) error { data, err := os.ReadFile(path) if err != nil { return err } return json.Unmarshal(data, &accountConfig) } // generateToken 生成认证令牌 // 格式:base64(账号:时间戳),简单但足够用于本地场景 func generateToken(account string) string { // 组合账号和时间戳 raw := fmt.Sprintf("%s:%d", account, time.Now().UnixNano()) // Base64 编码 token := base64.StdEncoding.EncodeToString([]byte(raw)) // 记录令牌创建时间 validTokens[token] = account tokenCreateTimes[token] = time.Now() return token } // validateToken 验证令牌是否有效 // 返回:是否有效、对应的账号 func validateToken(token string) (bool, string) { if token == "" { return false, "" } // 检查令牌是否存在 account, exists := validTokens[token] if !exists { return false, "" } // 检查令牌是否过期 createTime, ok := tokenCreateTimes[token] if !ok || time.Since(createTime) > tokenExpireDuration { // 令牌已过期,删除它 delete(validTokens, token) delete(tokenCreateTimes, token) return false, "" } return true, account } // cleanExpiredTokens 清理过期的令牌 func cleanExpiredTokens() { now := time.Now() for token, createTime := range tokenCreateTimes { if now.Sub(createTime) > tokenExpireDuration { delete(validTokens, token) delete(tokenCreateTimes, token) } } } // getAuthTokenFromRequest 从请求中提取认证令牌 // 支持从 Authorization 头或查询参数获取 func getAuthTokenFromRequest(r *http.Request) string { // 首先尝试从 Authorization 头获取 authHeader := r.Header.Get("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { return strings.TrimPrefix(authHeader, "Bearer ") } // 然后尝试从查询参数获取 return r.URL.Query().Get("token") } // ============================================================================ // 路由处理函数 // ============================================================================ // loginHandler 处理登录请求 // POST /login - 验证账号密码,返回认证令牌 func loginHandler(w http.ResponseWriter, r *http.Request) { // 只允许 POST 方法 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 检查是否配置了账号密码 if accountConfig.Account == "" { sendJSONResponse(w, false, "系统未配置登录账号", "") return } // 解析请求体 var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendJSONResponse(w, false, "无效的请求数据", "") return } // 去除前后空格 req.Account = strings.TrimSpace(req.Account) req.Password = strings.TrimSpace(req.Password) // 验证账号密码 if req.Account == "" || req.Password == "" { sendJSONResponse(w, false, "账号和密码不能为空", "") return } // 对比账号密码 if req.Account != accountConfig.Account || req.Password != accountConfig.Password { log.Printf("登录失败: 账号 %s 密码错误", req.Account) sendJSONResponse(w, false, "账号或密码错误", "") return } // 登录成功,生成令牌 token := generateToken(req.Account) log.Printf("登录成功: 账号 %s", req.Account) // 返回成功响应(使用 LoginResponse 结构) w.Header().Set("Content-Type", "application/json") resp := LoginResponse{ Success: true, Token: token, Message: "登录成功", } json.NewEncoder(w).Encode(resp) } // checkAuthHandler 检查认证状态 // GET /api/check-auth - 验证令牌是否有效 func checkAuthHandler(w http.ResponseWriter, r *http.Request) { token := getAuthTokenFromRequest(r) valid, account := validateToken(token) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "authenticated": valid, "account": account, }) } // uploadHandler 处理签名上传 // POST /upload - 接收 Base64 编码的签名图片并保存 func uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 解析请求体 var req UploadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendJSONResponse(w, false, "无效的请求数据", "") return } // 验证姓名不为空 req.Name = strings.TrimSpace(req.Name) if req.Name == "" { sendJSONResponse(w, false, "名字不能为空", "") return } // 验证签名数据不为空 if req.Image == "" { sendJSONResponse(w, false, "签名数据为空", "") return } // 去除 Base64 前缀(data:image/png;base64,) b64data := req.Image if idx := strings.Index(b64data, ","); idx != -1 { b64data = b64data[idx+1:] } // 解码 Base64 数据 imgBytes, err := base64.StdEncoding.DecodeString(b64data) if err != nil { sendJSONResponse(w, false, "图片数据解码失败", "") return } // 生成安全的文件名 dateStr := time.Now().Format("2006-1-2") safeName := filepath.Base(req.Name) // 创建保存目录 saveDir := filepath.Join("uploads", safeName) if err := os.MkdirAll(saveDir, 0755); err != nil { log.Printf("创建目录失败: %v", err) sendJSONResponse(w, false, "服务器存储错误", "") return } // 生成完整的文件路径 fileName := fmt.Sprintf("%s+%s.png", safeName, dateStr) fullPath := filepath.Join(saveDir, fileName) // 写入文件 if err := os.WriteFile(fullPath, imgBytes, 0644); err != nil { log.Printf("写入文件失败: %v", err) sendJSONResponse(w, false, "保存文件失败", "") return } log.Printf("已保存签名: %s", fullPath) sendJSONResponse(w, true, "签名保存成功!", fullPath) } // signaturesHandler 获取签名列表 // GET /api/signatures - 返回所有签名文件的信息(需要认证) func signaturesHandler(w http.ResponseWriter, r *http.Request) { // 验证认证 token := getAuthTokenFromRequest(r) valid, _ := validateToken(token) if !valid { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "未登录或登录已过期", }) return } // 收集所有签名文件 signatures := collectSignatureFiles() // 返回结果 w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "signatures": signatures, }) } // downloadHandler 下载签名文件 // GET /download?path=xxx - 下载指定的签名图片(需要认证) func downloadHandler(w http.ResponseWriter, r *http.Request) { // 验证认证 token := getAuthTokenFromRequest(r) valid, _ := validateToken(token) if !valid { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "未登录或登录已过期", }) return } // 获取文件路径参数 filePath := r.URL.Query().Get("path") if filePath == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "缺少文件路径参数", }) return } // 安全检查:确保路径在 uploads 目录内,防止路径穿越攻击 absPath, err := filepath.Abs(filePath) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "无效的文件路径", }) return } // 获取 uploads 目录的绝对路径 uploadsAbs, _ := filepath.Abs("uploads") if !strings.HasPrefix(absPath, uploadsAbs) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "无权访问该文件", }) return } // 检查文件是否存在 if _, err := os.Stat(absPath); os.IsNotExist(err) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": "文件不存在", }) return } // 设置响应头,触发浏览器下载 fileName := filepath.Base(absPath) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) w.Header().Set("Content-Type", "image/png") // 提供文件 http.ServeFile(w, r, absPath) } // ============================================================================ // 辅助函数 // ============================================================================ // sendJSONResponse 发送 JSON 格式的响应(上传接口专用) func sendJSONResponse(w http.ResponseWriter, success bool, message string, path string) { w.Header().Set("Content-Type", "application/json") resp := UploadResponse{ Success: success, Message: message, Path: path, } json.NewEncoder(w).Encode(resp) } // collectSignatureFiles 收集 uploads 目录下所有签名文件 // 返回按修改时间倒序排列的签名文件列表 func collectSignatureFiles() []SignatureItem { var signatures []SignatureItem // 检查 uploads 目录是否存在 if _, err := os.Stat("uploads"); os.IsNotExist(err) { return signatures } // 遍历 uploads 目录下的所有子目录(每个子目录对应一个签署人) err := filepath.Walk("uploads", func(path string, info fs.FileInfo, err error) error { if err != nil { return nil // 跳过错误的文件 } // 只处理 .png 文件 if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".png") { // 从文件路径中提取签署人姓名(目录名) relPath, _ := filepath.Rel("uploads", path) parts := strings.SplitN(relPath, string(filepath.Separator), 2) name := "" if len(parts) > 0 { name = parts[0] } // 从文件名中提取日期 // 文件名格式:<姓名>+<日期>.png baseName := strings.TrimSuffix(info.Name(), ".png") datePart := "" if idx := strings.LastIndex(baseName, "+"); idx != -1 { datePart = baseName[idx+1:] } signatures = append(signatures, SignatureItem{ Name: name, Path: path, Date: datePart, Size: info.Size(), }) } return nil }) if err != nil { log.Printf("遍历签名目录失败: %v", err) } // 按修改时间倒序排列 sort.Slice(signatures, func(i, j int) bool { fileInfoI, _ := os.Stat(signatures[i].Path) fileInfoJ, _ := os.Stat(signatures[j].Path) if fileInfoI == nil || fileInfoJ == nil { return false } return fileInfoI.ModTime().After(fileInfoJ.ModTime()) }) return signatures }