SpringSecurity整合SpringSession-Redis 限制用户登录

前言:

开发的前后端分离的项目,技术选用SpringBoot,用SpringSecurity控制权限,而后整合了SpringSession - Redis,将Session放到redis支持集群部署。项目一直是没问题的,但最近需要做一个限制用户登录的功能,即单个用户同一时刻仅能一处登录,虽然SpringSecurity很好的支持了这个功能,但是这个功能对集成了SpringSession-Redis项目产生的化学反应,非常的精彩!
首先看一下SpringSecurity 官网限制用户登录使用(单机非Redis)

Spring Security Reference 请查阅10.12.2 Concurrent Session Control

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

再看一下SpringSession官网给出与SpringSecurity整合

在这里插入图片描述
在这里插入图片描述

参照官网,代码如下
/**
 * @Description
 * In order for our Filter to do its magic, Spring needs to load our Config class.
 * Since our application is already loading Spring configuration by using our SecurityInitializer class,
 * we can add our configuration class to it.
 */
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public SecurityInitializer() {
        super(SecurityConfig.class);
    }
}


 @Configuration
@EnableWebSecurity
public class SecurityConfig<S extends Session> extends WebSecurityConfigurerAdapter {

    @Value("${maple.auth.api.public}")
    private String commonPublicPage;
    @Value("${maple.auth.api.admin}")
    private String adminPage;
    @Value("${maple.auth.api.common-get}")
    private String commonLoginGetPage;
    @Value("${maple.auth.api.common-post}")
    private String commonPostPage;
    @Value("${maple.auth.api.common-put}")
    private String commonPutPage;
	
	// SpringSecurity 重写的 Provider (代码不贴了,涉及业务)
    private AuthenticationProvider provider;
    // SpringSecurity 验证成功Handler 
    private AuthenticationSuccessHandler authenticationSuccessHandler;
	// SpringSecurity 验证成功Handler (代码不贴了,涉及业务)
    private AuthenticationFailureHandler authenticationFailureHandler;
    // SpringSecurity 登出成功Handler (代码不贴了,涉及业务)
    private LogoutSuccessHandler logoutSuccessHandler;
    // SpringSecurity 登录前过滤器 (业务需求,可忽略)
    private BeforeLoginFilter beforeLoginFilter;
    // SpringSecurity 判断Session过期过滤器,解决Sesssion失效请求拦截 (业务需求,可忽略)
    private SessionFilter sessionFilter;
    // 自定义Session失效策略
    private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(provider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .addFilterBefore(beforeLoginFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(sessionFilter, BeforeLoginFilter.class)
                .addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), sessionInformationExpiredStrategy), ConcurrentSessionFilter.class)
                //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
                .addFilterAt(loginByJsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessHandler(logoutSuccessHandler)
                .invalidateHttpSession(true)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, commonLoginGetPage == null ? new String[]{} : commonLoginGetPage.split(",")).authenticated()
                .antMatchers(HttpMethod.POST, commonPostPage == null ? new String[]{} : commonPostPage.split(",")).authenticated()
                .antMatchers(HttpMethod.PUT, commonPutPage == null ? new String[]{} : commonPutPage.split(",")).authenticated()
//                .antMatchers(HttpMethod.DELETE, commonDeletePage == null ? new String[]{} : commonDeletePage.split(",")).authenticated()
//                    .antMatchers(PUBLIC_PAGE).access(accessAttrGenerator.getPublicAccessIps())
                .antMatchers(commonPublicPage == null ? new String[]{} : commonPublicPage.split(",")).permitAll()
                .and()
                /* 禁用CSRF */
                .csrf().disable();
      
      http.sessionManagement()
