`
sb33060418
  • 浏览: 152858 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Spring Security3边学边写(N)会话管理和并行控制

阅读更多
在开发系统认证授权时,经常会碰到需要控制单个用户重复登录次数或者手动踢掉登录用户的需求。如果使用Spring Security 3.1.x该如何实现呢?

Spring Security中可以使用session management进行会话管理,设置concurrency control控制单个用户并行会话数量,并且可以通过代码将用户的某个会话置为失效状态以达到踢用户下线的效果。

本次实践的前提是已使用spring3+Spring Security 3.1.x实现基础认证授权。

1.简单实现

要实现会话管理,必须先启用HttpSessionEventPublisher监听器。
修改web.xml加入以下配置
<listener>
	<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

如果spring security是简单的配置,如
<http use-expressions="true" access-denied-page="/login/noRight.jsp" 
		auto-config="true">
	<form-login login-page="/login/login.jsp" default-target-url="/inde.jsp" 
		authentication-failure-url="/login/login.jsp" always-use-default-target="true"/>
...
</http>

且没有使用自定义的entry-point和custom-filter,只要在<http></http>标签中添加<session-management>就可以是实现会话管理和并行控制功能,配置如下
<!-- 会话管理 -->
<session-management invalid-session-url="/login/logoff.jsp">
	<!-- 并行控制 -->
	<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>
</session-management>

其中invalid-session-url是配置会话失效转向地址;max-sessions是设置单个用户最大并行会话数;error-if-maximum-exceeded是配置当用户登录数达到最大时是否报错,设置为true时会报错且后登录的会话不能登录,默认为false不报错且将前一会话置为失效。
配置完后使用不同浏览器登录系统,就可以看到同一用户后来的会话不能登录或将已登录会话踢掉。

2.自定义配置

如果spring security的一段<http/>中使用了自定义过滤器<custom-filter/>(特别是FORM_LOGIN_FILTER),或者配置了AuthenticationEntryPoint,或者使用了自定义的UserDetails、AccessDecisionManager、AbstractSecurityInterceptor、FilterInvocationSecurityMetadataSource、UsernamePasswordAuthenticationFilter等,上面的简单配置可能就不会生效了,Spring Security Reference Documentation里面3.3.3 Session Management是这样说的:
If you are using a customized authentication filter for form-based login, then you have to configure concurrent session control support explicitly. More details can be found in the Session Management chapter.

按照文章第12.3章中说明,auto-config已经失效,就需要自行配置ConcurrentSessionFilter、ConcurrentSessionControlStrategy和SessionRegistry,虽然配置内容和缺省一致。配置如下:
<http use-expressions="true" access-denied-page="/login/noRight.jsp" ... 
	auto-config="false">
	<!-- 登录fliter配置 -->
	<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
	<custom-filter position="FORM_LOGIN_FILTER" 
		ref="myUsernamePasswordAuthenticationFilter" />
	<session-management 
		session-authentication-strategy-ref="sessionAuthenticationStrategy" 
		invalid-session-url="/login/logoff.jsp"/>
...
</http>
...
<beans:bean id="myUsernamePasswordAuthenticationFilter" 
	class="com.sunbin.login.security.MyUsernamePasswordAuthenticationFilter">
	<beans:property name="sessionAuthenticationStrategy" 
	ref="sessionAuthenticationStrategy" />
	<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<!-- sessionManagementFilter -->
<beans:bean id="concurrencyFilter"
	class="org.springframework.security.web.session.ConcurrentSessionFilter">
	<beans:property name="sessionRegistry" ref="sessionRegistry" />
	<beans:property name="expiredUrl" value="/login/logoff.jsp" />
</beans:bean>
<beans:bean id="sessionAuthenticationStrategy"
	class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
	<beans:constructor-arg name="sessionRegistry"
		ref="sessionRegistry" />
	<beans:property name="maximumSessions" value="1" />
</beans:bean>
<beans:bean id="sessionRegistry"
	class="org.springframework.security.core.session.SessionRegistryImpl" />

如果没有什么问题,配置完成后就可以看到会话管理的效果了。
需要和简单配置一样启用HttpSessionEventPublisher监听器。

3.会话管理

很多人做完第二步以后可能会发现,使用不同浏览器先后登录会话还是不受影响,这是怎么回事呢?是配置的问题还是被我忽悠了?我配置的时候也出现过这个问题,调试时看到确实走到了配置的sessionRegistry里却没有效果,在网上找了很久也没有找到答案,最后还是只能出动老办法:查看源码。

ConcurrentSessionControlStrategy源码部分如下:
public void onAuthentication(Authentication authentication, HttpServletRequest request,
        HttpServletResponse response) {
    checkAuthenticationAllowed(authentication, request);

    // Allow the parent to create a new session if necessary
    super.onAuthentication(authentication, request, response);
    sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}

private void checkAuthenticationAllowed(Authentication authentication, HttpServletRequest request)
        throws AuthenticationException {

    final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);

    int sessionCount = sessions.size();
    int allowedSessions = getMaximumSessionsForThisUser(authentication);

    if (sessionCount < allowedSessions) {
        // They haven't got too many login sessions running at present
        return;
    }

    if (allowedSessions == -1) {
        // We permit unlimited logins
        return;
    }

    if (sessionCount == allowedSessions) {
        HttpSession session = request.getSession(false);

        if (session != null) {
            // Only permit it though if this request is associated with one of the already registered sessions
            for (SessionInformation si : sessions) {
                if (si.getSessionId().equals(session.getId())) {
                    return;
                }
            }
        }
        // If the session is null, a new one will be created by the parent class, exceeding the allowed number
    }

    allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}

...

protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
        SessionRegistry registry) throws SessionAuthenticationException {
    if (exceptionIfMaximumExceeded || (sessions == null)) {
        throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControlStrategy.exceededAllowed",
                new Object[] {Integer.valueOf(allowableSessions)},
                "Maximum sessions of {0} for this principal exceeded"));
    }

    // Determine least recently used session, and mark it for invalidation
    SessionInformation leastRecentlyUsed = null;

    for (SessionInformation session : sessions) {
        if ((leastRecentlyUsed == null)
                || session.getLastRequest().before(leastRecentlyUsed.getLastRequest())) {
            leastRecentlyUsed = session;
        }
    }

    leastRecentlyUsed.expireNow();
}

checkAuthenticationAllowed是在用户认证的时候被onAuthentication调用,该方法首先调用SessionRegistryImpl.getAllSessions(authentication.getPrincipal(), false)获得用户已登录会话。如果已登录会话数小于最大允许会话数,或最大允许会话数为-1(不限制),或相同用户在已登录会话中重新登录(有点绕口,但有时候会有这种用户自己在同一会话中重复登录的情况,不注意就会重复计数),就调用SessionRegistry.registerNewSession注册新会话信息,允许本次会话登录;否则调用
allowableSessionsExceeded方法抛出异常或最老的会话置为失效。

接下来看SessionRegistryImpl类的源码,关键就是getAllSessions方法:
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
    final Set<String> sessionsUsedByPrincipal = principals.get(principal);

    if (sessionsUsedByPrincipal == null) {
        return Collections.emptyList();
    }

    List<SessionInformation> list = new ArrayList<SessionInformation>(sessionsUsedByPrincipal.size());

    for (String sessionId : sessionsUsedByPrincipal) {
        SessionInformation sessionInformation = getSessionInformation(sessionId);

        if (sessionInformation == null) {
            continue;
        }

        if (includeExpiredSessions || !sessionInformation.isExpired()) {
            list.add(sessionInformation);
        }
    }

    return list;
}

SessionRegistryImpl自己维护一个private final ConcurrentMap<Object,Set<String>> principals,并以用户信息principal作为key来保存某一用户所有已登录会话编号。

再次调试代码时发现,principals中明明有该用户principal但principals.get(principal)取到的是null,然后认证成功,又往principals里面put了一个新的principal对象为key。查看debug控制台发现principals中两次登录的principal内容一致,但却无法从map中取得,这说明新登录的principal和旧的不相等。

再查看ConcurrentHashMap.get(Object key)方法源码就能找到问题了。我们知道Map中取值的时候都是要逻辑上相等的,即hash值相等且equals。如果两次登录的principal逻辑上不相等,自然被认为是两个用户,不会受最大会话数限制了。

这里会话管理不生效的原因是在自定义的UserDetails。一般配置Spring Security都会自己实现用户信息接口
public class User implements UserDetails, Serializable

并实现几个主要方法isAccountNonExpired()、getAuthorities()等,但却忘记重写继承自Object类的equals()和hashCode()方法,导致用户两次登录的信息无法被认为是同一个用户。

查看Spring Security的用户类org.springframework.security.core.userdetails.User源码
    /**
     * Returns {@code true} if the supplied object is a {@code User} instance with the
     * same {@code username} value.
     * <p>
     * In other words, the objects are equal if they have the same username, representing the
     * same principal.
     */
    @Override
    public boolean equals(Object rhs) {
        if (rhs instanceof User) {
            return username.equals(((User) rhs).username);
        }
        return false;
    }

    /**
     * Returns the hashcode of the {@code username}.
     */
    @Override
    public int hashCode() {
        return username.hashCode();
    }

只要把这两个方法加到自己实现的UserDetails类里面去就可以解决问题了。

4.自己管理会话

以下部分内容参考wei_ya_wen的http://blog.csdn.net/wei_ya_wen/article/details/8455415这篇文章。

管理员踢出一个账号的实现参考如下:
@RequestMapping(value = "logout.html") 
public String logout(String sessionId, String sessionRegistryId, String name, HttpServletRequest request, ModelMap model){    
    List<Object> userList=sessionRegistry.getAllPrincipals();  
    for(int i=0; i<userList.size(); i++){  
        User userTemp=(User) userList.get(i);      
        if(userTemp.getName().equals(name)){          
            List<SessionInformation> sessionInformationList = sessionRegistry.getAllSessions(userTemp, false);  
            if (sessionInformationList!=null) {   
                for (int j=0; j<sessionInformationList.size(); j++) {  
                    sessionInformationList.get(j).expireNow();  
                    sessionRegistry.removeSessionInformation(sessionInformationList.get(j).getSessionId());  
                    String remark=userTemp.getName()+"被管理员"+SecurityHolder.getUsername()+"踢出";  
                    loginLogService.logoutLog(userTemp, sessionId, remark);     //记录注销日志和减少在线用户1个  
                    logger.info(userTemp.getId()+"  "+userTemp.getName()+"用户会话销毁," + remark);  
                }  
            }  
        }  
    }  
    return "auth/onlineUser/onlineUserList.html";  
}  

如果想彻底删除, 需要加上
sessionRegistry.removeSessionInformation(sessionInformationList.get(j).getSessionId());

不需要删除用户,因为SessionRegistryImpl在removeSessionInformation时会自动判断用户是否无会话并删除用户,源码如下
if (sessionsUsedByPrincipal.isEmpty()) {
            // No need to keep object in principals Map anymore
            if (logger.isDebugEnabled()) {
                logger.debug("Removing principal " + info.getPrincipal() + " from registry");
            }
            principals.remove(info.getPrincipal());
        }
分享到:
评论
11 楼 守望麦穗 2017-01-13  
楼主好, 我按着你的步骤查找问题  ,到第二步,也没有自定义UserDetails,为什么还是没有效果(还是可以多个用户使用同一账号同时登录)  求解
10 楼 sb33060418 2015-11-18  
Notify 写道
楼主,但是配置<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>  后,直接关闭浏览器,不能销毁session,再登陆这个用户,会提示用户已登陆,只能等session过期, 有没有什么办法可以解决这个问题?

error-if-maximum-exceeded="true"表示如果用户登录会话数达到最大数量就报错,而不是踢掉第一个登录的会话,把这个属性去掉就可以了
9 楼 Notify 2015-11-13  
楼主,但是配置<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>  后,直接关闭浏览器,不能销毁session,再登陆这个用户,会提示用户已登陆,只能等session过期, 有没有什么办法可以解决这个问题?
8 楼 409421884 2015-10-24  
你好,我也在做这个功能,但sessionRegistry这个东西一直都获取不到值,启动也没报错。查了好几天原因也没查出来。大概问题处在哪
7 楼 sb33060418 2015-07-08  
左眼的彼岸 写道
sb33060418 写道
左眼的彼岸 写道
谢谢,我已经完美集成到我的框架里,不知道为什么做完第二步我的就可以用了,不同浏览器都试了,不能同时登录


可能因为你没有使用自定义的UserDetails吧
我在本地没有问题,可以运行, 房子服务器上出先了错误:SessionId required as per interface contract,该怎么解决

请看Dead_knight的文章 http://dead-knight.iteye.com/blog/1517716,
public void removeSessionInformation(String sessionId) { 
    Assert.hasText(sessionId, "SessionId required as per interface contract"); 

这里,估计是取不到sessionId了
6 楼 左眼的彼岸 2015-04-23  
sb33060418 写道
左眼的彼岸 写道
谢谢,我已经完美集成到我的框架里,不知道为什么做完第二步我的就可以用了,不同浏览器都试了,不能同时登录


可能因为你没有使用自定义的UserDetails吧
我在本地没有问题,可以运行, 房子服务器上出先了错误:SessionId required as per interface contract,该怎么解决
5 楼 sb33060418 2015-04-22  
左眼的彼岸 写道
谢谢,我已经完美集成到我的框架里,不知道为什么做完第二步我的就可以用了,不同浏览器都试了,不能同时登录


可能因为你没有使用自定义的UserDetails吧
4 楼 左眼的彼岸 2015-04-21  
谢谢,我已经完美集成到我的框架里,不知道为什么做完第二步我的就可以用了,不同浏览器都试了,不能同时登录
3 楼 sb33060418 2015-03-06  
MrwenQ 写道
楼主你好,我按你方法做的 还是不好使啊。连我自己手动踢出用户都踢不了。能请楼主说说嘛?

为什么踢不了?有没有调试过源码
2 楼 MrwenQ 2014-09-29  
楼主你好,我按你方法做的 还是不好使啊。连我自己手动踢出用户都踢不了。能请楼主说说嘛?
1 楼 abc08010051 2013-12-26  
我遇到楼主所说的问题,按照楼主提供的方法解决了,楼主非常棒!

相关推荐

    spring-security-reference-zh

    ### Spring Security 参考手册知识点概述 #### 一、引言 **Spring Security** 是一个为基于 Java EE 的企业级软件应用提供全面安全保障的框架。它不仅提供了丰富的安全功能,而且具备高度的可配置性,使得开发者...

    Spring Security 2.0.x完全中文参考文档

    Spring Security 是一个强大的、高度可定制的身份验证和访问控制框架。它是Spring项目的一个子项目,主要用来简化Java应用程序中的安全配置过程。Spring Security 提供了一套完整的解决方案来解决Web应用程序和普通...

    基于Acticiti7的Web工作流引擎,使用BPMN-JS绘制业务流程 集成SpringSecurity安全框架

    SpringSecurity是Java领域的安全框架,它提供了一套完整的访问控制和认证解决方案。在本项目中,SpringSecurity可能用于: 1. **用户认证**:通过用户名和密码验证用户身份。 2. **权限控制**:根据角色和权限分配...

    Spring_Security_2.0.x中文参考文档

    - Spring Security 是一个强大的、高度可定制的身份验证和访问控制框架。 - **历史** - Spring Security 的历史可以追溯到 RACOON 项目(RACOON 本身是基于 Acegi 安全框架),它于 2005 年被整合进 Spring 框架中...

    12 spring boot Security Jwt 前后端分离跨域登录

    标题 "12 spring boot Security Jwt 前后端分离跨域登录" 提示我们这个项目是关于使用Spring Boot Security和JWT(JSON Web Tokens)技术来实现一个前后端分离的跨域登录系统。在这个系统中,Spring Boot作为后端...

    Spring开发相关Jar包

    7. **Spring Web Flow**:这是一个用于构建复杂Web交互流程的框架,允许定义和管理用户会话状态,适用于多步骤的业务流程。 8. **Spring Boot**:近年来流行的快速启动Spring应用程序的方式,它默认配置了许多常见...

    Spring攻略(第二版 中文高清版).part1

    第3章 Spring AOP和AspectJ支持 112 3.1 启用Spring的AspectJ注解支持 113 3.1.1 问题 113 3.1.2 解决方案 113 3.1.3 工作原理 113 3.2 用AspectJ注解声明aspect 115 3.2.1 问题 115 3.2.2 解决方案...

    springcloud基础

    Spring Cloud 是一个基于 Spring Boot 实现的云应用开发工具集,它为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态等...

    Spring攻略(第二版 中文高清版).part2

    第3章 Spring AOP和AspectJ支持 112 3.1 启用Spring的AspectJ注解支持 113 3.1.1 问题 113 3.1.2 解决方案 113 3.1.3 工作原理 113 3.2 用AspectJ注解声明aspect 115 3.2.1 问题 115 3.2.2 解决方案...

    ELADMIN 后台管理系统

    在ELADMIN系统中,JWT用于生成和验证用户令牌,实现用户身份的安全认证和会话管理,避免了传统session存储带来的问题。 Spring Security是Spring生态中的安全框架,用于处理Web应用的安全需求,如权限控制、登录...

    boot-ssoserver

    3. **Spring Cloud**:Spring Cloud为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线等)中开发应用的一系列工具。在本项目中,Spring Cloud可能用于实现服务发现、配置中心...

    管理系统系列--SpringBoot+ant-design-pro-vue前后端分离,权限管理系统.zip

    1. **Spring Security**:SpringBoot的一个安全模块,用于实现认证和授权,可以轻松集成到SpringBoot应用中,为用户提供细粒度的权限控制。 2. **JWT(JSON Web Token)**:一种轻量级的身份验证机制,用于在客户端...

    spring-enterprise-in-practice:spring-enterprise-in-practice

    8. **Spring Security**:原名Acegi Security,是Spring的认证和授权组件,提供了一套完整的安全解决方案,包括身份验证、授权、CSRF保护、会话管理等。 9. **Spring Batch**:用于处理批量处理任务的模块,提供了...

    springboot235基于SpringBoot的房屋交易平台的设计与实现.zip

    3. 起步依赖:SpringBoot通过“starter”项目管理依赖,如web、data、security等,使得添加新功能变得简单。 二、房屋交易平台架构设计 1. 微服务架构:本平台可能采用了微服务架构,将复杂系统拆分为多个小型独立...

    webFlux:SpringwebFlux

    Spring WebFlux是Spring Framework 5.0及以上版本的一部分,与传统的Spring MVC并行存在,但设计上更侧重于非阻塞I/O和事件驱动模型,这使得它在现代云环境和微服务架构中表现出色。 1. **反应式编程基础** 反应式...

    java聊天系统毕业设计

    Spring Security框架可以简化这部分的实现,提供安全的登录和权限控制功能。 用户界面设计方面,JavaFX或Swing可以用来构建图形用户界面(GUI),提供友好的聊天窗口和交互体验。事件监听和处理机制使得用户可以...

    springboot + vue实现博客系统.zip

    SpringBoot提供了Spring Security框架,可以方便地集成到项目中,实现权限控制和会话管理。 【部署与运维】:项目完成后,可以使用Docker容器化技术进行部署,便于在不同环境之间迁移和扩展。同时,可以通过监控...

    jeecgframework.zip

    在安全方面,JeecgFramework集成了Shiro和Spring Security,提供权限管理、登录认证、会话管理等多重安全保障。同时,针对SQL注入、XSS攻击等常见安全威胁,框架也进行了有效的防护。 七、微服务支持 随着微服务...

    springboot401学生选课系统--论文pf.zip

    Spring Security或OAuth2可能被用来处理系统的认证和授权,确保只有经过身份验证的用户才能访问特定的资源。此外,还可以通过HTTPS协议加密传输,防止数据在传输过程中被窃取。 5. **前后端分离** 系统可能采用了...

Global site tag (gtag.js) - Google Analytics