美文网首页
一次关于Spring Security的session剔除问题

一次关于Spring Security的session剔除问题

作者: just_like_you | 来源:发表于2019-07-12 17:47 被阅读0次

    发生的主要情况是这样的,在使用Spring Security搭建后台的RBAC权限系统的时候遇到一个需要 :

    在管理员修改权限的时候,要求将已经被修改的用户的会话剔除。

    个人实现的解决方案以及步骤

    • 在修改权限的时候发出一个权限修改事件
    • 利用Spring的时间监听机制,来进行会话剔除

    自定义事件以及发送事件,携带角色id(方法获取该角色对应的所有用户id)

    • 自定义事件如下
    public class PermissionChangeEvent extends ApplicationEvent {
    
        private final Integer roleId;
    
        public PermissionChangeEvent(Object source, Integer roleId) {
            super(source);
            this.roleId = roleId;
        }
    
        public Integer getRoleId() {
            return roleId;
        }
    }
    
    • 在业务层权限改变时,发出事件
        @Transactional(rollbackFor = Exception.class)
        @Override
        public CommonResult setPermission(Integer roleId, List<Integer> resourceIds) {
            if (roleId == null || roleId <= 0) {
                return CommonResult.fail(AccountEnum.INVALID_ROLE_ID);
            }
            if (CollectionUtils.isEmpty(resourceIds)) {
                return CommonResult.fail(AccountEnum.NOT_EMPTY_RESOURCE);
            }
            int flag = changePermission(roleId, resourceIds);
            if (flag > 0) {
                //发送重置权限事件,让当前token失效
                applicationEventPublisher.publishEvent(new PermissionChangeEvent(this,roleId));
                return CommonResult.success();
            }
            return CommonResult.fail(AccountEnum.SET_PERMISSION_ERROR);
        }
    
    • 编写事件监听来处理会话剔除
      • 其中JWTUserDetails 是扩展自 UserDetails接口其中包含了用户id,用户权限url集合等一些信息
      • SessionRegistrySpring Security提供的操作Session的工具类
    @Component
    @Slf4j
    public class PermissionChangeListener {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Autowired
        private AdminAccountService adminAccountService;
        @Autowired
        private SessionRegistry sessionRegistry;
    
        @EventListener(PermissionChangeEvent.class)
        @Async
        public void permissionListenPostProcess(PermissionChangeEvent event) {
            Integer roleId = event.getRoleId();
            log.debug("receive permission changed event , eventClassName : [{}]  ,roleId : [{}]", event.getClass().getName(), roleId);
            //获取在线的用户列表
            List<JWTUserDetails> onlineUserList = getOnlineUserList();
    
            adminAccountService.findAccountListByRoleId(roleId)
                    .forEach(accountId -> {
                        stringRedisTemplate.delete(JWTUtils.REDIS_KEY_PREFIX + accountId);
                        //完成session踢出
                        excludeChangedSession(onlineUserList, accountId);
                    });
        }
    
        /**
         * 剔除被修改权限后的会话
         *
         * @param onlineUserList
         * @param accountId
         */
        private void excludeChangedSession(List<JWTUserDetails> onlineUserList, Integer accountId) {
            onlineUserList.stream().filter(userDetails -> userDetails.getUserId().equals(accountId))
                    .forEach(userDetails -> {
                        //session剔除
                        sessionRegistry.getAllSessions(userDetails, false).forEach(SessionInformation::expireNow);
                    });
        }
    
        /**
         * 获取在线的userDetails列表
         *
         * @return
         */
        private List<JWTUserDetails> getOnlineUserList() {
            return sessionRegistry.getAllPrincipals()
                    .stream()
                    .filter(p->p instanceof JWTUserDetails)
                    .map(p-> ((JWTUserDetails) p))
                    .collect(toList());
        }
    }
    

    主要步骤就是:

    • 通过SessionRegistry获取所有的在线的Session会话
    • 获取所有角色对应的用户id集合
    • 使用SessionInfomation.expireNow()方法剔除服务

    最后使用Spring Security的Session配置管理来完成自定义的Session失效处理,配置如下,这里我自己贴上了完整的配置,如果只需要自定义Session失效处理,只需要怕配置.sessionManagement().maximumSessions(1).expiredUrl(traceSystemProperties.getSessionInvalidUrl()).sessionRegistry(sessionRegistry()) 和注入一个SessionRegistryBean即可

    • 自定义Session管理配置
    @Configuration
    @EnableWebSecurity
    @Slf4j
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final JWTAuthenticationFilter jwtAuthenticationFilter;
    
        private final CustomSuccessHandler customSuccessHandler;
    
        private final CustomFailureHandler customFailureHandler;
    
        private final ObjectMapper objectMapper;
    
        private final TraceSystemProperties traceSystemProperties;
    
        private final CustomUserDetailsService customUserDetailsService;
    
        private final DataSource dataSource;
    
        public WebSecurityConfig(JWTAuthenticationFilter jwtAuthenticationFilter,
                                 CustomSuccessHandler customSuccessHandler,
                                 CustomFailureHandler customFailureHandler,
                                 ObjectMapper objectMapper,
                                 TraceSystemProperties traceSystemProperties,
                                 CustomUserDetailsService customUserDetailsService,
                                 DataSource dataSource) {
            this.jwtAuthenticationFilter = jwtAuthenticationFilter;
            this.customSuccessHandler = customSuccessHandler;
            this.customFailureHandler = customFailureHandler;
            this.objectMapper = objectMapper;
            this.traceSystemProperties = traceSystemProperties;
            this.customUserDetailsService = customUserDetailsService;
            this.dataSource = dataSource;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .formLogin()
                    .failureHandler(customFailureHandler)
                    .successHandler(customSuccessHandler)
                    .and()
                    .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(traceSystemProperties.getRememberMeExpire())
                    .userDetailsService(customUserDetailsService)
                    .and()
                    .sessionManagement().maximumSessions(1).expiredUrl(traceSystemProperties.getSessionInvalidUrl()).sessionRegistry(sessionRegistry())
                    .and().and()
                    .authorizeRequests()
                    .antMatchers(traceSystemProperties.getLogoutUrl(),traceSystemProperties.getSessionInvalidUrl())
                    .permitAll()
                    .and()
                    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //jwt校验filter
                    .authorizeRequests().anyRequest().access("@checkPermissionProcessor.checkPermission(request)")
                    .and()
                    .exceptionHandling().authenticationEntryPoint(
                    (req, rsp, e) -> rsp.getWriter().write(objectMapper.writeValueAsString(
                            CommonError.builder()
                                    .msg(e.getMessage())
                                    .status(HttpStatus.UNAUTHORIZED.value())
                                    .build())))//自定义401错误解析
                    .accessDeniedHandler(
                            (req, rsp, e) -> rsp.getWriter().write(objectMapper.writeValueAsString(
                                    CommonError.builder()
                                            .msg(e.getMessage())
                                            .status(HttpStatus.FORBIDDEN.value())
                                            .build())));//自定义403错误解析
    
        }
    
        /**
         * 配置remember-me
         *
         * @return
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
    //        repository.setCreateTableOnStartup(true); //开机启动生成表结构
            repository.setDataSource(dataSource);
            return repository;
        }
    
        /**
         * 设置session注册器
         * @return
         */
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        /**
         * 加密解密
         *
         * @return
         */
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    • SessionExpireHandler
    @RestController
    @Slf4j
    public class SessionHandler {
    
        @RequestMapping("/session/timeout")
        public CommonResult sessionTimeoutHandler() {
            log.debug("current session timeout");
            return CommonResult.fail("session timeout");
        }
    }
    

    然后就解决了上述的session剔除问题。

    相关文章

      网友评论

          本文标题:一次关于Spring Security的session剔除问题

          本文链接:https://www.haomeiwen.com/subject/yupykctx.html