声明一下,这篇文章不是基于acegi spring security2.0写的, 我发现很多文章都是基于老版本写的, 并不适用最新版。
下面跟大家分享一下在spring security3.0里如何正宗的做法达到控制多个账号请求的经验。
步骤1
- <http auto-config="true" >
- <!--session-fixation-protection="none" 防止伪造sessionid攻击. 用户登录成功后会销毁用户当前的session.创建新的session,并把用户信息复制到新session中.-->
- <session-management invalid-session-url="/common/login.jsp?invalid-session=true" >
- <concurrency-control error-if-maximum-exceeded="true" max-sessions="1" />
- </session>
- </http>
下面只贴出关键部分, 为了不影响阅读。
注意: 不需要配置 SessionRegistry 等bean( 假设你其他地方不用到的话, 如果用到需要在
<concurrency-control session-registry-ref="sessionRegistry" error-if-maximum-exceeded="true" max-sessions="1" />
加上一个属性
在做某个管理员踢出一个账号的时候, SessionRegistry 这个bean是需要用到的。 写法如下:
- @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";
- }
有时候按文档和网上配置出来是很华丽, 可事实有时候就是没有如期运行。
我打开火狐 360浏览器, 还是等两个账号同时登录。
无奈之下把源码下下载剖析(常干的事儿, 喜欢捣腾这些东西)
判断重复的类是ConcurrentSessionControlStrategy.java下的
checkAuthenticationAllowed这个函数的
- final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
- int sessionCount = sessions.size();
最重要的一句话是:
sessionInformationList.get(j).expireNow();
这句强制T出了用户, (设置为过期)
如果想彻底删除, 加上
sessionRegistry.removeSessionInformation(sessionInformationList.get(j).getSessionId());
即可,
这样使用getAllPrincipals 则获取不到被T出的用户了, 其实原理不是直接删除User对象, 只结束了它的sessionId,
因为这个User可能不止对应着1个sessionId
我发现, 无论我怎么配置, sessionCount老是烦人的 0。 即使我手动配置了ConcurrentSessionControlStrategy这个bean也没用(默认会自己调的)
无奈中想自己写一个自定义的计数器控制, 但细想它这东西不至于这个小问题都出这么大的漏洞吧?
现在的问题是:
如何让 int sessionCount = sessions.size(); 这句在第二个账号登陆的时候不为0。
于是我进入了sessionRegistry.getAllSessions(authentication.getPrincipal(), false); 这个函数。
也就是SessionRegistryImpl.java
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
final Set<String> sessionsUsedByPrincipal = principals.get(principal);
这个函数是通过在一个HashMap里拿到key value的。
而principals的声明这样写。
private final Map<Object,Set<String>> principals = Collections.synchronizedMap(new HashMap<Object,Set<String>>());
现在的问题变为了:
如何让两个principal , 也就是User, 也就是
public UserDetails loadUserByUsername(String username)
两次登陆的时候返回的是同一个对象。
那么如何做到两次在不同浏览器登陆的时候返回的是同一个User?
答案是java的基础, equal hashcode方法重写。
在User对象里添加以下方法:
- public boolean equals(Object object) {
- if (object instanceof BaseObject) {
- if (this.id.equals(((BaseObject) object).getId()))
- return true;
- }
- return false;
- }
- public int hashCode(){
- return this.id.hashCode();
- }
涉及这方面的基础请参考 http://blog.csdn.net/willielee/archive/2010/08/11/5804463.aspx
我就不浪费CSDN的硬盘空间了, 不过还是得贴出最后一句话:
HashMap的key判断key是否相等也是从hashcode和equals是否相等判断
从上面可以看出, 我们之前登陆的用户是存在 Map<Object,Set<String>> principals 的。
这个存储结构是, 一个User 对应多个Set集合的sessionId。
所以要判断用户是否已存在登陆的了, 当然要重写这2个方法。