Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
custom-server
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lizhonghong
custom-server
Commits
3c97de77
Commit
3c97de77
authored
Jul 14, 2026
by
Lizh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
迁移修改登录逻辑处理,兼容JJWT的老版本0.9.x
parent
f1db6a85
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1894 additions
and
152 deletions
+1894
-152
custom-server-app/src/main/java/com/jomalls/custom/app/constant/UserConstants.java
+114
-0
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/SysLoginService.java
+4
-2
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/impl/SysLoginServiceImpl.java
+270
-62
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/impl/SysPasswordService.java
+4
-2
custom-server-app/src/main/java/com/jomalls/custom/app/utils/CharsetKit.java
+89
-0
custom-server-app/src/main/java/com/jomalls/custom/app/utils/Convert.java
+852
-0
custom-server-app/src/main/java/com/jomalls/custom/app/utils/DateUtils.java
+162
-0
custom-server-app/src/main/java/com/jomalls/custom/app/vo/system/MetaVO.java
+35
-3
custom-server-app/src/main/java/com/jomalls/custom/app/vo/system/RouterVO.java
+2
-0
custom-server-core/src/main/java/com/jomalls/custom/security/TokenCompatibilityParser.java
+58
-25
custom-server-core/src/main/java/com/jomalls/custom/security/TokenHandle.java
+98
-38
custom-server-core/src/main/java/com/jomalls/custom/utils/R.java
+54
-6
custom-server-starter/src/main/java/com/jomalls/custom/config/RestResponseBodyConfig.java
+11
-1
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/system/CaptchaController.java
+122
-0
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/system/SysLoginController.java
+19
-13
No files found.
custom-server-app/src/main/java/com/jomalls/custom/app/constant/UserConstants.java
0 → 100644
View file @
3c97de77
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
;
}
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/SysLoginService.java
View file @
3c97de77
...
@@ -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
);
/**
/**
* 获取当前登录用户信息
* 获取当前登录用户信息
...
...
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/impl/SysLoginServiceImpl.java
View file @
3c97de77
...
@@ -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
(
user
.
getPwdUpdateDate
()
==
null
)
{
if
(
validateDays
>
0
)
{
// 如果从未修改过初始密码,直接提醒过期
if
(
user
.
getPwdUpdateDate
()
==
null
)
{
passwordExpired
=
true
;
// 从未修改过密码,视为过期
}
else
{
passwordExpired
=
true
;
int
diffDay
=
DateUtils
.
differentDaysByMillisecond
(
DateUtils
.
getNowDate
(),
user
.
getPwdUpdateDate
());
}
else
{
passwordExpired
=
diffDay
>
passwordValidateDays
;
long
diffMs
=
System
.
currentTimeMillis
()
-
user
.
getPwdUpdateDate
().
getTime
();
long
diffDays
=
diffMs
/
(
1000
*
60
*
60
*
24
);
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
->
{
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
.
setComponent
(
menu
.
getComponent
());
// 元数据
MetaVO
meta
=
new
MetaVO
();
meta
.
setTitle
(
menu
.
getMenuName
());
meta
.
setIcon
(
menu
.
getIcon
());
// isCache/isFrame 是 Integer 类型:1=否
meta
.
setIsCache
(!
Integer
.
valueOf
(
1
).
equals
(
menu
.
getIsCache
()));
meta
.
setIsFrame
(
Integer
.
valueOf
(
1
).
equals
(
menu
.
getIsFrame
()));
router
.
setMeta
(
meta
);
// 递归构建子路由
List
<
RouterVO
>
children
=
buildRouterTree
(
menu
.
getMenuId
(),
allMenus
);
router
.
setChildren
(
children
.
isEmpty
()
?
null
:
children
);
// 有子路由时设置 alwaysShow
if
(!
children
.
isEmpty
())
{
router
.
setAlwaysShow
(
Boolean
.
TRUE
);
}
return
router
;
})
.
collect
(
Collectors
.
toList
());
.
collect
(
Collectors
.
toList
());
List
<
RouterVO
>
routers
=
new
ArrayList
<>();
for
(
SysMenuEntity
menu
:
menus
)
{
RouterVO
router
=
new
RouterVO
();
router
.
setHidden
(
"1"
.
equals
(
menu
.
getVisible
()));
router
.
setName
(
getRouteName
(
menu
));
router
.
setPath
(
getRouterPath
(
menu
));
router
.
setComponent
(
getComponent
(
menu
));
router
.
setQuery
(
menu
.
getQuery
());
router
.
setMeta
(
new
MetaVO
(
menu
.
getMenuName
(),
menu
.
getIcon
(),
Integer
.
valueOf
(
1
).
equals
(
menu
.
getIsCache
()),
menu
.
getPath
()));
List
<
RouterVO
>
cMenus
=
buildRouterTree
(
menu
.
getMenuId
(),
allMenus
);
if
(!
cMenus
.
isEmpty
()
&&
"M"
.
equals
(
menu
.
getMenuType
()))
{
router
.
setAlwaysShow
(
true
);
router
.
setRedirect
(
"noRedirect"
);
router
.
setChildren
(
cMenus
);
}
else
if
(
isMenuFrame
(
menu
))
{
router
.
setMeta
(
null
);
List
<
RouterVO
>
childrenList
=
new
ArrayList
<>();
RouterVO
children
=
new
RouterVO
();
children
.
setPath
(
menu
.
getPath
());
children
.
setComponent
(
menu
.
getComponent
());
children
.
setName
(
getRouteName
(
menu
.
getRouteName
(),
menu
.
getPath
()));
children
.
setMeta
(
new
MetaVO
(
menu
.
getMenuName
(),
menu
.
getIcon
(),
Integer
.
valueOf
(
1
).
equals
(
menu
.
getIsCache
()),
menu
.
getPath
()));
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
;
}
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/service/system/impl/SysPasswordService.java
View file @
3c97de77
...
@@ -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
)
{
...
...
custom-server-app/src/main/java/com/jomalls/custom/app/utils/CharsetKit.java
0 → 100644
View file @
3c97de77
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
();
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/utils/Convert.java
0 → 100644
View file @
3c97de77
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
(
"^整$"
,
"零元整"
);
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/utils/DateUtils.java
0 → 100644
View file @
3c97de77
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
());
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/vo/system/MetaVO.java
View file @
3c97de77
...
@@ -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
;
}
}
}
}
custom-server-app/src/main/java/com/jomalls/custom/app/vo/system/RouterVO.java
View file @
3c97de77
...
@@ -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
;
...
...
custom-server-core/src/main/java/com/jomalls/custom/security/TokenCompatibilityParser.java
View file @
3c97de77
...
@@ -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
=
try
Legacy
Parse
(
token
,
secret
);
Claims
result
=
try
Standard
Parse
(
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
=
try
Standard
Parse
(
token
,
secret
);
result
=
try
Legacy
Parse
(
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
);
return
decodePayloadToClaims
(
token
);
if
(!
java
.
util
.
Arrays
.
equals
(
sha256Key
,
legacyKey
)
&&
verifyHmacSha512Signature
(
token
,
legacyKey
))
{
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
;
}
}
}
}
...
...
custom-server-core/src/main/java/com/jomalls/custom/security/TokenHandle.java
View file @
3c97de77
...
@@ -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
,
permissions
List
!=
null
?
permissionsList
.
size
()
:
0
);
log
.
debug
(
"从 Redis 获取用户[{}]信息成功,权限数: {}"
,
username
,
permissions
Set
.
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
*
60
L
,
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
);
}
/**
* 手动构建 JWT(与 custom-back 格式完全一致)
* <p>
* Header: {"alg":"HS512"}
* Payload: {"login_user_key":uuid, "sub":username, "account":username, "id":userId, "exp":24h}
* Sign: HMAC-SHA512(header.payload, secret.getBytes(UTF_8))
*/
private
String
buildJwtCompatibly
(
String
uuid
,
LoginUser
loginUser
)
{
try
{
try
{
java
.
security
.
MessageDigest
digest
=
java
.
security
.
MessageDigest
.
getInstance
(
"SHA-256"
);
java
.
util
.
Base64
.
Encoder
encoder
=
java
.
util
.
Base64
.
getUrlEncoder
().
withoutPadding
();
byte
[]
keyBytes
=
digest
.
digest
(
secret
.
getBytes
(
java
.
nio
.
charset
.
StandardCharsets
.
UTF_8
));
SecretKey
signingKey
=
Keys
.
hmacShaKeyFor
(
keyBytes
);
// Header: {"alg":"HS512"}
String
header
=
"{\"alg\":\"HS512\"}"
;
return
Jwts
.
builder
()
String
headerBase64
=
encoder
.
encodeToString
(
.
claim
(
"login_user_key"
,
token
)
header
.
getBytes
(
java
.
nio
.
charset
.
StandardCharsets
.
UTF_8
));
.
claim
(
"id"
,
loginUser
.
getUserId
())
.
claim
(
"account"
,
loginUser
.
getUsername
())
// Payload: 仅含 UUID + 基本标识,不含 permissions/deptId(全量信息在 Redis)
.
claim
(
"deptId"
,
loginUser
.
getDeptId
())
java
.
util
.
LinkedHashMap
<
String
,
Object
>
claims
=
new
java
.
util
.
LinkedHashMap
<>();
.
claim
(
"permissions"
,
loginUser
.
getPermissions
())
claims
.
put
(
"login_user_key"
,
uuid
);
.
expiration
(
new
java
.
util
.
Date
(
loginUser
.
getExpireTime
()))
claims
.
put
(
"sub"
,
loginUser
.
getUsername
());
.
signWith
(
signingKey
)
claims
.
put
(
"account"
,
loginUser
.
getUsername
());
.
compact
();
claims
.
put
(
"id"
,
loginUser
.
getUserId
());
}
catch
(
java
.
security
.
NoSuchAlgorithmException
e
)
{
// JWT 24 小时硬过期(与 custom-back TOKEN_TIMEOUT = 86400000L 一致)
throw
new
RuntimeException
(
"SHA-256 算法不可用,无法生成 JWT"
,
e
);
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
;
}
}
}
}
custom-server-core/src/main/java/com/jomalls/custom/utils/R.java
View file @
3c97de77
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
);
}
}
}
/**
\ No newline at end of file
* 添加平铺字段(展开到 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
;
}
}
custom-server-starter/src/main/java/com/jomalls/custom/config/RestResponseBodyConfig.java
View file @
3c97de77
...
@@ -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
;
}
}
}
}
}
}
...
...
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/system/CaptchaController.java
0 → 100644
View file @
3c97de77
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
""
;
}
}
}
custom-server-webapp/src/main/java/com/jomalls/custom/webapp/controller/system/SysLoginController.java
View file @
3c97de77
...
@@ -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
();
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment