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 USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER;
private CacheConstants() {
}
}

View File

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

View File

@@ -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;
/**
* 管理员角色编码
*/

View File

@@ -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);
/**
* 手机号登录

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.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()));
}

View File

@@ -32,7 +32,7 @@ public enum MessageTemplateEnum {
/**
* 第三方登录
*/
SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。");
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。");
private final String title;
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 = "测试部")
@NotBlank(message = "名称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String name;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

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

View File

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

View File

@@ -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());
}
/**
* 名称是否存在
*

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.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")

View File

@@ -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("重置密码成功");

View File

@@ -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&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>',
('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, 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号-3</a>',
'用于显示登录页面的底部版权信息。', NULL, NULL),
('系统LOGO16*16', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*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);
('系统LOGO16*16', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', 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-9990 表示永久有效)', 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`

View File

@@ -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,

View File

@@ -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&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>',
('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, 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号-3</a>',
'用于显示登录页面的底部版权信息。', NULL, NULL),
('系统LOGO16*16', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*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);
('系统LOGO16*16', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL),
('系统LOGO33*33', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL),
('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-100 表示不锁定)。', 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-9990 表示永久有效)', 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"

View File

@@ -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,