//                 同一个账号只能在一个地方登陆
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry());
    }

	@Autowired
	private FindByIndexNameSessionRepository<S> sessionRepository;
	
	@Bean
	public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
		return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
	}
	
	@Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * 注册自定义的UsernamePasswordAuthenticationFilter
     */
    @Bean
    LoginByJsonAuthenticationFilter loginByJsonAuthenticationFilter() throws Exception {
        LoginByJsonAuthenticationFilter filter = new LoginByJsonAuthenticationFilter();
        filter.setFilterProcessesUrl("/auth/login");
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(authenticationFailureHandler);
        //重用WebSecurityConfigurerAdapter配置的AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
         //session并发控制,因为默认的并发控制方法是空方法.这里必须自己配置一个
        filter.setSessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()));
        return filter;
    }

    @Autowired
    public void setProvider(UserPwdAuthenticationProvider provider) {
        this.provider = provider;
    }

    @Autowired
    public void setAuthenticationSuccessHandler(UserPwdAuthenticationSuccessHandler authenticationSuccessHandler) {
        this.authenticationSuccessHandler = authenticationSuccessHandler;
    }

    @Autowired
    public void setAuthenticationFailureHandler(UserPwdAuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Autowired
    public void setLogoutSuccessHandler(UserPwdLogoutSuccessHandler logoutSuccessHandler) {
        this.logoutSuccessHandler = logoutSuccessHandler;
    }

    /*@Autowired
    public void setAccessAttrGenerator(AccessAttrGenerator accessAttrGenerator) {
        this.accessAttrGenerator = accessAttrGenerator;
    }*/

    @Autowired
    public void setBeforeLoginFilter(BeforeLoginFilter beforeLoginFilter) {
        this.beforeLoginFilter = beforeLoginFilter;
    }

    @Autowired
    public void setSessionFilter(SessionFilter sessionFilter) {
        this.sessionFilter = sessionFilter;
    }
	
	@Autowired
    public void setSessionInformationExpiredStrategy(CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy) {
        this.sessionInformationExpiredStrategy = sessionInformationExpiredStrategy;
    }
}

@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {
    //过期时间30分钟
    private Duration timeToLive = Duration.ofMinutes(30);
    @Value("${redis.host}")
    private String redisHost;
    @Value("${redis.port}")
    private int redisPort;
    @Value("${redis.password}")
    private String redisPassword;
    @Value("${redis.database}")
    private int redisDatabase;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
        config.setDatabase(redisDatabase);
        config.setPassword(RedisPassword.of(redisPassword));
        return new JedisConnectionFactory(config);
    }

    /**
     * 注册自定义的 redisCacheManager; 实现自定义JSON 缓存序列化机制
     * @param connectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(timeToLive)        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .disableCachingNullValues();
        return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
    }


    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    private RedisSerializer keySerializer() {
        return new StringRedisSerializer();
    }

    private RedisSerializer valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}


@EnableRedisHttpSession
@Configuration
public class SpringSessionRedisConfiguration extends AbstractHttpSessionApplicationInitializer {

    /**
     * 设置 SameSite 为null,解决整合SpringSession, Session不一致问题
     * @return
     */
    @Bean
    public CookieSerializer httpSessionIdResolver() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        // 取消same site
        cookieSerializer.setSameSite(null);
        return cookieSerializer;
    }
}

@Component
public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    private JsonUtils jsonUtils;

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HttpServletResponse response = event.getResponse();
        DetailResponse<Object> detailResponse = new DetailResponse<>();
        // SESSION失效
        detailResponse.setResult(GlobalResponseCode.SESSION_EXPIRED);
        jsonUtils.writeJSONResponse(response, detailResponse);
    }

    @Autowired
    public void setJsonUtils(JsonUtils jsonUtils) {
        this.jsonUtils = jsonUtils;
    }
}

@Component
public class UserPwdAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private JsonUtils jsonUtils;
    private WebResourceService webResourceService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        User user = (User) authentication.getPrincipal();
    
        // 用户信息放入session
        request.getSession().setAttribute(CommonConstant.SESSION_CURR_USER, user);
		
		// 用户名密码验证通过后, 将userNamr放入Session, key固定,用于框架解析(此为集群版配置)
        request.getSession().setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, user.getUrName());

        DetailResponse<UserResponse> rsResponse = new DetailResponse<>(GlobalConstant.SUCCESS);
        jsonUtils.writeJSONResponse(response, rsResponse);
    }

    @Autowired
    public void setJsonUtils(JsonUtils jsonUtils) {
        this.jsonUtils = jsonUtils;
    }
}
启动项目,报错

在这里插入图片描述

