Skip to content

Commit

Permalink
refactor: 优化密码策略处理
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles7c committed May 15, 2024
1 parent d44fb3a commit 90ecaab
Show file tree
Hide file tree
Showing 23 changed files with 301 additions and 268 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public class CacheConstants {
*/
public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER;

/**
* 用户密码错误次数缓存键前缀
*/
public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER;

private CacheConstants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,32 @@
public class RegexConstants {

/**
* 用户名正则(长度为 4 到 64 位,可以包含字母、数字下划线,以字母开头)
* 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字下划线,以字母开头)
*/
public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$";

/**
* 密码正则(长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字
* 密码正则模板(密码长度为 min-max 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字
*/
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{6,32}$";
public static final String PASSWORD_TEMPLATE = "^(?=.*\\d)(?=.*[a-z]).{%s,%s}$";

/**
* 密码正则严格版(长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符
* 密码正则(密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字
*/
public static final String PASSWORD_STRICT = "^\\S*(?=\\S{8,32})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*$";
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{8,32}$";

/**
* 通用编码正则(长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头)
* 特殊字符正则
*/
public static final String SPECIAL_CHARACTER = "[-_`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\n|\\r|\\t";

/**
* 通用编码正则(长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头)
*/
public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$";

/**
* 通用名称正则(长度为 230 位,可以包含中文、字母、数字、下划线,短横线)
* 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线)
*/
public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
*/
public class SysConstants {

/**
* 否
*/
public static final Integer NO = 0;

/**
* 是
*/
public static final Integer YES = 1;

/**
* 管理员角色编码
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package top.continew.admin.auth.service;

import jakarta.servlet.http.HttpServletRequest;
import me.zhyd.oauth.model.AuthUser;
import top.continew.admin.auth.model.resp.RouteResp;

Expand All @@ -34,9 +35,10 @@ public interface LoginService {
*
* @param username 用户名
* @param password 密码
* @param request 请求对象
* @return 令牌
*/
String accountLogin(String username, String password);
String accountLogin(String username, String password, HttpServletRequest request);

/**
* 手机号登录
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.util.*;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -39,7 +41,7 @@
import top.continew.admin.common.model.dto.LoginUser;
import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.enums.MessageTemplateEnum;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.enums.PasswordPolicyEnum;
import top.continew.admin.system.model.entity.DeptDO;
import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.UserDO;
Expand Down Expand Up @@ -80,36 +82,15 @@ public class LoginServiceImpl implements LoginService {
private final OptionService optionService;

@Override
public String accountLogin(String username, String password) {
public String accountLogin(String username, String password, HttpServletRequest request) {
UserDO user = userService.getByUsername(username);
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword());
isPasswordLocked(username, isError);
this.checkUserLocked(username, request, isError);
CheckUtils.throwIf(isError, "用户名或密码错误");
this.checkUserStatus(user);
return this.login(user);
}

/**
* 检测用户是否被密码锁定
*
* @param username 用户名
*/
private void isPasswordLocked(String username, boolean isError) {
// 不锁定账户
int maxErrorCount = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_ERROR_COUNT);
if (maxErrorCount <= 0) {
return;
}
int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES);
String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username;
long currentErrorCount = 0;
if (isError) {
currentErrorCount = RedisUtils.incr(key);
RedisUtils.expire(key, Duration.ofMinutes(lockMinutes));
}
CheckUtils.throwIf(currentErrorCount > maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes);
}

@Override
public String phoneLogin(String phone) {
UserDO user = userService.getByPhone(phone);
Expand Down Expand Up @@ -229,6 +210,36 @@ private void checkUserStatus(UserDO user) {
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
}

/**
* 检测用户是否已被锁定
*
* @param username 用户名
* @param request 请求对象
* @param isError 是否登录错误
*/
private void checkUserLocked(String username, HttpServletRequest request, boolean isError) {
// 不锁定
int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name());
if (maxErrorCount <= SysConstants.NO) {
return;
}
// 检测是否已被锁定
String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil
.getClientIP(request));
int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name());
Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0);
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes);
// 登录成功清除计数
if (!isError) {
RedisUtils.delete(key);
return;
}
// 登录失败递增计数
currentErrorCount++;
RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes);
}

/**
* 发送系统消息
*
Expand All @@ -237,8 +248,8 @@ private void checkUserStatus(UserDO user) {
private void sendSystemMsg(UserDO user) {
MessageReq req = new MessageReq();
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
req.setTitle(StrUtil.format(socialRegister.getTitle(), projectProperties.getName()));
req.setContent(StrUtil.format(socialRegister.getContent(), user.getNickname()));
req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
req.setContent(socialRegister.getContent().formatted(user.getNickname()));
req.setType(MessageTypeEnum.SYSTEM);
messageService.add(req, CollUtil.toList(user.getId()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
/**
* 第三方登录
*/
SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。");
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");

private final String title;
private final String content;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package top.continew.admin.system.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.admin.common.constant.SysConstants;

/**
* 密码策略枚举
*
* @author Kils
* @author Charles7c
* @since 2024/5/9 11:25
*/
@Getter
@RequiredArgsConstructor
public enum PasswordPolicyEnum {

/**
* 登录密码错误锁定账号的次数
*/
PASSWORD_ERROR_LOCK_COUNT(null, SysConstants.NO, 10),

/**
* 登录密码错误锁定账号的时间(min)
*/
PASSWORD_ERROR_LOCK_MINUTES(null, 1, 1440),

/**
* 密码到期提前提示(天)
*/
PASSWORD_EXPIRATION_WARNING_DAYS(null, SysConstants.NO, Integer.MAX_VALUE),

/**
* 密码有效期(天)
*/
PASSWORD_EXPIRATION_DAYS(null, SysConstants.NO, 999),

/**
* 密码重复使用规则
*/
PASSWORD_REUSE_POLICY("不允许使用最近 %s 次的历史密码", 3, 32),

/**
* 密码最小长度
*/
PASSWORD_MIN_LENGTH("密码最小长度为 %s 个字符", 8, 32),

/**
* 密码是否允许包含正反序账号名
*/
PASSWORD_ALLOW_CONTAIN_USERNAME("密码不允许包含正反序账号名", SysConstants.NO, SysConstants.YES),

/**
* 密码是否必须包含特殊字符
*/
PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码必须包含特殊字符", SysConstants.NO, SysConstants.YES),;

private final String description;
private final Integer min;
private final Integer max;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class DeptReq extends BaseReq {
*/
@Schema(description = "名称", example = "测试部")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 230 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ public class DictReq extends BaseReq {
*/
@Schema(description = "名称", example = "公告类型")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 230 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;

/**
* 编码
*/
@Schema(description = "编码", example = "notice_type")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 230 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ public class RoleReq extends BaseReq {
*/
@Schema(description = "名称", example = "测试人员")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 230 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;

/**
* 编码
*/
@Schema(description = "编码", example = "test")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 230 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class StorageReq extends BaseReq {
*/
@Schema(description = "编码", example = "local")
@NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 230 位,可以包含字母、数字下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code;

/**
Expand Down
Loading

0 comments on commit 90ecaab

Please sign in to comment.