mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-08-31 01:41:39 +08:00
Merge pull request #860 from samwafgo/fix_password
fix(security): bcrypt password hashing + server-side forced password …
This commit is contained in:
@@ -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
@@ -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
@@ -235,7 +235,7 @@ Docker সম্পর্কে আরও বিস্তারিত: https://h
|
||||
|
||||
http://127.0.0.1:26666
|
||||
|
||||
ডিফল্ট অ্যাকাউন্ট: admin ডিফল্ট পাসওয়ার্ড: admin868 (অনুগ্রহ করে প্রথম লগইনের সময়ই ডিফল্ট পাসওয়ার্ড পরিবর্তন করুন)
|
||||
ডিফল্ট অ্যাকাউন্ট: admin প্রাথমিক পাসওয়ার্ড: নতুন ইনস্টলেশনে স্বয়ংক্রিয়ভাবে একটি এলোমেলো পাসওয়ার্ড তৈরি হয়ে `data/initial_password.txt` ফাইলে সংরক্ষিত হয় (বিদ্যমান ইনস্টলেশন আগের পাসওয়ার্ড অপরিবর্তিত রাখে; অনুগ্রহ করে প্রথম লগইনেই এটি পরিবর্তন করুন)
|
||||
|
||||
|
||||
## আপগ্রেড গাইড
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -235,7 +235,7 @@ Docker के बारे में अधिक विवरण https://hub.do
|
||||
|
||||
http://127.0.0.1:26666
|
||||
|
||||
डिफ़ॉल्ट अकाउंट: admin डिफ़ॉल्ट पासवर्ड: admin868 (कृपया पहले लॉगिन पर ही डिफ़ॉल्ट पासवर्ड बदल लें)
|
||||
डिफ़ॉल्ट अकाउंट: admin प्रारंभिक पासवर्ड: नई इंस्टॉलेशन पर स्वतः एक यादृच्छिक पासवर्ड बनकर `data/initial_password.txt` में सहेजा जाता है (मौजूदा इंस्टॉलेशन अपना पिछला पासवर्ड बनाए रखती हैं; कृपया पहले लॉगिन पर ही इसे बदल लें)
|
||||
|
||||
|
||||
## अपग्रेड गाइड
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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` میں محفوظ ہو جاتا ہے (موجودہ تنصیبات اپنا سابقہ پاس ورڈ برقرار رکھتی ہیں؛ براہِ کرم پہلی بار لاگ اِن کرتے ہی اسے تبدیل کریں)
|
||||
|
||||
|
||||
## اپ گریڈ گائیڈ
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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]"
|
||||
|
||||
@@ -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
@@ -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:"-")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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!@#$%^&*()-_=+[]{}|;:',.<>?/`~"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsChineseEnv 判断当前运行环境是否为中文环境(跨平台,默认英文)。
|
||||
//
|
||||
// 判定顺序:
|
||||
// 1. 优先读取 POSIX 语言环境变量 LC_ALL/LC_MESSAGES/LANG/LANGUAGE(Linux/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()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package utils
|
||||
|
||||
// isSystemChinese 非 Windows 平台在语言环境变量缺失时不做额外系统级判定,默认英文。
|
||||
// (Linux/macOS 上语言环境通常已由 LANG/LC_* 提供,见 IsChineseEnv。)
|
||||
func isSystemChinese() bool {
|
||||
return false
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user