1、前言
在前一篇《在SpringBoot项目中整合SpringSession,基于Redis实现对Session的管理和事件监听》笔记中,已经实践了在SpringBoot + SpringSecurity 项目中整合SpringSession,这里我们继续尝试如何统计当前在线用户,思路如下:通过统计当前所有未过期的Session信息,每个Session即对应一个登录用户(系统可以设置多端登录,所以可能存在一个登录用户对应多个Session情况,这里按照每个Session一个登录用户进行统计),同时在登录过程中,会把需要展示的用户信息,放到Session中,进而存到了Redis中,避免再次查询。
2、在线用户信息查询
2.1、查询所有在线用户信息
在SpringSession中未找到查询全部Session的相关方法,所以这里采用了直接查询Redis数据的方法实现,同时为了保证Redis序列化与存储的时候一致,这里直接使用了RedisIndexedSessionRepository中的操作Redis的RedisOperations对象。具体实现如下:
/**
* 查询当前在线的所有用户。通过查询Redis中所有未过期的Session信息实现。
* 查询字段(username、nickname、userCode、hostAddress、lastAccessedTime、maxInactiveInterval、creationTime等)
* @return
*/
public List<Map<String, Object>> queryOnlineUsers(){
// 获取所有存储会话的键,并根据需要进行过滤和处理
Set<Object> keys = sessionRepository.getSessionRedisOperations().keys("spring:session:sessions:*");
List<Map<String,Object>> list = new ArrayList<>();
Object[] arr = {"lastAccessedTime","maxInactiveInterval","creationTime","sessionAttr:SPRING_SECURITY_CONTEXT"};
keys.stream().forEach(item->{
if(((String)item).indexOf("expires")==-1){//排除过期信息
List<Object> values = sessionRepository.getSessionRedisOperations().opsForHash()
.multiGet(item, Arrays.asList(arr));
Map<String, Object> re = new HashMap<>();
re.put("lastAccessedTime",values.get(0));
re.put("maxInactiveInterval",values.get(1));
re.put("creationTime",values.get(2));
re.put("sessionId",parseUserToken((String)item));
re.putAll(this.parseUserInfo(values.get(3)));
list.add(re);
}
});
return list;
}
/**
* 根据Redis中存储对象的Key解析对应的SessionId
* @param key
* @return
*/
private String parseUserToken(String key){
if(StringUtils.isNotEmpty(key)){
String[] arr = key.split(":");
if(arr != null && arr.length == 4){
return arr[3];
}
}
return null;
}
/**
* 解析Redis中存储的用户信息和登录信息
* @param val
* @return
*/
private Map<String, Object> parseUserInfo(Object val){
Map<String, Object> userInfo = new HashMap<>();
if(val != null && val instanceof SecurityContextImpl){
JSONObject json = (JSONObject) JSONObject.toJSON(val);
if(json != null && json.containsKey("authentication")){
JSONObject authInfo = json.getJSONObject("authentication");
if(authInfo != null){
if(authInfo.containsKey("name")){//用户登录名称
userInfo.put("username",authInfo.getString("name"));
}
if(authInfo.containsKey("details")){//用户登录时的地址
JSONObject details = authInfo.getJSONObject("details");
if(details != null && details.containsKey("remoteAddress")){
userInfo.put("hostAddress",details.getString("remoteAddress"));
}
}
if(authInfo.containsKey("principal")){
JSONObject principal = authInfo.getJSONObject("principal");
if(principal.containsKey("sysUser")){
JSONObject sysUser = principal.getJSONObject("sysUser");
userInfo.put("nickname",sysUser.getString("nickname"));
userInfo.put("userCode",sysUser.getString("userCode"));
}
}
}
}
}
return userInfo;
}
在上述代码中,我们主要查询了“spring:session:sessions:*”中对应的数据,并过滤其中的“spring:session:sessions:expires”数据,然后在这里保存了四个可以值:
- creationTime Session创建时间
- lastAccessedTime 最后访问时间
- maxInactiveInterval Session的有效时长
- sessionAttr:SPRING_SECURITY_CONTEXT 默认存储了当前登录用户的认证信息
上述用户的用户登录信息就是从上述四个参数中解析,其中用户登录的认证信息,一般只保存了默认的authentication信息,包括了details和principal信息,为了获取更多的用户信息,我会在登录时,将用户信息放到principal信息中。
2.2、填充登录用户信息
填充用户登录信息,是在SpringSecurity的认证相关逻辑中实现的,方式有很多,这里选择了一种比较简单的方式,即在重写的UserDetailsService实现类的loadUserByUsername()方法中实现,同时,我们还需要构建一个UserDetails实现类,用于存储额外的登录用户信息,代码如下:
//UserDetails实现类,添加一个了sysUser字段,用于存储额外的用户信息
//这里的User 是org.springframework.security.core.userdetails.User
public class LoginUser extends User {
private SysUserEntity sysUser;
public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public SysUserEntity getSysUser() {
return sysUser;
}
public void setSysUser(SysUserEntity sysUser) {
this.sysUser = sysUser;
}
}
//UserDetailsService实现类的重写loadUserByUsername()方法的逻辑如下:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SysUserEntity param = new SysUserEntity();
param.setUsername(username);
SysUserEntity user = sysUserService.getOne(param,true);
if(user == null) {
logger.info("用户名不存在,用户名:" + username);
throw new UsernameNotFoundException("用户名不存在");
}
LoginUser loginUser = new LoginUser(user.getUsername(), user.getPassword(),authorities);
loginUser.setSysUser(user);
return loginUser;
}
至此,我们就完成了用户额外信息的填充,在上述获取用户认证信息sessionAttr:SPRING_SECURITY_CONTEXT对应数据时,内容如下:
{
"data": [{
"lastAccessedTime": 1694676717338,
"maxInactiveInterval": 1800,
"creationTime": 1694676716358,
"sessionAttr:SPRING_SECURITY_CONTEXT": {
"authentication": {
"authenticated": true,
"authorities": [],
"details": {
"remoteAddress": "192.168.1.87"
},
"name": "test",
"principal": {
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [],
"credentialsNonExpired": true,
"enabled": true,
"sysUser": {
"nickname": "测试",
"roleCode": "test",
"roleName": "测试",
"userCode": "test_1693296199148",
"username": "test"
},
"username": "test"
}
}
}
}]
}
3、剔除在线用户(是一个用户的Token失效)
根据sessionId使一个Session实现的方法,可以借助SpringSession的RedisIndexedSessionRepository方法中deleteById()方法实现,具体实现如下:
/**
* 通过SessionId使一个Session失效
* @param sessionId
* @return
*/
public boolean expireSession(String sessionId){
sessionRepository.deleteById(sessionId);
return true;
}