只允许一个用户在一个地方登录,也是每个用户在系统中只能有一个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

posted on 2021-01-24 02:19  西门夜说  阅读(2735)  评论(4编辑  收藏  举报