关于spring Security重复登录的配置,百度一大堆,我这里就不啰嗦了。
今天碰到 按照网上的配置,但是 感觉配置无效,同一用户还是可以登录,不知道为什么,开始以为是自己配置的又问题。再三确认感觉自己的配置没有一点问题,
所有查找原因:查看源代码 发现 org.springframework.security.core.session.SessionRegistryImpl 类中的
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
final Set<String> sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
}
的 sessionsUsedByPrincipal 一直为null。
直接把final Set<String> sessionsUsedByPrincipal = principals.get(principal); 放入百度中搜索,果然找到了自己想要的文章:
连接为:http://sb33060418.iteye.com/blog/1953515
当然要主要修改自己的 public class XaUserDetails implements UserDetails
@Override
public boolean equals(Object rhs) {
if (rhs instanceof XaUserDetails) {
return username.equals(((XaUserDetails) rhs).getUsername());
}
return false;
}
/**
* Returns the hashcode of the {@code username}.
*/
@Override
public int hashCode() {
return username.hashCode();
}
这里记的要改成 自己的类哦。
害怕大神把他的文章删掉了,我自己复制下来,为自己以后查看方便,再次感谢大神。
在开发系统认证授权时,经常会碰到需要控制单个用户重复登录次数或者手动踢掉登录用户的需求。如果使用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());
- }
然后附上我自己的配置,这个还需要优化,感觉有重复的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<global-method-security pre-post-annotations="enabled" />
<!-- 测试阶段使用 -->
<http pattern="/" security="none"/>
<!-- HTTP安全配置 -->
<http auto-config="false" entry-point-ref="authenticationEntryPoint" access-denied-page="/denied.html">
<intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/index.html" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/m/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/commons/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/upload/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/cms/**" access="ROLE_USER"/>
<!-- <intercept-url pattern="/adminIndex.html" access="ROLE_USER"/> -->
<intercept-url pattern="/pages/*.html" access="ROLE_USER"/>
<!-- logout-success-url="/login.html" -->
<logout logout-url="/j_spring_security_logout" invalidate-session="true"
delete-cookies="JSESSIONID" success-handler-ref="myLogoutSuccessHandler"/>
<custom-filter ref="corsFilter" after="PRE_AUTH_FILTER"/>
<custom-filter ref="myLoginFilter" position="FORM_LOGIN_FILTER" />
<custom-filter ref="mySecurityFilter" before="FILTER_SECURITY_INTERCEPTOR" />
<!-- lj添加 -->
<session-management invalid-session-url="/login.html"
session-authentication-error-url="/login.html"
session-authentication-strategy-ref="sas" />
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<!-- lj添加 结束 -->
</http>
<beans:bean id="corsFilter" class="com.threeti.danfoss.base.filter.SecurityCorsFilter" />
<beans:bean id="sas"
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
<beans:property name="maximumSessions" value="1"></beans:property>
<beans:property name="exceptionIfMaximumExceeded"
value="true"></beans:property>
<beans:constructor-arg name="sessionRegistry"
ref="sessionRegistry"></beans:constructor-arg>
</beans:bean>
<beans:bean id="sessionRegistry"
class="org.springframework.security.core.session.SessionRegistryImpl"></beans:bean>
<!-- lj添加 -->
<beans:bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:property name="sessionRegistry" ref="sessionRegistry" />
<beans:property name="expiredUrl" value="/timeout.jsp" /><!-- 过期的Url -->
</beans:bean>
<!-- lj添加 结束-->
<beans:bean id="myLoginFilter"
class="com.threeti.danfoss.base.filter.MyUsernamePasswordAuthenticationFilter">
<beans:property name="authenticationManager" ref="myAuthenticationManager"/>
<beans:property name="authenticationFailureHandler" ref="failureHandler"/>
<beans:property name="authenticationSuccessHandler" ref="successHandler"/>
<beans:property name="sessionAuthenticationStrategy"
ref="sas"></beans:property>
</beans:bean>
<beans:bean id="successHandler" class="com.threeti.danfoss.base.handler.MyAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" value="/pages/menu.html#current/wind/surface/level/anim=off/overlay=misery_index/orthographic=39.08,42.42,294/loc=96.475,39.357" />
</beans:bean>
<beans:bean id="failureHandler" class="com.threeti.danfoss.base.handler.MySimpleUrlAuthenticationFailureHandler">
<beans:property name="defaultFailureUrl" value="/index.html"/>
</beans:bean>
<beans:bean id="myLogoutSuccessHandler" class="com.threeti.danfoss.base.handler.MyLogoutSuccessHandler">
<beans:property name="defaultTargetUrl" value="/login.html"/>
<!-- 下面的 是通过在url参数进行跳转 -->
<!-- <property name="targetUrlParameter" value="target-url"/>
<property name="redirectStrategy" ref="safeRedirectStrategy"/> -->
</beans:bean>
<!-- 安全的RedirectStrategy,主要是判断跳转地址是否在白名单中 public class SafeRedirectStrategy implements RedirectStrategy -->
<!-- <beans:bean id="safeRedirectStrategy" class="com.snsxiu.job.web.security.SafeRedirectStrategy"/> -->
<!-- 1.URL过滤器或方法拦截器:用来拦截URL或者方法资源对其进行验证,其抽象基类为AbstractSecurityInterceptor
2.资源权限获取器:用来取得访问某个URL或者方法所需要的权限,接口为SecurityMetadataSource 3.访问决策器:用来决定用户是否拥有访问权限的关键类,其接口为AccessDecisionManager
调用顺序为:AbstractSecurityInterceptor调用SecurityMetadataSource取得资源的所有可访问权限, 然后再调用AccessDecisionManager来实现决策,确定用户是否有权限访问该资源。 -->
<!-- 自定义的filter, 必须包含authenticationManager, accessDecisionManager, securityMetadataSource三个属性 -->
<beans:bean id="mySecurityFilter" class="com.threeti.danfoss.base.security.XaFilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="myAuthenticationManager" />
<beans:property name="accessDecisionManager" ref="myAccessDecisionManager" />
<beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" />
</beans:bean>
<!-- 取HTTP配置中的authenticationManager 设置alias别名 -->
<authentication-manager alias="myAuthenticationManager">
<authentication-provider ref="myAuthenticationProvider"/>
</authentication-manager>
<!-- 用户详细信息管理:数据源、用户缓存(通过数据库管理用户、角色、权限、资源) -->
<beans:bean id="userDetailsManager" class="com.threeti.danfoss.base.security.XaUserDetailsService">
</beans:bean>
<!-- <beans:bean
class="org.springframework.security.authentication.encoding.Md5PasswordEncoder"
id="passwordEncoder">
</beans:bean> -->
<beans:bean id="myAuthenticationProvider" class="com.threeti.danfoss.base.filter.MyAuthenticationProvider">
<!-- <beans:property name="userDetailsService" ref="userDetailsManager"/> -->
<beans:constructor-arg name="userDetailsService" ref="userDetailsManager"/>
<!-- <beans:property name="passwordEncoder" ref="passwordEncoder"/> -->
</beans:bean>
<!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源。 -->
<beans:bean id="myAccessDecisionManager"
class="com.threeti.danfoss.base.security.XaAccessDecisionManagerService" />
<!-- 资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色去访问。 -->
<beans:bean id="mySecurityMetadataSource" init-method="loadResourceDefine"
class="com.threeti.danfoss.base.security.XaSecurityMetadataSourceService">
</beans:bean>
<beans:bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<beans:property name="loginFormUrl" value="/login.html" />
</beans:bean>
</beans:beans>