spring security(七) session 并发,一个用户在线后其他的设备登录此用户失败

原创 2017年08月23日 14:35:16

这又是 一片 关于security 的文章,用于解决 session 并发问题 ,同时只有一个用户在线。 有一个用户在线后其他的设备登录此用户失败。

本文有两个实现方法,第一种实现方法稍微繁琐。
第二种方法有个小bug 但是可以通过前端的配合解决此bug。

本文代码,是基于 springboot+security restful权限控制官方推荐(五) 的代码

方法一

1. 修改security配置

修改 WebSecurityConfig 文件
添加 SessionRegistry 和 httpSessionEventPublisher
在configure 方法中 添加 maximumSessions(1).maxSessionsPreventsLogin(true).sessionRegistry(sessionRegistry) 配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
     private  CustomUserService customUserService;
    @Autowired
    SessionRegistry sessionRegistry;

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/users/**")
                .authenticated()
                .antMatchers(HttpMethod.POST)
                .authenticated()
                .antMatchers(HttpMethod.PUT)
                .authenticated()
                .antMatchers(HttpMethod.DELETE)
                .authenticated()
                .antMatchers("/**")
                .permitAll()
                .and()
                .sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true).sessionRegistry(sessionRegistry);
        http.httpBasic();
    }

    @Bean
    public SessionRegistry getSessionRegistry(){
        SessionRegistry sessionRegistry=new SessionRegistryImpl();
        return sessionRegistry;
    }

    @Bean
    public ServletListenerRegistrationBean httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }
}

2. UserDetails 添加比较

修改 实现 UserDetails 接口的类,一般都是 user 实体。

因为SpringSecurity是通过管理UserDetails对象来实现用户管理的,按照上一步的原理,是需要进行比较实现效果的,然后呢,类的比较是不能用==比较的,类之间的比较是通过类的equals方法进行比较的。

我们现在开始重写equals方法吧,关于重写的规则是:如果要重写equals方法,那么就必须要重写toStirng方法,如果要重写toString方法就最好要重写hashCode方法,所以我们需要在自定义的User对象中重写三个方法,hashCode、toString和equals方法。

@Override
    public String toString() {
        return this.username;
    }

    @Override
    public int hashCode() {
        return username.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return this.toString().equals(obj.toString());
    }

3. loadUserByUsername 方法 添加相同用户检测代码

@Service
public class CustomUserService implements UserDetailsService { //自定义UserDetailsService 接口

    @Autowired
    UserDao userDao;
    @Autowired
    private SessionRegistry sessionRegistry;
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CustomUserService.class);

    @Override
    public UserDetails loadUserByUsername(String username) { //重写loadUserByUsername 方法获得 userdetails 类型用户

        SysUser user = userDao.findByUserName(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        //用户已经登录则此次登录失败
        List<Object> o = sessionRegistry.getAllPrincipals();
        for ( Object principal : o) {
            if (principal instanceof SysUser && (user.getUsername().equals(((SysUser) principal).getUsername()))) {
                throw new SessionAuthenticationException("当前用户已经在线,登录失败!!!");
            }
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        //用于添加用户的权限。只要把用户权限添加到authorities 就万事大吉。
        for(SysRole role:user.getRoles())
        {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
            logger.info("loadUserByUsername: " + user);
        }
        user.setGrantedAuthorities(authorities); //用于登录时 @AuthenticationPrincipal 标签取值
        return user;
    }

}

4. 定时session失效

由于我们 添加了SessionRegistry 所以session 失效要自己维护一下。写一个定时事件,

    /**
     * 超时事件检查
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    public void ScanUserOnline() {
    //获取所有在线用户
        List<User> users = userDao.getUsersWithOnLine(4);
        users.stream().parallel().forEach(user -> {
        //通过时间判断是否session过期
            if (CommonUtil.CalculationData(user.getAccessLastTime(),30)) {
                //如果过期则设置用户为下下线状态
                updateOnline(user.getId(),4,null);
            //session 置为失效  SessionUtil.expireSession(null,user,sessionRegistry);
            }
        });
    }


 /**
     * session 失效
     * @param request
     * @param sessionRegistry
     */
    public static void expireSession(HttpServletRequest request,User user, SessionRegistry sessionRegistry){
        List<SessionInformation>  sessionsInfo = null;
        if (null != user) {
            List<Object> o = sessionRegistry.getAllPrincipals();
            for ( Object principal : o) {
                if (principal instanceof User && (user.getUsername().equals(((User) principal).getUsername()))) {
                    sessionsInfo = sessionRegistry.getAllSessions(principal, false);
                }
            }
        }else if (null != request ) {
            SecurityContext sc = (SecurityContext) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
            if (null != sc.getAuthentication().getPrincipal()) {
                sessionsInfo = sessionRegistry.getAllSessions(sc.getAuthentication().getPrincipal(), false);
                sc.setAuthentication(null);
            }
        }
        if(null != sessionsInfo && sessionsInfo.size() > 0) {
            for (SessionInformation sessionInformation : sessionsInfo) {
                //当前session失效
                sessionInformation.expireNow();
                sessionRegistry.removeSessionInformation(sessionInformation.getSessionId());
            }
        }
    }

