• 在SpringSecurity + SpringSession项目中如何实现当前在线用户的查询、剔除登录用户等操作


    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

      在上述代码中,我们主要查询了“spring:session:sessions:*”中对应的数据,并过滤其中的“spring:session:sessions:expires”数据,然后在这里保存了四个可以值:

    1. creationTime Session创建时间
    2. lastAccessedTime 最后访问时间
    3. maxInactiveInterval Session的有效时长
    4. 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    //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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

      至此,我们就完成了用户额外信息的填充,在上述获取用户认证信息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"
    				}
    			}
    		}
    	}]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    3、剔除在线用户(是一个用户的Token失效)

      根据sessionId使一个Session实现的方法,可以借助SpringSession的RedisIndexedSessionRepository方法中deleteById()方法实现,具体实现如下:

      /**
         * 通过SessionId使一个Session失效
         * @param sessionId
         * @return
         */
        public boolean expireSession(String sessionId){
            sessionRepository.deleteById(sessionId);
            return true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    韩顺平java 515-520即时笔记
    Java-对象的构造及初始化
    SystemVerilog(十二)-$unit声明空间
    【Leetcode】2100. Find Good Days to Rob the Bank
    Hexo添加jVectorMap足迹地图
    【动手学深度学习】RNN浅记
    vim常用命令
    前端研习录(32)——JavaScript 基于定时器实现防抖、节流
    Sentinel源码解析-源码环境搭建
    小黑子—MyBatis:第一章
  • 原文地址:https://blog.csdn.net/hou_ge/article/details/132896488