Enhance user authentication and authentication

Fix the modification of personal information
Fix the formatted page template
This commit is contained in:
2025-10-26 03:05:27 +08:00
parent 3e170ad526
commit c93ee377fe
22 changed files with 2728 additions and 2420 deletions

View File

@@ -55,6 +55,25 @@ func GetDefaultTemplateData() map[string]interface{} {
}
}
// GetTemplateDataWithCSRF 获取包含CSRF令牌的模板数据
// 合并默认数据和CSRF令牌用于需要CSRF保护的页面
func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interface{}) map[string]interface{} {
// 获取默认模板数据
data := GetDefaultTemplateData()
// 添加CSRF令牌
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
// 合并额外数据
if additionalData != nil {
for key, value := range additionalData {
data[key] = value
}
}
return data
}
// GetClientIP 获取客户端IP地址
// 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr
func GetClientIP(r *http.Request) string {

77
utils/cookie.go Normal file
View File

@@ -0,0 +1,77 @@
package utils
import (
"net/http"
"time"
"github.com/spf13/viper"
)
// CreateSecureCookie 创建安全的Cookie
// name: Cookie名称
// value: Cookie值
// maxAge: 过期时间0表示会话Cookie-1表示立即过期
func CreateSecureCookie(name, value string, maxAge int) *http.Cookie {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
HttpOnly: true,
MaxAge: maxAge,
}
// 从配置读取安全设置
if viper.GetBool("security.cookie.secure") {
cookie.Secure = true
}
// 设置SameSite属性
sameSite := viper.GetString("security.cookie.same_site")
switch sameSite {
case "Strict":
cookie.SameSite = http.SameSiteStrictMode
case "Lax":
cookie.SameSite = http.SameSiteLaxMode
case "None":
cookie.SameSite = http.SameSiteNoneMode
// SameSite=None 必须配合 Secure=true 使用
cookie.Secure = true
default:
cookie.SameSite = http.SameSiteStrictMode
}
// 设置Domain如果配置了
domain := viper.GetString("security.cookie.domain")
if domain != "" {
cookie.Domain = domain
}
// 如果maxAge > 0设置Expires时间
if maxAge > 0 {
cookie.Expires = time.Now().Add(time.Duration(maxAge) * time.Second)
} else if maxAge == -1 {
// 立即过期
cookie.Expires = time.Unix(0, 0)
}
return cookie
}
// CreateSessionCookie 创建会话Cookie浏览器关闭时过期
func CreateSessionCookie(name, value string) *http.Cookie {
return CreateSecureCookie(name, value, 0)
}
// CreateExpiredCookie 创建立即过期的Cookie用于清理
func CreateExpiredCookie(name string) *http.Cookie {
return CreateSecureCookie(name, "", -1)
}
// GetDefaultCookieMaxAge 获取默认Cookie过期时间
func GetDefaultCookieMaxAge() int {
maxAge := viper.GetInt("security.cookie.max_age")
if maxAge <= 0 {
return 86400 // 默认24小时
}
return maxAge
}

View File

@@ -291,7 +291,8 @@ func DecryptStringWithSalt(enc, salt string) (string, error) {
}
// HashPasswordWithSalt 使用盐值对密码进行哈希处理
// 将密码和盐值组合后使用bcrypt进行哈希
// 将密码和盐值组合后先用SHA256处理使用bcrypt进行哈希
// 这样可以避免bcrypt的72字节限制问题
// password: 原始密码
// salt: 密码盐值
// 返回: bcrypt哈希值和错误信息
@@ -299,8 +300,12 @@ func HashPasswordWithSalt(password, salt string) (string, error) {
// 将密码和盐值组合
combined := password + salt
// 先使用SHA256处理组合后的字符串确保长度固定且不超过bcrypt限制
hash := sha256.Sum256([]byte(combined))
sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串
// 使用bcrypt进行哈希成本因子10平衡安全性和性能
hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 10)
hashed, err := bcrypt.GenerateFromPassword([]byte(sha256Hash), 10)
if err != nil {
return "", err
}
@@ -317,8 +322,12 @@ func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool {
// 将密码和盐值组合
combined := password + salt
// 先使用SHA256处理组合后的字符串与哈希生成逻辑保持一致
hash := sha256.Sum256([]byte(combined))
sha256Hash := fmt.Sprintf("%x", hash) // 64字节的十六进制字符串
// 使用bcrypt验证
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined))
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(sha256Hash))
return err == nil
}