5. logout方法修改

 @RequestMapping(value="/offline", method = RequestMethod.GET)
    @ResponseBody
    public String offline (HttpServletRequest request, HttpServletResponse response) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null){
            //设置为离线状态
            userService.updateOnline(((User)auth.getPrincipal()).getId(), 4 ,null);
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "ok";
    }

方法二

方法二就是 http://blog.csdn.net/xusanhong/article/details/53260373 博客写的方法。

bug

不过此方法有个 bug 就是 当用户A在设备1上登录, 在设备2上再次登录 用户A 会被拒绝, 但是 在设备2 上登录 用户B,在用户B不登出的情况下,在设备2 上登录用户A 。此时用户A会登录成功,系统中将会有两个用户A 的session 存在。

解决:这个bug 可以通过于前端的配合规避掉。当前端在login 页面时检测浏览器是否有session,如果有session 就强制跳转到 首页,不允许用户在有session 的情况下,通过在浏览器敲路由的方式进入 login 页面。

1.在WebSecurityConfig 文件中添加配置

.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true).sessionRegistry(sessionRegistry)

2.在 UserDetails 添加比较

```
@Override
    public String toString() {
        return this.username;
    }

    @Override
    public int hashCode() {
        return username.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return this.toString().equals(obj.toString());
    }

“`

参考:http://blog.csdn.net/xusanhong/article/details/53260373

版权声明:本文为博主编写文章,未经博主允许转载,转载请注明出处。

相关文章推荐

Spring开发中的异常处理

1.经常会出现@Autowired注入失败,说什么依赖找不到,原因之一是注入依赖对象存在错误,另外一个就是依赖对象确实是不存在 例如看到官网直接注入RestTemplate,如下 @Autowire...

Spring Security教程(14)---- Logout和SessionManager

Logout的配置很简单,只需要在http中加入下面的配置就可以了 invalidate-session是否销毁Session logout-url logout地址 logout-success-u...
  • jaune161
  • jaune161
  • 2014年01月24日 16:23
  • 19226

spring-security+spring-session配置

spring-session的配置1.dependency 2.applicationContext.xml 3.web.xml 4.分布式 5.遇到的一些问题1. dependencyspr...

spring security3.x学习(23)_session管理和session监听

在看一下session的管理 我们只需要在http中配置session-management就可以了。那我们仔细看一下这个标签吧 session-fixation-prot...

基于java config的springSecurity(五)--session并发控制

参考spring-security-reference.pdf的Session Management.特别是Concurrency Control小节. 管理session可以做到: a.跟踪活跃的s...

使用Spring-Security进行登录控制的session问题

这边文章Spring Security Form Login Using Database对Spring Security做了非常简单明了的介绍。而且有一个可以down下来运行的示例工程。笔者试验...

SpringBoot整合SpringSecurity,SESSION 并发管理,同账号只允许登录一次

重写了UsernamePasswordAuthenticationFilter,里面继承AbstractAuthenticationProcessingFilter,这个类里面的session认证策略...
  • LWJdear
  • LWJdear
  • 2017年06月19日 16:52
  • 1512

Session的详解,Session导致并发问题

来源:http://www.cnblogs.com/fish-li/archive/2011/07/31/2123191.html 阅读目录 开始 Session的来龙去脉 Ses...

万恶的session,同一sessionid不能并发,session锁

只要网站使用了session,那么每次请求就会在整个生命周期中,锁住session,这样同一sessionid的请求就必须等待解锁 这就表示,如果网站有个超时的页面,那就什么事也干不了了,必须等这个...
  • zb219
  • zb219
  • 2015年01月30日 00:52
  • 1629

在项目中遇到的一个并发访问时session不一致的问题

出现sessionID不一致的Action类: public class MaincontrgenAction extends AbstractAction{ private Logger log =...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:spring security(七) session 并发,一个用户在线后其他的设备登录此用户失败
举报原因:
原因补充:

(最多只允许输入30个字)