feat: 密码强度校验/密码过期时间功能

This commit is contained in:
TsMask
2025-03-31 15:18:17 +08:00
parent 70c84e4950
commit 36aa32dc94
11 changed files with 293 additions and 40 deletions

View File

@@ -75,6 +75,13 @@ func (s AccountController) Login(c *gin.Context) {
return
}
// 强制改密码
forcePasswdChange, err := s.accountService.PasswordExpireTime(loginUser.User.LoginCount, loginUser.User.PasswordUpdateTime)
if err != nil {
c.JSON(200, resp.ErrMsg(i18n.TKey(language, err.Error())))
return
}
// 生成令牌,创建系统访问记录
tokenStr := token.Create(&loginUser, [4]string{ipaddr, location, os, browser})
if tokenStr == "" {
@@ -89,12 +96,16 @@ func (s AccountController) Login(c *gin.Context) {
)
}
c.JSON(200, resp.OkData(map[string]any{
data := map[string]any{
"accessToken": tokenStr,
"tokenType": strings.TrimRight(constants.HEADER_PREFIX, " "),
"expiresIn": (loginUser.ExpireTime - loginUser.LoginTime) / 1000,
"userId": loginUser.UserId,
}))
}
if forcePasswdChange {
data["forcePasswdChange"] = true
}
c.JSON(200, resp.OkData(data))
}
// Me 登录用户信息
@@ -127,11 +138,19 @@ func (s AccountController) Me(c *gin.Context) {
for ri := range info.User.Roles {
info.User.Roles[ri].RoleName = i18n.TKey(language, info.User.Roles[ri].RoleName)
}
c.JSON(200, resp.OkData(map[string]any{
data := map[string]any{
"user": info.User,
"roles": roles,
"permissions": perms,
}))
}
// 强制改密码
forcePasswdChange, _ := s.accountService.PasswordExpireTime(info.User.LoginCount, info.User.PasswordUpdateTime)
if forcePasswdChange {
data["forcePasswdChange"] = true
}
c.JSON(200, resp.OkData(data))
}
// Router 登录用户路由信息

View File

