Merge pull request #860 from samwafgo/fix_password

fix(security): bcrypt password hashing + server-side forced password …
This commit is contained in:
samwafgo
2026-07-03 14:52:33 +08:00
committed by GitHub
28 changed files with 271 additions and 62 deletions
+1 -1
View File
@@ -235,7 +235,7 @@ Example: `SamWaf64.exe resetpwd` (on Linux: `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Default account: admin Default password: admin868 (Please change the default password upon first login)
Default account: admin Initial password: fresh installs auto-generate a random password saved to `data/initial_password.txt` (existing installs keep their previous password; please change it upon first login)
## Upgrade Guide
+1 -1
View File
@@ -235,7 +235,7 @@ docker run -d --name=samwaf-instance \
http://127.0.0.1:26666
الحساب الافتراضي: admin كلمة المرور الافتراضية: admin868 (يُرجى تغيير كلمة المرور الافتراضية عند أول تسجيل دخول)
الحساب الافتراضي: admin كلمة المرور الأولية: تُنشئ عمليات التثبيت الجديدة كلمة مرور عشوائية تلقائيًا وتُحفظ في `data/initial_password.txt` (تحتفظ عمليات التثبيت الحالية بكلمة مرورها السابقة؛ يُرجى تغييرها عند أول تسجيل دخول)
## دليل الترقية
+1 -1
View File
@@ -235,7 +235,7 @@ Docker সম্পর্কে আরও বিস্তারিত: https://h
http://127.0.0.1:26666
ডিফল্ট অ্যাকাউন্ট: admin ডিফল্ট পাসওয়ার্ড: admin868 (অনুগ্রহ করে প্রথম লগইনের সময়ই ডিফল্ট পাসওয়ার্ড পরিবর্তন করুন)
ডিফল্ট অ্যাকাউন্ট: admin প্রাথমিক পাসওয়ার্ড: নতুন ইনস্টলেশনে স্বয়ংক্রিয়ভাবে একটি এলোমেলো পাসওয়ার্ড তৈরি হয়ে `data/initial_password.txt` ফাইলে সংরক্ষিত হয় (বিদ্যমান ইনস্টলেশন আগের পাসওয়ার্ড অপরিবর্তিত রাখে; অনুগ্রহ করে প্রথম লগইনেই এটি পরিবর্তন করুন)
## আপগ্রেড গাইড
+1 -1
View File
@@ -236,7 +236,7 @@ docker run -d --name=samwaf-instance \
http://127.0.0.1:26666
默认帐号:admin 默认密码:admin868 (注意首次进入请把默认密码改掉)
默认帐号:admin 初始密码:全新安装会自动生成随机密码并保存到 `data/initial_password.txt`(存量安装沿用原密码;请首次登录后立即修改)
## 升级指南
+1 -1
View File
@@ -235,7 +235,7 @@ Beispiel: `SamWaf64.exe resetpwd` (unter Linux: `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Standardkonto: admin Standardpasswort: admin868 (Bitte ändern Sie das Standardpasswort bei der ersten Anmeldung)
Standardkonto: admin Initialpasswort: Bei Neuinstallationen wird automatisch ein zufälliges Passwort erzeugt und in `data/initial_password.txt` gespeichert (bestehende Installationen behalten ihr bisheriges Passwort; bitte ändern Sie es bei der ersten Anmeldung)
## Upgrade-Anleitung
+1 -1
View File
@@ -235,7 +235,7 @@ Ejemplo: `SamWaf64.exe resetpwd` (en Linux: `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Cuenta predeterminada: admin Contraseña predeterminada: admin868 (cambie la contraseña predeterminada al iniciar sesión por primera vez)
Cuenta predeterminada: admin Contraseña inicial: las instalaciones nuevas generan automáticamente una contraseña aleatoria guardada en `data/initial_password.txt` (las instalaciones existentes conservan su contraseña anterior; cámbiela al iniciar sesión por primera vez)
## Guía de actualización
+1 -1
View File
@@ -235,7 +235,7 @@ Exemple : `SamWaf64.exe resetpwd` (sous Linux : `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Compte par défaut : admin Mot de passe par défaut : admin868 (veuillez modifier le mot de passe par défaut dès la première connexion)
Compte par défaut : admin Mot de passe initial : les nouvelles installations génèrent automatiquement un mot de passe aléatoire enregistré dans `data/initial_password.txt` (les installations existantes conservent leur mot de passe précédent ; veuillez le modifier dès la première connexion)
## Guide de mise à niveau
+1 -1
View File
@@ -235,7 +235,7 @@ Docker के बारे में अधिक विवरण https://hub.do
http://127.0.0.1:26666
डिफ़ॉल्ट अकाउंट: admin डिफ़ॉल्ट पासवर्ड: admin868 (कृपया पहले लॉगिन पर ही डिफ़ॉल्ट पासवर्ड बदल लें)
डिफ़ॉल्ट अकाउंट: admin प्रारंभिक पासवर्ड: नई इंस्टॉलेशन पर स्वतः एक यादृच्छिक पासवर्ड बनकर `data/initial_password.txt` में सहेजा जाता है (मौजूदा इंस्टॉलेशन अपना पिछला पासवर्ड बनाए रखती हैं; कृपया पहले लॉगिन पर ही इसे बदल लें)
## अपग्रेड गाइड
+1 -1
View File
@@ -235,7 +235,7 @@ Contoh: `SamWaf64.exe resetpwd` (di Linux: `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Akun default: admin Kata sandi default: admin868 (Harap ubah kata sandi default saat login pertama kali)
Akun default: admin Kata sandi awal: instalasi baru otomatis membuat kata sandi acak yang disimpan di `data/initial_password.txt` (instalasi yang sudah ada tetap memakai kata sandi sebelumnya; harap ubah saat login pertama kali)
## Panduan Upgrade
+1 -1
View File
@@ -235,7 +235,7 @@ Docker の詳細はこちら https://hub.docker.com/r/samwaf/samwaf
http://127.0.0.1:26666
デフォルトアカウント: admin デフォルトパスワード: admin868(初回ログイン時に必ずデフォルトパスワードを変更してください)
デフォルトアカウント: admin 初期パスワード: 新規インストールではランダムなパスワードが自動生成され `data/initial_password.txt` に保存されます(既存インストールは以前のパスワードを維持します。初回ログイン時に必ず変更してください)
## アップグレードガイド
+1 -1
View File
@@ -235,7 +235,7 @@ Docker 상세 정보: https://hub.docker.com/r/samwaf/samwaf
http://127.0.0.1:26666
기본 계정: admin 기 비밀번호: admin868 (첫 로그인 시 기본 비밀번호를 변경하시기 바랍니다)
기본 계정: admin 기 비밀번호: 새로 설치하면 임의의 비밀번호가 자동 생성되어 `data/initial_password.txt` 파일에 저장됩니다 (기존 설치는 이전 비밀번호를 유지합니다; 첫 로그인 시 즉시 변경하세요)
## 업그레이드 가이드
+1 -1
View File
@@ -235,7 +235,7 @@ Exemplo: `SamWaf64.exe resetpwd` (no Linux: `./SamWafLinux64 resetpwd`)
http://127.0.0.1:26666
Conta padrão: admin Senha padrão: admin868 (Altere a senha padrão no primeiro login)
Conta padrão: admin Senha inicial: instalações novas geram automaticamente uma senha aleatória salva em `data/initial_password.txt` (instalações existentes mantêm a senha anterior; altere-a no primeiro login)
## Guia de Atualização
+1 -1
View File
@@ -235,7 +235,7 @@ docker run -d --name=samwaf-instance \
http://127.0.0.1:26666
Учётная запись по умолчанию: admin Пароль по умолчанию: admin868 (пожалуйста, смените пароль по умолчанию при первом входе)
Учётная запись по умолчанию: admin Начальный пароль: при новой установке автоматически создаётся случайный пароль, сохраняемый в `data/initial_password.txt` (существующие установки сохраняют прежний пароль; смените его при первом входе)
## Руководство по обновлению
+1 -1
View File
@@ -235,7 +235,7 @@ Docker کی مزید تفصیلات: https://hub.docker.com/r/samwaf/samwaf
http://127.0.0.1:26666
ڈیفالٹ اکاؤنٹ: admin ڈیفالٹ پاس ورڈ: admin868 (براہِ کرم پہلی بار لاگ اِن کرتے ہی ڈیفالٹ پاس ورڈ تبدیل کر لیں)
ڈیفالٹ اکاؤنٹ: admin ابتدائی پاس ورڈ: نئی تنصیب پر خودکار طور پر ایک بے ترتیب پاس ورڈ بن کر `data/initial_password.txt` میں محفوظ ہو جاتا ہے (موجودہ تنصیبات اپنا سابقہ پاس ورڈ برقرار رکھتی ہیں؛ براہِ کرم پہلی بار لاگ اِن کرتے ہی اسے تبدیل کریں)
## اپ گریڈ گائیڈ
+27
View File
@@ -7,6 +7,7 @@ import (
"SamWaf/model/common/response"
"SamWaf/model/request"
"errors"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -148,9 +149,35 @@ func (w *WafAccountApi) ChangeMyPasswordApi(c *gin.Context) {
response.FailWithMessage(err.Error(), c)
return
}
// 改密成功后清除当前令牌缓存里的强制改密标记,使其可正常访问其他接口(无需重新登录)
clearTokenNeedChangePassword(c)
response.OkWithMessage("修改密码成功", c)
}
// clearTokenNeedChangePassword 改密成功后,把当前令牌缓存里的强制改密标记清零。
func clearTokenNeedChangePassword(c *gin.Context) {
tokenStr := c.GetHeader("X-Token")
if tokenStr == "" && c.GetHeader("X-Login-Type") == "mobile" {
tokenStr = c.GetHeader("X-Mobile-Token")
}
if tokenStr == "" {
return
}
key := enums.CACHE_TOKEN + tokenStr
var tokenInfo model.TokenInfo
if err := global.GCACHE_WAFCACHE.GetAs(key, &tokenInfo); err != nil {
return
}
tokenInfo.NeedChangePassword = 0
if expireTime, err := global.GCACHE_WAFCACHE.GetExpireTime(key); err == nil {
if remain := time.Until(expireTime); remain > 0 {
global.GCACHE_WAFCACHE.SetWithTTl(key, tokenInfo, remain)
return
}
}
global.GCACHE_WAFCACHE.SetWithTTl(key, tokenInfo, time.Duration(global.GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES)*time.Minute)
}
func (w *WafAccountApi) ResetAccountPwdApi(c *gin.Context) {
var req request.WafAccountResetPwdReq
err := c.ShouldBindJSON(&req)
+24 -17
View File
@@ -31,10 +31,7 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
response.FailWithMessage("帐号为空", c)
return
}
accountCount, _ := wafAccountService.GetAccountCountApi()
if accountCount == 0 {
wafAccountService.InitDefaultAccount()
}
// 默认管理员由程序启动初始化时按"账户表为空"判定并创建(见 cmd/samwaf/main.go),此处不再惰性创建
bean := wafAccountService.GetInfoByLoginApi(req)
if bean.Id != "" {
clientIP := utils.GetManageClientIP(c)
@@ -57,8 +54,8 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
return
}
// 校验密码是否正确
if bean.LoginPassword != utils.Md5String(req.LoginPassword+global.GWAF_DEFAULT_ACCOUNT_SALT) {
// 校验密码是否正确bcrypt,兼容存量 MD5
if !wafAccountService.VerifyPassword(bean.LoginPassword, req.LoginPassword) {
// 密码错误,增加错误计数
hitCounter++
global.GCACHE_WAFCACHE.SetWithTTl(cacheKey, hitCounter, time.Duration(global.GCONFIG_RECORD_LOGIN_LIMIT_MINTUTES)*time.Minute)
@@ -108,6 +105,22 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
// 密码正确,清除错误计数
global.GCACHE_WAFCACHE.Remove(cacheKey)
// 透明升级:命中旧 MD5 的口令,登录成功后重算为 bcrypt 落库
if !utils.IsBcryptHash(bean.LoginPassword) {
wafAccountService.UpgradePasswordHash(bean.LoginAccount, req.LoginPassword)
}
// 口令强制改密判定:首次登录/被重置(NeedChangePassword) 或 口令已过有效期(提前计算,用于写入令牌与响应)
needChangePwd := false
changePwdReason := ""
if bean.NeedChangePassword == 1 {
needChangePwd = true
changePwdReason = "首次登录或密码已被重置,请立即修改密码"
} else if utils.IsPasswordExpired(bean.PwdUpdateTime, int(global.GCONFIG_PWD_EXPIRE_DAYS), time.Now()) {
needChangePwd = true
changePwdReason = "密码已超过有效期,请修改密码"
}
// 获取登录类型
loginType := c.GetHeader("X-Login-Type")
if loginType == "" {
@@ -136,6 +149,11 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
accessToken := utils.Md5String(uuid.GenUUID())
tokenInfo := wafTokenInfoService.AddApiWithFingerprintAndType(bean.LoginAccount, accessToken, utils.GetManageClientIP(c), deviceFingerprint, loginType, bean.Role)
// 强制改密标记随令牌下发,供鉴权中间件拦截未改密令牌(缓存副本即可,Auth 从缓存读取)
if needChangePwd {
tokenInfo.NeedChangePassword = 1
}
//令牌记录到cache里
global.GCACHE_WAFCACHE.SetWithTTl(enums.CACHE_TOKEN+accessToken, *tokenInfo, time.Duration(global.GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES)*time.Minute)
@@ -173,17 +191,6 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
}
global.GQEQUE_LOG_DB.Enqueue(&wafSysLog)
// 口令强制改密判定:首次登录/被重置(NeedChangePassword) 或 口令已过有效期
needChangePwd := false
changePwdReason := ""
if bean.NeedChangePassword == 1 {
needChangePwd = true
changePwdReason = "首次登录或密码已被重置,请立即修改密码"
} else if utils.IsPasswordExpired(bean.PwdUpdateTime, int(global.GCONFIG_PWD_EXPIRE_DAYS), time.Now()) {
needChangePwd = true
changePwdReason = "密码已超过有效期,请修改密码"
}
response.OkWithDetailed(response2.LoginRep{
AccessToken: tokenInfo.AccessToken,
NeedChangePassword: needChangePwd,
+20
View File
@@ -13,6 +13,7 @@ import (
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"SamWaf/plugin"
"SamWaf/service/waf_service"
"SamWaf/supervisor"
"SamWaf/utils"
"SamWaf/wafai"
@@ -390,6 +391,17 @@ func (m *wafSystenService) run() {
os.Exit(1)
}
// 全新安装引导:账户表为空(新用户判定)时创建默认管理员并生成随机初始口令。
// 放在启动初始化(核心库就绪后)执行,不再等首次登录才触发,便于新装即时拿到 data/initial_password.txt。
isFreshInstall := false
if cnt, _ := waf_service.WafAccountServiceApp.GetAccountCountApi(); cnt == 0 {
if err := waf_service.WafAccountServiceApp.InitDefaultAccount(); err != nil {
zlog.Error("初始化默认管理员账户失败", "error", err)
} else {
isFreshInstall = true
}
}
//初始化插件系统(从配置文件加载)
if err := plugin.InitPluginSystem(); err != nil {
zlog.Error("初始化插件系统失败", "error", err)
@@ -575,6 +587,14 @@ func (m *wafSystenService) run() {
} else {
zlog.Info("SamWaf has started successfully.You can open http://127.0.0.1:" + strconv.Itoa(global.GWAF_LOCAL_SERVER_PORT) + " in your Browser")
}
// 仅全新安装(本次启动创建了默认管理员)时提示随机初始口令位置;按系统语言环境选择中/英文(默认英文,见 utils.IsChineseEnv 跨平台实现)
if isFreshInstall {
if utils.IsChineseEnv() {
zlog.Info("【全新安装】已生成随机初始管理员口令,请查看当前目录文件 data/initial_password.txt ,并在首次登录后立即修改密码")
} else {
zlog.Info("[Fresh Install] A random initial admin password has been generated, please see file: data/initial_password.txt and change it right after first login")
}
}
for {
select {
case msg := <-global.GWAF_CHAN_MSG:
+2 -2
View File
@@ -225,8 +225,8 @@ var (
//默认创建的账户和密码
GWAF_DEFAULT_ACCOUNT string = "admin" //默认创建的账户
GWAF_DEFAULT_ACCOUNT_PWD string = "admin868" //默认创建的密码
GWAF_DEFAULT_ACCOUNT_SALT string = "%@311*abDop#*" //盐值
GWAF_DEFAULT_ACCOUNT_PWD string = "admin868" //历史默认口令:新装已改为随机口令(见 InitDefaultAccount),此值仅作随机生成失败兜底与存量识别
GWAF_DEFAULT_ACCOUNT_SALT string = "%@311*abDop#*" //盐值(仅用于校验/识别存量 MD5,bcrypt 迁移后新账户不再使用)
//通讯加密
GWAF_COMMUNICATION_KEY = []byte("7E@u*has$d*@s5YX") //通讯加密密钥
+3 -1
View File
@@ -133,7 +133,9 @@ function show_info() {
[ "$ipv4_address" ] && echo -e " 🌐 外网管理地址: http://${ipv4_address}:26666"
[ "$local_ip" ] && echo -e " 🏠 内网管理地址: http://${local_ip}:26666"
echo -e " 👤 默认用户名: admin"
echo -e " 🔑 默认密码: admin123"
echo -e " 🔑 初始密码: v1.3.21-beta.8 之后的版本为随机生成,请查看文件 ${setup_path}/data/initial_password.txt"
echo -e " (v1.3.21-beta.8 及更早版本默认密码为 admin868)"
echo -e " ⚠️ 首次登录后请立即修改密码"
echo -e ""
echo -e " 📁 安装目录: ${setup_path}"
echo -e " 🔧 服务管理: cd ${setup_path} && ./${exec_name} [start|stop]"
+10
View File
@@ -147,6 +147,16 @@ func Auth() gin.HandlerFunc {
}
}
//强制改密门:需改密的令牌只放行改密/注销/ws,其余一律拦截(默认口令/被重置账号在改密前拿不到其他权限)
if tokenInfo.NeedChangePassword == 1 {
p := c.Request.URL.Path
if p != "/api/v1/account/changemypwd" && p != "/api/v1/logout" && p != "/api/v1/ws" {
response.NeedChangePwdWithMessage("请先修改初始/重置密码后再进行其他操作", c)
c.Abort()
return
}
}
//检测是否强制2Fa绑定
if global.GCONFIG_RECORD_FORCE_BIND_2FA == 1 && c.Request.RequestURI != "/api/v1/ws" && c.Request.RequestURI != "/api/v1/logout" {
otpBean := wafOtpService.GetDetailByUserNameApi(tokenInfo.LoginAccount)
+7 -6
View File
@@ -23,10 +23,11 @@ type AccountLog struct {
}
type TokenInfo struct {
baseorm.BaseOrm
LoginAccount string `gorm:"size:100" json:"login_account"` //登录账号
LoginIp string `gorm:"size:64" json:"login_ip"` //登录IP
AccessToken string `gorm:"type:text" json:"access_token" crypto:"aes"` //访问码
DeviceFingerprint string `gorm:"size:255" json:"device_fingerprint"` //设备指纹
LoginType string `gorm:"size:50" json:"login_type"` //登录类型 web/mobile
Role string `gorm:"size:50" json:"role"` //登录角色(冗余自Account,便于鉴权中间件快速判定)
LoginAccount string `gorm:"size:100" json:"login_account"` //登录账号
LoginIp string `gorm:"size:64" json:"login_ip"` //登录IP
AccessToken string `gorm:"type:text" json:"access_token" crypto:"aes"` //访问码
DeviceFingerprint string `gorm:"size:255" json:"device_fingerprint"` //设备指纹
LoginType string `gorm:"size:50" json:"login_type"` //登录类型 web/mobile
Role string `gorm:"size:50" json:"role"` //登录角色(冗余自Account,便于鉴权中间件快速判定)
NeedChangePassword int `gorm:"-" json:"need_change_password"` //是否需强制改密:随令牌缓存下发,Auth 中间件据此拦截未改密令牌;不落库(gorm:"-")
}
+6
View File
@@ -19,6 +19,7 @@ const (
SUCCESS = 0
INPUT_SECRET_CODE = -2
NEED_BIND_2FA = -3
NEED_CHANGE_PWD = -4
FORBIDDEN = -403
AUTHFAIL = -999
)
@@ -84,3 +85,8 @@ func SecretCodeFailWithMessage(message string, c *gin.Context) {
func NeedBind2FAWithMessage(message string, c *gin.Context) {
Result(NEED_BIND_2FA, map[string]interface{}{}, message, c)
}
// NeedChangePwdWithMessage 令牌需强制改密:前端据此码引导用户先改密后再操作
func NeedChangePwdWithMessage(message string, c *gin.Context) {
Result(NEED_CHANGE_PWD, map[string]interface{}{}, message, c)
}
+75 -19
View File
@@ -2,6 +2,7 @@ package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/common/zlog"
"SamWaf/customtype"
"SamWaf/enums"
"SamWaf/global"
@@ -11,6 +12,8 @@ import (
"SamWaf/utils"
"errors"
"fmt"
"os"
"path/filepath"
"time"
)
@@ -20,9 +23,27 @@ type WafAccountService struct{}
var WafAccountServiceApp = new(WafAccountService)
// pwdHash 计算口令指纹(md5+盐)
func (receiver *WafAccountService) pwdHash(plain string) string {
return utils.Md5String(plain + global.GWAF_DEFAULT_ACCOUNT_SALT)
// pwdHash 计算口令哈希(bcrypt,内含随机 per-password 盐)
func (receiver *WafAccountService) pwdHash(plain string) (string, error) {
return utils.BcryptHash(plain)
}
// VerifyPassword 校验明文口令是否匹配存储哈希:bcrypt 走 bcrypt;存量裸 MD5 走旧算法(透明升级期间兼容)。
func (receiver *WafAccountService) VerifyPassword(stored, plain string) bool {
if utils.IsBcryptHash(stored) {
return utils.BcryptVerify(stored, plain)
}
return stored != "" && stored == utils.Md5String(plain+global.GWAF_DEFAULT_ACCOUNT_SALT)
}
// UpgradePasswordHash 透明升级:命中旧 MD5 的账户,登录成功后用 bcrypt 重算落库
// (同一口令,不改 PwdUpdateTime、不记历史)。
func (receiver *WafAccountService) UpgradePasswordHash(loginAccount, plain string) {
newHash, err := receiver.pwdHash(plain)
if err != nil || newHash == "" {
return
}
global.GWAF_LOCAL_DB.Model(model.Account{}).Where("login_account = ?", loginAccount).Update("LoginPassword", newHash)
}
// ValidateNewPassword 校验新口令:复杂度 + 历史防重用
@@ -32,15 +53,13 @@ func (receiver *WafAccountService) ValidateNewPassword(loginAccount, newPlain st
}
historyCount := int(global.GCONFIG_PWD_HISTORY_COUNT)
if historyCount > 0 {
newHash := receiver.pwdHash(newPlain)
var histories []model.AccountPwdHistory
global.GWAF_LOCAL_DB.Where("login_account = ?", loginAccount).Order("CREATE_TIME desc").Limit(historyCount).Find(&histories)
hashes := make([]string, 0, len(histories))
for _, h := range histories {
hashes = append(hashes, h.PasswordHash)
}
if utils.IsPasswordReused(newHash, hashes) {
return fmt.Errorf("新密码不能与最近 %d 次使用过的密码相同", historyCount)
// bcrypt 非确定性,不能按等值比较;逐条校验明文是否命中历史哈希(同时兼容存量 MD5)
if receiver.VerifyPassword(h.PasswordHash, newPlain) {
return fmt.Errorf("新密码不能与最近 %d 次使用过的密码相同", historyCount)
}
}
}
return nil
@@ -83,14 +102,45 @@ func (receiver *WafAccountService) AddApi(req request.WafAccountAddReq) error {
return receiver.createAccount(req.LoginAccount, req.Role, req.LoginPassword, req.Status, req.Remarks, global.GCONFIG_PWD_FORCE_CHANGE_DEFAULT == 1)
}
// InitDefaultAccount 系统引导创建默认管理员(不做复杂度校验,避免严格策略下引导被锁死)
// InitDefaultAccount 系统引导创建默认管理员(全新安装)。
// 安全加固:不再使用固定默认口令 admin868,改为生成强随机初始口令,写入 data/ 供用户查看,并强制首登改密。
func (receiver *WafAccountService) InitDefaultAccount() error {
return receiver.createAccount(global.GWAF_DEFAULT_ACCOUNT, enums.ROLE_SUPER_ADMIN, global.GWAF_DEFAULT_ACCOUNT_PWD, 0, "密码生成", global.GCONFIG_PWD_FORCE_CHANGE_DEFAULT == 1)
plainPwd, err := utils.GenerateRandomPassword(16)
if err != nil || plainPwd == "" {
// 极端兜底:随机生成失败时退回旧默认口令,但仍强制改密
plainPwd = global.GWAF_DEFAULT_ACCOUNT_PWD
}
// 强制首登改密(needChange=true),不做复杂度校验避免严格策略下引导被锁死
if e := receiver.createAccount(global.GWAF_DEFAULT_ACCOUNT, enums.ROLE_SUPER_ADMIN, plainPwd, 0, "系统初始化生成", true); e != nil {
return e
}
receiver.writeInitialPasswordFile(global.GWAF_DEFAULT_ACCOUNT, plainPwd)
return nil
}
// writeInitialPasswordFile 全新安装时把随机初始口令写入 data/ 供用户查看,并打印日志提示。
func (receiver *WafAccountService) writeInitialPasswordFile(account, plainPwd string) {
dir := "data"
if err := os.MkdirAll(dir, 0700); err != nil {
zlog.Error("创建 data 目录失败", err)
return
}
fp := filepath.Join(dir, "initial_password.txt")
content := fmt.Sprintf("SamWaf 初始管理员账号: %s\n初始随机口令: %s\n生成时间: %s\n请立即登录并修改密码,登录后本文件可删除。\n",
account, plainPwd, time.Now().Format(pwdTimeLayout))
if err := os.WriteFile(fp, []byte(content), 0600); err != nil {
zlog.Error("写入初始口令文件失败", err)
return
}
zlog.Info(fmt.Sprintf("首次安装已生成随机管理员初始口令,请查看文件: %s 并在登录后立即修改", fp))
}
// createAccount 内部统一建账逻辑
func (receiver *WafAccountService) createAccount(loginAccount, role, plainPwd string, status int, remarks string, needChange bool) error {
hash := receiver.pwdHash(plainPwd)
hash, err := receiver.pwdHash(plainPwd)
if err != nil {
return err
}
needChangeFlag := 0
if needChange {
needChangeFlag = 1
@@ -150,16 +200,19 @@ func (receiver *WafAccountService) ChangeMyPasswordApi(loginAccount, oldPlain, n
if err != nil {
return errors.New("帐号信息不存在")
}
if bean.LoginPassword != receiver.pwdHash(oldPlain) {
if !receiver.VerifyPassword(bean.LoginPassword, oldPlain) {
return errors.New("旧密码不正确")
}
if bean.LoginPassword == receiver.pwdHash(newPlain) {
if receiver.VerifyPassword(bean.LoginPassword, newPlain) {
return errors.New("新旧密码相同")
}
if err = receiver.ValidateNewPassword(loginAccount, newPlain); err != nil {
return err
}
newHash := receiver.pwdHash(newPlain)
newHash, err := receiver.pwdHash(newPlain)
if err != nil {
return err
}
beanMap := map[string]interface{}{
"LoginPassword": newHash,
"NeedChangePassword": 0,
@@ -176,7 +229,7 @@ func (receiver *WafAccountService) ResetPwdApi(req request.WafAccountResetPwdReq
var superAccount model.Account
global.GWAF_LOCAL_DB.Where("`role` = ?", enums.ROLE_SUPER_ADMIN).First(&superAccount)
if superAccount.LoginPassword != receiver.pwdHash(req.LoginSuperPassword) {
if !receiver.VerifyPassword(superAccount.LoginPassword, req.LoginSuperPassword) {
return errors.New("超级管理员密码不正确")
}
@@ -186,7 +239,7 @@ func (receiver *WafAccountService) ResetPwdApi(req request.WafAccountResetPwdReq
return errors.New("帐号信息不存在")
}
if bean.LoginPassword == receiver.pwdHash(req.LoginNewPassword) {
if receiver.VerifyPassword(bean.LoginPassword, req.LoginNewPassword) {
return errors.New("新旧密码相同")
}
@@ -195,14 +248,17 @@ func (receiver *WafAccountService) ResetPwdApi(req request.WafAccountResetPwdReq
return err
}
newHash := receiver.pwdHash(req.LoginNewPassword)
newHash, err := receiver.pwdHash(req.LoginNewPassword)
if err != nil {
return err
}
beanMap := map[string]interface{}{
"LoginPassword": newHash,
"NeedChangePassword": 1, // 管理员重置后,目标账号下次登录需强制改密
"PwdUpdateTime": time.Now().Format(pwdTimeLayout),
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
err := global.GWAF_LOCAL_DB.Model(model.Account{}).Where("id = ?", bean.Id).Updates(beanMap).Error
err = global.GWAF_LOCAL_DB.Model(model.Account{}).Where("id = ?", bean.Id).Updates(beanMap).Error
if err != nil {
return err
}
+22
View File
@@ -8,6 +8,8 @@ import (
"io"
"math/big"
"strings"
"golang.org/x/crypto/bcrypt"
)
func Md5String(str string) string {
@@ -17,6 +19,26 @@ func Md5String(str string) string {
return hex.EncodeToString(sum[:])
}
// BcryptHash 用 bcrypt 计算口令哈希(内含随机 per-password 盐,无需单独盐列)。
// bcrypt 仅取口令前 72 字节,过长口令返回错误由上层提示。
func BcryptHash(plain string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(b), nil
}
// BcryptVerify 校验明文口令是否匹配 bcrypt 哈希。
func BcryptVerify(hash, plain string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
}
// IsBcryptHash 判断是否为 bcrypt 哈希串($2a$/$2b$/$2y$ 前缀),用于区分存量 MD5 与新 bcrypt。
func IsBcryptHash(s string) bool {
return strings.HasPrefix(s, "$2a$") || strings.HasPrefix(s, "$2b$") || strings.HasPrefix(s, "$2y$")
}
// 生成指定长度的密码
func GenerateRandomPassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:',.<>?/`~"
+23
View File
@@ -0,0 +1,23 @@
package utils
import (
"os"
"strings"
)
// IsChineseEnv 判断当前运行环境是否为中文环境(跨平台,默认英文)。
//
// 判定顺序:
// 1. 优先读取 POSIX 语言环境变量 LC_ALL/LC_MESSAGES/LANG/LANGUAGELinux/macOS 常见),含 "zh" 即视为中文;
// 2. 上述变量为空时(常见于 Windows 服务进程),回退到操作系统级语言检测(见各平台 isSystemChinese 实现);
// 3. 均无法判定时返回 false(英文)。
func IsChineseEnv() bool {
envLang := strings.ToLower(
os.Getenv("LC_ALL") + os.Getenv("LC_MESSAGES") + os.Getenv("LANG") + os.Getenv("LANGUAGE"),
)
if envLang != "" {
return strings.Contains(envLang, "zh")
}
// 环境变量为空时按操作系统语言判定(如 Windows 通过系统 API
return isSystemChinese()
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !windows
package utils
// isSystemChinese 非 Windows 平台在语言环境变量缺失时不做额外系统级判定,默认英文。
// Linux/macOS 上语言环境通常已由 LANG/LC_* 提供,见 IsChineseEnv。)
func isSystemChinese() bool {
return false
}
+16
View File
@@ -0,0 +1,16 @@
//go:build windows
package utils
import "syscall"
// isSystemChinese 在语言环境变量缺失时,通过 Windows API 取用户默认 UI 语言判定是否为中文。
// GetUserDefaultUILanguage 返回 LANGID,主语言标识 LANG_CHINESE=0x04 即视为中文。
func isSystemChinese() bool {
proc := syscall.NewLazyDLL("kernel32.dll").NewProc("GetUserDefaultUILanguage")
ret, _, _ := proc.Call()
langID := uint16(ret)
const langChinese = 0x04 // LANG_CHINESE
// PRIMARYLANGID(langID) = langID & 0x3ff
return (langID & 0x3ff) == langChinese
}
+13 -3
View File
@@ -67,10 +67,20 @@ func ResetAdminPwd() {
return
}
randomPassword, _ := utils.GenerateRandomPassword(12)
randomPassword, gerr := utils.GenerateRandomPassword(12)
if gerr != nil || randomPassword == "" {
fmt.Printf("Failed to generate new password: %v\n", gerr)
return
}
hash, herr := utils.BcryptHash(randomPassword)
if herr != nil {
fmt.Printf("Failed to hash new password: %v\n", herr)
return
}
beanMap := map[string]interface{}{
"LoginPassword": utils.Md5String(randomPassword + global.GWAF_DEFAULT_ACCOUNT_SALT),
"UPDATE_TIME": customtype.JsonTime(time.Now()),
"LoginPassword": hash,
"NeedChangePassword": 1, // 控制台重置后,目标账号下次登录需强制改密
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
err := global.GWAF_LOCAL_DB.Model(model.Account{}).Where("login_account = ?", chosenAccount).Updates(beanMap).Error
if err != nil {