mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-09 20:57:21 +08:00
refactor: 优化密码策略处理
This commit is contained in:
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
@@ -25,27 +25,32 @@ package top.continew.admin.common.constant;
|
||||
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}$";
|
||||
|
||||
/**
|
||||
* 通用名称正则(长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线)
|
||||
* 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线)
|
||||
*/
|
||||
public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$";
|
||||
|
||||
|
@@ -24,6 +24,16 @@ package top.continew.admin.common.constant;
|
||||
*/
|
||||
public class SysConstants {
|
||||
|
||||
/**
|
||||
* 否
|
||||
*/
|
||||
public static final Integer NO = 0;
|
||||
|
||||
/**
|
||||
* 是
|
||||
*/
|
||||
public static final Integer YES = 1;
|
||||
|
||||
/**
|
||||
* 管理员角色编码
|
||||
*/
|
||||
|
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 手机号登录
|
||||
|
@@ -21,7 +21,9 @@ import cn.hutool.core.collection.CollUtil;
|
||||
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;
|
||||
@@ -39,7 +41,7 @@ import top.continew.admin.common.enums.MessageTypeEnum;
|
||||
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;
|
||||
@@ -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);
|
||||
@@ -229,6 +210,36 @@ public class LoginServiceImpl implements LoginService {
|
||||
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) {
|
||||
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()));
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
|
||||
/**
|
||||
* 第三方登录
|
||||
*/
|
||||
SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。");
|
||||
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");
|
||||
|
||||
private final String title;
|
||||
private final String content;
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -54,7 +54,7 @@ public class DeptReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "名称", example = "测试部")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
|
@@ -44,7 +44,7 @@ public class DictReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "名称", example = "公告类型")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ public class DictReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "编码", example = "notice_type")
|
||||
@NotBlank(message = "编码不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
|
@@ -48,7 +48,7 @@ public class RoleReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "名称", example = "测试人员")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
@@ -56,7 +56,7 @@ public class RoleReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "编码", example = "test")
|
||||
@NotBlank(message = "编码不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
|
@@ -55,7 +55,7 @@ public class StorageReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "编码", example = "local")
|
||||
@NotBlank(message = "编码不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
|
@@ -45,7 +45,7 @@ public class UserBasicInfoUpdateReq implements Serializable {
|
||||
*/
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
@NotBlank(message = "昵称不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
|
@@ -51,7 +51,7 @@ public class UserReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4 到 64 位,可以包含字母、数字,下划线,以字母开头")
|
||||
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
@@ -59,7 +59,7 @@ public class UserReq extends BaseReq {
|
||||
*/
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
@NotBlank(message = "昵称不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
|
@@ -16,15 +16,14 @@
|
||||
|
||||
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.req.OptionReq;
|
||||
import top.continew.admin.system.model.req.OptionResetValueReq;
|
||||
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);
|
||||
|
||||
/**
|
||||
* 根据code获取int参数值
|
||||
*
|
||||
* @param code code
|
||||
* @return 参数值
|
||||
* 根据编码查询参数值
|
||||
*
|
||||
* @param code 编码
|
||||
* @return 参数值(自动转换为 int 类型)
|
||||
*/
|
||||
int getValueByCode2Int(OptionCodeEnum code);
|
||||
int getValueByCode2Int(String code);
|
||||
|
||||
/**
|
||||
* 根据code获取参数值
|
||||
*
|
||||
* @param code code
|
||||
* @param mapper 类型转换 ex:value -> Integer.parseInt(value)
|
||||
* 根据编码查询参数值
|
||||
*
|
||||
* @param code 编码
|
||||
* @param mapper 转换方法 e.g.:value -> Integer.parseInt(value)
|
||||
* @return 参数值
|
||||
*/
|
||||
<T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper);
|
||||
}
|
||||
<T> T getValueByCode(String code, Function<String, T> mapper);
|
||||
}
|
||||
|
@@ -17,14 +17,10 @@
|
||||
package top.continew.admin.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
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 org.springframework.stereotype.Service;
|
||||
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.model.entity.OptionDO;
|
||||
import top.continew.admin.system.model.query.OptionQuery;
|
||||
@@ -70,25 +66,24 @@ public class OptionServiceImpl implements OptionService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValueByCode2Int(OptionCodeEnum code) {
|
||||
public int getValueByCode2Int(String code) {
|
||||
return this.getValueByCode(code, Integer::parseInt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper) {
|
||||
String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue());
|
||||
public <T> T getValueByCode(String code, Function<String, T> mapper) {
|
||||
String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
return mapper.apply(value);
|
||||
}
|
||||
LambdaQueryWrapper<OptionDO> queryWrapper = Wrappers.<OptionDO>lambdaQuery()
|
||||
.eq(OptionDO::getCode, code.getValue())
|
||||
.select(OptionDO::getValue, OptionDO::getDefaultValue);
|
||||
OptionDO optionDO = baseMapper.selectOne(queryWrapper);
|
||||
CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code);
|
||||
value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue());
|
||||
CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code);
|
||||
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value);
|
||||
OptionDO option = baseMapper.lambdaQuery()
|
||||
.eq(OptionDO::getCode, code)
|
||||
.select(OptionDO::getValue, OptionDO::getDefaultValue)
|
||||
.one();
|
||||
CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code);
|
||||
value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue());
|
||||
CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code);
|
||||
RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value);
|
||||
return mapper.apply(value);
|
||||
}
|
||||
|
||||
}
|
@@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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.common.constant.CacheConstants;
|
||||
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.util.helper.LoginHelper;
|
||||
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 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 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), "当前密码错误");
|
||||
}
|
||||
// 校验密码合法性
|
||||
checkPassword(newPassword, user);
|
||||
this.checkPassword(newPassword, user);
|
||||
// 更新密码和密码重置时间
|
||||
user.setPassword(newPassword);
|
||||
user.setPwdResetTime(LocalDateTime.now());
|
||||
@@ -205,52 +208,11 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
|
||||
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
|
||||
public Boolean isPasswordExpired(LocalDateTime pwdResetTime) {
|
||||
// 永久有效
|
||||
int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS);
|
||||
if (passwordExpirationDays <= 0) {
|
||||
int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name());
|
||||
if (passwordExpirationDays <= SysConstants.NO) {
|
||||
return false;
|
||||
}
|
||||
// 初始密码也提示修改
|
||||
@@ -375,6 +337,33 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 名称是否存在
|
||||
*
|
||||
|
@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -68,7 +69,7 @@ public class AuthController {
|
||||
@SaIgnore
|
||||
@Operation(summary = "账号登录", description = "根据账号和密码进行登录认证")
|
||||
@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 captcha = RedisUtils.get(captchaKey);
|
||||
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
|
||||
@@ -77,21 +78,7 @@ public class AuthController {
|
||||
// 用户登录
|
||||
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword()));
|
||||
ValidationUtils.throwIfBlank(rawPassword, "密码解密失败");
|
||||
String token = loginService.accountLogin(loginReq.getUsername(), rawPassword);
|
||||
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);
|
||||
String token = loginService.accountLogin(loginReq.getUsername(), rawPassword, request);
|
||||
return R.ok(LoginResp.builder().token(token).build());
|
||||
}
|
||||
|
||||
@@ -109,6 +96,20 @@ public class AuthController {
|
||||
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 = "注销用户的当前登录")
|
||||
@Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER)
|
||||
@PostMapping("/logout")
|
||||
|
@@ -61,7 +61,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe
|
||||
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
|
||||
ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
|
||||
ValidationUtils.throwIf(!ReUtil
|
||||
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字");
|
||||
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
|
||||
req.setPassword(rawPassword);
|
||||
return super.add(req);
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe
|
||||
String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword()));
|
||||
ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败");
|
||||
ValidationUtils.throwIf(!ReUtil
|
||||
.isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字");
|
||||
.isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
|
||||
req.setNewPassword(rawNewPassword);
|
||||
baseService.resetPassword(req, id);
|
||||
return R.ok("重置密码成功");
|
||||
|
@@ -1,6 +1,6 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset Charles7c:2.5.0
|
||||
-- changeset Charles7c:1
|
||||
-- comment 初始化表数据
|
||||
-- 初始化默认菜单
|
||||
INSERT INTO `sys_menu`
|
||||
@@ -109,19 +109,20 @@ VALUES
|
||||
INSERT INTO `sys_option`
|
||||
(`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`)
|
||||
VALUES
|
||||
('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
|
||||
('版权信息', 'site_copyright', NULL,
|
||||
'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>',
|
||||
('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
|
||||
('版权信息', 'SITE_COPYRIGHT', NULL,
|
||||
'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
|
||||
'用于显示登录页面的底部版权信息。', NULL, NULL),
|
||||
('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
|
||||
('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
|
||||
('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL),
|
||||
('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL),
|
||||
('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL),
|
||||
('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL),
|
||||
('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL),
|
||||
('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL),
|
||||
('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL);
|
||||
('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
|
||||
('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
|
||||
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL),
|
||||
('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL),
|
||||
('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL),
|
||||
('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL),
|
||||
('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
|
||||
('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
|
||||
('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
|
||||
('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
|
||||
|
||||
-- 初始化默认字典
|
||||
INSERT INTO `sys_dict`
|
||||
|
@@ -1,6 +1,6 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset Charles7c:2.5.0
|
||||
-- changeset Charles7c:1
|
||||
-- comment 初始化表结构
|
||||
CREATE TABLE IF NOT EXISTS `sys_menu` (
|
||||
`id` bigint(20) NOT NULL COMMENT 'ID',
|
||||
@@ -256,19 +256,19 @@ CREATE TABLE IF NOT EXISTS `sys_storage` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sys_file` (
|
||||
`id` bigint(20) NOT NULL COMMENT 'ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '名称',
|
||||
`size` bigint(20) NOT NULL COMMENT '大小(字节)',
|
||||
`url` varchar(512) NOT NULL COMMENT 'URL',
|
||||
`extension` varchar(100) DEFAULT NULL COMMENT '扩展名',
|
||||
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)',
|
||||
`storage_id` bigint(20) NOT NULL COMMENT '存储ID',
|
||||
`create_user` bigint(20) NOT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_user` bigint(20) NOT NULL COMMENT '修改人',
|
||||
`update_time` datetime NOT NULL COMMENT '修改时间',
|
||||
`thumbnail_size` bigint(20) NULL DEFAULT NULL COMMENT '缩略图大小(字节)',
|
||||
`thumbnail_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图URL',
|
||||
`id` bigint(20) NOT NULL COMMENT 'ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '名称',
|
||||
`size` bigint(20) NOT NULL COMMENT '大小(字节)',
|
||||
`url` varchar(512) NOT NULL COMMENT 'URL',
|
||||
`extension` varchar(100) DEFAULT NULL COMMENT '扩展名',
|
||||
`thumbnail_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小(字节)',
|
||||
`thumbnail_url` varchar(512) DEFAULT NULL COMMENT '缩略图URL',
|
||||
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)',
|
||||
`storage_id` bigint(20) NOT NULL COMMENT '存储ID',
|
||||
`create_user` bigint(20) NOT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_user` bigint(20) NOT NULL COMMENT '修改人',
|
||||
`update_time` datetime NOT NULL COMMENT '修改时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_url`(`url`) USING BTREE,
|
||||
INDEX `idx_type`(`type`) USING BTREE,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset Charles7c:2.5.0
|
||||
-- changeset Charles7c:1
|
||||
-- comment 初始化表数据
|
||||
-- 初始化默认菜单
|
||||
INSERT INTO "sys_menu"
|
||||
@@ -109,19 +109,20 @@ VALUES
|
||||
INSERT INTO "sys_option"
|
||||
("name", "code", "value", "default_value", "description", "update_user", "update_time")
|
||||
VALUES
|
||||
('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
|
||||
('版权信息', 'site_copyright', NULL,
|
||||
'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>',
|
||||
('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL),
|
||||
('版权信息', 'SITE_COPYRIGHT', NULL,
|
||||
'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>',
|
||||
'用于显示登录页面的底部版权信息。', NULL, NULL),
|
||||
('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
|
||||
('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
|
||||
('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL),
|
||||
('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL),
|
||||
('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL),
|
||||
('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL),
|
||||
('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL),
|
||||
('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL),
|
||||
('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL);
|
||||
('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
|
||||
('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
|
||||
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL),
|
||||
('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL),
|
||||
('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL),
|
||||
('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL),
|
||||
('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL),
|
||||
('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL),
|
||||
('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL),
|
||||
('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL);
|
||||
|
||||
-- 初始化默认字典
|
||||
INSERT INTO "sys_dict"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset Charles7c:2.5.0
|
||||
-- changeset Charles7c:1
|
||||
-- comment 初始化表结构
|
||||
CREATE TABLE IF NOT EXISTS "sys_menu" (
|
||||
"id" int8 NOT NULL,
|
||||
@@ -430,39 +430,39 @@ COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间';
|
||||
COMMENT ON TABLE "sys_storage" IS '存储表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sys_file" (
|
||||
"id" int8 NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"size" int8 NOT NULL,
|
||||
"url" varchar(512) NOT NULL,
|
||||
"extension" varchar(100) DEFAULT NULL,
|
||||
"type" int2 NOT NULL DEFAULT 1,
|
||||
"storage_id" int8 NOT NULL,
|
||||
"create_user" int8 NOT NULL,
|
||||
"create_time" timestamp NOT NULL,
|
||||
"update_user" int8 NOT NULL,
|
||||
"update_time" timestamp NOT NULL,
|
||||
"thumbnail_size" int8 DEFAULT NULL,
|
||||
"thumbnail_url" varchar(512) DEFAULT NULL,
|
||||
"id" int8 NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"size" int8 NOT NULL,
|
||||
"url" varchar(512) NOT NULL,
|
||||
"extension" varchar(100) DEFAULT NULL,
|
||||
"thumbnail_size" int8 DEFAULT NULL,
|
||||
"thumbnail_url" varchar(512) DEFAULT NULL,
|
||||
"type" int2 NOT NULL DEFAULT 1,
|
||||
"storage_id" int8 NOT NULL,
|
||||
"create_user" int8 NOT NULL,
|
||||
"create_time" timestamp NOT NULL,
|
||||
"update_user" int8 NOT NULL,
|
||||
"update_time" timestamp NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "idx_file_url" ON "sys_file" ("url");
|
||||
CREATE INDEX "idx_file_type" ON "sys_file" ("type");
|
||||
CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_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"."name" IS '名称';
|
||||
COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)';
|
||||
COMMENT ON COLUMN "sys_file"."url" IS 'URL';
|
||||
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"."id" IS 'ID';
|
||||
COMMENT ON COLUMN "sys_file"."name" IS '名称';
|
||||
COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)';
|
||||
COMMENT ON COLUMN "sys_file"."url" IS 'URL';
|
||||
COMMENT ON COLUMN "sys_file"."extension" IS '扩展名';
|
||||
COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)';
|
||||
COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL';
|
||||
COMMENT ON TABLE "sys_file" IS '文件表';
|
||||
COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL';
|
||||
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" (
|
||||
"table_name" varchar(64) NOT NULL,
|
||||
|
Reference in New Issue
Block a user