温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

发布时间:2021-05-12 10:39:50 来源:亿速云 阅读:840 作者:小新 栏目:开发技术

这篇文章主要介绍了SpringSecurity整合springBoot、redis实现登录互踢功能的示例,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

背景

要实现的功能是要实现一个用户不可以同时在两台设备上登录,有两种思路:
(1)后来的登录自动踢掉前面的登录。
(2)如果用户已经登录,则不允许后来者登录。
需要特别说明的是,项目的基础是已经是redis维护的session。

配置redisHttpSession

设置spring session由redis 管理。
2.1去掉yml中的http session 配置,yml和注解两者只选其一(同时配置,只有注解配置生效)。至于为什么不用yml,待会提到。

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

2.2 webSecurityConfig中加入注解@EnableRedisHttpSession

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

@EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 1700
        , flushMode = FlushMode.ON_SAVE)

登录后发现redis session namespace已经是我们命名的了

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

获取redis管理的sessionRepository

我们要限制一个用户的登录,自然要获取他在系统中的所有session。

2.再去查看springsSession官网的文档。springsession官网 提供文档https://docs.spring.io/spring-session/docs/   2.2.2.RELEASE/reference/html5/#api-findbyindexnamesessionrepository

SessionRepository实现也可以选择实现FindByIndexNameSessionRepository

FindByIndexNameSessionRepository提供一种方法,用于查找具有给定索引名称和索引值的所有会话

FindByIndexNameSessionRepository实现时,可以使用方便的方法查找特定用户的所有会话

/**
     * redis获取sessionRepository
     * RedisIndexedSessionRepository实现 FindByIndexNameSessionRepository接口
     */
    @Autowired
    //不加@Lazy这个会报什么循环引用...
    // Circular reference involving containing bean '.RedisHttpSessionConfiguration' 
    @Lazy   
    private FindByIndexNameSessionRepository<? extends Session> sessionRepository;

这里注意一点,当我通过yml配置redis session是,sessionRepository下面会有红线。

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

虽然不影响运行,但是强迫症,所以改用@EnableWebSecurity注解(至于为什么?我也不想知道…)。

将sessionRepository注入SpringSessionBackedSessionRegistry

是spring session为Spring Security提供的什么会话并发的会话注册表实现,大概是让springSecurity帮我们去限制登录,光一个sessionRepository是不行的,还得自己加点工具什么的。
webSecurityConfig加入:

/**
     * 是spring session为Spring Security提供的,
     * 用于在集群环境下控制会话并发的会话注册表实现
     * @return
     */
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(){
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

注:
https://blog.csdn.net/qq_34136709/article/details/106012825 这篇文章说还需要加一个HttpSessionEventPublisher来监听session销毁云云,大概是因为我用的是redis session吧,不需要这个,要了之后还会报错,啥错?我忘了。

新增一个session过期后的处理类

先创建一个CustomSessionInformationExpiredStrategy.java来处理session过期后如何通知前端的处理类,内容如下:

public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
        if (log.isDebugEnabled()) {
           log.debug("{} {}", event.getSessionInformation(), MessageConstant.SESSION_EVICT);
        }
        HttpServletResponse response = event.getResponse();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.SESSION_EVICT, MessageConstant.SESSION_EVICT));
        response.getWriter().write(responseJson);
    }
}

注:一般都是自己重新写返回前端的信息,不会直接用框架抛出的错误信息

配置到configure(HttpSecurity http)方法上

.csrf().disable()
//登录互踢
.sessionManagement()
//在这里设置session的认证策略无效
//.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry()))
.maximumSessions(1)
.sessionRegistry(sessionRegistry())
.maxSessionsPreventsLogin(false) //false表示不阻止登录,就是新的覆盖旧的
//session失效后要做什么(提示前端什么内容)
.expiredSessionStrategy(new CustomSessionInformationExpiredStrategy());

注意:https://blog.csdn.net/qq_34136709/article/details/106012825 这篇文章说session认证的原理,我看到它是执行了一个session的认证策略,但是我debug对应的代码时,发现

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

这个session认证策略是NullAuthenticatedSessionStrategy,而不是它说的ConcurrentSessionControlAuthenticationStrategy。就是说我需要在哪里去配置这个session 认证策略。第一时间想到了configure(HttpSecurity http)里面配置

SpringSecurity整合springBoot、redis实现登录互踢功能的示例

结果无效。之后看到别人的代码,想到这个策略应该是要在登录的时候加上去,而我们的登录一般都需要自己重写,自然上面的写法会无效。于是我找到了自定义的登录过滤器。

