`
sb33060418
  • 浏览: 150106 次
  • 性别: 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 3.x session-management 会话管理失效

    实现会话控制,权限控制,免登陆的spring security完整项目 博文链接:https://abc08010051.iteye.com/blog/1995886

    spring security3 中文版本

    spring security3 中文版本

    springboot springsecurity动态权限控制

    springboot springsecurity动态权限控制,实现数据库动态管理菜单权限

    SpringSecurity学习总结源代码

    SpringSecurity学习总结源代码

    SpringSecurity.zip

    什么是安全框架? 解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。...Apache Shiro 是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

    Spring Security 3.pdf

    Spring Security 3.pdf Spring Security 3.pdf Spring Security 3.pdf Spring Security 3.pdf

    spring security学习资料

    spring security方面的学习资料,包含:Spring+Security+3+与+CAS单点登录配置;Spring+Security3中文教程;Spring-Security安全权限管理手册;Spring+Security文库;还有一个学习笔记!

    spring security3动态权限

    struts2 + spring3 + hibernate3 + spring security3 + mysql + tomcat sys_users;sys_roles;sys_authorities;sys_resources;sys_users_roles;sys_roles_authorities;sys_authorities_resources; PS:此项目运行不...

    spring3+struts2+hibernate3+spring security3 权限管理

    (1)该项目是基于spring3+struts2+hibernate3+spring security3的权限管理项目 (2)后台我已经实现了权限管理,包括用户,角色和资源的分配。前台实现了spring security3的管理 (3)网上案例普遍是后台单一登陆。...

    最详细Spring Security学习资料(源码)

    会话管理:Spring Security支持对用户会话状态的管理,包括会话超时、并发控制、集群环境下的分布式会话管理等。 Web集成:Spring Security能够无缝集成到Spring框架和Spring MVC中,提供了过滤器、标签库等工具,...

    精彩:Spring Security 演讲PPT

    3、Spring Security 2.x Overview 4、Dive Into Spring Security Authentication Authorization 5、Development Experiences & Demo 6、Q & A 张明星 5年以上保险、电信大中型项目开发经验,对JavaEE有较深入理解...

    Spring Security 资料合集

    Spring Security三份资料,实战Spring Security 3.x.pdf;Spring Security 3.pdf;Spring Security使用手册.pdf

    spring security spring security

    spring security spring security 中文文档

    springSecurity3例子

    简单的 springSecurity3例子代码

    Spring Security学习总结一

    Spring Security学习总结一

    Spring Security3 Demo

    Spring Security3 Demo ,根据Spring Security 安全权限管理手册 整理出的例子。 通过eclipse部署。

    spring security 3 登录 退出 认证 最少配置

    项目应用到spring3,security3,hibernate4,struts2;应用中涉及到安全认证,目前项目有独立的统一认证网关,所以登录时只需要将安全认证网关的认证后信息塞到spring security中,由security3来管理用户的权限设置。...

    springsecurity学习笔记

    三更springsecurity学习笔记

    Spring Security OAuth2.0学习笔记.zip

    理解Spring Security的工作原理,Spring Security结构总览,认证流程和授权,中间涉及到哪些组件,这些组件分 别处理什么,如何自定义这些组件满足个性需求。 OAuth2.0认证的四种模式?它们的大体流程是什么? ...

    SpringSecurity项目

    springsecurity是一个功能强大且高度可定制的身份验证和访问控制框架。springsecurity是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring安全性的真正威力在于它可以很容易地扩展以...

Global site tag (gtag.js) - Google Analytics