背景

基于我的文章——《springsecurity整合springboot、redis token动态url权限校验》。要实现的功能是要实现一个用户不可以同时在两台设备上登录,有两种思路:
(1)后来的登录自动踢掉前面的登录。
(2)如果用户已经登录,则不允许后来者登录。
需要特别说明的是,项目的基础是已经是redis维护的session。

配置redishttpsession

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

2.2 websecurityconfig中加入注解@enableredishttpsession

@enableredishttpsession(redisnamespace = "spring:session:myframe", maxinactiveintervalinseconds = 1700
        , flushmode = flushmode.on_save)

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

获取redis管理的sessionrepository

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

2.再去查看springssession官网的文档。springsession官网 提供文档   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下面会有红线。

虽然不影响运行,但是强迫症,所以改用@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对应的代码时,发现

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

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

然后发现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——实现登录互踢的文章就介绍到这了,更多相关springsecurity登录互踢内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!