76 lines
2.1 KiB
Go
76 lines
2.1 KiB
Go
package csrf
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"git.pcux.cn/xuans/Smail/session"
|
|
)
|
|
|
|
const (
|
|
CookieName = "smail_csrf"
|
|
HeaderName = "X-CSRF-Token"
|
|
SessionKey = "csrf_token"
|
|
)
|
|
|
|
func Issue(w http.ResponseWriter, r *http.Request, secure bool) (string, error) {
|
|
raw := make([]byte, 32)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return "", err
|
|
}
|
|
token := base64.RawURLEncoding.EncodeToString(raw)
|
|
session.Instance.Put(r.Context(), SessionKey, token)
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: CookieName, Value: token, Path: "/", MaxAge: 86400,
|
|
Secure: secure, HttpOnly: false, SameSite: http.SameSiteStrictMode,
|
|
})
|
|
return token, nil
|
|
}
|
|
|
|
// Ensure 为已认证会话恢复双提交 CSRF Cookie。
|
|
// Cookie 缺失或与服务端会话不一致时轮换令牌,避免继续信任过期的浏览器状态。
|
|
func Ensure(w http.ResponseWriter, r *http.Request, secure bool) (string, error) {
|
|
if session.Instance == nil {
|
|
return "", errors.New("session manager is unavailable")
|
|
}
|
|
stored := session.Instance.GetString(r.Context(), SessionKey)
|
|
cookie, err := r.Cookie(CookieName)
|
|
if err == nil && stored != "" && constantTimeEqual(stored, cookie.Value) {
|
|
return stored, nil
|
|
}
|
|
return Issue(w, r, secure)
|
|
}
|
|
|
|
func Clear(w http.ResponseWriter, r *http.Request, secure bool) {
|
|
if session.Instance != nil {
|
|
session.Instance.Remove(r.Context(), SessionKey)
|
|
}
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: CookieName, Value: "", Path: "/", MaxAge: -1,
|
|
Secure: secure, HttpOnly: false, SameSite: http.SameSiteStrictMode,
|
|
})
|
|
}
|
|
|
|
func Valid(r *http.Request) bool {
|
|
if session.Instance == nil {
|
|
return false
|
|
}
|
|
header := r.Header.Get(HeaderName)
|
|
cookie, err := r.Cookie(CookieName)
|
|
if err != nil || header == "" || cookie.Value == "" {
|
|
return false
|
|
}
|
|
stored := session.Instance.GetString(r.Context(), SessionKey)
|
|
if stored == "" {
|
|
return false
|
|
}
|
|
return constantTimeEqual(stored, header) && constantTimeEqual(cookie.Value, header)
|
|
}
|
|
|
|
func constantTimeEqual(left, right string) bool {
|
|
return len(left) == len(right) && subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
|
|
}
|