Surprise! 官网的示例报错了! 开始google、stackoverflow上搜,围着这个问题看了一圈,无果后开始查看源码;
FindByIndexNameSessionRepository 的实现类

在这里插入图片描述

在这里插入图片描述

它要RedisTemplate,但是我再IOC里注入了RedisTemplateRedisConnectionFactory,但是它没有获取到,OK,我手动给你SET,SecurityConfig 配置修改如下:
	 @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    // 集群使用sessionRegistry
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry() {
        RedisOperationsSessionRepository redisOperationsSessionRepository = new RedisOperationsSessionRepository(redisTemplate);
        return new SpringSessionBackedSessionRegistry(redisOperationsSessionRepository);
    }
OK, 项目起来了,运行项目,登录又报错了!

在这里插入图片描述

反序列化失败了! 为啥? 正常使用Redis Value的序列化推荐使用JSON格式、因为Jdk序列方式会添加很多不必要的东西,很浪费,可是使用GenericJackson2JsonRedisSerializer, 却总是报错序列化错误,网上又开始了查找 … 很多人说是ObjectMapper 在read的时候要设置,于是RedisSerializer 修改如下:
 	private RedisSerializer<Object> valueSerializer() {
        // 反序列化问题
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        mapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
        mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) ;
        mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return new GenericJackson2JsonRedisSerializer(mapper);
    }
嗯,一如既往的报错,于是我开始了研究,各种google、 Bing ,在反序列化的时候,总是报错,我尝试了将 key 的序列化修改,但是在SpringSecurity Redis中读FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 时就一直读不到,序列化进去后,无法读取出来,就导致限制用户登录无法实现,而后又尝试使用Jackson2JsonRedisSerializer,但它内部使用的也是ObjectMapper, 答案也是无果。
重新阅读官网,结合查阅的博客,可以通过springSessionDefaultRedisSerializer来控制他默认的序列化机制, Redis配置修改如下:
@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {

    //过期时间30分钟
    private Duration timeToLive = Duration.ofMinutes(30);
    @Value("${redis.host}")
    private String redisHost;
    @Value("${redis.port}")
    private int redisPort;
    @Value("${redis.password}")
    private String redisPassword;
    @Value("${redis.database}")
    private int redisDatabase;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
        config.setDatabase(redisDatabase);
        config.setPassword(RedisPassword.of(redisPassword));
        return new JedisConnectionFactory(config);
    }

    /**
     * 自定义key. 这个可以不用
     * 此方法将会根据类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
     */
    @Override
    public KeyGenerator keyGenerator() {
        return (o, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName());
            sb.append(method.getName());
            for (Object obj : objects) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    /**
     * 注册自定义的 redisCacheManager; 实现自定义JSON 缓存序列化机制
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(timeToLive)                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .disableCachingNullValues();
        return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
    }


    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(valueSerializer());
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    private RedisSerializer keySerializer() {
        return new StringRedisSerializer();
    }

    @Bean(name = {"defaultRedisSerializer", "springSessionDefaultRedisSerializer"})
    public RedisSerializer<Object> valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

又报错了

在这里插入图片描述

很明显,序列化的时候懒加载无限循环导致内存爆了,用户模块使用的是SpringData JPA,JPA 和Hibernate 虽然如出一辙,但是JPA的懒加载机制和Hibernate 不同,这边后面的问题就好解决了,但是比较急,没有再往后看,这个问题肯定是可以解决的,我换了另一种方式, 将GenericJackson2JsonRedisSerializer 修改为 JdkSerializationRedisSerializer,问题解决了,没错,就是它,他会有更多的消耗,这是临时解决方案,当然如果你项目不大,完全可以忽略这个问题,这就是最终解决方案,但是还是推荐大家把懒加载问题解决了,对Hibernate、JPA 或 MybatisPlus的解决方案,相信大家都能找到
最后Redis配置中 - valueSerializer 配置修改如下:
	 @Bean(name = {"defaultRedisSerializer", "springSessionDefaultRedisSerializer"})
    public RedisSerializer<Object> valueSerializer() {
        return new JdkSerializationRedisSerializer();
    }
最终,这个坑算是趟过去了,希望给同样踩坑的你一点帮助
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值