SpringSecurity整合springBoot、redis实现登录互踢功能的示例
SpringSecurity整合springBoot、redis实现登录互踢功能的示例

然后发现this.setSessionAuthenticationStrategy(sessionStrategy);确实存在。

public LoginFilter(UserVerifyAuthenticationProvider authenticationManager,
                       CustomAuthenticationSuccessHandler successHandler,
                       CustomAuthenticationFailureHandler failureHandler,
                       SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry) {
        //设置认证管理器(对登录请求进行认证和授权)
        this.authenticationManager = authenticationManager;
        //设置认证成功后的处理类
        this.setAuthenticationSuccessHandler(successHandler);
        //设置认证失败后的处理类
        this.setAuthenticationFailureHandler(failureHandler);
        //配置session认证策略(将springSecurity包装redis Session作为参数传入)
        ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new
                ConcurrentSessionControlAuthenticationStrategy(springSessionBackedSessionRegistry);
        //最多允许一个session
        sessionStrategy.setMaximumSessions(1);
        this.setSessionAuthenticationStrategy(sessionStrategy);
        //可以自定义登录请求的url
        super.setFilterProcessesUrl("/myLogin");
    }

启动 后就发现session认证策略已经改为我们设定的策略了。

完整的webSecurityConfig如下:

@Configuration
@EnableWebSecurity
//RedisFlushMode有两个参数:ON_SAVE(表示在response commit前刷新缓存),IMMEDIATE(表示只要有更新,就刷新缓存)
//yml和注解两者只选其一(同时配置,只有注解配置生效)
@EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 5000
        , flushMode = FlushMode.ON_SAVE)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

    @Autowired
    private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

    @Autowired
    private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

    @Autowired
    private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
    @Autowired
    private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验


    /**
     * redis获取sessionRepository
     * RedisIndexedSessionRepository实现 FindByIndexNameSessionRepository接口
     */
    @Autowired
    //不加@Lazy这个会报什么循环引用...
    // Circular reference involving containing bean '.RedisHttpSessionConfiguration'
    @Lazy
    private FindByIndexNameSessionRepository<? extends Session> sessionRepository;


    /**
     * 是spring session为Spring Security提供的,
     * 用于在集群环境下控制会话并发的会话注册表实现
     * @return
     */
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(){
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

    /**
     * 密码加密
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(PasswordEncoder.class)
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置 HttpSessionIdResolver Bean
     * 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
     * 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
     */
    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return HeaderHttpSessionIdResolver.xAuthToken();
    }
    /**
     * Swagger等静态资源不进行拦截
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js",
                "/error",
                "/webjars/**",
                "/resources/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口,这里配置失效了,放到了securityMetadataSource里面
                //.antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                //登录后的接口权限校验
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(accessDecisionManager);
                        object.setSecurityMetadataSource(securityMetadataSource);
                        return object;
                    }
                })
                .and()
                //配置登出处理
                .logout().logoutUrl("/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .clearAuthentication(true)
                .and()
                //用来解决匿名用户访问无权限资源时的异常
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                //用来解决登陆认证过的用户访问无权限资源时的异常
                .accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                //配置登录过滤器
                .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler, sessionRegistry()))

                .csrf().disable()
                //登录互踢
                .sessionManagement()
                //在这里设置session的认证策略无效
                //.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry()))
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .maxSessionsPreventsLogin(false) //false表示不阻止登录,就是新的覆盖旧的
                //session失效后要做什么(提示前端什么内容)
                .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy());
        //配置头部
        http.headers()
                .contentTypeOptions()
                .and()
                .xssProtection()
                .and()
                //禁用缓存
                .cacheControl()
                .and()
                .httpStrictTransportSecurity()
                .and()
                //禁用页面镶嵌frame劫持安全协议  // 防止iframe 造成跨域
                .frameOptions().disable();
    }

}

其他

@Lazy
private FindByIndexNameSessionRepository<? extends Session> sessionRepository;

至于这个不加@lazy会什么循环引用的问题,我就真的不想理会了。看了好长时间,都不知道谁和谁发生了循环引用。。。。。

感谢你能够认真阅读完这篇文章,希望小编分享的“SpringSecurity整合springBoot、redis实现登录互踢功能的示例”这篇文章对大家有帮助,同时也希望大家多多支持亿速云,关注亿速云行业资讯频道,更多相关知识等着你来学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI