mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Enhance user authentication and authentication
Fix the modification of personal information Fix the formatted page template
This commit is contained in:
@@ -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
77
utils/cookie.go
Normal 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
|
||||
}
|
||||
@@ -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
168
utils/csrf.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user