Commit 3c97de77 by Lizh

迁移修改登录逻辑处理,兼容JJWT的老版本0.9.x

parent f1db6a85
package com.jomalls.custom.app.constant;
/**
* 用户常量信息
*/
public class UserConstants {
/**
* 平台内系统用户的唯一标志
*/
public static final String SYS_USER = "SYS_USER";
/**
* 正常状态
*/
public static final String NORMAL = "0";
/**
* 异常状态
*/
public static final String EXCEPTION = "1";
/**
* 用户封禁状态
*/
public static final String USER_DISABLE = "1";
/**
* 角色正常状态
*/
public static final String ROLE_NORMAL = "0";
/**
* 角色封禁状态
*/
public static final String ROLE_DISABLE = "1";
/**
* 部门正常状态
*/
public static final String DEPT_NORMAL = "0";
/**
* 部门停用状态
*/
public static final String DEPT_DISABLE = "1";
/**
* 字典正常状态
*/
public static final String DICT_NORMAL = "0";
/**
* 是否为系统默认(是)
*/
public static final String YES = "Y";
/**
* 是否菜单外链(是)
*/
public static final String YES_FRAME = "0";
/**
* 是否菜单外链(否)
*/
public static final String NO_FRAME = "1";
/**
* 菜单类型(目录)
*/
public static final String TYPE_DIR = "M";
/**
* 菜单类型(菜单)
*/
public static final String TYPE_MENU = "C";
/**
* 菜单类型(按钮)
*/
public static final String TYPE_BUTTON = "F";
/**
* Layout组件标识
*/
public final static String LAYOUT = "Layout";
/**
* ParentView组件标识
*/
public final static String PARENT_VIEW = "ParentView";
/**
* InnerLink组件标识
*/
public final static String INNER_LINK = "InnerLink";
/**
* 校验是否唯一的返回标识
*/
public final static boolean UNIQUE = true;
public final static boolean NOT_UNIQUE = false;
/**
* 用户名长度限制
*/
public static final int USERNAME_MIN_LENGTH = 2;
public static final int USERNAME_MAX_LENGTH = 20;
/**
* 密码长度限制
*/
public static final int PASSWORD_MIN_LENGTH = 5;
public static final int PASSWORD_MAX_LENGTH = 20;
}
...@@ -6,7 +6,7 @@ import com.jomalls.custom.app.vo.system.RouterVO; ...@@ -6,7 +6,7 @@ import com.jomalls.custom.app.vo.system.RouterVO;
import java.util.List; import java.util.List;
/** /**
* 登录服务接口 * 登录服务接口(与 custom-back SysLoginService 对齐)
* *
* @author Lizh * @author Lizh
*/ */
...@@ -17,9 +17,11 @@ public interface SysLoginService { ...@@ -17,9 +17,11 @@ public interface SysLoginService {
* *
* @param username 用户名 * @param username 用户名
* @param password 密码 * @param password 密码
* @param code 验证码
* @param uuid 验证码唯一标识
* @return JWT token * @return JWT token
*/ */
String login(String username, String password); String login(String username, String password, String code, String uuid);
/** /**
* 获取当前登录用户信息 * 获取当前登录用户信息
......
...@@ -2,6 +2,7 @@ package com.jomalls.custom.app.service.system.impl; ...@@ -2,6 +2,7 @@ package com.jomalls.custom.app.service.system.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jomalls.custom.app.constant.SecurityConstants; import com.jomalls.custom.app.constant.SecurityConstants;
import com.jomalls.custom.app.constant.UserConstants;
import com.jomalls.custom.app.enums.SysUserStatusEnums; import com.jomalls.custom.app.enums.SysUserStatusEnums;
import com.jomalls.custom.app.exception.ServiceException; import com.jomalls.custom.app.exception.ServiceException;
import com.jomalls.custom.app.service.system.SysConfigService; import com.jomalls.custom.app.service.system.SysConfigService;
...@@ -9,6 +10,8 @@ import com.jomalls.custom.app.service.system.SysLoginService; ...@@ -9,6 +10,8 @@ import com.jomalls.custom.app.service.system.SysLoginService;
import com.jomalls.custom.app.service.system.SysLogininforService; import com.jomalls.custom.app.service.system.SysLogininforService;
import com.jomalls.custom.app.service.system.impl.SysPasswordService; import com.jomalls.custom.app.service.system.impl.SysPasswordService;
import com.jomalls.custom.app.utils.BeanMapper; import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.utils.Convert;
import com.jomalls.custom.app.utils.DateUtils;
import com.jomalls.custom.app.vo.system.*; import com.jomalls.custom.app.vo.system.*;
import com.jomalls.custom.dal.entity.SysMenuEntity; import com.jomalls.custom.dal.entity.SysMenuEntity;
import com.jomalls.custom.dal.entity.SysUserEntity; import com.jomalls.custom.dal.entity.SysUserEntity;
...@@ -20,6 +23,7 @@ import com.jomalls.custom.security.SecurityUtils; ...@@ -20,6 +23,7 @@ import com.jomalls.custom.security.SecurityUtils;
import com.jomalls.custom.security.TokenHandle; import com.jomalls.custom.security.TokenHandle;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -45,14 +49,82 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -45,14 +49,82 @@ public class SysLoginServiceImpl implements SysLoginService {
private final SysPasswordService sysPasswordService; private final SysPasswordService sysPasswordService;
private final TokenHandle tokenHandle; private final TokenHandle tokenHandle;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final StringRedisTemplate stringRedisTemplate;
@Override /**
public String login(String username, String password) { * 校验验证码(与 custom-back SysLoginService.validateCaptcha 对齐)
// 登录前置校验:用户名密码不能为空 *
* @param username 用户名
* @param code 用户输入的验证码
* @param uuid 验证码唯一标识
*/
private void validateCaptcha(String username, String code, String uuid) {
// 读取验证码开关配置
boolean captchaEnabled = sysConfigService.selectCaptchaEnabled();
if (!captchaEnabled) {
return;
}
// 验证码未开启则不校验
if (!StringUtils.hasText(uuid)) {
sysLogininforService.recordLogininfor(username, "1", "验证码参数缺失");
throw new ServiceException("验证码参数缺失");
}
if (stringRedisTemplate == null) {
return;
}
String verifyKey = "captcha_codes:" + uuid;
String captcha = stringRedisTemplate.opsForValue().get(verifyKey);
if (captcha == null) {
sysLogininforService.recordLogininfor(username, "1", "验证码已失效");
throw new ServiceException("验证码已失效");
}
// 删除 Redis 中的验证码(一次性使用)
stringRedisTemplate.delete(verifyKey);
if (!captcha.equalsIgnoreCase(code)) {
sysLogininforService.recordLogininfor(username, "1", "验证码错误");
throw new ServiceException("验证码错误");
}
}
/**
* 登录前置校验(与 custom-back SysLoginService.loginPreCheck 对齐)
* <p>
* 校验项:用户名/密码长度、IP 黑名单
*/
private void loginPreCheck(String username, String password) {
// 用户名或密码为空
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
log.warn("登录失败:用户名或密码为空"); sysLogininforService.recordLogininfor(username, "1", "用户名或密码不能为空");
throw new ServiceException("用户名或密码不能为空"); throw new ServiceException("用户名或密码不能为空");
} }
// 密码长度校验(与 custom-back PASSWORD_MIN_LENGTH=5, PASSWORD_MAX_LENGTH=20 对齐)
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
sysLogininforService.recordLogininfor(username, "1", "密码长度不在允许范围内");
throw new ServiceException("密码长度不正确");
}
// 用户名长度校验(与 custom-back USERNAME_MIN_LENGTH=2, USERNAME_MAX_LENGTH=20 对齐)
if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
sysLogininforService.recordLogininfor(username, "1", "用户名长度不在允许范围内");
throw new ServiceException("用户名长度不正确");
}
// IP 黑名单校验(与 custom-back 对齐)
String blackIpList = sysConfigService.selectConfigByKey("sys.login.blackIPList");
if (StringUtils.hasText(blackIpList)) {
String currentIp = getCurrentIp();
if (isIpMatched(blackIpList, currentIp)) {
sysLogininforService.recordLogininfor(username, "1", "IP 已被列入黑名单");
throw new ServiceException("IP 已被列入黑名单,请联系管理员");
}
}
}
@Override
public String login(String username, String password, String code, String uuid) {
// 验证码校验(与 custom-back validateCaptcha 对齐)
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 检查账户是否被锁定 // 检查账户是否被锁定
sysPasswordService.checkLocked(username); sysPasswordService.checkLocked(username);
...@@ -113,13 +185,14 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -113,13 +185,14 @@ public class SysLoginServiceImpl implements SysLoginService {
} }
/** /**
* 记录登录信息(登录IP和登录时间) * 记录登录信息(登录IP和登录时间,与 custom-back recordLoginInfo 对齐
*/ */
private void recordLoginInfo(SysUserEntity user) { private void recordLoginInfo(SysUserEntity user) {
try { try {
SysUserEntity updateUser = new SysUserEntity(); SysUserEntity updateUser = new SysUserEntity();
updateUser.setUserId(user.getUserId()); updateUser.setUserId(user.getUserId());
updateUser.setLoginDate(new Date()); updateUser.setLoginDate(new Date());
updateUser.setLoginIp(getCurrentIp());
sysUserDomainService.updateById(updateUser); sysUserDomainService.updateById(updateUser);
} catch (Exception e) { } catch (Exception e) {
log.warn("记录用户[{}]登录信息失败: {}", user.getUserName(), e.getMessage()); log.warn("记录用户[{}]登录信息失败: {}", user.getUserName(), e.getMessage());
...@@ -148,34 +221,40 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -148,34 +221,40 @@ public class SysLoginServiceImpl implements SysLoginService {
Set<String> roles = sysRoleDomainService.selectRoleKeysByUserId(user.getUserId()); Set<String> roles = sysRoleDomainService.selectRoleKeysByUserId(user.getUserId());
result.setRoles(roles != null ? roles : Collections.emptySet()); result.setRoles(roles != null ? roles : Collections.emptySet());
// 权限集合 // 权限集合 — 从数据库刷新,检测变更则更新 LoginUser(与 custom-back getInfo 权限刷新对齐)
result.setPermissions(loginUser.getPermissions() != null ? Set<String> dbPermissions;
loginUser.getPermissions() : Collections.emptySet()); if (SecurityConstants.ADMIN_USER_ID.equals(loginUser.getUserId())) {
dbPermissions = new HashSet<>();
dbPermissions.add("*:*:*");
} else {
List<String> permList = sysMenuDomainService.selectMenuPermsByUserId(user.getUserId());
dbPermissions = permList != null ? new HashSet<>(permList) : Collections.emptySet();
}
Set<String> cachedPermissions = loginUser.getPermissions();
if (cachedPermissions == null || !cachedPermissions.equals(dbPermissions)) {
// 权限发生变化,刷新 LoginUser 中的权限并持久化到 Redis
loginUser.setPermissions(dbPermissions);
tokenHandle.refreshToken(loginUser);
log.info("用户[{}]权限已刷新并持久化到 Redis", loginUser.getUsername());
}
result.setPermissions(dbPermissions);
// 初始密码修改状态(与 custom-back SysLoginController.getInfo 对齐) // 初始密码修改状态(与 custom-back SysLoginController.getInfo 对齐)
// 读取 sys.account.initPasswordModify 配置,为 true 且 pwdUpdateDate 为 null 时才提示 // 与 custom-back initPasswordIsModify 对齐:initPasswordModify == 1 且 pwdUpdateDate == null
String initPwdModify = sysConfigService.selectConfigByKey("sys.account.initPasswordModify"); String initPwdModify = sysConfigService.selectConfigByKey("sys.account.initPasswordModify");
result.setIsDefaultModifyPwd("true".equals(initPwdModify) && user.getPwdUpdateDate() == null); result.setIsDefaultModifyPwd("1".equals(initPwdModify) && user.getPwdUpdateDate() == null);
// 密码过期状态 // 密码过期状态
// 读取 sys.account.passwordValidateDays 配置,大于 0 时检查是否超期 // 读取 sys.account.passwordValidateDays 配置,大于 0 时检查是否超期
String validateDaysStr = sysConfigService.selectConfigByKey("sys.account.passwordValidateDays");
boolean passwordExpired = false; boolean passwordExpired = false;
if (validateDaysStr != null) { Integer passwordValidateDays = Convert.toInt(sysConfigService.selectConfigByKey("sys.account.passwordValidateDays"));
try { if (passwordValidateDays != null && passwordValidateDays > 0) {
int validateDays = Integer.parseInt(validateDaysStr);
if (validateDays > 0) {
if (user.getPwdUpdateDate() == null) { if (user.getPwdUpdateDate() == null) {
// 从未修改过密码,视为过期 // 如果从未修改过初始密码,直接提醒过期
passwordExpired = true; passwordExpired = true;
} else { } else {
long diffMs = System.currentTimeMillis() - user.getPwdUpdateDate().getTime(); int diffDay = DateUtils.differentDaysByMillisecond(DateUtils.getNowDate(), user.getPwdUpdateDate());
long diffDays = diffMs / (1000 * 60 * 60 * 24); passwordExpired = diffDay > passwordValidateDays;
passwordExpired = diffDays > validateDays;
}
}
} catch (NumberFormatException e) {
log.warn("密码有效期配置格式异常: {}", validateDaysStr);
} }
} }
result.setIsPasswordExpired(passwordExpired); result.setIsPasswordExpired(passwordExpired);
...@@ -201,47 +280,121 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -201,47 +280,121 @@ public class SysLoginServiceImpl implements SysLoginService {
} }
/** /**
* 递归构建前端路由树 * 构建前端路由菜单(与 custom-back SysMenuService.buildMenus 完全对齐)
*
* @param parentId 父菜单ID
* @param allMenus 全部菜单列表
* @return 路由树
*/ */
private List<RouterVO> buildRouterTree(Long parentId, List<SysMenuEntity> allMenus) { private List<RouterVO> buildRouterTree(Long parentId, List<SysMenuEntity> allMenus) {
return allMenus.stream() List<SysMenuEntity> menus = allMenus.stream()
.filter(menu -> !"F".equals(menu.getMenuType()))
.filter(menu -> menu.getParentId().equals(parentId)) .filter(menu -> menu.getParentId().equals(parentId))
.filter(menu -> !"F".equals(menu.getMenuType())) // 过滤按钮类型
.sorted(Comparator.comparingInt(m -> m.getOrderNum() != null ? m.getOrderNum() : 0)) .sorted(Comparator.comparingInt(m -> m.getOrderNum() != null ? m.getOrderNum() : 0))
.map(menu -> { .collect(Collectors.toList());
List<RouterVO> routers = new ArrayList<>();
for (SysMenuEntity menu : menus) {
RouterVO router = new RouterVO(); RouterVO router = new RouterVO();
// 路由名称:优先用 routeName,无则用 path 驼峰
router.setName(StringUtils.hasText(menu.getRouteName()) ?
menu.getRouteName() : menu.getPath());
router.setPath(menu.getPath());
// 隐藏判断:visible = "1" 表示隐藏
router.setHidden("1".equals(menu.getVisible())); router.setHidden("1".equals(menu.getVisible()));
router.setComponent(menu.getComponent()); router.setName(getRouteName(menu));
router.setPath(getRouterPath(menu));
// 元数据 router.setComponent(getComponent(menu));
MetaVO meta = new MetaVO(); router.setQuery(menu.getQuery());
meta.setTitle(menu.getMenuName()); router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(),
meta.setIcon(menu.getIcon()); Integer.valueOf(1).equals(menu.getIsCache()), menu.getPath()));
// isCache/isFrame 是 Integer 类型:1=否
meta.setIsCache(!Integer.valueOf(1).equals(menu.getIsCache())); List<RouterVO> cMenus = buildRouterTree(menu.getMenuId(), allMenus);
meta.setIsFrame(Integer.valueOf(1).equals(menu.getIsFrame())); if (!cMenus.isEmpty() && "M".equals(menu.getMenuType())) {
router.setMeta(meta); router.setAlwaysShow(true);
router.setRedirect("noRedirect");
// 递归构建子路由 router.setChildren(cMenus);
List<RouterVO> children = buildRouterTree(menu.getMenuId(), allMenus); } else if (isMenuFrame(menu)) {
router.setChildren(children.isEmpty() ? null : children); router.setMeta(null);
// 有子路由时设置 alwaysShow List<RouterVO> childrenList = new ArrayList<>();
if (!children.isEmpty()) { RouterVO children = new RouterVO();
router.setAlwaysShow(Boolean.TRUE); children.setPath(menu.getPath());
} children.setComponent(menu.getComponent());
children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
return router; children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(),
}) Integer.valueOf(1).equals(menu.getIsCache()), menu.getPath()));
.collect(Collectors.toList()); children.setQuery(menu.getQuery());
childrenList.add(children);
router.setChildren(childrenList);
} else if (menu.getParentId() == 0 && isInnerLink(menu)) {
router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon()));
router.setPath("/");
List<RouterVO> childrenList = new ArrayList<>();
RouterVO children = new RouterVO();
String routerPath = innerLinkReplaceEach(menu.getPath());
children.setPath(routerPath);
children.setComponent("InnerLink");
children.setName(getRouteName(menu.getRouteName(), routerPath));
children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), menu.getPath()));
childrenList.add(children);
router.setChildren(childrenList);
} else {
router.setChildren(cMenus.isEmpty() ? null : cMenus);
}
routers.add(router);
}
return routers;
}
// ========== 路由构建辅助方法(与 custom-back SysMenuService 对齐) ==========
private String getRouteName(SysMenuEntity menu) {
if (isMenuFrame(menu)) return "";
return getRouteName(menu.getRouteName(), menu.getPath());
}
private String getRouteName(String name, String path) {
String routerName = StringUtils.hasText(name) ? name : path;
if (!StringUtils.hasText(routerName)) return "";
return routerName.substring(0, 1).toUpperCase() + (routerName.length() > 1 ? routerName.substring(1) : "");
}
private String getRouterPath(SysMenuEntity menu) {
String routerPath = menu.getPath();
if (menu.getParentId() != 0 && isInnerLink(menu)) {
routerPath = innerLinkReplaceEach(routerPath);
}
if (menu.getParentId() == 0 && "M".equals(menu.getMenuType())
&& Integer.valueOf(1).equals(menu.getIsFrame())) {
routerPath = "/" + menu.getPath();
} else if (isMenuFrame(menu)) {
routerPath = "/";
}
return routerPath;
}
private String getComponent(SysMenuEntity menu) {
String component = "Layout";
if (StringUtils.hasText(menu.getComponent()) && !isMenuFrame(menu)) {
component = menu.getComponent();
} else if (!StringUtils.hasText(menu.getComponent()) && menu.getParentId() != 0 && isInnerLink(menu)) {
component = "InnerLink";
} else if (!StringUtils.hasText(menu.getComponent()) && isParentView(menu)) {
component = "ParentView";
}
return component;
}
private boolean isMenuFrame(SysMenuEntity menu) {
return menu.getParentId() == 0 && "C".equals(menu.getMenuType())
&& Integer.valueOf(1).equals(menu.getIsFrame());
}
private boolean isInnerLink(SysMenuEntity menu) {
return Integer.valueOf(1).equals(menu.getIsFrame())
&& menu.getPath() != null
&& (menu.getPath().startsWith("http://") || menu.getPath().startsWith("https://"));
}
private boolean isParentView(SysMenuEntity menu) {
return menu.getParentId() != 0 && "M".equals(menu.getMenuType());
}
private String innerLinkReplaceEach(String path) {
if (path == null) return "";
return path.replace("http://", "").replace("https://", "")
.replace("www.", "").replace(".", "/").replace(":", "/");
} }
@Override @Override
...@@ -253,4 +406,59 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -253,4 +406,59 @@ public class SysLoginServiceImpl implements SysLoginService {
log.info("用户[{}]退出登录,token 已失效", loginUser.getUsername()); log.info("用户[{}]退出登录,token 已失效", loginUser.getUsername());
} }
} }
/**
* 获取当前请求的客户端 IP
*/
private String getCurrentIp() {
try {
org.springframework.web.context.request.ServletRequestAttributes attributes =
(org.springframework.web.context.request.ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
if (attributes != null) {
jakarta.servlet.http.HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
} catch (Exception e) {
log.debug("获取客户端 IP 失败: {}", e.getMessage());
}
return "";
}
/**
* 检查 IP 是否匹配黑名单(与 custom-back IpUtils.isMatchedIp 对齐)
* <p>
* 支持:精确匹配、通配符匹配(如 192.168.*.100)
*/
private boolean isIpMatched(String blackIpList, String currentIp) {
if (!StringUtils.hasText(currentIp)) {
return false;
}
String[] blackIps = blackIpList.split(",");
for (String pattern : blackIps) {
String trimmed = pattern.trim();
if (trimmed.isEmpty()) {
continue;
}
// 通配符匹配
if (trimmed.contains("*")) {
String regex = "\\Q" + trimmed.replace("*", "\\E.*\\Q") + "\\E";
if (currentIp.matches(regex)) {
return true;
}
} else if (currentIp.equals(trimmed)) {
return true;
}
}
return false;
}
} }
...@@ -38,7 +38,7 @@ public class SysPasswordService { ...@@ -38,7 +38,7 @@ public class SysPasswordService {
} }
/** /**
* 验证密码(与 custom-back SysPasswordService.validate 对齐) * 验证密码
* <p> * <p>
* 登录失败时累计重试次数并锁定账户 * 登录失败时累计重试次数并锁定账户
*/ */
...@@ -57,7 +57,9 @@ public class SysPasswordService { ...@@ -57,7 +57,9 @@ public class SysPasswordService {
* 登录失败后增加重试计数,超过阈值锁定账户 * 登录失败后增加重试计数,超过阈值锁定账户
*/ */
public void recordLoginFailure(String username) { public void recordLoginFailure(String username) {
if (stringRedisTemplate == null) return; if (stringRedisTemplate == null) {
return;
}
String key = getCacheKey(username); String key = getCacheKey(username);
Long retryCount = stringRedisTemplate.opsForValue().increment(key); Long retryCount = stringRedisTemplate.opsForValue().increment(key);
if (retryCount != null && retryCount == 1) { if (retryCount != null && retryCount == 1) {
......
package com.jomalls.custom.app.utils;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* 字符集工具类
*/
public class CharsetKit {
/**
* ISO-8859-1
*/
public static final String ISO_8859_1 = "ISO-8859-1";
/**
* UTF-8
*/
public static final String UTF_8 = "UTF-8";
/**
* GBK
*/
public static final String GBK = "GBK";
/**
* ISO-8859-1
*/
public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
/**
* UTF-8
*/
public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
/**
* GBK
*/
public static final Charset CHARSET_GBK = Charset.forName(GBK);
/**
* 转换为Charset对象
*
* @param charset 字符集,为空则返回默认字符集
* @return Charset
*/
public static Charset charset(String charset) {
return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集,默认ISO-8859-1
* @param destCharset 目标字符集,默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, String srcCharset, String destCharset) {
return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集,默认ISO-8859-1
* @param destCharset 目标字符集,默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, Charset srcCharset, Charset destCharset) {
if (null == srcCharset) {
srcCharset = StandardCharsets.ISO_8859_1;
}
if (null == destCharset) {
destCharset = StandardCharsets.UTF_8;
}
if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) {
return source;
}
return new String(source.getBytes(srcCharset), destCharset);
}
/**
* @return 系统字符集编码
*/
public static String systemCharset() {
return Charset.defaultCharset().name();
}
}
package com.jomalls.custom.app.utils;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.util.Set;
/**
* 类型转换器
*/
public class Convert {
/**
* 转换为字符串<br>
* 如果给定的值为null,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static String toStr(Object value, String defaultValue) {
if (null == value) {
return defaultValue;
}
if (value instanceof String) {
return (String) value;
}
return value.toString();
}
/**
* 转换为字符串<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static String toStr(Object value) {
return toStr(value, null);
}
/**
* 转换为字符<br>
* 如果给定的值为null,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Character toChar(Object value, Character defaultValue) {
if (null == value) {
return defaultValue;
}
if (value instanceof Character) {
return (Character) value;
}
final String valueStr = toStr(value, null);
return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0);
}
/**
* 转换为字符<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Character toChar(Object value) {
return toChar(value, null);
}
/**
* 转换为byte<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Byte toByte(Object value, Byte defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Byte) {
return (Byte) value;
}
if (value instanceof Number) {
return ((Number) value).byteValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return Byte.parseByte(valueStr);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为byte<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Byte toByte(Object value) {
return toByte(value, null);
}
/**
* 转换为Short<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Short toShort(Object value, Short defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Short) {
return (Short) value;
}
if (value instanceof Number) {
return ((Number) value).shortValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return Short.parseShort(valueStr.trim());
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为Short<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Short toShort(Object value) {
return toShort(value, null);
}
/**
* 转换为Number<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Number toNumber(Object value, Number defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Number) {
return (Number) value;
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return NumberFormat.getInstance().parse(valueStr);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为Number<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Number toNumber(Object value) {
return toNumber(value, null);
}
/**
* 转换为int<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Integer toInt(Object value, Integer defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return Integer.parseInt(valueStr.trim());
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为int<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Integer toInt(Object value) {
return toInt(value, null);
}
/**
* 转换为Integer数组<br>
*
* @param str 被转换的值
* @return 结果
*/
public static Integer[] toIntArray(String str) {
return toIntArray(",", str);
}
/**
* 转换为Long数组<br>
*
* @param str 被转换的值
* @return 结果
*/
public static Long[] toLongArray(String str) {
return toLongArray(",", str);
}
/**
* 转换为Integer数组<br>
*
* @param split 分隔符
* @param split 被转换的值
* @return 结果
*/
public static Integer[] toIntArray(String split, String str) {
if (StringUtils.isEmpty(str)) {
return new Integer[]{};
}
String[] arr = str.split(split);
final Integer[] ints = new Integer[arr.length];
for (int i = 0; i < arr.length; i++) {
final Integer v = toInt(arr[i], 0);
ints[i] = v;
}
return ints;
}
/**
* 转换为Long数组<br>
*
* @param split 分隔符
* @param str 被转换的值
* @return 结果
*/
public static Long[] toLongArray(String split, String str) {
if (StringUtils.isEmpty(str)) {
return new Long[]{};
}
String[] arr = str.split(split);
final Long[] longs = new Long[arr.length];
for (int i = 0; i < arr.length; i++) {
final Long v = toLong(arr[i], null);
longs[i] = v;
}
return longs;
}
/**
* 转换为String数组<br>
*
* @param str 被转换的值
* @return 结果
*/
public static String[] toStrArray(String str) {
if (StringUtils.isEmpty(str)) {
return new String[]{};
}
return toStrArray(",", str);
}
/**
* 转换为String数组<br>
*
* @param split 分隔符
* @param split 被转换的值
* @return 结果
*/
public static String[] toStrArray(String split, String str) {
return str.split(split);
}
/**
* 转换为long<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Long toLong(Object value, Long defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
// 支持科学计数法
return new BigDecimal(valueStr.trim()).longValue();
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为long<br>
* 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Long toLong(Object value) {
return toLong(value, null);
}
/**
* 转换为double<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Double toDouble(Object value, Double defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
// 支持科学计数法
return new BigDecimal(valueStr.trim()).doubleValue();
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为double<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Double toDouble(Object value) {
return toDouble(value, null);
}
/**
* 转换为Float<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Float toFloat(Object value, Float defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Float) {
return (Float) value;
}
if (value instanceof Number) {
return ((Number) value).floatValue();
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return Float.parseFloat(valueStr.trim());
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为Float<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Float toFloat(Object value) {
return toFloat(value, null);
}
/**
* 转换为boolean<br>
* String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static Boolean toBool(Object value, Boolean defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
valueStr = valueStr.trim().toLowerCase();
return switch (valueStr) {
case "true", "yes", "ok", "1", "是" -> true;
case "false", "no", "0", "否" -> false;
default -> defaultValue;
};
}
/**
* 转换为boolean<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static Boolean toBool(Object value) {
return toBool(value, null);
}
/**
* 转换为Enum对象<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
*
* @param clazz Enum的Class
* @param value 值
* @param defaultValue 默认值
* @return Enum
*/
public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value, E defaultValue) {
if (value == null) {
return defaultValue;
}
if (clazz.isAssignableFrom(value.getClass())) {
@SuppressWarnings("unchecked")
E myE = (E) value;
return myE;
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return Enum.valueOf(clazz, valueStr);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为Enum对象<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
*
* @param clazz Enum的Class
* @param value 值
* @return Enum
*/
public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value) {
return toEnum(clazz, value, null);
}
/**
* 转换为BigInteger<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static BigInteger toBigInteger(Object value, BigInteger defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof BigInteger) {
return (BigInteger) value;
}
if (value instanceof Long) {
return BigInteger.valueOf((Long) value);
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return new BigInteger(valueStr);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为BigInteger<br>
* 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static BigInteger toBigInteger(Object value) {
return toBigInteger(value, null);
}
/**
* 转换为BigDecimal<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @param defaultValue 转换错误时的默认值
* @return 结果
*/
public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof BigDecimal) {
return (BigDecimal) value;
}
if (value instanceof Long) {
return new BigDecimal((Long) value);
}
if (value instanceof Double) {
return BigDecimal.valueOf((Double) value);
}
if (value instanceof Integer) {
return new BigDecimal((Integer) value);
}
final String valueStr = toStr(value, null);
if (StringUtils.isEmpty(valueStr)) {
return defaultValue;
}
try {
return new BigDecimal(valueStr);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 转换为BigDecimal<br>
* 如果给定的值为空,或者转换失败,返回默认值<br>
* 转换失败不会报错
*
* @param value 被转换的值
* @return 结果
*/
public static BigDecimal toBigDecimal(Object value) {
return toBigDecimal(value, null);
}
/**
* 将对象转为字符串<br>
* 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
*
* @param obj 对象
* @return 字符串
*/
public static String utf8Str(Object obj) {
return str(obj, CharsetKit.CHARSET_UTF_8);
}
/**
* 将对象转为字符串<br>
* 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
*
* @param obj 对象
* @param charsetName 字符集
* @return 字符串
*/
public static String str(Object obj, String charsetName) {
return str(obj, Charset.forName(charsetName));
}
/**
* 将对象转为字符串<br>
* 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
*
* @param obj 对象
* @param charset 字符集
* @return 字符串
*/
public static String str(Object obj, Charset charset) {
if (null == obj) {
return null;
}
if (obj instanceof String) {
return (String) obj;
} else if (obj instanceof byte[] || obj instanceof Byte[]) {
if (obj instanceof byte[]) {
return str((byte[]) obj, charset);
} else {
Byte[] bytes = (Byte[]) obj;
int length = bytes.length;
byte[] dest = new byte[length];
for (int i = 0; i < length; i++) {
dest[i] = bytes[i];
}
return str(dest, charset);
}
} else if (obj instanceof ByteBuffer) {
return str((ByteBuffer) obj, charset);
}
return obj.toString();
}
/**
* 将byte数组转为字符串
*
* @param bytes byte数组
* @param charset 字符集
* @return 字符串
*/
public static String str(byte[] bytes, String charset) {
return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset));
}
/**
* 解码字节码
*
* @param data 字符串
* @param charset 字符集,如果此字段为空,则解码的结果取决于平台
* @return 解码后的字符串
*/
public static String str(byte[] data, Charset charset) {
if (data == null) {
return null;
}
if (null == charset) {
return new String(data);
}
return new String(data, charset);
}
/**
* 将编码的byteBuffer数据转换为字符串
*
* @param data 数据
* @param charset 字符集,如果为空使用当前系统字符集
* @return 字符串
*/
public static String str(ByteBuffer data, String charset) {
if (data == null) {
return null;
}
return str(data, Charset.forName(charset));
}
/**
* 将编码的byteBuffer数据转换为字符串
*
* @param data 数据
* @param charset 字符集,如果为空使用当前系统字符集
* @return 字符串
*/
public static String str(ByteBuffer data, Charset charset) {
if (null == charset) {
charset = Charset.defaultCharset();
}
return charset.decode(data).toString();
}
// ----------------------------------------------------------------------- 全角半角转换
/**
* 半角转全角
*
* @param input String.
* @return 全角字符串.
*/
public static String toSBC(String input) {
return toSBC(input, null);
}
/**
* 半角转全角
*
* @param input String
* @param notConvertSet 不替换的字符集合
* @return 全角字符串.
*/
public static String toSBC(String input, Set<Character> notConvertSet) {
char[] c = input.toCharArray();
for (int i = 0; i < c.length; i++) {
if (null != notConvertSet && notConvertSet.contains(c[i])) {
// 跳过不替换的字符
continue;
}
if (c[i] == ' ') {
c[i] = '\u3000';
} else if (c[i] < '\177') {
c[i] = (char) (c[i] + 65248);
}
}
return new String(c);
}
/**
* 全角转半角
*
* @param input String.
* @return 半角字符串
*/
public static String toDBC(String input) {
return toDBC(input, null);
}
/**
* 替换全角为半角
*
* @param text 文本
* @param notConvertSet 不替换的字符集合
* @return 替换后的字符
*/
public static String toDBC(String text, Set<Character> notConvertSet) {
char[] c = text.toCharArray();
for (int i = 0; i < c.length; i++) {
if (null != notConvertSet && notConvertSet.contains(c[i])) {
// 跳过不替换的字符
continue;
}
if (c[i] == '\u3000') {
c[i] = ' ';
} else if (c[i] > '\uFF00' && c[i] < '\uFF5F') {
c[i] = (char) (c[i] - 65248);
}
}
return new String(c);
}
/**
* 数字金额大写转换 先写个完整的然后将如零拾替换成零
*
* @param n 数字
* @return 中文大写数字
*/
public static String digitUppercase(double n) {
String[] fraction = {"角", "分"};
String[] digit = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
String[][] unit = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}};
String head = n < 0 ? "负" : "";
n = Math.abs(n);
String s = "";
for (int i = 0; i < fraction.length; i++) {
// 优化double计算精度丢失问题
BigDecimal nNum = new BigDecimal(n);
BigDecimal decimal = new BigDecimal(10);
BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN);
double d = scale.doubleValue();
s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", "");
}
if (s.length() < 1) {
s = "整";
}
int integerPart = (int) Math.floor(n);
for (int i = 0; i < unit[0].length && integerPart > 0; i++) {
String p = "";
for (int j = 0; j < unit[1].length && n > 0; j++) {
p = digit[integerPart % 10] + unit[1][j] + p;
integerPart = integerPart / 10;
}
s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
}
return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
}
}
package com.jomalls.custom.app.utils;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.lang.management.ManagementFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.util.Date;
/**
* 时间工具类
*/
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
public static String YYYY = "yyyy";
public static String YYYY_MM = "yyyy-MM";
public static String YYYY_MM_DD = "yyyy-MM-dd";
public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
private static String[] parsePatterns = {
"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
"yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
/**
* 获取当前Date型日期
*
* @return Date() 当前日期
*/
public static Date getNowDate() {
return new Date();
}
/**
* 获取当前日期, 默认格式为yyyy-MM-dd
*
* @return String
*/
public static String getDate() {
return dateTimeNow(YYYY_MM_DD);
}
public static final String getTime() {
return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
}
public static final String dateTimeNow() {
return dateTimeNow(YYYYMMDDHHMMSS);
}
public static final String dateTimeNow(final String format) {
return parseDateToStr(format, new Date());
}
public static final String dateTime(final Date date) {
return parseDateToStr(YYYY_MM_DD, date);
}
public static final String parseDateToStr(final String format, final Date date) {
return new SimpleDateFormat(format).format(date);
}
public static final Date dateTime(final String format, final String ts) {
try {
return new SimpleDateFormat(format).parse(ts);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* 日期路径 即年/月/日 如2018/08/08
*/
public static final String datePath() {
Date now = new Date();
return DateFormatUtils.format(now, "yyyy/MM/dd");
}
/**
* 日期路径 即年/月/日 如20180808
*/
public static final String dateTime() {
Date now = new Date();
return DateFormatUtils.format(now, "yyyyMMdd");
}
/**
* 日期型字符串转化为日期 格式
*/
public static Date parseDate(Object str) {
if (str == null) {
return null;
}
try {
return parseDate(str.toString(), parsePatterns);
} catch (ParseException e) {
return null;
}
}
/**
* 获取服务器启动时间
*/
public static Date getServerStartDate() {
long time = ManagementFactory.getRuntimeMXBean().getStartTime();
return new Date(time);
}
/**
* 计算相差天数
*/
public static int differentDaysByMillisecond(Date date1, Date date2) {
return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
}
/**
* 计算时间差
*
* @param endDate 最后时间
* @param startTime 开始时间
* @return 时间差(天/小时/分钟)
*/
public static String timeDistance(Date endDate, Date startTime) {
long nd = 1000 * 24 * 60 * 60;
long nh = 1000 * 60 * 60;
long nm = 1000 * 60;
// long ns = 1000;
// 获得两个时间的毫秒时间差异
long diff = endDate.getTime() - startTime.getTime();
// 计算差多少天
long day = diff / nd;
// 计算差多少小时
long hour = diff % nd / nh;
// 计算差多少分钟
long min = diff % nd % nh / nm;
// 计算差多少秒//输出结果
// long sec = diff % nd % nh % nm / ns;
return day + "天" + hour + "小时" + min + "分钟";
}
/**
* 增加 LocalDateTime ==> Date
*/
public static Date toDate(LocalDateTime temporalAccessor) {
ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
return Date.from(zdt.toInstant());
}
/**
* 增加 LocalDate ==> Date
*/
public static Date toDate(LocalDate temporalAccessor) {
LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
return Date.from(zdt.toInstant());
}
}
...@@ -2,17 +2,49 @@ package com.jomalls.custom.app.vo.system; ...@@ -2,17 +2,49 @@ package com.jomalls.custom.app.vo.system;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* 路由元数据 VO * 路由元数据 VO(与 custom-back MetaVo 对齐)
* *
* @author Lizh * @author Lizh
*/ */
@Data @Data
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaVO { public class MetaVO {
/** 路由在侧边栏和面包屑中展示的名字 */
private String title; private String title;
/** 路由图标 */
private String icon; private String icon;
private Boolean isCache; /** 是否不缓存(true = 不被 keep-alive 缓存) */
private Boolean isFrame; private Boolean noCache;
/** 内链地址(http(s):// 开头) */
private String link;
public MetaVO(String title, String icon) {
this.title = title;
this.icon = icon;
}
public MetaVO(String title, String icon, boolean noCache) {
this.title = title;
this.icon = icon;
this.noCache = noCache;
}
public MetaVO(String title, String icon, String link) {
this.title = title;
this.icon = icon;
this.link = link;
}
public MetaVO(String title, String icon, boolean noCache, String link) {
this.title = title;
this.icon = icon;
this.noCache = noCache;
if (link != null && (link.startsWith("http://") || link.startsWith("https://"))) {
this.link = link;
}
}
} }
...@@ -18,6 +18,8 @@ public class RouterVO { ...@@ -18,6 +18,8 @@ public class RouterVO {
private Boolean hidden; private Boolean hidden;
private String redirect; private String redirect;
private String component; private String component;
/** 路由参数(如 {"id": 1, "name": "ry"}) */
private String query;
private Boolean alwaysShow; private Boolean alwaysShow;
private MetaVO meta; private MetaVO meta;
private List<RouterVO> children; private List<RouterVO> children;
......
...@@ -13,6 +13,7 @@ import javax.crypto.SecretKey; ...@@ -13,6 +13,7 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64; import java.util.Base64;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
...@@ -48,13 +49,13 @@ public class TokenCompatibilityParser { ...@@ -48,13 +49,13 @@ public class TokenCompatibilityParser {
* @throws JwtException 解析失败 * @throws JwtException 解析失败
*/ */
public Claims parseTokenCompatibly(String token, String secret) throws JwtException { public Claims parseTokenCompatibly(String token, String secret) throws JwtException {
// 方式1:兼容旧版本 JJWT 0.9.1 短密钥 token // 方式1:JJWT 0.12.x 标准方式 + SHA-256 派生密钥(custom-server 新 token)
Claims result = tryLegacyParse(token, secret); Claims result = tryStandardParse(token, secret);
if (result != null) { if (result != null) {
return result; return result;
} }
// 方式2:优先尝试 JJWT 0.12.x 标准方式(适用于新版长密钥生成的 token // 方式2:手动 HMAC-SHA512 验证(兼容 JJWT 0.9.1 旧 token,先试 SHA-256 再试原始密钥
result = tryStandardParse(token, secret); result = tryLegacyParse(token, secret);
if (result != null) { if (result != null) {
return result; return result;
} }
...@@ -63,17 +64,19 @@ public class TokenCompatibilityParser { ...@@ -63,17 +64,19 @@ public class TokenCompatibilityParser {
} }
/** /**
* JJWT 0.12.x 标准方式:secret 直接作为原始密钥字节 * JJWT 0.12.x 标准方式:对 secret 做 SHA-256 派生密钥(与 TokenHandle.createToken 一致)
*/ */
private Claims tryStandardParse(String token, String secret) { private Claims tryStandardParse(String token, String secret) {
try { try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] derivedKey = digest.digest(secret.getBytes(StandardCharsets.UTF_8));
SecretKey key = Keys.hmacShaKeyFor(derivedKey);
return Jwts.parser() return Jwts.parser()
.verifyWith(key) .verifyWith(key)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.getPayload(); .getPayload();
} catch (JwtException e) { } catch (JwtException | NoSuchAlgorithmException e) {
log.debug("标准方式解析失败: {}", e.getMessage()); log.debug("标准方式解析失败: {}", e.getMessage());
} }
return null; return null;
...@@ -81,24 +84,28 @@ public class TokenCompatibilityParser { ...@@ -81,24 +84,28 @@ public class TokenCompatibilityParser {
/** /**
* 兼容 JJWT 0.9.1 旧格式 token * 兼容 JJWT 0.9.1 旧格式 token
* * <p>
* <p>custom-back 密钥推导: signingKey = Base64(secret.getBytes(UTF_8)) * 依次尝试两种密钥验证签名:
* <p>因为密钥只有 8 字节,JJWT 0.12.x 拒绝,所以手工验证签名后解码 payload * <ol>
* <li>SHA-256 派生密钥(32字节)— custom-server 新 token</li>
* <li>原始密钥字节 — custom-back 旧 token</li>
* </ol>
*/ */
private Claims tryLegacyParse(String token, String secret) { private Claims tryLegacyParse(String token, String secret) {
try { try {
// 1. 按 JJWT 0.9.1 方式推导签名密钥 // 先尝试 SHA-256 派生密钥(与 createToken 一致)
byte[] signingKey = deriveLegacySigningKey(secret); byte[] sha256Key = deriveSha256Key(secret);
if (verifyHmacSha512Signature(token, sha256Key)) {
// 2. 手动验证 HMAC-SHA512 签名 return decodePayloadToClaims(token);
if (!verifyHmacSha512Signature(token, signingKey)) {
log.debug("旧版 token 签名验证失败");
return null;
} }
// 回退:尝试原始密钥(兼容 custom-back 旧 token)
// 3. 签名有效,手动解码 payload 为 Claims byte[] legacyKey = deriveLegacySigningKey(secret);
if (!java.util.Arrays.equals(sha256Key, legacyKey)
&& verifyHmacSha512Signature(token, legacyKey)) {
return decodePayloadToClaims(token); return decodePayloadToClaims(token);
}
log.debug("旧版 token 签名验证失败");
return null;
} catch (Exception e) { } catch (Exception e) {
log.debug("旧版兼容解析失败: {}", e.getMessage()); log.debug("旧版兼容解析失败: {}", e.getMessage());
return null; return null;
...@@ -106,6 +113,18 @@ public class TokenCompatibilityParser { ...@@ -106,6 +113,18 @@ public class TokenCompatibilityParser {
} }
/** /**
* SHA-256 派生 32 字节密钥(与 TokenHandle.createToken 一致)
*/
private static byte[] deriveSha256Key(String secret) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(secret.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 算法不可用", e);
}
}
/**
* 按 JJWT 0.9.1 方式推导签名密钥 * 按 JJWT 0.9.1 方式推导签名密钥
* *
* <p>custom-back 调用链: * <p>custom-back 调用链:
...@@ -128,7 +147,7 @@ public class TokenCompatibilityParser { ...@@ -128,7 +147,7 @@ public class TokenCompatibilityParser {
} }
/** /**
* 手动验证 HMAC-SHA512 签名 * 手动验证 HMAC 签名(根据密钥长度自动选择 HS256/HS384/HS512)
* *
* @param token JWT token (header.payload.signature) * @param token JWT token (header.payload.signature)
* @param keyBytes 签名密钥字节 * @param keyBytes 签名密钥字节
...@@ -141,18 +160,32 @@ public class TokenCompatibilityParser { ...@@ -141,18 +160,32 @@ public class TokenCompatibilityParser {
return false; return false;
} }
// 读取 JWT header 确定算法
String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked")
Map<String, Object> headerMap = OBJECT_MAPPER.readValue(headerJson, Map.class);
String alg = (String) headerMap.getOrDefault("alg", "HS512");
String headerPayload = parts[0] + "." + parts[1]; String headerPayload = parts[0] + "." + parts[1];
byte[] signatureBytes = Base64.getUrlDecoder().decode(parts[2]); byte[] signatureBytes = Base64.getUrlDecoder().decode(parts[2]);
Mac mac = Mac.getInstance("HmacSHA512"); // 根据算法选择对应的 Mac 实例
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA512"); String macAlgorithm;
switch (alg) {
case "HS256": macAlgorithm = "HmacSHA256"; break;
case "HS384": macAlgorithm = "HmacSHA384"; break;
default: macAlgorithm = "HmacSHA512"; break;
}
Mac mac = Mac.getInstance(macAlgorithm);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, macAlgorithm);
mac.init(keySpec); mac.init(keySpec);
byte[] computedSignature = mac.doFinal( byte[] computedSignature = mac.doFinal(
headerPayload.getBytes(StandardCharsets.UTF_8)); headerPayload.getBytes(StandardCharsets.UTF_8));
return MessageDigest.isEqual(computedSignature, signatureBytes); return MessageDigest.isEqual(computedSignature, signatureBytes);
} catch (Exception e) { } catch (Exception e) {
log.debug("HMAC-SHA512 签名验证异常: {}", e.getMessage()); log.debug("HMAC 签名验证异常: {}", e.getMessage());
return false; return false;
} }
} }
......
...@@ -70,7 +70,6 @@ public class TokenHandle { ...@@ -70,7 +70,6 @@ public class TokenHandle {
* <li>Redis 不可用或 key 不存在 → 回退到 JWT claims 直接提取</li> * <li>Redis 不可用或 key 不存在 → 回退到 JWT claims 直接提取</li>
* </ol> * </ol>
* *
* @param request 请求对象
* @return 用户信息,解析失败返回 null * @return 用户信息,解析失败返回 null
*/ */
public LoginUser getLoginUser(String token) { public LoginUser getLoginUser(String token) {
...@@ -145,16 +144,19 @@ public class TokenHandle { ...@@ -145,16 +144,19 @@ public class TokenHandle {
loginUser.setExpireTime(((Number) expireObj).longValue()); loginUser.setExpireTime(((Number) expireObj).longValue());
} }
// 权限列表 // 权限列表(fastjson2 反序列化为 JSONArray,需兼容 Collection 类型)
@SuppressWarnings("unchecked") Set<String> permissionsSet = new HashSet<>();
HashSet<String> permissionsList = (HashSet<String>) data.get("permissions"); Object permsObj = data.get("permissions");
if (permissionsList != null) { if (permsObj instanceof java.util.Collection) {
loginUser.setPermissions(new HashSet<>(permissionsList)); for (Object p : (java.util.Collection<?>) permsObj) {
} else { if (p != null) {
loginUser.setPermissions(new HashSet<>()); permissionsSet.add(String.valueOf(p));
}
} }
}
loginUser.setPermissions(permissionsSet);
log.debug("从 Redis 获取用户[{}]信息成功,权限数: {}", username, permissionsList != null ? permissionsList.size() : 0); log.debug("从 Redis 获取用户[{}]信息成功,权限数: {}", username, permissionsSet.size());
return loginUser; return loginUser;
} catch (Exception e) { } catch (Exception e) {
...@@ -168,13 +170,17 @@ public class TokenHandle { ...@@ -168,13 +170,17 @@ public class TokenHandle {
*/ */
private LoginUser buildFromClaims(Claims claims) { private LoginUser buildFromClaims(Claims claims) {
Long userId = claims.get("id", Long.class); Long userId = claims.get("id", Long.class);
// 兼容 custom-back (sub) 和旧 custom-server (account) 两种 username 位置
String username = claims.get("account", String.class); String username = claims.get("account", String.class);
if (username == null) { if (username == null) {
username = claims.get("sub", String.class); // custom-back JWT_USERNAME
}
if (username == null) {
username = claims.get("username", String.class); username = claims.get("username", String.class);
} }
if (userId == null || !StringUtils.hasText(username)) { if (userId == null || !StringUtils.hasText(username)) {
log.warn("JWT claims 缺少 id 或 account/username"); log.warn("JWT claims 缺少 id 或 account/sub/username");
return null; return null;
} }
...@@ -228,46 +234,74 @@ public class TokenHandle { ...@@ -228,46 +234,74 @@ public class TokenHandle {
} }
/** /**
* 创建令牌(JJWT 0.12.x 标准格式 * 创建令牌(与 custom-back TokenService.createToken 生成格式完全一致
* * <p>
* @param loginUser 登录用户信息 * JWT:HS512 签名 + raw secret 密钥,JWT 仅含 login_user_key UUID + 基本标识。
* @return JWT token 字符串 * 用户权限、部门等全量信息存储在 Redis login_tokens:{uuid} 中。
*/ */
public String createToken(LoginUser loginUser) { public String createToken(LoginUser loginUser) {
String token = java.util.UUID.randomUUID().toString(); String uuid = java.util.UUID.randomUUID().toString().replace("-", "");
loginUser.setToken(token); loginUser.setToken(uuid);
loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(System.currentTimeMillis() + expireTime * 60 * 1000L); loginUser.setExpireTime(System.currentTimeMillis() + expireTime * 60 * 1000L);
// 存入 Redis(custom-back 兼容路径 // 存入 Redis(全量用户信息 + 权限,与 custom-back 一致
if (stringRedisTemplate != null) { if (stringRedisTemplate != null) {
String redisKey = REDIS_TOKEN_PREFIX + token; String redisKey = REDIS_TOKEN_PREFIX + uuid;
stringRedisTemplate.opsForValue().set( stringRedisTemplate.opsForValue().set(
redisKey, redisKey,
JSON.toJSONString(loginUser), JSON.toJSONString(loginUser),
expireTime * 60, expireTime * 60L,
java.util.concurrent.TimeUnit.SECONDS java.util.concurrent.TimeUnit.SECONDS
); );
} }
// 生成 JWT // 手动构建 JWT(与 custom-back JJWT 0.9.1 HS512 + raw secret 字节级一致)
// 对 secret 做 SHA-256 哈希以派生足够长度的密钥(JJWT 0.12.x 要求 ≥256 位) return buildJwtCompatibly(uuid, loginUser);
try { }
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] keyBytes = digest.digest(secret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
SecretKey signingKey = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder() /**
.claim("login_user_key", token) * 手动构建 JWT(与 custom-back 格式完全一致)
.claim("id", loginUser.getUserId()) * <p>
.claim("account", loginUser.getUsername()) * Header: {"alg":"HS512"}
.claim("deptId", loginUser.getDeptId()) * Payload: {"login_user_key":uuid, "sub":username, "account":username, "id":userId, "exp":24h}
.claim("permissions", loginUser.getPermissions()) * Sign: HMAC-SHA512(header.payload, secret.getBytes(UTF_8))
.expiration(new java.util.Date(loginUser.getExpireTime())) */
.signWith(signingKey) private String buildJwtCompatibly(String uuid, LoginUser loginUser) {
.compact(); try {
} catch (java.security.NoSuchAlgorithmException e) { java.util.Base64.Encoder encoder = java.util.Base64.getUrlEncoder().withoutPadding();
throw new RuntimeException("SHA-256 算法不可用,无法生成 JWT", e);
// Header: {"alg":"HS512"}
String header = "{\"alg\":\"HS512\"}";
String headerBase64 = encoder.encodeToString(
header.getBytes(java.nio.charset.StandardCharsets.UTF_8));
// Payload: 仅含 UUID + 基本标识,不含 permissions/deptId(全量信息在 Redis)
java.util.LinkedHashMap<String, Object> claims = new java.util.LinkedHashMap<>();
claims.put("login_user_key", uuid);
claims.put("sub", loginUser.getUsername());
claims.put("account", loginUser.getUsername());
claims.put("id", loginUser.getUserId());
// JWT 24 小时硬过期(与 custom-back TOKEN_TIMEOUT = 86400000L 一致)
claims.put("exp", (System.currentTimeMillis() + 86400000L) / 1000);
String payload = JSON.toJSONString(claims);
String payloadBase64 = encoder.encodeToString(
payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
// HS512 签名(raw secret 字节,与 custom-back 一致)
String headerPayload = headerBase64 + "." + payloadBase64;
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA512");
javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(
secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA512");
mac.init(keySpec);
byte[] signature = mac.doFinal(
headerPayload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
String signatureBase64 = encoder.encodeToString(signature);
return headerBase64 + "." + payloadBase64 + "." + signatureBase64;
} catch (Exception e) {
throw new RuntimeException("构建 JWT 失败", e);
} }
} }
...@@ -301,13 +335,39 @@ public class TokenHandle { ...@@ -301,13 +335,39 @@ public class TokenHandle {
return tokenCompatibilityParser.parseTokenCompatibly(token, secret); return tokenCompatibilityParser.parseTokenCompatibly(token, secret);
} }
/** Token 自动刷新阈值:剩余不足 20 分钟时自动续期 */
private static final long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L;
/** /**
* 验证令牌有效期 * 刷新 Redis 中缓存的用户信息(权限变更后调用)
*/
public void refreshToken(LoginUser loginUser) {
if (stringRedisTemplate != null && loginUser != null
&& org.springframework.util.StringUtils.hasText(loginUser.getToken())) {
String redisKey = REDIS_TOKEN_PREFIX + loginUser.getToken();
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(System.currentTimeMillis() + expireTime * 60 * 1000L);
stringRedisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(loginUser),
expireTime * 60,
java.util.concurrent.TimeUnit.SECONDS
);
}
}
/**
* 验证令牌有效期(剩余不足20分钟时自动续期,与 custom-back TokenService.verifyToken 对齐)
*/ */
public boolean verifyToken(LoginUser loginUser) { public boolean verifyToken(LoginUser loginUser) {
if (loginUser == null || loginUser.getExpireTime() == null) { if (loginUser == null || loginUser.getExpireTime() == null) {
return false; return false;
} }
return loginUser.getExpireTime() > System.currentTimeMillis(); long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) {
refreshToken(loginUser);
}
return expireTime > currentTime;
} }
} }
package com.jomalls.custom.utils; package com.jomalls.custom.utils;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.jomalls.custom.enums.CodeEnum; import com.jomalls.custom.enums.CodeEnum;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/** /**
* @Author: Lizh * 统一返回结果类
* @Date: 2026/5/28 10:29 * <p>
* @Description: 统一返回结果类 * 支持两种返回风格:
* @Version: 1.0 * <ul>
* <li><b>v3 包裹风格</b>:{@code R.ok(data)} → {@code {"code":10000, "msg":"OK", "data":{...}}}</li>
* <li><b>v2 平铺风格</b>:{@code R.ok().put("token", xxx).put("user", {...})} →
* {@code {"code":200, "msg":"success", "token":"xxx", "user":{...}}}(仿 custom-back AjaxResult)</li>
* </ul>
*
* @param <T> data 类型
* @author Lizh
* @date 2026/5/28 10:29
*/ */
@Setter @Setter
@Getter @Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable { public class R<T> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
...@@ -23,6 +37,12 @@ public class R<T> implements Serializable { ...@@ -23,6 +37,12 @@ public class R<T> implements Serializable {
private String msg; private String msg;
private T data; private T data;
/**
* 平铺字段(仿 AjaxResult HashMap 风格),序列化时展开到 JSON 根级别
*/
@JsonAnySetter
private Map<String, Object> extra;
public R() { public R() {
} }
...@@ -56,4 +76,33 @@ public class R<T> implements Serializable { ...@@ -56,4 +76,33 @@ public class R<T> implements Serializable {
return new R<>(CodeEnum.FAIL.getCode(), msg, null); return new R<>(CodeEnum.FAIL.getCode(), msg, null);
} }
/**
* 添加平铺字段(展开到 JSON 根级别,不包在 data 内),兼容custom_back中的某些接口,避免前端修改
*
* @param key 字段名
* @param value 字段值
* @return this(链式调用)
*/
public R<T> put(String key, Object value) {
if (this.extra == null) {
this.extra = new LinkedHashMap<>();
}
this.extra.put(key, value);
return this;
}
/**
* 获取所有平铺字段(供 Jackson 序列化展开到根级别)
*/
@JsonAnyGetter
public Map<String, Object> getExtra() {
return extra;
}
/**
* 获取某个平铺字段
*/
public Object get(String key) {
return extra != null ? extra.get(key) : null;
}
} }
...@@ -73,7 +73,17 @@ public class RestResponseBodyConfig implements ResponseBodyAdvice<Object> { ...@@ -73,7 +73,17 @@ public class RestResponseBodyConfig implements ResponseBodyAdvice<Object> {
return body; return body;
} }
} }
return body instanceof R ? body : R.ok(body); R<?> r = body instanceof R ? (R<?>) body : R.ok(body);
// 未避免前端修改,适配原来的返回结果,/api/v2 返回 code=200(兼容 custom-back AjaxResult),/api/v3 返回 code=10000
if (requestPath.startsWith("/api/v2")) {
if (r.getCode() == CodeEnum.OK.getCode()) {
r.setCode(CodeEnum.SUCCESS.getCode());
r.setMsg(CodeEnum.SUCCESS.getMsg());
}
}
return r;
} }
} }
} }
......
package com.jomalls.custom.webapp.controller.system;
import com.jomalls.custom.utils.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 验证码控制器(与 custom-back CaptchaController 对齐)
* <p>
* 生成数学验证码图片,返回 base64 图片和 UUID 标识
*
* @author Lizh
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "验证码")
public class CaptchaController {
private static final String CAPTCHA_CODE_KEY = "captcha_codes:";
private static final int CAPTCHA_EXPIRE_MINUTES = 2;
private static final int IMAGE_WIDTH = 130;
private static final int IMAGE_HEIGHT = 48;
private final StringRedisTemplate stringRedisTemplate;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
@Operation(summary = "生成验证码")
@SuppressWarnings("rawtypes")
public R getCode() {
// 生成 UUID 标识
String uuid = UUID.randomUUID().toString().replace("-", "");
// 生成数学验证码
int a = new Random().nextInt(50) + 1;
int b = new Random().nextInt(50) + 1;
int result = a + b;
String expression = a + " + " + b + " = ?";
String captchaText = String.valueOf(result);
// 存入 Redis,2 分钟有效
try {
if (stringRedisTemplate != null) {
stringRedisTemplate.opsForValue().set(
CAPTCHA_CODE_KEY + uuid,
captchaText,
CAPTCHA_EXPIRE_MINUTES,
TimeUnit.MINUTES
);
}
} catch (Exception e) {
log.warn("验证码存入 Redis 失败: {}", e.getMessage());
}
// 生成验证码图片
String imgBase64 = generateCaptchaImage(expression);
return R.ok()
.put("uuid", uuid)
.put("img", imgBase64)
.put("captchaEnabled", true);
}
/**
* 生成数学验证码图片,返回 base64 字符串
*/
private String generateCaptchaImage(String text) {
try {
BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
// 干扰线
Random random = new Random();
g.setColor(new Color(200, 200, 200));
for (int i = 0; i < 5; i++) {
int x1 = random.nextInt(IMAGE_WIDTH);
int y1 = random.nextInt(IMAGE_HEIGHT);
int x2 = random.nextInt(IMAGE_WIDTH);
int y2 = random.nextInt(IMAGE_HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
// 文字
g.setColor(new Color(50, 50, 50));
g.setFont(new Font("Arial", Font.BOLD, 22));
g.drawString(text, 10, 30);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return "data:image/png;base64," + Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (Exception e) {
log.warn("生成验证码图片失败: {}", e.getMessage());
return "";
}
}
}
...@@ -11,12 +11,10 @@ import lombok.RequiredArgsConstructor; ...@@ -11,12 +11,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 登录鉴权控制器 * 登录鉴权控制器(返回风格与 custom-back AjaxResult 对齐)
* *
* @author Lizh * @author Lizh
*/ */
...@@ -29,29 +27,36 @@ public class SysLoginController { ...@@ -29,29 +27,36 @@ public class SysLoginController {
private final SysLoginService sysLoginService; private final SysLoginService sysLoginService;
/** /**
* 用户登录 * 用户登录 — 平铺返回 token(仿 AjaxResult.put("token", xxx))
*/ */
@PostMapping("/api/v2/login") @PostMapping("/api/v2/login")
@Operation(summary = "用户登录") @Operation(summary = "用户登录")
public R<Map<String, String>> login(@RequestBody LoginDTO loginDTO) { @SuppressWarnings("rawtypes")
public R login(@RequestBody LoginDTO loginDTO) {
String username = loginDTO.getUsername(); String username = loginDTO.getUsername();
String password = loginDTO.getPassword(); String password = loginDTO.getPassword();
String code = loginDTO.getCode();
String uuid = loginDTO.getUuid();
log.info("用户[{}]尝试登录", username); log.info("用户[{}]尝试登录", username);
String token = sysLoginService.login(username, password); String token = sysLoginService.login(username, password, code, uuid);
Map<String, String> result = new HashMap<>();
result.put("token", token);
log.info("用户[{}]登录成功", username); log.info("用户[{}]登录成功", username);
return R.ok(result); return R.ok().put("token", token);
} }
/** /**
* 获取用户信息 * 获取用户信息 — 平铺返回 user/roles/permissions(仿 AjaxResult.put 风格)
*/ */
@GetMapping("/api/v2/getInfo") @GetMapping("/api/v2/getInfo")
@Operation(summary = "获取当前用户信息") @Operation(summary = "获取当前用户信息")
public R<LoginUserInfoVO> getInfo() { @SuppressWarnings("rawtypes")
public R getInfo() {
LoginUserInfoVO info = sysLoginService.getInfo(); LoginUserInfoVO info = sysLoginService.getInfo();
return R.ok(info); return R.ok()
.put("user", info.getUser())
.put("roles", info.getRoles())
.put("permissions", info.getPermissions())
.put("isDefaultModifyPwd", info.getIsDefaultModifyPwd())
.put("isPasswordExpired", info.getIsPasswordExpired());
} }
/** /**
...@@ -69,7 +74,8 @@ public class SysLoginController { ...@@ -69,7 +74,8 @@ public class SysLoginController {
*/ */
@PostMapping("/api/v2/logout") @PostMapping("/api/v2/logout")
@Operation(summary = "退出登录") @Operation(summary = "退出登录")
public R<Void> logout() { @SuppressWarnings("rawtypes")
public R logout() {
sysLoginService.logout(); sysLoginService.logout();
return R.ok(); return R.ok();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment