Files
jiang13-forum/modules/webctx/context.go
freefire fde5f628ec feat: opaque session、安装/发帖 SSR 与最小 Admin 后台
浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings;
落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 05:44:16 +08:00

182 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package webctx
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/modules/auth"
"git.iioio.com/freefire/jiang13-forum/modules/webrender"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
const (
csrfCookie = "jiang13_csrf"
flashCookie = "jiang13_flash"
)
// Context 浏览器请求上下文(对齐 Gitea context 的精简版)
type Context struct {
C *gin.Context
Doer *models.User
Secret string
}
// New 从 Gin 构造;依赖 OptionalAuth / RequireAuth 已写入 user 信息时可再查库
func New(c *gin.Context, secret string) *Context {
ctx := &Context{C: c, Secret: secret}
if id, ok := c.Get(auth.CtxUserID); ok {
if uid, ok := id.(uint); ok && uid > 0 {
var u models.User
if err := models.DB.First(&u, uid).Error; err == nil {
ctx.Doer = &u
}
}
}
return ctx
}
func (ctx *Context) IsSigned() bool { return ctx.Doer != nil }
func (ctx *Context) IsAdmin() bool {
return ctx.Doer != nil && ctx.Doer.Role == models.RoleAdmin
}
func (ctx *Context) UserID() uint {
if ctx.Doer == nil {
return 0
}
return ctx.Doer.ID
}
// SkipsModeration 管理员或认证用户免审
func (ctx *Context) SkipsModeration() bool {
return ctx.Doer != nil && ctx.Doer.SkipsModeration()
}
// HTML 渲染命名模板
func (ctx *Context) HTML(status int, name string, data any) {
ctx.C.Header("Content-Type", "text/html; charset=utf-8")
ctx.C.Status(status)
if err := webrender.Execute(ctx.C.Writer, name, data); err != nil {
ctx.C.String(http.StatusInternalServerError, "模板渲染失败")
}
}
// Redirect 303 见其它 URIPRG
func (ctx *Context) Redirect(url string) {
ctx.C.Redirect(http.StatusSeeOther, url)
}
// SetFlash 一次性提示(下一请求读取)
func (ctx *Context) SetFlash(msg string) {
v := base64.RawURLEncoding.EncodeToString([]byte(msg))
ctx.writeCookie(flashCookie, v, 120, true)
}
// TakeFlash 读取并清除
func (ctx *Context) TakeFlash() string {
v, err := ctx.C.Cookie(flashCookie)
if err != nil || v == "" {
return ""
}
ctx.writeCookie(flashCookie, "", -1, true)
b, err := base64.RawURLEncoding.DecodeString(v)
if err != nil {
return ""
}
return string(b)
}
// EnsureCSRF 保证 CSRF cookie并返回表单 token
func (ctx *Context) EnsureCSRF() string {
if t, err := ctx.C.Cookie(csrfCookie); err == nil && t != "" && ctx.validCSRF(t) {
return t
}
t := ctx.newCSRF()
ctx.writeCookie(csrfCookie, t, int((12 * time.Hour).Seconds()), true)
return t
}
// CheckCSRF 校验表单 _csrf或请求头 X-CSRF-Token供上传 fetch
func (ctx *Context) CheckCSRF() bool {
form := strings.TrimSpace(ctx.C.PostForm("_csrf"))
if form == "" {
form = strings.TrimSpace(ctx.C.GetHeader("X-CSRF-Token"))
}
cookie, _ := ctx.C.Cookie(csrfCookie)
if form == "" || cookie == "" || form != cookie {
return false
}
return ctx.validCSRF(form)
}
func (ctx *Context) newCSRF() string {
ts := strconv.FormatInt(time.Now().Unix(), 10)
mac := hmac.New(sha256.New, []byte(ctx.Secret))
_, _ = mac.Write([]byte(ts))
sig := hex.EncodeToString(mac.Sum(nil))[:32]
return ts + "." + sig
}
func (ctx *Context) validCSRF(token string) bool {
parts := strings.SplitN(token, ".", 2)
if len(parts) != 2 {
return false
}
ts, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return false
}
if time.Since(time.Unix(ts, 0)) > 12*time.Hour {
return false
}
mac := hmac.New(sha256.New, []byte(ctx.Secret))
_, _ = mac.Write([]byte(parts[0]))
sig := hex.EncodeToString(mac.Sum(nil))[:32]
return hmac.Equal([]byte(sig), []byte(parts[1]))
}
// SetLoginCookie 写入 opaque 会话 Cookie
func (ctx *Context) SetLoginCookie(sessionID string) {
auth.SetSessionCookie(ctx.C, sessionID)
}
// ClearLoginCookie 退出并删 Cookie若有 session id 则吊销
func (ctx *Context) ClearLoginCookie() {
if sid, err := ctx.C.Cookie(auth.CookieName); err == nil && sid != "" {
services.DeleteSession(sid)
}
auth.ClearAuthCookie(ctx.C)
}
func (ctx *Context) writeCookie(name, value string, maxAge int, httpOnly bool) {
secure := requestIsHTTPS(ctx.C)
http.SetCookie(ctx.C.Writer, &http.Cookie{
Name: name,
Value: value,
Path: "/",
MaxAge: maxAge,
HttpOnly: httpOnly,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
}
// requestIsHTTPS 直连 TLS 或反代 X-Forwarded-Proto
func requestIsHTTPS(c *gin.Context) bool {
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}
// SafeHTML 供模板使用的类型别名说明(实际转换在 webrender FuncMap
type SafeHTML = template.HTML