Compare commits
3 Commits
1d9b568bef
...
83f3c6e11f
| Author | SHA1 | Date | |
|---|---|---|---|
| 83f3c6e11f | |||
| 8911c6708a | |||
| c09b51ba02 |
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/main .
|
||||
COPY config.yaml ./
|
||||
EXPOSE 8080
|
||||
CMD ["./main"]
|
||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: training_db
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
3
go.mod
3
go.mod
@ -4,7 +4,8 @@ go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/spf13/viper v1.20.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
|
||||
6
go.sum
6
go.sum
@ -31,6 +31,10 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@ -58,8 +62,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
84
middleware/auth.go
Normal file
84
middleware/auth.go
Normal file
@ -0,0 +1,84 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiSecret = "your-super-secret-key" // 预共享密钥
|
||||
TokenExp = 1 * time.Hour // Token有效期
|
||||
)
|
||||
|
||||
type JWTService struct {
|
||||
secretKey []byte
|
||||
expiresIn time.Duration
|
||||
}
|
||||
|
||||
func NewJWTService(secret string, expiresIn time.Duration) *JWTService {
|
||||
return &JWTService{
|
||||
secretKey: []byte(secret),
|
||||
expiresIn: expiresIn,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成带HMAC签名的Token
|
||||
func (s *JWTService) GenerateToken() (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"exp": time.Now().Add(s.expiresIn).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.secretKey)
|
||||
}
|
||||
|
||||
// 验证HMAC签名的Token
|
||||
func (s *JWTService) ValidateToken(tokenString string) (jwt.Claims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return s.secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
|
||||
// 鉴权中间件
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte(ApiSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ type HeartRate struct {
|
||||
BeltAddr string `gorm:"column:belt_addr;size:255" json:"beltAddr"` // 对应Dart的beltAddr字段
|
||||
Time int64 `gorm:"type:bigint" json:"time"` // 保持与前端一致的毫秒时间戳[3](@ref)
|
||||
Value int `gorm:"type:int" json:"value"`
|
||||
PredictValue int `gorm:"type:int" json:"predictValue"`
|
||||
Identifier string `gorm:"uniqueIndex;type:varchar(255)" json:"identifier"`
|
||||
}
|
||||
|
||||
|
||||
@ -4,20 +4,41 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"hr_receiver/controllers"
|
||||
"hr_receiver/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
jwtService := middleware.NewJWTService(middleware.ApiSecret, middleware.TokenExp)
|
||||
r := gin.Default()
|
||||
r.Use(middleware.GzipMiddleware())
|
||||
trainingController := controllers.NewTrainingController()
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
records := v1.Group("/train-records")
|
||||
records := v1.Group("/train-records").Use(middleware.AuthMiddleware())
|
||||
{
|
||||
records.POST("", trainingController.CreateTrainingRecord)
|
||||
// 可扩展其他路由:GET, PUT, DELETE等
|
||||
}
|
||||
auth := v1.Group("/auth")
|
||||
{
|
||||
auth.GET("/token", func(c *gin.Context) {
|
||||
|
||||
clientSecret := c.GetHeader("X-API-Key")
|
||||
if clientSecret != middleware.ApiSecret {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid secret"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwtService.GenerateToken()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
})
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user