refactor: 优化密码策略处理

This commit is contained in:
2024-05-15 23:14:51 +08:00
parent d44fb3a681
commit 90ecaab632
23 changed files with 303 additions and 270 deletions

View File

@@ -66,6 +66,11 @@ public class CacheConstants {
*/ */
public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER; 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() { private CacheConstants() {
} }
} }

View File

@@ -25,27 +25,32 @@ package top.continew.admin.common.constant;
public class RegexConstants { public class RegexConstants {
/** /**
* 用户名正则(长度为 4 到 64 位,可以包含字母、数字下划线,以字母开头) * 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字下划线,以字母开头)
*/ */
public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,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}$"; public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$";
/** /**
* 通用名称正则(长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线) * 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线)
*/ */
public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$"; public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$";

View File

@@ -24,6 +24,16 @@ package top.continew.admin.common.constant;
*/ */
public class SysConstants { public class SysConstants {
/**
* 否
*/
public static final Integer NO = 0;
/**
* 是
*/
public static final Integer YES = 1;
/** /**
* 管理员角色编码 * 管理员角色编码
*/ */

View File

@@ -16,6 +16,7 @@
package top.continew.admin.auth.service; package top.continew.admin.auth.service;
import jakarta.servlet.http.HttpServletRequest;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import top.continew.admin.auth.model.resp.RouteResp; import top.continew.admin.auth.model.resp.RouteResp;
@@ -34,9 +35,10 @@ public interface LoginService {
* *
* @param username 用户名 * @param username 用户名
* @param password 密码 * @param password 密码
* @param request 请求对象
* @return 令牌 * @return 令牌
*/ */
String accountLogin(String username, String password); String accountLogin(String username, String password, HttpServletRequest request);
/** /**
* 手机号登录 * 手机号登录

View File

@@ -21,7 +21,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.util.*; import cn.hutool.core.util.*;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@@ -39,7 +41,7 @@ import top.continew.admin.common.enums.MessageTypeEnum;
import top.continew.admin.common.model.dto.LoginUser; import top.continew.admin.common.model.dto.LoginUser;
import top.continew.admin.common.util.helper.LoginHelper; import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.enums.MessageTemplateEnum; 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.DeptDO;
import top.continew.admin.system.model.entity.RoleDO; import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.UserDO; import top.continew.admin.system.model.entity.UserDO;
@@ -80,36 +82,15 @@ public class LoginServiceImpl implements LoginService {
private final OptionService optionService; private final OptionService optionService;
@Override @Override
public String accountLogin(String username, String password) { public String accountLogin(String username, String password, HttpServletRequest request) {
UserDO user = userService.getByUsername(username); UserDO user = userService.getByUsername(username);
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword()); boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword());
isPasswordLocked(username, isError); this.checkUserLocked(username, request, isError);
CheckUtils.throwIf(isError, "用户名或密码错误"); CheckUtils.throwIf(isError, "用户名或密码错误");
this.checkUserStatus(user); this.checkUserStatus(user);
return this.login(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 @Override
public String phoneLogin(String phone) { public String phoneLogin(String phone) {
UserDO user = userService.getByPhone(phone); UserDO user = userService.getByPhone(phone);
@@ -229,6 +210,36 @@ public class LoginServiceImpl implements LoginService {
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); 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);
}
/** /**
* 发送系统消息 * 发送系统消息
* *
@@ -237,8 +248,8 @@ public class LoginServiceImpl implements LoginService {
private void sendSystemMsg(UserDO user) { private void sendSystemMsg(UserDO user) {
MessageReq req = new MessageReq(); MessageReq req = new MessageReq();
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER; MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
req.setTitle(StrUtil.format(socialRegister.getTitle(), projectProperties.getName())); req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
req.setContent(StrUtil.format(socialRegister.getContent(), user.getNickname())); req.setContent(socialRegister.getContent().formatted(user.getNickname()));
req.setType(MessageTypeEnum.SYSTEM); req.setType(MessageTypeEnum.SYSTEM);
messageService.add(req, CollUtil.toList(user.getId())); messageService.add(req, CollUtil.toList(user.getId()));
} }

View File

@@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
/** /**
* 第三方登录 * 第三方登录
*/ */
SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。"); SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");
private final String title; private final String title;
private final String content; private final String content;

View File

@@ -1,63 +0,0 @@
/*
* 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;
/**
* 参数枚举
*
* @author Kils
* @since 2024/05/09 11:25
*/
@Getter
@RequiredArgsConstructor
public enum OptionCodeEnum {
/**
* 密码是否允许包含正反序帐户名
*/
PASSWORD_CONTAIN_NAME("password_contain_name", "密码不允许包含正反序帐户名"),
/**
* 密码错误锁定帐户次数
*/
PASSWORD_ERROR_COUNT("password_error_count", "密码错误锁定帐户次数"),
/**
* 密码有效期
*/
PASSWORD_EXPIRATION_DAYS("password_expiration_days", "密码有效期"),
/**
* 密码是否允许包含正反序帐户名
*/
PASSWORD_LOCK_MINUTES("password_lock_minutes", "密码错误锁定帐户的时间"),
/**
* 密码最小长度
*/
PASSWORD_MIN_LENGTH("password_min_length", "密码最小长度"),
/**
* 密码是否必须包含特殊字符
*/
PASSWORD_SPECIAL_CHAR("password_special_char", "密码是否必须包含特殊字符"),
/**
* 修改密码最短间隔
*/
PASSWORD_UPDATE_INTERVAL("password_update_interval", "修改密码最短间隔");
private final String value;
private final String description;
}

View File

@@ -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;
}

View File

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

View File

@@ -44,7 +44,7 @@ public class DictReq extends BaseReq {
*/ */
@Schema(description = "名称", example = "公告类型") @Schema(description = "名称", example = "公告类型")
@NotBlank(message = "名称不能为空") @NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name; private String name;
/** /**
@@ -52,7 +52,7 @@ public class DictReq extends BaseReq {
*/ */
@Schema(description = "编码", example = "notice_type") @Schema(description = "编码", example = "notice_type")
@NotBlank(message = "编码不能为空") @NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字下划线,以字母开头") @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code; private String code;
/** /**

View File

@@ -48,7 +48,7 @@ public class RoleReq extends BaseReq {
*/ */
@Schema(description = "名称", example = "测试人员") @Schema(description = "名称", example = "测试人员")
@NotBlank(message = "名称不能为空") @NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name; private String name;
/** /**
@@ -56,7 +56,7 @@ public class RoleReq extends BaseReq {
*/ */
@Schema(description = "编码", example = "test") @Schema(description = "编码", example = "test")
@NotBlank(message = "编码不能为空") @NotBlank(message = "编码不能为空")
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字下划线,以字母开头") @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字下划线,以字母开头")
private String code; private String code;
/** /**

View File

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

View File

@@ -45,7 +45,7 @@ public class UserBasicInfoUpdateReq implements Serializable {
*/ */
@Schema(description = "昵称", example = "张三") @Schema(description = "昵称", example = "张三")
@NotBlank(message = "昵称不能为空") @NotBlank(message = "昵称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String nickname; private String nickname;
/** /**

View File

@@ -51,7 +51,7 @@ public class UserReq extends BaseReq {
*/ */
@Schema(description = "用户名", example = "zhangsan") @Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空") @NotBlank(message = "用户名不能为空")
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4 到 64 位,可以包含字母、数字下划线,以字母开头") @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字下划线,以字母开头")
private String username; private String username;
/** /**
@@ -59,7 +59,7 @@ public class UserReq extends BaseReq {
*/ */
@Schema(description = "昵称", example = "张三") @Schema(description = "昵称", example = "张三")
@NotBlank(message = "昵称不能为空") @NotBlank(message = "昵称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String nickname; private String nickname;
/** /**

View File

@@ -16,15 +16,14 @@
package top.continew.admin.system.service; package top.continew.admin.system.service;
import java.util.List;
import java.util.function.Function;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.model.query.OptionQuery; import top.continew.admin.system.model.query.OptionQuery;
import top.continew.admin.system.model.req.OptionReq; import top.continew.admin.system.model.req.OptionReq;
import top.continew.admin.system.model.req.OptionResetValueReq; import top.continew.admin.system.model.req.OptionResetValueReq;
import top.continew.admin.system.model.resp.OptionResp; import top.continew.admin.system.model.resp.OptionResp;
import java.util.List;
import java.util.function.Function;
/** /**
* 参数业务接口 * 参数业务接口
* *
@@ -56,19 +55,19 @@ public interface OptionService {
void resetValue(OptionResetValueReq req); void resetValue(OptionResetValueReq req);
/** /**
* 根据code获取int参数值 * 根据编码查询参数值
* *
* @param code code * @param code 编码
* @return 参数值 * @return 参数值(自动转换为 int 类型)
*/ */
int getValueByCode2Int(OptionCodeEnum code); int getValueByCode2Int(String code);
/** /**
* 根据code获取参数值 * 根据编码查询参数值
* *
* @param code code * @param code 编码
* @param mapper 类型转换 ex:value -> Integer.parseInt(value) * @param mapper 转换方法 e.g.value -> Integer.parseInt(value)
* @return 参数值 * @return 参数值
*/ */
<T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper); <T> T getValueByCode(String code, Function<String, T> mapper);
} }

