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
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
770 additions
and
87 deletions
+770
-87
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
+0
-0
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
+0
-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
+57
-24
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
+53
-4
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
This diff is collapsed.
Click to expand it.
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
This diff is collapsed.
Click to expand it.
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
);
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
;
}
}
}
}
...
...
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
);
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
;
}
}
}
}
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
);
}
}
/**
* 添加平铺字段(展开到 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