refactor(auth): move authentication params to headers and simplify validation

- Move token and timestamp validation to HTTP headers
- Simplify ValidateTimeStamp to return boolean
- Update AddUser to use default "visitor" type
- Remove redundant timestamp and token fields from request structs
- Update API documentation to reflect header-based authentication
This commit is contained in:
2026-02-28 15:32:32 +08:00
parent aa70e7c0f0
commit b661118180
6 changed files with 113 additions and 134 deletions
+50 -20
View File
@@ -69,7 +69,8 @@ All API responses are returned in JSON format:
``` ```
**Important Notes:** **Important Notes:**
- For **POST** requests: All data is sent in the request body as JSON - For all requests: Authentication token and timestamp are sent in HTTP headers (prefixed with `X-`)
- For **POST** requests: Other data is sent in the request body as JSON
- For **GET** requests: All data is sent in HTTP headers (prefixed with `X-`) - For **GET** requests: All data is sent in HTTP headers (prefixed with `X-`)
- All requests require authentication (except `/register` and `/login`) - All requests require authentication (except `/register` and `/login`)
- Timestamps are in milliseconds (Unix timestamp) - Timestamps are in milliseconds (Unix timestamp)
@@ -82,21 +83,28 @@ All API responses are returned in JSON format:
**Method:** POST **Method:** POST
**Content-Type:** application/json **Content-Type:** application/json
**Request:** **Request Headers:**
```
X-Timestamp: 1704067200000
```
**Request Body:**
```json ```json
{ {
"username": "your_username", "username": "your_username",
"passwd": "YourPass123!", "passwd": "YourPass123!",
"timeStamp": 1704067200000,
"type": "admin" "type": "admin"
} }
``` ```
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| X-Timestamp | int64 | Yes | Client timestamp in milliseconds |
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| username | string | Yes | Username (no special characters) | | username | string | Yes | Username (no special characters) |
| passwd | string | Yes | Password (must contain uppercase, lowercase, digit, and special character, min 8 chars) | | passwd | string | Yes | Password (must contain uppercase, lowercase, digit, and special character, min 8 chars) |
| timeStamp | int64 | Yes | Client timestamp in milliseconds |
| type | string | No | User type: superuser, admin, visitor (default: visitor) | | type | string | No | User type: superuser, admin, visitor (default: visitor) |
**Response:** **Response:**
@@ -120,20 +128,27 @@ All API responses are returned in JSON format:
**Method:** POST **Method:** POST
**Content-Type:** application/json **Content-Type:** application/json
**Request:** **Request Headers:**
```
X-Timestamp: 1704067200000
```
**Request Body:**
```json ```json
{ {
"username": "your_username", "username": "your_username",
"passwd": "YourPass123!", "passwd": "YourPass123!"
"timeStamp": 1704067200000
} }
``` ```
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| X-Timestamp | int64 | Yes | Client timestamp in milliseconds |
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| username | string | Yes | Username | | username | string | Yes | Username |
| passwd | string | Yes | Password | | passwd | string | Yes | Password |
| timeStamp | int64 | Yes | Client timestamp in milliseconds |
**Response:** **Response:**
```json ```json
@@ -158,11 +173,15 @@ All API responses are returned in JSON format:
**Content-Type:** application/json **Content-Type:** application/json
**Auth Required:** Yes (token) **Auth Required:** Yes (token)
**Request:** **Request Headers:**
```
X-Token: your_token
X-Timestamp: 1704067200000
```
**Request Body:**
```json ```json
{ {
"token": "your_token",
"timeStamp": 1704067200000,
"instanceInfo": { "instanceInfo": {
"name": "my_frpc", "name": "my_frpc",
"serverAddr": "127.0.0.1", "serverAddr": "127.0.0.1",
@@ -177,10 +196,13 @@ All API responses are returned in JSON format:
} }
``` ```
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| X-Token | string | Yes | Authentication token |
| X-Timestamp | int64 | Yes | Client timestamp in milliseconds |
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| token | string | Yes | Authentication token |
| timeStamp | int64 | Yes | Client timestamp in milliseconds |
| instanceInfo.name | string | Yes | Instance name | | instanceInfo.name | string | Yes | Instance name |
| instanceInfo.serverAddr | string | Yes | frps server address | | instanceInfo.serverAddr | string | Yes | frps server address |
| instanceInfo.serverPort | string | Yes | frps server port | | instanceInfo.serverPort | string | Yes | frps server port |
@@ -211,12 +233,16 @@ All API responses are returned in JSON format:
**Content-Type:** application/json **Content-Type:** application/json
**Auth Required:** Yes (token) **Auth Required:** Yes (token)
**Request:** **Request Headers:**
```
X-Token: your_token
X-Timestamp: 1704067200000
```
**Request Body:**
```json ```json
{ {
"instanceName": "my_frpc", "instanceName": "my_frpc"
"token": "your_token",
"timeStamp": 1704067200000
} }
``` ```
@@ -242,12 +268,16 @@ All API responses are returned in JSON format:
You can modify multiple fields at once: You can modify multiple fields at once:
**Request:** **Request Headers:**
```
X-Token: your_token
X-Timestamp: 1704067200000
```
**Request Body:**
```json ```json
{ {
"instanceName": "my_frpc", "instanceName": "my_frpc",
"token": "your_token",
"timeStamp": 1704067200000,
"name": "new_name", "name": "new_name",
"serverAddr": "192.168.1.1", "serverAddr": "192.168.1.1",
"serverPort": "7000", "serverPort": "7000",
+18 -5
View File
@@ -6,6 +6,8 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http"
"strconv"
"strings" "strings"
"super-frpc/postLog" "super-frpc/postLog"
"sync" "sync"
@@ -175,13 +177,24 @@ func isValidPassword(password string) bool { // Validate password complexity and
return hasUpper && hasLower && hasDigit && hasSpecial return hasUpper && hasLower && hasDigit && hasSpecial
} }
func ValidateTimeStamp(timeStamp int64) error { func ValidateTimeStamp(header http.Header) bool {
if globalConfig.Debug { timeStampStr := header.Get("X-Timestamp")
return nil if timeStampStr == "" {
return false
} }
timeStamp, err := strconv.ParseInt(timeStampStr, 10, 64)
if err != nil {
return false
}
if globalConfig.Debug {
return true
}
currentTime := time.Now().UnixMilli() currentTime := time.Now().UnixMilli()
if currentTime-timeStamp > 3000 || timeStamp-currentTime > 3000 { if currentTime-timeStamp > 3000 || timeStamp-currentTime > 3000 {
return fmt.Errorf("Timestamp out of valid range: %d", timeStamp) return false
} }
return nil return true
} }
+2 -2
View File
@@ -42,7 +42,7 @@ func isValidInput(input string) bool {
return true return true
} }
func AddUser(username, passwd, userType string) (int, error) { func AddUser(username, passwd string) (int, error) { // New user registration with default type "visitor"
if !isValidInput(username) || !isValidInput(passwd) { if !isValidInput(username) || !isValidInput(passwd) {
return 0, errors.New("invalid input: contains illegal characters") return 0, errors.New("invalid input: contains illegal characters")
} }
@@ -57,7 +57,7 @@ func AddUser(username, passwd, userType string) (int, error) {
} }
result, err := db.Exec("INSERT INTO userLogin (username, passwd, type) VALUES (?, ?, ?)", result, err := db.Exec("INSERT INTO userLogin (username, passwd, type) VALUES (?, ?, ?)",
username, hashedPasswd, userType) username, hashedPasswd, "visitor")
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "UNIQUE constraint failed") {
return 0, errors.New("username already exists") return 0, errors.New("username already exists")
+14 -39
View File
@@ -10,7 +10,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"super-frpc/postLog" "super-frpc/postLog"
"time" "time"
@@ -27,8 +26,6 @@ type InstanceInfo struct {
} }
type CreateInstanceRequest struct { type CreateInstanceRequest struct {
Token string `json:"token"`
TimeStamp int64 `json:"timeStamp"`
InstanceInfo InstanceInfo `json:"instanceInfo"` InstanceInfo InstanceInfo `json:"instanceInfo"`
BootAtStart bool `json:"bootAtStart"` BootAtStart bool `json:"bootAtStart"`
RunUser string `json:"runUser"` RunUser string `json:"runUser"`
@@ -80,21 +77,6 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// 处理timeStamp字段
timeStamp := int64(0)
if ts, ok := reqMap["timeStamp"]; ok {
switch v := ts.(type) {
case float64:
timeStamp = int64(v)
case string:
if v != "" {
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil {
timeStamp = parsed
}
}
}
}
// 处理bootAtStart字段 // 处理bootAtStart字段
bootAtStart := false bootAtStart := false
if bas, ok := reqMap["bootAtStart"]; ok { if bas, ok := reqMap["bootAtStart"]; ok {
@@ -128,31 +110,22 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) {
instanceInfo.Additional = additional instanceInfo.Additional = additional
} }
// 从Header中验证token和timeStamp
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
// 构建请求结构体 // 构建请求结构体
req := CreateInstanceRequest{ req := CreateInstanceRequest{
Token: getStringFromMap(reqMap, "token"),
TimeStamp: timeStamp,
InstanceInfo: instanceInfo, InstanceInfo: instanceInfo,
BootAtStart: bootAtStart, BootAtStart: bootAtStart,
RunUser: instanceInfo.RunUser, RunUser: instanceInfo.RunUser,
Additional: instanceInfo.Additional, Additional: instanceInfo.Additional,
} }
// 重新序列化为JSON,用于ValidateRequestWithBody
reqBody, err := json.Marshal(req)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to marshal request body: %v", err))
SendErrorResponse(w, http.StatusBadRequest, "Invalid request format")
return
}
userID, _, err := ValidateRequestWithBody(w, r, reqBody)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request body: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if err := CheckPermission(userID, "superuser", "admin"); err != nil { if err := CheckPermission(userID, "superuser", "admin"); err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to check permission: %v", err)) postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to check permission: %v", err))
SendErrorResponse(w, http.StatusForbidden, err.Error()) SendErrorResponse(w, http.StatusForbidden, err.Error())
@@ -251,9 +224,10 @@ func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
userID, _, err := ValidateRequestWithBody(w, r, body) // 从Header中验证token和timeStamp
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil { if err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request body: %v", err)) postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error()) SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return return
} }
@@ -345,9 +319,10 @@ func ModifyInstanceHandler(w http.ResponseWriter, r *http.Request, field string)
return return
} }
userID, _, err := ValidateRequestWithBody(w, r, body) // 从Header中验证token和timeStamp
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil { if err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request body: %v", err)) postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error()) SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return return
} }
+19 -58
View File
@@ -13,14 +13,11 @@ import (
type RegisterRequest struct { type RegisterRequest struct {
Username string `json:"username"` Username string `json:"username"`
Passwd string `json:"passwd"` Passwd string `json:"passwd"`
TimeStamp int64 `json:"timeStamp"`
Type string `json:"type"`
} }
type LoginRequest struct { type LoginRequest struct {
Username string `json:"username"` Username string `json:"username"`
Passwd string `json:"passwd"` Passwd string `json:"passwd"`
TimeStamp int64 `json:"timeStamp"`
} }
type Response struct { type Response struct {
@@ -36,6 +33,11 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if !ValidateTimeStamp(r.Header) {
SendErrorResponse(w, http.StatusBadRequest, "Invalid or missing X-Timestamp in header")
return
}
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body")
@@ -57,11 +59,6 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := ValidateTimeStamp(req.TimeStamp); err != nil {
SendErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
if !isValidInput(req.Username) || !isValidInput(req.Passwd) { if !isValidInput(req.Username) || !isValidInput(req.Passwd) {
SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters") SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters")
postLog.Debug(fmt.Sprintf("[RegisterHandler] New user registration failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd)) postLog.Debug(fmt.Sprintf("[RegisterHandler] New user registration failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd))
@@ -74,23 +71,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
userType := req.Type userID, err := AddUser(req.Username, req.Passwd)
if userType == "" {
userType = "visitor"
}
validTypes := map[string]bool{
"superuser": true,
"admin": true,
"visitor": true,
}
if !validTypes[userType] {
SendErrorResponse(w, http.StatusBadRequest, "Invalid user type")
postLog.Warning(fmt.Sprintf("[RegisterHandler] New user registration failed: invalid user type \"%s\"", userType))
return
}
userID, err := AddUser(req.Username, req.Passwd, userType)
if err != nil { if err != nil {
SendErrorResponse(w, http.StatusInternalServerError, err.Error()) SendErrorResponse(w, http.StatusInternalServerError, err.Error())
postLog.Error(fmt.Sprintf("[RegisterHandler] Failed to register user \"%s\": %v", req.Username, err)) postLog.Error(fmt.Sprintf("[RegisterHandler] Failed to register user \"%s\": %v", req.Username, err))
@@ -118,6 +99,11 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if !ValidateTimeStamp(r.Header) {
SendErrorResponse(w, http.StatusBadRequest, "Invalid or missing X-Timestamp in header")
return
}
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body")
@@ -139,12 +125,6 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := ValidateTimeStamp(req.TimeStamp); err != nil {
SendErrorResponse(w, http.StatusBadRequest, err.Error())
postLog.Warning(fmt.Sprintf("[LoginHandler] User \"%s\" Login failed: invalid timestamp \"%d\"", req.Username, req.TimeStamp))
return
}
if !isValidInput(req.Username) || !isValidInput(req.Passwd) { if !isValidInput(req.Username) || !isValidInput(req.Passwd) {
SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters") SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters")
postLog.Debug(fmt.Sprintf("[LoginHandler] Login failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd)) postLog.Debug(fmt.Sprintf("[LoginHandler] Login failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd))
@@ -218,7 +198,7 @@ func SendSuccessResponse(w http.ResponseWriter, message string, data interface{}
w.Write(jsonResp) w.Write(jsonResp)
} }
func ValidateRequest(w http.ResponseWriter, r *http.Request, requiredFields ...string) (int, string, error) { func ValidateRequest(w http.ResponseWriter, r *http.Request, requiredFields ...string) (int, string, error) { // ValidateRequest validates the request body and header
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return 0, "", fmt.Errorf("Failed to read request body: %w", err) return 0, "", fmt.Errorf("Failed to read request body: %w", err)
@@ -234,20 +214,13 @@ func ValidateRequestWithBody(w http.ResponseWriter, r *http.Request, body []byte
return 0, "", fmt.Errorf("Invalid request format: %w", err) return 0, "", fmt.Errorf("Invalid request format: %w", err)
} }
token, ok := reqMap["token"].(string) token := r.Header.Get("X-Token")
if !ok || token == "" { if token == "" {
return 0, "", fmt.Errorf("Token is required: %s", token) return 0, "", fmt.Errorf("Token is required in header: %s", token)
} }
timeStamp := int64(0) if !ValidateTimeStamp(r.Header) {
if ts, ok := reqMap["timeStamp"].(float64); ok { return 0, "", fmt.Errorf("Invalid or missing X-Timestamp in header")
timeStamp = int64(ts)
} else if !globalConfig.Debug {
return 0, "", fmt.Errorf("Timestamp is required: %d", timeStamp)
}
if err := ValidateTimeStamp(timeStamp); err != nil {
return 0, "", fmt.Errorf("Invalid timestamp: %w", err)
} }
userID, err := extractUserIDFromToken(token) userID, err := extractUserIDFromToken(token)
@@ -274,20 +247,8 @@ func ValidateRequestWithHeader(w http.ResponseWriter, r *http.Request, requiredF
return 0, "", fmt.Errorf("Token is required in header: %s", token) return 0, "", fmt.Errorf("Token is required in header: %s", token)
} }
timeStampStr := r.Header.Get("X-Timestamp") if !ValidateTimeStamp(r.Header) {
timeStamp := int64(0) return 0, "", fmt.Errorf("Invalid or missing X-Timestamp in header")
if timeStampStr != "" {
var err error
timeStamp, err = strconv.ParseInt(timeStampStr, 10, 64)
if err != nil {
return 0, "", fmt.Errorf("Invalid timestamp format in header: %w", err)
}
} else if !globalConfig.Debug {
return 0, "", fmt.Errorf("Timestamp is required in header: %s", timeStampStr)
}
if err := ValidateTimeStamp(timeStamp); err != nil {
return 0, "", err
} }
userID, err := extractUserIDFromToken(token) userID, err := extractUserIDFromToken(token)
+6 -6
View File
@@ -16,27 +16,27 @@ func setupRoutes() {
http.HandleFunc("/frpcAct/instanceMgr/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/frpcAct/instanceMgr/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path path := r.URL.Path
if len(path) < len("/frpcAct/instanceMgr/") { if len(path) < len("/frpcAct/instanceMgr/") { // Check if path is at least as long as the base path
SendErrorResponse(w, http.StatusNotFound, "invalid path") SendErrorResponse(w, http.StatusNotFound, "invalid path")
return return
} }
remainingPath := path[len("/frpcAct/instanceMgr/"):] remainingPath := path[len("/frpcAct/instanceMgr/"):] // Get the remaining path after the base path
if r.Method == http.MethodGet { if r.Method == http.MethodGet { // Handle `/list` and `/list/<instanceName>` by GET request
if remainingPath == "list" { if remainingPath == "list" {
ListInstancesHandler(w, r) ListInstancesHandler(w, r)
return return
} }
instanceName := strings.Trim(remainingPath, "/") instanceName := strings.Trim(remainingPath, "/") // Get the instance name from the remaining path
if instanceName != "" { if instanceName != "" {
ListInstancesHandler(w, r) ListInstancesHandler(w, r)
return return
} }
} }
if r.Method == http.MethodPost { if r.Method == http.MethodPost { // Handle `/create`, `/delete`, and `/modify/<field>` by POST request
if remainingPath == "create" { if remainingPath == "create" {
CreateInstanceHandler(w, r) CreateInstanceHandler(w, r)
return return
@@ -57,6 +57,6 @@ func setupRoutes() {
} }
} }
SendErrorResponse(w, http.StatusNotFound, "endpoint not found") SendErrorResponse(w, http.StatusNotFound, "endpoint not found") // Send error response if no endpoint is found
}) })
} }