168
utils/csrf.go Normal file
View File

@@ -0,0 +1,168 @@
package utils
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"
)
const (
CSRFTokenLength = 32
CSRFCookieName = "csrf_token"
CSRFHeaderName = "X-CSRF-Token"
CSRFFormField = "csrf_token"
)
// generateRandomBytes 生成指定长度的随机字节
func generateRandomBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
// GenerateCSRFToken 生成CSRF令牌
func GenerateCSRFToken() (string, error) {
bytes, err := generateRandomBytes(CSRFTokenLength)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// SetCSRFToken 设置CSRF令牌到Cookie和响应头
func SetCSRFToken(w http.ResponseWriter, token string) {
// 设置CSRF令牌到Cookie
cookie := CreateSecureCookie(CSRFCookieName, token, 3600) // 1小时过期
http.SetCookie(w, cookie)
// 设置CSRF令牌到响应头方便JavaScript获取
w.Header().Set("X-CSRF-Token", token)
}
// GetCSRFTokenFromRequest 从请求中获取CSRF令牌
// 优先级Header > Form > Cookie
func GetCSRFTokenFromRequest(r *http.Request) string {
// 1. 从Header获取
if token := r.Header.Get(CSRFHeaderName); token != "" {
return token
}
// 2. 从Form获取
if token := r.FormValue(CSRFFormField); token != "" {
return token
}
// 3. 从Cookie获取作为备选
if cookie, err := r.Cookie(CSRFCookieName); err == nil {
return cookie.Value
}
return ""
}
// GetCSRFTokenFromCookie 从Cookie中获取CSRF令牌
func GetCSRFTokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie(CSRFCookieName)
if err != nil {
return ""
}
return cookie.Value
}
// ValidateCSRFToken 验证CSRF令牌
func ValidateCSRFToken(r *http.Request) bool {
// 获取Cookie中的令牌服务器端存储的
cookieToken := GetCSRFTokenFromCookie(r)
if cookieToken == "" {
return false
}
// 获取请求中的令牌(客户端提交的)
requestToken := GetCSRFTokenFromRequest(r)
if requestToken == "" {
return false
}
// 使用常量时间比较防止时序攻击
return subtle.ConstantTimeCompare([]byte(cookieToken), []byte(requestToken)) == 1
}
// CSRFProtection CSRF保护中间件
func CSRFProtection(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 对于GET、HEAD、OPTIONS请求只生成令牌不验证
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
// 生成新的CSRF令牌
token, err := GenerateCSRFToken()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
SetCSRFToken(w, token)
next(w, r)
return
}
// 对于POST、PUT、DELETE等修改性请求验证CSRF令牌
if !ValidateCSRFToken(r) {
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
return
}
// 验证通过,继续处理请求
next(w, r)
}
}
// RequireCSRFToken 要求CSRF令牌的中间件用于特定路由
func RequireCSRFToken(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !ValidateCSRFToken(r) {
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
return
}
next(w, r)
}
}
// GetCSRFTokenForTemplate 获取用于模板的CSRF令牌
func GetCSRFTokenForTemplate(r *http.Request) string {
// 尝试从Cookie获取现有令牌
if token := GetCSRFTokenFromCookie(r); token != "" {
return token
}
// 如果没有现有令牌,生成新的(但不设置到响应中)
token, err := GenerateCSRFToken()
if err != nil {
return ""
}
return token
}
// CSRFTokenHandler 专门用于获取CSRF令牌的API端点
func CSRFTokenHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
JsonResponse(w, http.StatusMethodNotAllowed, false, "只支持GET请求", nil)
return
}
// 生成新的CSRF令牌
token, err := GenerateCSRFToken()
if err != nil {
JsonResponse(w, http.StatusInternalServerError, false, "生成CSRF令牌失败", nil)
return
}
// 设置令牌到Cookie和响应头
SetCSRFToken(w, token)
// 返回令牌给前端
JsonResponse(w, http.StatusOK, true, "CSRF令牌获取成功", map[string]interface{}{
"csrf_token": token,
})
}