SpringSecurity限制同一用户同时登录数

用户的同时登录场景,因地制宜。

一、背景

    在有些场景下,我们需要限制同一用户的登录数量限制。

    比如有些重要的账号,同一时刻只能有登录一次。

    再比如类似爱奇艺的会员制,同一时刻只能登录5次。

    此文对于登录的限制,采取的做法是:若发现同一用户登录了两次,则后者会将前者的 session 剔除,即要求前者再次登录。(有些场景下的限制是限制后者无法登录,本文暂不介绍)


</p>

二、实现

    本文介绍的是基于 SpringSecurity 的一套实现逻辑,个人实现部分比较简单,底层实现交由框架处理了。

    本文介绍的是基于 SpringBoot 的注解式配置,xml 版本的配置大同小异。

    ① 配置

    最重要的是下面代码的最后一行配置,设置 maximumSessions(2) 的值为2,代表同一个用户最大能同时登录2次,调整为1则代表同时只能登录一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
&nbsp;*&nbsp;Created&nbsp;by&nbsp;jet.chen&nbsp;on&nbsp;2018/4/12.
&nbsp;*/
@Configuration
@EnableWebSecurity
public&nbsp;class&nbsp;WebSecurityConfig&nbsp;extends&nbsp;WebSecurityConfigurerAdapter&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;@Autowired
&nbsp;&nbsp;&nbsp;&nbsp;SuccessLoginHandler&nbsp;successLoginHandler;
&nbsp;&nbsp;&nbsp;&nbsp;@Override
&nbsp;&nbsp;&nbsp;&nbsp;protected&nbsp;void&nbsp;configure(HttpSecurity&nbsp;httpSecurity)&nbsp;throws&nbsp;Exception&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;httpSecurity.authorizeRequests()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//所有静态资源及mobile资源都忽略权限控制
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.antMatchers("/resources/**",&nbsp;"/staff/forgotPassword/**",&nbsp;"/sms/sendVaildateCode",&nbsp;"/sms/login",&nbsp;"/auth/**",&nbsp;"/api/**"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.permitAll()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//任意请求角色必须为用户或者管理员
//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.anyRequest().hasAnyRole("USER",&nbsp;"ADMIN")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.anyRequest().authenticated()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.and()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//下面标示的路径不用进行CSRF验证
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.csrf().disable()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//配置表单登陆
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.formLogin()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//配置登陆页面路径并允许所有人访问登陆页面
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.loginPage("/login").permitAll()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.successHandler(successLoginHandler)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//登陆成功时的处理类
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.and()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//配置登出页面路径并允许所有人访问登陆页面
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.logout().logoutUrl("/logout").permitAll()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.and()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.rememberMe()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.rememberMeParameter("remember-me")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.tokenValiditySeconds(30*60*1000)&nbsp;//&nbsp;cookie有效期是30分钟
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.rememberMeCookieName("RM_COOKIE");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;httpSecurity.sessionManagement().maximumSessions(2).expiredUrl("/login");
&nbsp;&nbsp;&nbsp;&nbsp;}
}


    ② 用户实体类的改造

        我们自定义了一个实体类 public class StaffProfile implements Serializable, UserDetails{  }

        注意,UserDetails 来自如下包:org.springframework.security.core.userdetails.UserDetails;

        关键点是实体类需要重写如下三个方法:toString、hashCode、equals,

        原有下文介绍,代码如下:

        

1
2
3
4
5
6
7
8
9
10
11
12
13
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override
&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;String&nbsp;toString()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;this.loginName;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;@Override
&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;int&nbsp;hashCode()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;loginName.hashCode();
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;@Override
&nbsp;&nbsp;&nbsp;&nbsp;public&nbsp;boolean&nbsp;equals(Object&nbsp;obj)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(obj.getClass());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;obj&nbsp;instanceof&nbsp;UserDetails&nbsp;&&&nbsp;this.toString().equals(obj.toString());
&nbsp;&nbsp;&nbsp;&nbsp;}


三、源码解析

    此文所说的控制用户的登录数,实现方式其实就是控制同一用户创建的 session 数,达到阀值,则剔除最早的 session。

    关键代码是 org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy 类下的方法:onAuthentication()

    大致流程是,当按照上文所述设置了maximumSessions()之后,所有的请求都会走这个方法,这个方法会先获取该用户建立的所有 session,如果已建立的 session数量大于预设值,则会删除最早建立的 session;


session01.png














------ 本文结束 感谢阅读 ------
0%