View File

@@ -17,14 +17,10 @@
package top.continew.admin.system.service.impl; package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import top.continew.admin.common.constant.CacheConstants; import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.system.enums.OptionCodeEnum;
import top.continew.admin.system.mapper.OptionMapper; import top.continew.admin.system.mapper.OptionMapper;
import top.continew.admin.system.model.entity.OptionDO; import top.continew.admin.system.model.entity.OptionDO;
import top.continew.admin.system.model.query.OptionQuery; import top.continew.admin.system.model.query.OptionQuery;
@@ -70,25 +66,24 @@ public class OptionServiceImpl implements OptionService {
} }
@Override @Override
public int getValueByCode2Int(OptionCodeEnum code) { public int getValueByCode2Int(String code) {
return this.getValueByCode(code, Integer::parseInt); return this.getValueByCode(code, Integer::parseInt);
} }
@Override @Override
public <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper) { public <T> T getValueByCode(String code, Function<String, T> mapper) {
String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue()); String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code);
if (StrUtil.isNotBlank(value)) { if (StrUtil.isNotBlank(value)) {
return mapper.apply(value); return mapper.apply(value);
} }
LambdaQueryWrapper<OptionDO> queryWrapper = Wrappers.<OptionDO>lambdaQuery() OptionDO option = baseMapper.lambdaQuery()
.eq(OptionDO::getCode, code.getValue()) .eq(OptionDO::getCode, code)
.select(OptionDO::getValue, OptionDO::getDefaultValue); .select(OptionDO::getValue, OptionDO::getDefaultValue)
OptionDO optionDO = baseMapper.selectOne(queryWrapper); .one();
CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code); CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code);
value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue()); value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue());
CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code); CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code);
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value); RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value);
return mapper.apply(value); return mapper.apply(value);
} }
} }

