发生的主要情况是这样的,在使用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集合等一些信息 -
SessionRegistry
是Spring 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())
和注入一个SessionRegistry
的Bean
即可
- 自定义
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");
}
}
网友评论