只允许一个用户在一个地方登录,也是每个用户在系统中只能有一个Session。如果同一用户在第2个地方登录,则将第1个踢下线。
1.自定义 CustomSessionInformationExpiredStrategy 实现类来定制策略
/** * 同一用户只允许一台电脑登录 * 同一用户在第2个地方登录,则将第1个踢下线 * 当同一用户的 session 达到指定数量时,执行此类 */ @Component("customSessionInformationExpiredStrategy") public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException { //获取用户名 UserDetails userDetails = (UserDetails) event.getSessionInformation().getPrincipal(); AuthenticationException exception = new AuthenticationServiceException(String.format("[%s]用户在另外一台电脑登录,您已被下线", userDetails.getUsername())); try { //当用户在另外一台电脑登录后,交给失败处理器响应给前端json数据 customAuthenticationFailureHandler.onAuthenticationFailure(event.getRequest(), event.getResponse(), exception); } catch (ServletException e) { e.printStackTrace(); } } }
2.将自定义CustomSessionInformationExpiredStrategy实例 注入到安全配置类SpringSecurityConfig中,并进行配置
/** * 安全配置类作为安全控制中心, 用于实现身份认证与授权配置功能 */ @Configuration @EnableWebSecurity //启动 SpringSecurity 过滤器链功能 public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired SecurityProperties securityProperties; Logger logger = LoggerFactory.getLogger(SpringSecurityConfig.class); @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { // 加密存储 明文+随机盐值 return new BCryptPasswordEncoder(); } @Autowired CustomUserDetailsService customUserDetailsService; /** * 认证管理器: * 1、认证信息提供方式(用户名、密码、当前用户的资源权限) * 2、可采用内存存储方式,也可能采用数据库方式等 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //基于内存存储认证信息 存储的密码必须是加密后的 不然会报错:There is no PasswordEncoder mapped for the id "null" //auth.inMemoryAuthentication().withUser("zcc").password("123").authorities("ADMIN"); /*String password = bCryptPasswordEncoder().encode("123"); logger.info("加密后的密码:" + password); auth.inMemoryAuthentication().withUser("zcc").password(password).authorities("ADMIN");*/ // 指定使用自定义查询用户信息来完成身份认证 auth.userDetailsService(customUserDetailsService); } @Autowired CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Autowired ImageVerifyCodeValidateFilter imageVerifyCodeValidateFilter; @Autowired SmsVerifyCodeValidateFilter smsVerifyCodeValidateFilter; @Autowired MobileAuthenticationConfig mobileAuthenticationConfig; @Autowired CustomInvalidSessionStrategy customInvalidSessionStrategy; @Autowired CustomSessionInformationExpiredStrategy customSessionInformationExpiredStrategy; /** * 记住我 功能 */ @Autowired DataSource dataSource; @Bean public JdbcTokenRepositoryImpl jdbcTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // 是否启动时自动创建表,第一次启动创建就行,后面启动把这个注释掉,不然报错已存在表 //jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } /** * 资源权限配置(过滤器链): * 1、被拦截的资源 * 2、资源所对应的角色权限 * 3、定义认证方式:httpBasic 、httpForm * 4、定制登录页面、登录请求地址、错误处理方式 * 5、自定义 spring security 过滤器 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //http.httpBasic()//采用httpBasic 认证方式 /*http.formLogin() .loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login .usernameParameter("name") // 默认用户名的属性名是 username .passwordParameter("pwd") // 默认密码的属性名是 password .and() .authorizeRequests()//认证请求 .antMatchers("/login/page").permitAll()//自定义登录页不需要认证 .anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/ http .addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .formLogin() .loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login .usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username .passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password .successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器 .failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器 .and() .authorizeRequests()//认证请求 .antMatchers(securityProperties.getLoginPage(),securityProperties.getMobilePage(),securityProperties.getImageCodeUrl(),securityProperties.getMobileCodeUrl()).permitAll()//自定义登录页不需要认证,生成图片验证码,发送短信获取验证码也不需要验证 .anyRequest().authenticated()// 所有进入应用的HTTP请求都要进行认证 .and() .rememberMe()//记住我功能 .tokenRepository(jdbcTokenRepository())//保存登录信息 .tokenValiditySeconds(securityProperties.getTokenValiditySeconds())//记住我有效时长一周 .and() .sessionManagement()//session会话管理 .invalidSessionStrategy(customInvalidSessionStrategy)//当session失效后的处理类 .maximumSessions(1)// 每个用户在系统中的最大session数 .expiredSessionStrategy(customSessionInformationExpiredStrategy)//当用户达到最大session数后,则调用此处的实现 ; // 将手机相关的配置绑定过滤器链上 http.apply(mobileAuthenticationConfig); } /** * 放行静态资源(js css 等) * * @param web */ @Override public void configure(WebSecurity web) { //web.ignoring().antMatchers("/dist/**", "/modules/**", "/plugins/**"); web.ignoring().antMatchers(securityProperties.getStaticPaths()); } }
3、测试
完整代码地址:https://gitee.com/zhechaochao/security-parent.git