View File

@@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.ReUtil;
@@ -41,6 +40,7 @@ import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.auth.service.OnlineUserService; import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.constant.CacheConstants; import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.constant.RegexConstants; import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.common.util.helper.LoginHelper; import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.mapper.UserMapper; import top.continew.admin.system.mapper.UserMapper;
@@ -63,10 +63,13 @@ import top.continew.starter.extension.crud.service.CommonUserService;
import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static top.continew.admin.system.enums.OptionCodeEnum.*; import static top.continew.admin.system.enums.PasswordPolicyEnum.*;
/** /**
* 用户业务实现 * 用户业务实现
@@ -197,7 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误"); CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误");
} }
// 校验密码合法性 // 校验密码合法性
checkPassword(newPassword, user); this.checkPassword(newPassword, user);
// 更新密码和密码重置时间 // 更新密码和密码重置时间
user.setPassword(newPassword); user.setPassword(newPassword);
user.setPwdResetTime(LocalDateTime.now()); user.setPwdResetTime(LocalDateTime.now());
@@ -205,52 +208,11 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
onlineUserService.cleanByUserId(user.getId()); onlineUserService.cleanByUserId(user.getId());
} }
/**
* 检测修改密码合法性
*
* @param password 密码
* @param user 用户
*/
private void checkPassword(String password, UserDO user) {
// 密码最小长度
int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH);
ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH
.getDescription() + "为 {}", passwordMinLength);
// 密码是否允许包含正反序用户名
int passwordContainName = optionService.getValueByCode2Int(PASSWORD_CONTAIN_NAME);
if (passwordContainName == 1) {
String username = user.getUsername();
ValidationUtils.throwIf(StrUtil.containsIgnoreCase(password, username) || StrUtil
.containsIgnoreCase(password, StrUtil.reverse(username)), PASSWORD_CONTAIN_NAME.getDescription());
}
// 密码是否必须包含特殊字符
int passwordSpecialChar = optionService.getValueByCode2Int(PASSWORD_SPECIAL_CHAR);
String match = RegexConstants.PASSWORD;
String desc = "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字";
if (passwordSpecialChar == 1) {
match = RegexConstants.PASSWORD_STRICT;
desc = "密码长度为 8 到 32 位包含至少1个大写字母、1个小写字母、1个数字1个特殊字符";
}
ValidationUtils.throwIf(!ReUtil.isMatch(match, password), desc);
// 密码修改间隔
if (ObjectUtil.isNull(user.getPwdResetTime())) {
return;
}
int passwordUpdateInterval = optionService.getValueByCode2Int(PASSWORD_UPDATE_INTERVAL);
if (passwordUpdateInterval <= 0) {
return;
}
LocalDateTime lastResetTime = user.getPwdResetTime();
LocalDateTime limitUpdateTime = lastResetTime.plusMinutes(passwordUpdateInterval);
ValidationUtils.throwIf(LocalDateTime.now().isBefore(limitUpdateTime), "上次修改于:{},下次可修改时间:{}", LocalDateTimeUtil
.formatNormal(lastResetTime), LocalDateTimeUtil.formatNormal(limitUpdateTime));
}
@Override @Override
public Boolean isPasswordExpired(LocalDateTime pwdResetTime) { public Boolean isPasswordExpired(LocalDateTime pwdResetTime) {
// 永久有效 // 永久有效
int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS); int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name());
if (passwordExpirationDays <= 0) { if (passwordExpirationDays <= SysConstants.NO) {
return false; return false;
} }
// 初始密码也提示修改 // 初始密码也提示修改
@@ -375,6 +337,33 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
userRoleService.add(req.getRoleIds(), userId); userRoleService.add(req.getRoleIds(), userId);
} }
/**
* 检测密码合法性
*
* @param password 密码
* @param user 用户信息
*/
private void checkPassword(String password, UserDO user) {
// 密码最小长度
int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH.name());
ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH.getDescription()
.formatted(passwordMinLength));
// 密码是否允许包含正反序账号名
int passwordAllowContainUsername = optionService.getValueByCode2Int(PASSWORD_ALLOW_CONTAIN_USERNAME.name());
if (passwordAllowContainUsername == SysConstants.NO) {
String username = user.getUsername();
ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil
.reverse(username)), PASSWORD_ALLOW_CONTAIN_USERNAME.getDescription());
}
int passwordMaxLength = PASSWORD_MIN_LENGTH.getMax();
ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE
.formatted(passwordMinLength, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", passwordMinLength, passwordMaxLength);
// 密码是否必须包含特殊字符
int passwordContainSpecialChar = optionService.getValueByCode2Int(PASSWORD_CONTAIN_SPECIAL_CHARACTERS.name());
ValidationUtils.throwIf(passwordContainSpecialChar == SysConstants.YES && !ReUtil
.isMatch(RegexConstants.SPECIAL_CHARACTER, password), PASSWORD_CONTAIN_SPECIAL_CHARACTERS.getDescription());
}
/** /**
* 名称是否存在 * 名称是否存在
* *

View File

@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -68,7 +69,7 @@ public class AuthController {
@SaIgnore @SaIgnore
@Operation(summary = "账号登录", description = "根据账号和密码进行登录认证") @Operation(summary = "账号登录", description = "根据账号和密码进行登录认证")
@PostMapping("/account") @PostMapping("/account")
public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq) { public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq, HttpServletRequest request) {
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid(); String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid();
String captcha = RedisUtils.get(captchaKey); String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
@@ -77,21 +78,7 @@ public class AuthController {
// 用户登录 // 用户登录
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword())); String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword()));
ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); ValidationUtils.throwIfBlank(rawPassword, "密码解密失败");
String token = loginService.accountLogin(loginReq.getUsername(), rawPassword); String token = loginService.accountLogin(loginReq.getUsername(), rawPassword, request);
return R.ok(LoginResp.builder().token(token).build());
}
@SaIgnore
@Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证")
@PostMapping("/email")
public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) {
String email = loginReq.getEmail();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
String token = loginService.emailLogin(email);
return R.ok(LoginResp.builder().token(token).build()); return R.ok(LoginResp.builder().token(token).build());
} }
@@ -109,6 +96,20 @@ public class AuthController {
return R.ok(LoginResp.builder().token(token).build()); return R.ok(LoginResp.builder().token(token).build());
} }
@SaIgnore
@Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证")
@PostMapping("/email")
public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) {
String email = loginReq.getEmail();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
String token = loginService.emailLogin(email);
return R.ok(LoginResp.builder().token(token).build());
}
@Operation(summary = "用户退出", description = "注销用户的当前登录") @Operation(summary = "用户退出", description = "注销用户的当前登录")
@Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER) @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER)
@PostMapping("/logout") @PostMapping("/logout")

View File

@@ -61,7 +61,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
ValidationUtils.throwIfNull(rawPassword, "密码解密失败"); ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
ValidationUtils.throwIf(!ReUtil ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"); .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
req.setPassword(rawPassword); req.setPassword(rawPassword);
return super.add(req); return super.add(req);
} }
@@ -74,7 +74,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe
String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword())); String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword()));
ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败");
ValidationUtils.throwIf(!ReUtil ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"); .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
req.setNewPassword(rawNewPassword); req.setNewPassword(rawNewPassword);
baseService.resetPassword(req, id); baseService.resetPassword(req, id);
return R.ok("重置密码成功"); return R.ok("重置密码成功");

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql -- liquibase formatted sql
-- changeset Charles7c:2.5.0 -- changeset Charles7c:1
-- comment 初始化表数据 -- comment 初始化表数据
-- 初始化默认菜单 -- 初始化默认菜单
INSERT INTO `sys_menu` INSERT INTO `sys_menu`
@@ -109,19 +109,20 @@ VALUES
INSERT INTO `sys_option` INSERT INTO `sys_option`
(`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`) (`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`)
VALUES VALUES
('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), ('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
('版权信息', 'site_copyright', NULL, ('版权信息', 'SITE_COPYRIGHT', NULL,
'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>', 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
'用于显示登录页面的底部版权信息。', NULL, NULL), '用于显示登录页面的底部版权信息。', NULL, NULL),
('系统LOGO16*16', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), ('系统LOGO16*16', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*33', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), ('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL),
('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制', NULL, NULL), ('登录密码错误锁定账号的时间min', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440一天', NULL, NULL),
('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效', NULL, NULL), ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示0 表示不提示)', NULL, NULL),
('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁', NULL, NULL), ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-9990 表示永久有效)', NULL, NULL),
('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
-- 初始化默认字典 -- 初始化默认字典
INSERT INTO `sys_dict` INSERT INTO `sys_dict`

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql -- liquibase formatted sql
-- changeset Charles7c:2.5.0 -- changeset Charles7c:1
-- comment 初始化表结构 -- comment 初始化表结构
CREATE TABLE IF NOT EXISTS `sys_menu` ( CREATE TABLE IF NOT EXISTS `sys_menu` (
`id` bigint(20) NOT NULL COMMENT 'ID', `id` bigint(20) NOT NULL COMMENT 'ID',
@@ -256,19 +256,19 @@ CREATE TABLE IF NOT EXISTS `sys_storage` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表';
CREATE TABLE IF NOT EXISTS `sys_file` ( CREATE TABLE IF NOT EXISTS `sys_file` (
`id` bigint(20) NOT NULL COMMENT 'ID', `id` bigint(20) NOT NULL COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '名称', `name` varchar(255) NOT NULL COMMENT '名称',
`size` bigint(20) NOT NULL COMMENT '大小(字节)', `size` bigint(20) NOT NULL COMMENT '大小(字节)',
`url` varchar(512) NOT NULL COMMENT 'URL', `url` varchar(512) NOT NULL COMMENT 'URL',
`extension` varchar(100) DEFAULT NULL COMMENT '扩展名', `extension` varchar(100) DEFAULT NULL COMMENT '扩展名',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1其他2图片3文档4视频5音频', `thumbnail_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小(字节)',
`storage_id` bigint(20) NOT NULL COMMENT '存储ID', `thumbnail_url` varchar(512) DEFAULT NULL COMMENT '缩略图URL',
`create_user` bigint(20) NOT NULL COMMENT '创建人', `type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1其他2图片3文档4视频5音频',
`create_time` datetime NOT NULL COMMENT '创建时间', `storage_id` bigint(20) NOT NULL COMMENT '存储ID',
`update_user` bigint(20) NOT NULL COMMENT '修改', `create_user` bigint(20) NOT NULL COMMENT '创建',
`update_time` datetime NOT NULL COMMENT '修改时间', `create_time` datetime NOT NULL COMMENT '创建时间',
`thumbnail_size` bigint(20) NULL DEFAULT NULL COMMENT '缩略图大小(字节)', `update_user` bigint(20) NOT NULL COMMENT '修改人',
`thumbnail_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图URL', `update_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_url`(`url`) USING BTREE, INDEX `idx_url`(`url`) USING BTREE,
INDEX `idx_type`(`type`) USING BTREE, INDEX `idx_type`(`type`) USING BTREE,

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql -- liquibase formatted sql
-- changeset Charles7c:2.5.0 -- changeset Charles7c:1
-- comment 初始化表数据 -- comment 初始化表数据
-- 初始化默认菜单 -- 初始化默认菜单
INSERT INTO "sys_menu" INSERT INTO "sys_menu"
@@ -109,19 +109,20 @@ VALUES
INSERT INTO "sys_option" INSERT INTO "sys_option"
("name", "code", "value", "default_value", "description", "update_user", "update_time") ("name", "code", "value", "default_value", "description", "update_user", "update_time")
VALUES VALUES
('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), ('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
('版权信息', 'site_copyright', NULL, ('版权信息', 'SITE_COPYRIGHT', NULL,
'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>', 'Copyright © 2022-present&nbsp;<a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a>&nbsp;<span>⋅</span>&nbsp;<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
'用于显示登录页面的底部版权信息。', NULL, NULL), '用于显示登录页面的底部版权信息。', NULL, NULL),
('系统LOGO16*16', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), ('系统LOGO16*16', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*33', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), ('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', NULL, NULL),
('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制', NULL, NULL), ('登录密码错误锁定账号的时间min', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440一天', NULL, NULL),
('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效', NULL, NULL), ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示0 表示不提示)', NULL, NULL),
('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁', NULL, NULL), ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-9990 表示永久有效)', NULL, NULL),
('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
-- 初始化默认字典 -- 初始化默认字典
INSERT INTO "sys_dict" INSERT INTO "sys_dict"

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql -- liquibase formatted sql
-- changeset Charles7c:2.5.0 -- changeset Charles7c:1
-- comment 初始化表结构 -- comment 初始化表结构
CREATE TABLE IF NOT EXISTS "sys_menu" ( CREATE TABLE IF NOT EXISTS "sys_menu" (
"id" int8 NOT NULL, "id" int8 NOT NULL,
@@ -430,39 +430,39 @@ COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间';
COMMENT ON TABLE "sys_storage" IS '存储表'; COMMENT ON TABLE "sys_storage" IS '存储表';
CREATE TABLE IF NOT EXISTS "sys_file" ( CREATE TABLE IF NOT EXISTS "sys_file" (
"id" int8 NOT NULL, "id" int8 NOT NULL,
"name" varchar(255) NOT NULL, "name" varchar(255) NOT NULL,
"size" int8 NOT NULL, "size" int8 NOT NULL,
"url" varchar(512) NOT NULL, "url" varchar(512) NOT NULL,
"extension" varchar(100) DEFAULT NULL, "extension" varchar(100) DEFAULT NULL,
"type" int2 NOT NULL DEFAULT 1, "thumbnail_size" int8 DEFAULT NULL,
"storage_id" int8 NOT NULL, "thumbnail_url" varchar(512) DEFAULT NULL,
"create_user" int8 NOT NULL, "type" int2 NOT NULL DEFAULT 1,
"create_time" timestamp NOT NULL, "storage_id" int8 NOT NULL,
"update_user" int8 NOT NULL, "create_user" int8 NOT NULL,
"update_time" timestamp NOT NULL, "create_time" timestamp NOT NULL,
"thumbnail_size" int8 DEFAULT NULL, "update_user" int8 NOT NULL,
"thumbnail_url" varchar(512) DEFAULT NULL, "update_time" timestamp NOT NULL,
PRIMARY KEY ("id") PRIMARY KEY ("id")
); );
CREATE INDEX "idx_file_url" ON "sys_file" ("url"); CREATE INDEX "idx_file_url" ON "sys_file" ("url");
CREATE INDEX "idx_file_type" ON "sys_file" ("type"); CREATE INDEX "idx_file_type" ON "sys_file" ("type");
CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user"); CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user");
CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user"); CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user");
COMMENT ON COLUMN "sys_file"."id" IS 'ID'; COMMENT ON COLUMN "sys_file"."id" IS 'ID';
COMMENT ON COLUMN "sys_file"."name" IS '名称'; COMMENT ON COLUMN "sys_file"."name" IS '名称';
COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)'; COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)';
COMMENT ON COLUMN "sys_file"."url" IS 'URL'; COMMENT ON COLUMN "sys_file"."url" IS 'URL';
COMMENT ON COLUMN "sys_file"."extension" IS '扩展名'; COMMENT ON COLUMN "sys_file"."extension" IS '扩展名';
COMMENT ON COLUMN "sys_file"."type" IS '类型1其他2图片3文档4视频5音频';
COMMENT ON COLUMN "sys_file"."storage_id" IS '存储ID';
COMMENT ON COLUMN "sys_file"."create_user" IS '创建人';
COMMENT ON COLUMN "sys_file"."create_time" IS '创建时间';
COMMENT ON COLUMN "sys_file"."update_user" IS '修改人';
COMMENT ON COLUMN "sys_file"."update_time" IS '修改时间';
COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)'; COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)';
COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL'; COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL';
COMMENT ON TABLE "sys_file" IS '文件表'; COMMENT ON COLUMN "sys_file"."type" IS '类型1其他2图片3文档4视频5音频';
COMMENT ON COLUMN "sys_file"."storage_id" IS '存储ID';
COMMENT ON COLUMN "sys_file"."create_user" IS '创建人';
COMMENT ON COLUMN "sys_file"."create_time" IS '创建时间';
COMMENT ON COLUMN "sys_file"."update_user" IS '修改人';
COMMENT ON COLUMN "sys_file"."update_time" IS '修改时间';
COMMENT ON TABLE "sys_file" IS '文件表';
CREATE TABLE IF NOT EXISTS "gen_config" ( CREATE TABLE IF NOT EXISTS "gen_config" (
"table_name" varchar(64) NOT NULL, "table_name" varchar(64) NOT NULL,