用户登录优化,踢出用户性能优化,在线用户查询性能优化

close https://github.com/elunez/eladmin/issues/802
pull/805/head
Zheng Jie 2023-07-04 22:30:30 +08:00
parent f0ed88c51e
commit cf3655adf4
10 changed files with 73 additions and 90 deletions

View File

@ -19,13 +19,11 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*; import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -36,9 +34,8 @@ import java.util.concurrent.TimeUnit;
@SuppressWarnings({"unchecked", "all"}) @SuppressWarnings({"unchecked", "all"})
public class RedisUtils { public class RedisUtils {
private static final Logger log = LoggerFactory.getLogger(RedisUtils.class); private static final Logger log = LoggerFactory.getLogger(RedisUtils.class);
private RedisTemplate<Object, Object> redisTemplate; private RedisTemplate<Object, Object> redisTemplate;
@Value("${jwt.online-key}")
private String onlineKey;
public RedisUtils(RedisTemplate<Object, Object> redisTemplate) { public RedisUtils(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = 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============================= // ============================String=============================
/** /**

View File

@ -39,7 +39,7 @@ public class LoginProperties {
private LoginCode loginCode; private LoginCode loginCode;
public static final String cacheKey = "USER-LOGIN-DATA"; public static final String cacheKey = "user-login-cache:";
public boolean isSingleLogin() { public boolean isSingleLogin() {
return singleLogin; return singleLogin;

View File

@ -98,8 +98,6 @@ public class AuthorizationController {
// SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication); String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal(); final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 保存在线信息
onlineUserService.save(jwtUserDto, token, request);
// 返回 token 与 用户信息 // 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{ Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token); put("token", properties.getTokenStartWith() + token);
@ -107,8 +105,11 @@ public class AuthorizationController {
}}; }};
if (loginProperties.isSingleLogin()) { if (loginProperties.isSingleLogin()) {
// 踢掉之前已经登录的token // 踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUser.getUsername(), token); onlineUserService.kickOutForUsername(authUser.getUsername());
} }
// 保存在线信息
onlineUserService.save(jwtUserDto, token, request);
// 返回登录信息
return ResponseEntity.ok(authInfo); return ResponseEntity.ok(authInfo);
} }

View File

@ -45,25 +45,25 @@ public class OnlineController {
@ApiOperation("查询在线用户") @ApiOperation("查询在线用户")
@GetMapping @GetMapping
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<PageResult<OnlineUserDto>> queryOnlineUser(String filter, Pageable pageable){ public ResponseEntity<PageResult<OnlineUserDto>> queryOnlineUser(String username, Pageable pageable){
return new ResponseEntity<>(onlineUserService.getAll(filter, pageable),HttpStatus.OK); return new ResponseEntity<>(onlineUserService.getAll(username, pageable),HttpStatus.OK);
} }
@ApiOperation("导出数据") @ApiOperation("导出数据")
@GetMapping(value = "/download") @GetMapping(value = "/download")
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public void exportOnlineUser(HttpServletResponse response, String filter) throws IOException { public void exportOnlineUser(HttpServletResponse response, String username) throws IOException {
onlineUserService.download(onlineUserService.getAll(filter), response); onlineUserService.download(onlineUserService.getAll(username), response);
} }
@ApiOperation("踢出用户") @ApiOperation("踢出用户")
@DeleteMapping @DeleteMapping
@PreAuthorize("@el.check()") @PreAuthorize("@el.check()")
public ResponseEntity<Object> deleteOnlineUser(@RequestBody Set<String> keys) throws Exception { public ResponseEntity<Object> deleteOnlineUser(@RequestBody Set<String> keys) throws Exception {
for (String key : keys) { for (String token : keys) {
// 解密Key // 解密Key
key = EncryptUtils.desDecrypt(key); token = EncryptUtils.desDecrypt(token);
onlineUserService.kickOut(key); onlineUserService.logout(token);
} }
return new ResponseEntity<>(HttpStatus.OK); return new ResponseEntity<>(HttpStatus.OK);
} }

View File

@ -70,7 +70,8 @@ public class TokenFilter extends GenericFilterBean {
OnlineUserDto onlineUserDto = null; OnlineUserDto onlineUserDto = null;
boolean cleanUserCache = false; boolean cleanUserCache = false;
try { try {
onlineUserDto = onlineUserService.getOne(properties.getOnlineKey() + token); String loginKey = tokenProvider.loginKey(token);
onlineUserDto = onlineUserService.getOne(loginKey);
} catch (ExpiredJwtException e) { } catch (ExpiredJwtException e) {
log.error(e.getMessage()); log.error(e.getMessage());
cleanUserCache = true; cleanUserCache = true;

View File

@ -18,6 +18,7 @@ package me.zhengjie.modules.security.security;
import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
@ -120,4 +121,15 @@ public class TokenProvider implements InitializingBean {
} }
return null; 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;
}
} }

View File

@ -15,7 +15,9 @@
*/ */
package me.zhengjie.modules.security.service; package me.zhengjie.modules.security.service;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhengjie.modules.security.security.TokenProvider;
import me.zhengjie.utils.PageResult; import me.zhengjie.utils.PageResult;
import me.zhengjie.modules.security.config.bean.SecurityProperties; import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.service.dto.JwtUserDto; import me.zhengjie.modules.security.service.dto.JwtUserDto;
@ -28,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit;
/** /**
* @author Zheng Jie * @author Zheng Jie
@ -35,16 +38,13 @@ import java.util.*;
*/ */
@Service @Service
@Slf4j @Slf4j
@AllArgsConstructor
public class OnlineUserService { public class OnlineUserService {
private final SecurityProperties properties; private final SecurityProperties properties;
private final TokenProvider tokenProvider;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
public OnlineUserService(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
this.redisUtils = redisUtils;
}
/** /**
* 线 * 线
* @param jwtUserDto / * @param jwtUserDto /
@ -62,17 +62,18 @@ public class OnlineUserService {
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(),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 / * @param pageable /
* @return / * @return /
*/ */
public PageResult<OnlineUserDto> getAll(String filter, Pageable pageable){ public PageResult<OnlineUserDto> getAll(String username, Pageable pageable){
List<OnlineUserDto> onlineUserDtos = getAll(filter); List<OnlineUserDto> onlineUserDtos = getAll(username);
return PageUtil.toPage( return PageUtil.toPage(
PageUtil.paging(pageable.getPageNumber(),pageable.getPageSize(), onlineUserDtos), PageUtil.paging(pageable.getPageNumber(),pageable.getPageSize(), onlineUserDtos),
onlineUserDtos.size() onlineUserDtos.size()
@ -81,43 +82,29 @@ public class OnlineUserService {
/** /**
* *
* @param filter / * @param username /
* @return / * @return /
*/ */
public List<OnlineUserDto> getAll(String filter){ public List<OnlineUserDto> getAll(String username){
List<String> keys = redisUtils.scan(properties.getOnlineKey() + "*"); String loginKey = properties.getOnlineKey() +
(StringUtils.isBlank(username) ? "" : "*" + username);
List<String> keys = redisUtils.scan(loginKey + "*");
Collections.reverse(keys); Collections.reverse(keys);
List<OnlineUserDto> onlineUserDtos = new ArrayList<>(); List<OnlineUserDto> onlineUserDtos = new ArrayList<>();
for (String key : keys) { for (String key : keys) {
OnlineUserDto onlineUserDto = (OnlineUserDto) redisUtils.get(key); onlineUserDtos.add((OnlineUserDto) redisUtils.get(key));
if(StringUtils.isNotBlank(filter)){
if(onlineUserDto.toString().contains(filter)){
onlineUserDtos.add(onlineUserDto);
}
} else {
onlineUserDtos.add(onlineUserDto);
}
} }
onlineUserDtos.sort((o1, o2) -> o2.getLoginTime().compareTo(o1.getLoginTime())); onlineUserDtos.sort((o1, o2) -> o2.getLoginTime().compareTo(o1.getLoginTime()));
return onlineUserDtos; return onlineUserDtos;
} }
/**
*
* @param key /
*/
public void kickOut(String key){
key = properties.getOnlineKey() + key;
redisUtils.del(key);
}
/** /**
* 退 * 退
* @param token / * @param token /
*/ */
public void logout(String token) { public void logout(String token) {
String key = properties.getOnlineKey() + token; String loginKey = tokenProvider.loginKey(token);
redisUtils.del(key); redisUtils.del(loginKey);
} }
/** /**
@ -150,43 +137,13 @@ public class OnlineUserService {
return (OnlineUserDto)redisUtils.get(key); 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 / * @param username /
*/ */
@Async @Async
public void kickOutForUsername(String username) throws Exception { public void kickOutForUsername(String username) {
List<OnlineUserDto> onlineUsers = getAll(username); String loginKey = properties.getOnlineKey() + username + "*";
for (OnlineUserDto onlineUser : onlineUsers) { redisUtils.scanDel(loginKey);
if (onlineUser.getUserName().equals(username)) {
String token =EncryptUtils.desDecrypt(onlineUser.getKey());
kickOut(token);
}
}
} }
} }

View File

@ -46,7 +46,7 @@ public class UserCacheManager {
public JwtUserDto getUserCache(String userName) { public JwtUserDto getUserCache(String userName) {
if (StringUtils.isNotEmpty(userName)) { if (StringUtils.isNotEmpty(userName)) {
// 获取数据 // 获取数据
Object obj = redisUtils.hget(LoginProperties.cacheKey, userName); Object obj = redisUtils.get(LoginProperties.cacheKey + userName);
if(obj != null){ if(obj != null){
return (JwtUserDto)obj; return (JwtUserDto)obj;
} }
@ -63,7 +63,7 @@ public class UserCacheManager {
if (StringUtils.isNotEmpty(userName)) { if (StringUtils.isNotEmpty(userName)) {
// 添加数据, 避免数据同时过期 // 添加数据, 避免数据同时过期
long time = idleTime + RandomUtil.randomInt(900, 1800); long time = idleTime + RandomUtil.randomInt(900, 1800);
redisUtils.hset(LoginProperties.cacheKey, userName, user, time); redisUtils.set(LoginProperties.cacheKey + userName, user, time);
} }
} }
@ -76,7 +76,7 @@ public class UserCacheManager {
public void cleanUserCache(String userName) { public void cleanUserCache(String userName) {
if (StringUtils.isNotEmpty(userName)) { if (StringUtils.isNotEmpty(userName)) {
// 清除数据 // 清除数据
redisUtils.hdel(LoginProperties.cacheKey, userName); redisUtils.del(LoginProperties.cacheKey + userName);
} }
} }
} }

View File

@ -56,7 +56,7 @@ login:
# Redis用户登录缓存配置 # Redis用户登录缓存配置
user-cache: user-cache:
# 存活时间/秒 # 存活时间/秒
idle-time: 7200 idle-time: 21600
# 验证码 # 验证码
login-code: login-code:
# 验证码类型配置 查看 LoginProperties 类 # 验证码类型配置 查看 LoginProperties 类
@ -84,9 +84,9 @@ jwt:
# 令牌过期时间 此处单位/毫秒 默认4小时可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html # 令牌过期时间 此处单位/毫秒 默认4小时可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds: 14400000 token-validity-in-seconds: 14400000
# 在线用户key # 在线用户key
online-key: online-token- online-key: "online-token:"
# 验证码 # 验证码
code-key: code-key- code-key: "captcha-code:"
# token 续期检查时间范围默认30分钟单位毫秒在token即将过期的一段时间内用户操作了则给用户的token续期 # token 续期检查时间范围默认30分钟单位毫秒在token即将过期的一段时间内用户操作了则给用户的token续期
detect: 1800000 detect: 1800000
# 续期时间范围默认1小时单位毫秒 # 续期时间范围默认1小时单位毫秒

View File

@ -58,7 +58,7 @@ login:
# Redis用户登录缓存配置 # Redis用户登录缓存配置
user-cache: user-cache:
# 存活时间/秒 # 存活时间/秒
idle-time: 7200 idle-time: 21600
# 验证码 # 验证码
login-code: login-code:
# 验证码类型配置 查看 LoginProperties 类 # 验证码类型配置 查看 LoginProperties 类
@ -86,9 +86,9 @@ jwt:
# 令牌过期时间 此处单位/毫秒 默认2小时可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html # 令牌过期时间 此处单位/毫秒 默认2小时可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds: 7200000 token-validity-in-seconds: 7200000
# 在线用户key # 在线用户key
online-key: online-token- online-key: "online-token:"
# 验证码 # 验证码
code-key: code-key- code-key: "captcha-code:"
# token 续期检查时间范围默认30分钟单位默认毫秒在token即将过期的一段时间内用户操作了则给用户的token续期 # token 续期检查时间范围默认30分钟单位默认毫秒在token即将过期的一段时间内用户操作了则给用户的token续期
detect: 1800000 detect: 1800000
# 续期时间范围,默认 1小时这里单位毫秒 # 续期时间范围,默认 1小时这里单位毫秒