@@ -9,7 +9,6 @@ import (
"be.ems/src/framework/resp"
"be.ems/src/framework/token"
"be.ems/src/framework/utils/machine"
"be.ems/src/framework/utils/regular"
"be.ems/src/modules/auth/service"
systemService "be.ems/src/modules/system/service"
@@ -146,11 +145,17 @@ func (s *BootloaderController) Account(c *gin.Context) {
return
}
if !regular.ValidPassword(body.Password) {
// 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "user.errPasswd")))
// 检查用户密码策略强度
ok, errMsg := s.sysUserService.ValidatePasswordPolicy(body.Password, language)
if !ok {
c.JSON(200, resp.ErrMsg(errMsg))
return
}
// if !regular.ValidPassword(body.Password) {
// // 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
// c.JSON(200, resp.ErrMsg(i18n.TKey(language, "user.errPasswd")))
// return
// }
// 是否完成引导
launchInfo := machine.LaunchInfo

View File

@@ -47,9 +47,15 @@ func (s *RegisterController) Register(c *gin.Context) {
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errUsername")))
return
}
if !regular.ValidPassword(body.Password) {
// 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errPasswd")))
// if !regular.ValidPassword(body.Password) {
// // 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
// c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errPasswd")))
// return
// }
// 检查用户密码策略强度
ok, errMsg := s.registerService.ValidatePasswordPolicy(body.Password, language)
if !ok {
c.JSON(200, resp.ErrMsg(errMsg))
return
}
if body.Password != body.ConfirmPassword {

View File

@@ -104,6 +104,7 @@ func (s Account) ByUsername(username, password string) (token.TokenInfo, error)
func (s Account) UpdateLoginDateAndIP(tokenInfo token.TokenInfo) bool {
user := s.sysUserService.FindById(tokenInfo.UserId)
user.Password = "" // 密码不更新
user.LoginCount += 1
user.LoginIp = tokenInfo.LoginIp
user.LoginTime = tokenInfo.LoginTime
return s.sysUserService.Update(user) > 0
@@ -144,6 +145,22 @@ func (s Account) passwordRetryCount(userName string) (string, int64, time.Durati
return retryKey, retryCountInt64, time.Duration(lockTime) * time.Minute, nil
}
// passwordRetryCount 密码过期时间
func (s Account) PasswordExpireTime(loginCount, passwordUpdateTime int64) (bool, error) {
// 首次登录
forcePasswdChange := loginCount < 1
// 非首次登录,判断密码是否过期
if !forcePasswdChange {
alert, err := s.sysUserService.ValidatePasswordExpireTime(passwordUpdateTime)
if err != nil {
return alert, err
}
forcePasswdChange = alert
}
return forcePasswdChange, nil
}
// RoleAndMenuPerms 角色和菜单数据权限
func (s Account) RoleAndMenuPerms(userId int64, isSystemUser bool) ([]string, []string) {
if isSystemUser {

View File

@@ -1,10 +1,13 @@
package service
import (
"encoding/json"
"fmt"
"regexp"
"be.ems/src/framework/constants"
"be.ems/src/framework/database/redis"
"be.ems/src/framework/i18n"
"be.ems/src/framework/utils/parse"
systemModel "be.ems/src/modules/system/model"
systemService "be.ems/src/modules/system/service"
@@ -90,3 +93,45 @@ func (s Register) registerRoleInit() []int64 {
func (s Register) registerPostInit() []int64 {
return []int64{}
}
// ValidatePasswordPolicy 判断密码策略强度
func (s Register) ValidatePasswordPolicy(password string, errLang string) (bool, string) {
passwordPolicyStr := s.sysConfigService.FindValueByKey("sys.user.passwordPolicy")
if passwordPolicyStr == "" {
// 未配置密码策略
return false, i18n.TKey(errLang, "config.sys.user.passwordPolicyNot")
}
var policy struct {
MinLength int `json:"minLength"`
SpecialChars int `json:"specialChars"`
Uppercase int `json:"uppercase"`
Lowercase int `json:"lowercase"`
}
err := json.Unmarshal([]byte(passwordPolicyStr), &policy)
if err != nil {
return false, err.Error()
}
errMsg := i18n.TTemplate(errLang, "sys.user.passwordPolicyError", map[string]any{
"minLength": policy.MinLength,
"specialChars": policy.SpecialChars,
"uppercase": policy.Uppercase,
"lowercase": policy.Lowercase,
})
specialChars := len(regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).FindAllString(password, -1))
if specialChars < policy.SpecialChars {
return false, errMsg
}
uppercase := len(regexp.MustCompile(`[A-Z]`).FindAllString(password, -1))
if uppercase < policy.Uppercase {
return false, errMsg
}
lowercase := len(regexp.MustCompile(`[a-z]`).FindAllString(password, -1))
if lowercase < policy.Lowercase {
return false, errMsg
}
return true, ""
}

View File

@@ -194,8 +194,8 @@ func (s *SysProfileController) UpdateProfile(c *gin.Context) {
// @Security TokenAuth
// @Summary Personal Reset Password
// @Description Personal Reset Password
// @Router /system/user/profile/updatePwd [put]
func (s *SysProfileController) UpdatePassword(c *gin.Context) {
// @Router /system/user/profile/password [put]
func (s *SysProfileController) PasswordUpdate(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
var body struct {
OldPassword string `json:"oldPassword" binding:"required"` // 旧密码
@@ -247,3 +247,65 @@ func (s *SysProfileController) UpdatePassword(c *gin.Context) {
}
c.JSON(200, resp.Err(nil))
}
// 强制重置密码
//
// PUT /password-force
func (s *SysProfileController) PasswordForce(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
var body struct {
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindBodyWithJSON(&body); err != nil {
errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err))
c.JSON(422, resp.CodeMsg(40422, errMsgs))
return
}
// 检查用户密码策略强度
ok, errMsg := s.sysUserService.ValidatePasswordPolicy(body.Password, language)
if !ok {
c.JSON(200, resp.ErrMsg(errMsg))
return
}
// 登录用户信息
loginUser, err := reqctx.LoginUser(c)
if err != nil {
c.JSON(401, resp.CodeMsg(401, i18n.TKey(language, err.Error())))
return
}
// 检查是否存在
userInfo := s.sysUserService.FindById(loginUser.UserId)
if userInfo.UserId != loginUser.UserId {
// c.JSON(200, resp.ErrMsg("没有权限访问用户数据!"))
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "user.noData")))
return
}
// 首次登录
forcePasswdChange := userInfo.LoginCount <= 2
// 非首次登录,判断密码是否过期
if !forcePasswdChange {
alert, _ := s.sysUserService.ValidatePasswordExpireTime(userInfo.PasswordUpdateTime)
forcePasswdChange = alert
}
if !forcePasswdChange {
c.JSON(403, resp.ErrMsg("not matching the amendment"))
return
}
userInfo.Password = body.Password
userInfo.UpdateBy = reqctx.LoginUserToUserName(c)
rows := s.sysUserService.Update(userInfo)
if rows > 0 {
// 更新缓存用户信息
userInfo.Password = ""
// 移除令牌信息
token.Remove(reqctx.Authorization(c))
c.JSON(200, resp.Ok(nil))
return
}
c.JSON(200, resp.Err(nil))
}

View File

@@ -189,13 +189,19 @@ func (s *SysUserController) Add(c *gin.Context) {
c.JSON(400, resp.ErrMsg(msg))
return
}
if !regular.ValidPassword(body.Password) {
// msg := fmt.Sprintf("新增用户【%s】失败登录密码至少包含大小写字母、数字、特殊符号且不少于6位", body.UserName)
msg := fmt.Sprintf("New user [%s] failed, the login password contains at least upper and lower case letters, numbers, special symbols, and not less than 6 bits", body.UserName)
c.JSON(400, resp.ErrMsg(msg))
// if !regular.ValidPassword(body.Password) {
// // msg := fmt.Sprintf("新增用户【%s】失败登录密码至少包含大小写字母、数字、特殊符号且不少于6位", body.UserName)
// msg := fmt.Sprintf("New user [%s] failed, the login password contains at least upper and lower case letters, numbers, special symbols, and not less than 6 bits", body.UserName)
// c.JSON(400, resp.ErrMsg(msg))
// return
// }
// 检查用户密码策略强度
ok, errMsg := s.sysUserService.ValidatePasswordPolicy(body.Password, language)
if !ok {
c.JSON(200, resp.ErrMsg(errMsg))
return
}
// 检查用户登录账号是否唯一
uniqueUserName := s.sysUserService.CheckUniqueByUserName(body.UserName, 0)
if !uniqueUserName {
@@ -431,11 +437,17 @@ func (s *SysUserController) Password(c *gin.Context) {
return
}
if !regular.ValidPassword(body.Password) {
c.JSON(200, resp.ErrMsg("Login password contains at least upper and lower case letters, numbers, special symbols, and not less than 6 digits"))
// if !regular.ValidPassword(body.Password) {
// c.JSON(200, resp.ErrMsg("Login password contains at least upper and lower case letters, numbers, special symbols, and not less than 6 digits"))
// return
// }
// 检查用户密码策略强度
ok, errMsg := s.sysUserService.ValidatePasswordPolicy(body.Password, language)
if !ok {
c.JSON(200, resp.ErrMsg(errMsg))
return
}
// 检查是否存在
userInfo := s.sysUserService.FindById(body.UserId)
if userInfo.UserId != body.UserId {

View File

@@ -13,6 +13,8 @@ type SysUser struct {
Password string `json:"-" gorm:"column:password"` // 密码
StatusFlag string `json:"statusFlag" gorm:"column:status_flag"` // 账号状态0停用 1正常
DelFlag string `json:"-" gorm:"column:del_flag"` // 删除标记0存在 1删除
PasswordUpdateTime int64 `json:"passwordUpdateTime" gorm:"column:password_update_time"` // 密码更新时间
LoginCount int64 `json:"loginCount" gorm:"column:login_count"` // 登录次数
LoginIp string `json:"loginIp" gorm:"column:login_ip"` // 最后登录IP
LoginTime int64 `json:"loginTime" gorm:"column:login_time"` // 最后登录时间
CreateBy string `json:"createBy" gorm:"column:create_by"` // 创建者

View File

@@ -134,6 +134,7 @@ func (r SysUser) Insert(sysUser model.SysUser) int64 {
}
if sysUser.Password != "" {
sysUser.Password = crypto.BcryptHash(sysUser.Password)
sysUser.PasswordUpdateTime = time.Now().UnixMilli()
}
// 执行插入
if err := db.DB("").Create(&sysUser).Error; err != nil {
@@ -153,6 +154,7 @@ func (r SysUser) Update(sysUser model.SysUser) int64 {
}
if sysUser.Password != "" {
sysUser.Password = crypto.BcryptHash(sysUser.Password)
sysUser.PasswordUpdateTime = time.Now().UnixMilli()
}
tx := db.DB("").Model(&model.SysUser{})
// 构建查询条件

View File

@@ -1,9 +1,13 @@
package service
import (
"encoding/json"
"fmt"
"regexp"
"time"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/modules/system/model"
"be.ems/src/modules/system/repository"
)
@@ -242,3 +246,77 @@ func (s SysUser) FindAuthUsersPage(query map[string]string, dataScopeSQL string)
}
return rows, total
}
// ValidatePasswordPolicy 判断密码策略强度
func (s SysUser) ValidatePasswordPolicy(password string, errLang string) (bool, string) {
passwordPolicyStr := s.sysConfigService.FindValueByKey("sys.user.passwordPolicy")
if passwordPolicyStr == "" {
// 未配置密码策略
return false, i18n.TKey(errLang, "config.sys.user.passwordPolicyNot")
}
var policy struct {
MinLength int `json:"minLength"`
SpecialChars int `json:"specialChars"`
Uppercase int `json:"uppercase"`
Lowercase int `json:"lowercase"`
}
err := json.Unmarshal([]byte(passwordPolicyStr), &policy)
if err != nil {
return false, err.Error()
}
errMsg := i18n.TTemplate(errLang, "config.sys.user.passwordPolicyError", map[string]any{
"minLength": policy.MinLength,
"specialChars": policy.SpecialChars,
"uppercase": policy.Uppercase,
"lowercase": policy.Lowercase,
})
specialChars := len(regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).FindAllString(password, -1))
if specialChars < policy.SpecialChars {
return false, errMsg
}
uppercase := len(regexp.MustCompile(`[A-Z]`).FindAllString(password, -1))
if uppercase < policy.Uppercase {
return false, errMsg
}
lowercase := len(regexp.MustCompile(`[a-z]`).FindAllString(password, -1))
if lowercase < policy.Lowercase {
return false, errMsg
}
return true, ""
}
// ValidatePasswordExpireTime 密码过期时间
func (s SysUser) ValidatePasswordExpireTime(passwordUpdateTime int64) (bool, error) {
passwdExpireStr := s.sysConfigService.FindValueByKey("sys.user.passwdExpire")
if passwdExpireStr == "" {
return false, nil
}
var expire struct {
ExpHours int `json:"expHours"`
AlertHours int `json:"alertHours"`
}
err := json.Unmarshal([]byte(passwdExpireStr), &expire)
if err != nil {
return false, err
}
if expire.ExpHours <= 0 {
return false, nil
}
if passwordUpdateTime <= 1e9 {
return false, fmt.Errorf("login.errPasswdExpire")
}
// 计算时间差
lastUpdateTime := time.UnixMilli(passwordUpdateTime)
now := time.Now()
remainingHour := now.Sub(lastUpdateTime).Hours()
alertFlag := remainingHour > float64(expire.AlertHours)
if remainingHour > float64(expire.ExpHours) {
return alertFlag, fmt.Errorf("login.errPasswdExpire")
}
return alertFlag, nil
}

View File

@@ -314,7 +314,12 @@ func Setup(router *gin.Engine) {
sysProfileGroup.PUT("/password",
middleware.PreAuthorize(nil),
middleware.OperateLog(middleware.OptionNew("log.operate.title.sysProfile", middleware.BUSINESS_TYPE_UPDATE)),
controller.NewSysProfile.UpdatePassword,
controller.NewSysProfile.PasswordUpdate,
)
sysProfileGroup.PUT("/password-force",
middleware.PreAuthorize(nil),
middleware.OperateLog(middleware.OptionNew("log.operate.title.sysProfile", middleware.BUSINESS_TYPE_UPDATE)),
controller.NewSysProfile.PasswordForce,
)
}