spring Security 重复登录配置无效的问题
关于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>
spring Security 重复登录配置无效的问题相关推荐
- Spring Security MVC登录注销示例教程
Spring Security MVC登录注销示例教程 今天我们将了解Spring Security Login Example.在阅读这篇文章之前,请先阅读我在"Spring 4 Secu ...
- spring security+jwt 登录认证
spring security+jwt 登录认证 1.综述 2.版本与环境 3.架构 4.数据库认证逻辑图 5.案例 security+jwt 5.1引入依赖 5.2新建工具类 5.2新建组件类 5. ...
- Spring Security:自定义登录页面
本文来说下Spring Security中如何自定义登录页面 文章目录 准备工作 自定义登录界面 本文小结 准备工作 添加模板引擎 这里使用了thymeleaf模板引擎,在pom.xml进行添加: & ...
- Spring Security默认登录页面
使用Spring Security作为权限管理模块的小伙伴们一定醉心于其极少的配置即可满足权限管理需求,以及比springMVC更简洁的filter配置. 在刚开始技术验证的demo阶段相信很多人试过 ...
- Spring Security自定义登录验证及登录返回结果
Spring Security自定义登录验证及登录返回结果 一.功能描述 二.处理逻辑 简单流程 自定义UserDetails 自定义UserDetailsDAO 自定义UserDetailsServ ...
- SpringSecurity权限管理框架系列(六)-Spring Security框架自定义配置类详解(二)之authorizeRequests配置详解
1.预置演示环境 这个演示环境继续沿用 SpringSecurit权限管理框架系列(五)-Spring Security框架自定义配置类详解(一)之formLogin配置详解的环境. 2.自定义配置类 ...
- (二)Spring Security自定义登录成功或失败处理器
目录 一:创建登录成功处理器 二:创建登录失败处理器 三:添加处理器 三. 项目地址 我们接着上一章 Spring Security最简单的搭建,进行开发 LoginSuccessHandler 和L ...
- spring security导致登录后从https跳转至http解决方案
1. 项目为spring boot项目,由原来的http连接更换为https连接,因项目中配置的了spring security,登录被spring security拦截重定向后会跳转到http 解决 ...
- 关闭Spring security的登录验证
目的关闭Spring security 默认登录页 Springboot 2.x关闭需要在启动类上排除SecurityAutoConfiguration和ManagementWebSecurityAu ...
最新文章
- 一个古老的问题HashMap与Hashtable区别
- Bengio参与、LeCun点赞:图神经网络权威基准现已开源
- mysql 获取姓名首字母_MySQL取姓名的首字母
- Sliverlight之 画刷
- 深度学习核心技术精讲100篇(十四)-一文带你看懂GPflow的前世今生
- Bamboolib -- 十分钟教会业务小姐姐做数据分析
- 网络:TCP通讯之 time_wait 状态
- 需要gmail的朋友请留下你们的email,还有86个
- JS纯前端导出PDF及分页和使用window.print()保存PDF
- CVPR 2022 | AAAI2022 | WACV 2022 超分辨率(super-resolution)方向上接收论文总结
- python瀑布图怎么做_用Matplotlib模拟原始瀑布图
- 提高工作效率必备的生产力工具
- 基础拓扑学笔记(3)——连续映像
- 《设计进化论日本版式设计速查手查手册》菜单版式
- 【Java】【系列篇】【Spring源码解析】【三】【体系】【BeanFactory体系】
- 前端自动化测试(webdriverio+mocha+chai)
- 一行代码搞定Android弧形卫星动画菜单(附Demo)
- (转载)使用Android Studio对代码进行重构
- java毕业设计保险公司风险测评管理系统Mybatis+系统+数据库+调试部署
- 超屌的多线程锁分类,了解一下?