Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
jinli gu
Eladmin
Commits
cf3655ad
Commit
cf3655ad
authored
Jul 04, 2023
by
Zheng Jie
Browse files
用户登录优化,踢出用户性能优化,在线用户查询性能优化
close
https://github.com/elunez/eladmin/issues/802
parent
f0ed88c5
Changes
10
Hide whitespace changes
Inline
Side-by-side
eladmin-common/src/main/java/me/zhengjie/utils/RedisUtils.java
View file @
cf3655ad
...
...
@@ -19,13 +19,11 @@ import com.google.common.collect.Lists;
import
com.google.common.collect.Sets
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.data.redis.connection.RedisConnection
;
import
org.springframework.data.redis.connection.RedisConnectionFactory
;
import
org.springframework.data.redis.core.*
;
import
org.springframework.data.redis.serializer.StringRedisSerializer
;
import
org.springframework.stereotype.Component
;
import
java.util.*
;
import
java.util.concurrent.TimeUnit
;
...
...
@@ -36,9 +34,8 @@ import java.util.concurrent.TimeUnit;
@SuppressWarnings
({
"unchecked"
,
"all"
})
public
class
RedisUtils
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
RedisUtils
.
class
);
private
RedisTemplate
<
Object
,
Object
>
redisTemplate
;
@Value
(
"${jwt.online-key}"
)
private
String
onlineKey
;
public
RedisUtils
(
RedisTemplate
<
Object
,
Object
>
redisTemplate
)
{
this
.
redisTemplate
=
redisTemplate
;
...
...
@@ -197,6 +194,21 @@ public class RedisUtils {
}
}
/**
* 批量模糊删除key
* @param pattern
*/
public
void
scanDel
(
String
pattern
){
ScanOptions
options
=
ScanOptions
.
scanOptions
().
match
(
pattern
).
build
();
try
(
Cursor
<
byte
[]>
cursor
=
redisTemplate
.
executeWithStickyConnection
(
(
RedisCallback
<
Cursor
<
byte
[]>>)
connection
->
(
Cursor
<
byte
[]>)
new
ConvertingCursor
<>(
connection
.
scan
(
options
),
redisTemplate
.
getKeySerializer
()::
deserialize
)))
{
while
(
cursor
.
hasNext
())
{
redisTemplate
.
delete
(
cursor
.
next
());
}
}
}
// ============================String=============================
/**
...
...
eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginProperties.java
View file @
cf3655ad
...
...
@@ -39,7 +39,7 @@ public class LoginProperties {
private
LoginCode
loginCode
;
public
static
final
String
cacheKey
=
"
USER-LOGIN-DATA
"
;
public
static
final
String
cacheKey
=
"
user-login-cache:
"
;
public
boolean
isSingleLogin
()
{
return
singleLogin
;
...
...
eladmin-system/src/main/java/me/zhengjie/modules/security/rest/AuthorizationController.java
View file @
cf3655ad
...
...
@@ -98,17 +98,18 @@ public class AuthorizationController {
// SecurityContextHolder.getContext().setAuthentication(authentication);
String
token
=
tokenProvider
.
createToken
(
authentication
);
final
JwtUserDto
jwtUserDto
=
(
JwtUserDto
)
authentication
.
getPrincipal
();
// 保存在线信息
onlineUserService
.
save
(
jwtUserDto
,
token
,
request
);
// 返回 token 与 用户信息
Map
<
String
,
Object
>
authInfo
=
new
HashMap
<
String
,
Object
>(
2
)
{{
put
(
"token"
,
properties
.
getTokenStartWith
()
+
token
);
put
(
"user"
,
jwtUserDto
);
}};
if
(
loginProperties
.
isSingleLogin
())
{
//踢掉之前已经登录的token
onlineUserService
.
checkLoginOnUser
(
authUser
.
getUsername
()
,
token
);
//
踢掉之前已经登录的token
onlineUserService
.
kickOutForUsername
(
authUser
.
getUsername
());
}
// 保存在线信息
onlineUserService
.
save
(
jwtUserDto
,
token
,
request
);
// 返回登录信息
return
ResponseEntity
.
ok
(
authInfo
);
}
...
...
eladmin-system/src/main/java/me/zhengjie/modules/security/rest/OnlineController.java
View file @
cf3655ad
...
...
@@ -45,25 +45,25 @@ public class OnlineController {
@ApiOperation
(
"查询在线用户"
)
@GetMapping
@PreAuthorize
(
"@el.check()"
)
public
ResponseEntity
<
PageResult
<
OnlineUserDto
>>
queryOnlineUser
(
String
filter
,
Pageable
pageable
){
return
new
ResponseEntity
<>(
onlineUserService
.
getAll
(
filter
,
pageable
),
HttpStatus
.
OK
);
public
ResponseEntity
<
PageResult
<
OnlineUserDto
>>
queryOnlineUser
(
String
username
,
Pageable
pageable
){
return
new
ResponseEntity
<>(
onlineUserService
.
getAll
(
username
,
pageable
),
HttpStatus
.
OK
);
}
@ApiOperation
(
"导出数据"
)
@GetMapping
(
value
=
"/download"
)
@PreAuthorize
(
"@el.check()"
)
public
void
exportOnlineUser
(
HttpServletResponse
response
,
String
filter
)
throws
IOException
{
onlineUserService
.
download
(
onlineUserService
.
getAll
(
filter
),
response
);
public
void
exportOnlineUser
(
HttpServletResponse
response
,
String
username
)
throws
IOException
{
onlineUserService
.
download
(
onlineUserService
.
getAll
(
username
),
response
);
}
@ApiOperation
(
"踢出用户"
)
@DeleteMapping
@PreAuthorize
(
"@el.check()"
)
public
ResponseEntity
<
Object
>
deleteOnlineUser
(
@RequestBody
Set
<
String
>
keys
)
throws
Exception
{
for
(
String
ke
y
:
keys
)
{
for
(
String
to
ke
n
:
keys
)
{
// 解密Key
ke
y
=
EncryptUtils
.
desDecrypt
(
ke
y
);
onlineUserService
.
kickO
ut
(
ke
y
);
to
ke
n
=
EncryptUtils
.
desDecrypt
(
to
ke
n
);
onlineUserService
.
logo
ut
(
to
ke
n
);
}
return
new
ResponseEntity
<>(
HttpStatus
.
OK
);
}
...
...
eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenFilter.java
View file @
cf3655ad
...
...
@@ -70,7 +70,8 @@ public class TokenFilter extends GenericFilterBean {
OnlineUserDto
onlineUserDto
=
null
;
boolean
cleanUserCache
=
false
;
try
{
onlineUserDto
=
onlineUserService
.
getOne
(
properties
.
getOnlineKey
()
+
token
);
String
loginKey
=
tokenProvider
.
loginKey
(
token
);
onlineUserDto
=
onlineUserService
.
getOne
(
loginKey
);
}
catch
(
ExpiredJwtException
e
)
{
log
.
error
(
e
.
getMessage
());
cleanUserCache
=
true
;
...
...
eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java
View file @
cf3655ad
...
...
@@ -18,6 +18,7 @@ package me.zhengjie.modules.security.security;
import
cn.hutool.core.date.DateField
;
import
cn.hutool.core.date.DateUtil
;
import
cn.hutool.core.util.IdUtil
;
import
cn.hutool.crypto.digest.DigestUtil
;
import
io.jsonwebtoken.*
;
import
io.jsonwebtoken.io.Decoders
;
import
io.jsonwebtoken.security.Keys
;
...
...
@@ -120,4 +121,15 @@ public class TokenProvider implements InitializingBean {
}
return
null
;
}
/**
* 获取登录用户RedisKey
* @param token /
* @return key
*/
public
String
loginKey
(
String
token
)
{
Claims
claims
=
getClaims
(
token
);
String
md5Token
=
DigestUtil
.
md5Hex
(
token
);
return
properties
.
getOnlineKey
()
+
claims
.
getSubject
()
+
"-"
+
md5Token
;
}
}
eladmin-system/src/main/java/me/zhengjie/modules/security/service/OnlineUserService.java
View file @
cf3655ad
...
...
@@ -15,7 +15,9 @@
*/
package
me.zhengjie.modules.security.service
;
import
lombok.AllArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
me.zhengjie.modules.security.security.TokenProvider
;
import
me.zhengjie.utils.PageResult
;
import
me.zhengjie.modules.security.config.bean.SecurityProperties
;
import
me.zhengjie.modules.security.service.dto.JwtUserDto
;
...
...
@@ -28,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpServletResponse
;
import
java.io.IOException
;
import
java.util.*
;
import
java.util.concurrent.TimeUnit
;
/**
* @author Zheng Jie
...
...
@@ -35,16 +38,13 @@ import java.util.*;
*/
@Service
@Slf4j
@AllArgsConstructor
public
class
OnlineUserService
{
private
final
SecurityProperties
properties
;
private
final
TokenProvider
tokenProvider
;
private
final
RedisUtils
redisUtils
;
public
OnlineUserService
(
SecurityProperties
properties
,
RedisUtils
redisUtils
)
{
this
.
properties
=
properties
;
this
.
redisUtils
=
redisUtils
;
}
/**
* 保存在线用户信息
* @param jwtUserDto /
...
...
@@ -62,17 +62,18 @@ public class OnlineUserService {
}
catch
(
Exception
e
)
{
log
.
error
(
e
.
getMessage
(),
e
);
}
redisUtils
.
set
(
properties
.
getOnlineKey
()
+
token
,
onlineUserDto
,
properties
.
getTokenValidityInSeconds
()/
1000
);
String
loginKey
=
tokenProvider
.
loginKey
(
token
);
redisUtils
.
set
(
loginKey
,
onlineUserDto
,
properties
.
getTokenValidityInSeconds
(),
TimeUnit
.
MILLISECONDS
);
}
/**
* 查询全部数据
* @param
filter
/
* @param
username
/
* @param pageable /
* @return /
*/
public
PageResult
<
OnlineUserDto
>
getAll
(
String
filter
,
Pageable
pageable
){
List
<
OnlineUserDto
>
onlineUserDtos
=
getAll
(
filter
);
public
PageResult
<
OnlineUserDto
>
getAll
(
String
username
,
Pageable
pageable
){
List
<
OnlineUserDto
>
onlineUserDtos
=
getAll
(
username
);
return
PageUtil
.
toPage
(
PageUtil
.
paging
(
pageable
.
getPageNumber
(),
pageable
.
getPageSize
(),
onlineUserDtos
),
onlineUserDtos
.
size
()
...
...
@@ -81,43 +82,29 @@ public class OnlineUserService {
/**
* 查询全部数据,不分页
* @param
filter
/
* @param
username
/
* @return /
*/
public
List
<
OnlineUserDto
>
getAll
(
String
filter
){
List
<
String
>
keys
=
redisUtils
.
scan
(
properties
.
getOnlineKey
()
+
"*"
);
public
List
<
OnlineUserDto
>
getAll
(
String
username
){
String
loginKey
=
properties
.
getOnlineKey
()
+
(
StringUtils
.
isBlank
(
username
)
?
""
:
"*"
+
username
);
List
<
String
>
keys
=
redisUtils
.
scan
(
loginKey
+
"*"
);
Collections
.
reverse
(
keys
);
List
<
OnlineUserDto
>
onlineUserDtos
=
new
ArrayList
<>();
for
(
String
key
:
keys
)
{
OnlineUserDto
onlineUserDto
=
(
OnlineUserDto
)
redisUtils
.
get
(
key
);
if
(
StringUtils
.
isNotBlank
(
filter
)){
if
(
onlineUserDto
.
toString
().
contains
(
filter
)){
onlineUserDtos
.
add
(
onlineUserDto
);
}
}
else
{
onlineUserDtos
.
add
(
onlineUserDto
);
}
onlineUserDtos
.
add
((
OnlineUserDto
)
redisUtils
.
get
(
key
));
}
onlineUserDtos
.
sort
((
o1
,
o2
)
->
o2
.
getLoginTime
().
compareTo
(
o1
.
getLoginTime
()));
return
onlineUserDtos
;
}
/**
* 踢出用户
* @param key /
*/
public
void
kickOut
(
String
key
){
key
=
properties
.
getOnlineKey
()
+
key
;
redisUtils
.
del
(
key
);
}
/**
* 退出登录
* @param token /
*/
public
void
logout
(
String
token
)
{
String
key
=
properties
.
getOnl
in
e
Key
(
)
+
token
;
redisUtils
.
del
(
k
ey
);
String
loginKey
=
tokenProvider
.
log
inKey
(
token
)
;
redisUtils
.
del
(
loginK
ey
);
}
/**
...
...
@@ -150,43 +137,13 @@ public class OnlineUserService {
return
(
OnlineUserDto
)
redisUtils
.
get
(
key
);
}
/**
* 检测用户是否在之前已经登录,已经登录踢下线
* @param userName 用户名
*/
public
void
checkLoginOnUser
(
String
userName
,
String
igoreToken
){
List
<
OnlineUserDto
>
onlineUserDtos
=
getAll
(
userName
);
if
(
onlineUserDtos
==
null
||
onlineUserDtos
.
isEmpty
()){
return
;
}
for
(
OnlineUserDto
onlineUserDto
:
onlineUserDtos
){
if
(
onlineUserDto
.
getUserName
().
equals
(
userName
)){
try
{
String
token
=
EncryptUtils
.
desDecrypt
(
onlineUserDto
.
getKey
());
if
(
StringUtils
.
isNotBlank
(
igoreToken
)&&!
igoreToken
.
equals
(
token
)){
this
.
kickOut
(
token
);
}
else
if
(
StringUtils
.
isBlank
(
igoreToken
)){
this
.
kickOut
(
token
);
}
}
catch
(
Exception
e
)
{
log
.
error
(
"checkUser is error"
,
e
);
}
}
}
}
/**
* 根据用户名强退用户
* @param username /
*/
@Async
public
void
kickOutForUsername
(
String
username
)
throws
Exception
{
List
<
OnlineUserDto
>
onlineUsers
=
getAll
(
username
);
for
(
OnlineUserDto
onlineUser
:
onlineUsers
)
{
if
(
onlineUser
.
getUserName
().
equals
(
username
))
{
String
token
=
EncryptUtils
.
desDecrypt
(
onlineUser
.
getKey
());
kickOut
(
token
);
}
}
public
void
kickOutForUsername
(
String
username
)
{
String
loginKey
=
properties
.
getOnlineKey
()
+
username
+
"*"
;
redisUtils
.
scanDel
(
loginKey
);
}
}
eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserCacheManager.java
View file @
cf3655ad
...
...
@@ -46,7 +46,7 @@ public class UserCacheManager {
public
JwtUserDto
getUserCache
(
String
userName
)
{
if
(
StringUtils
.
isNotEmpty
(
userName
))
{
// 获取数据
Object
obj
=
redisUtils
.
h
get
(
LoginProperties
.
cacheKey
,
userName
);
Object
obj
=
redisUtils
.
get
(
LoginProperties
.
cacheKey
+
userName
);
if
(
obj
!=
null
){
return
(
JwtUserDto
)
obj
;
}
...
...
@@ -63,7 +63,7 @@ public class UserCacheManager {
if
(
StringUtils
.
isNotEmpty
(
userName
))
{
// 添加数据, 避免数据同时过期
long
time
=
idleTime
+
RandomUtil
.
randomInt
(
900
,
1800
);
redisUtils
.
h
set
(
LoginProperties
.
cacheKey
,
userName
,
user
,
time
);
redisUtils
.
set
(
LoginProperties
.
cacheKey
+
userName
,
user
,
time
);
}
}
...
...
@@ -76,7 +76,7 @@ public class UserCacheManager {
public
void
cleanUserCache
(
String
userName
)
{
if
(
StringUtils
.
isNotEmpty
(
userName
))
{
// 清除数据
redisUtils
.
h
del
(
LoginProperties
.
cacheKey
,
userName
);
redisUtils
.
del
(
LoginProperties
.
cacheKey
+
userName
);
}
}
}
\ No newline at end of file
eladmin-system/src/main/resources/config/application-dev.yml
View file @
cf3655ad
...
...
@@ -56,7 +56,7 @@ login:
# Redis用户登录缓存配置
user-cache
:
# 存活时间/秒
idle-time
:
7
200
idle-time
:
2
16
00
# 验证码
login-code
:
# 验证码类型配置 查看 LoginProperties 类
...
...
@@ -84,9 +84,9 @@ jwt:
# 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds
:
14400000
# 在线用户key
online-key
:
online-token
-
online-key
:
"
online-token
:"
# 验证码
code-key
:
code-key-
code-key
:
"
captcha-code:"
# token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect
:
1800000
# 续期时间范围,默认1小时,单位毫秒
...
...
eladmin-system/src/main/resources/config/application-prod.yml
View file @
cf3655ad
...
...
@@ -58,7 +58,7 @@ login:
# Redis用户登录缓存配置
user-cache
:
# 存活时间/秒
idle-time
:
7
200
idle-time
:
2
16
00
# 验证码
login-code
:
# 验证码类型配置 查看 LoginProperties 类
...
...
@@ -86,9 +86,9 @@ jwt:
# 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds
:
7200000
# 在线用户key
online-key
:
online-token
-
online-key
:
"
online-token
:"
# 验证码
code-key
:
code-key-
code-key
:
"
captcha-code:"
# token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect
:
1800000
# 续期时间范围,默认 1小时,这里单位毫秒
...
...
Write
Preview
Supports
Markdown
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