fix(server): 安全加固与版本链路修复
- 微信登录改为真实 jscode2session,session_key 不再下发客户端 - 支付回调增加 HMAC 验签(X-Pay-Sign)与幂等处理,未配置密钥时拒绝回调 - 订单金额一律以服务端商品表定价,禁止客户端传入金额 - 付费许愿改为支付成功后创建,不再先许愿后付款 - 管理后台增加登录认证(ADMIN_PASSWORD + role=admin JWT + HttpOnly Cookie) - 订单详情/取消增加本人归属校验,修复越权访问 - 版本信息改为 ldflags 注入单一链路,GoVersion 用 runtime.Version() - 恢复 gin 默认访问日志(原 Logger 中间件输出为空) - 加载 HTML 模板修复后台页面 500;godotenv 加载 .env.local - CORS 支持 CORS_ORIGINS 白名单配置
This commit is contained in:
@@ -1,47 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gouki/lunar-server/internal/config"
|
||||
"github.com/gouki/lunar-server/internal/service"
|
||||
)
|
||||
|
||||
// adminPage 统一的管理后台页面渲染(补充 Inertia 需要的 url 属性)
|
||||
func adminPage(c *gin.Context, title, page string) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": title,
|
||||
"page": page,
|
||||
"url": c.Request.URL.Path,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminLogin 管理员登录:密码正确签发 role=admin 的 JWT
|
||||
func AdminLogin(c *gin.Context) {
|
||||
cfg := config.Load()
|
||||
if cfg.Admin.Password == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"code": 503,
|
||||
"msg": "后台登录未启用",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 恒定时间比较,防时序攻击
|
||||
if subtle.ConstantTimeCompare([]byte(req.Password), []byte(cfg.Admin.Password)) != 1 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"msg": "密码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userService := service.NewUserService()
|
||||
token, err := userService.GenerateAdminToken(cfg.JWT.Secret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "生成token失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 同时写入 HttpOnly Cookie,支持浏览器直接导航后台页面
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie(service.AdminTokenCookie, token, 12*3600, "/admin", "", cfg.Server.Env == "production", true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": gin.H{
|
||||
"token": token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminLogout 管理员登出:清除会话 Cookie
|
||||
func AdminLogout(c *gin.Context) {
|
||||
cfg := config.Load()
|
||||
c.SetCookie(service.AdminTokenCookie, "", -1, "/admin", "", cfg.Server.Env == "production", true)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// AdminDashboard 管理后台首页
|
||||
func AdminDashboard(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "管理后台",
|
||||
"page": "dashboard",
|
||||
})
|
||||
adminPage(c, "管理后台", "dashboard")
|
||||
}
|
||||
|
||||
// AdminUsers 用户管理
|
||||
func AdminUsers(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "用户管理",
|
||||
"page": "users",
|
||||
})
|
||||
adminPage(c, "用户管理", "users")
|
||||
}
|
||||
|
||||
// AdminOrders 订单管理
|
||||
func AdminOrders(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "订单管理",
|
||||
"page": "orders",
|
||||
})
|
||||
adminPage(c, "订单管理", "orders")
|
||||
}
|
||||
|
||||
// AdminWishes 许愿管理
|
||||
func AdminWishes(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "许愿管理",
|
||||
"page": "wishes",
|
||||
})
|
||||
adminPage(c, "许愿管理", "wishes")
|
||||
}
|
||||
|
||||
// AdminSettings 系统设置
|
||||
func AdminSettings(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "系统设置",
|
||||
"page": "settings",
|
||||
})
|
||||
adminPage(c, "系统设置", "settings")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user