spring boot security 防止使用者重複登入(原創)
阿新 • • 發佈:2018-11-11
原理:在認證成功通過後,在顯示登入成功頁面之前,也就是在SavedRequestAwareAuthenticationSuccessHandler類中操作。
新增一個集合sessionMap 用於儲存認證成功的會話,鍵名為會話ID,
每次有使用者登入認證通過都要判斷一下是否重複登入,如果不是繼續執行,將會話儲存在集合sessionMap裡。
如果是就踢除之前登入過的使用者的會話,將舊的會話從集合sessionMap裡刪除,還有一步就是將舊的會話從sessionRegistry裡刪除(不然你如果要獲取所有認證通過的使用者,還會從sessionRegistry中獲取舊使用者),然後將自己的新會話儲存到集合sessionMap裡。(每次認證通過的使用者會話都會儲存到sessionRegistry裡,所以不必自己再寫一遍)
package com.mayocase.handler; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.mayocase.domain.SysUser; import com.mayocase.util.SessionUtil; //登入使用者認證通過後,顯示登入成功頁面前,做的操作。 @Component public class MyAuthenctiationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private static final Logger logger = LoggerFactory.getLogger(MyAuthenctiationSuccessHandler.class); // key為sessionId,value為HttpSession,使用static,定義靜態變數,使之程式執行時,一直存在記憶體中。 // 儲存所有已經登入使用者的會話(每個瀏覽器一個會話) public static HashMap<String, HttpSession> sessionMap = new HashMap<String, HttpSession>(); @Autowired // @Qualifier("sessionRegistry") private SessionRegistry sessionRegistry; // @Bean(name="sessionRegistry",value="sessionRegistry") @Bean // @Bean(name="sessionRegistry") public SessionRegistry getSessionRegistry() { return new SessionRegistryImpl(); } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // 1.登入認證成功後,獲取使用者名稱 //(只能在認證成功通過後,才能獲得sc,不然在CustomUserService implements UserDetailsService的loadUserByUsername方法中是第二次才能獲取到) SecurityContext sc = SecurityContextHolder.getContext(); String currentuser = ((SysUser) sc.getAuthentication().getPrincipal()).getUsername(); logger.info("當前登入使用者:" + currentuser); // 2.先判斷使用者是否重複登入 Iterator<Entry<String, HttpSession>> iterator = sessionMap.entrySet().iterator(); while(iterator.hasNext()) { Map.Entry<String, HttpSession> entry = iterator.next(); HttpSession session = entry.getValue(); // 2.1 判斷session中所包含的使用者名稱稱是否有當前登入使用者 String username = SessionUtil.getUserName(session); if (currentuser.equals(username)) { logger.info("使用者:" + currentuser + "已經在其它地方登入過,將踢除!"); SessionUtil.expireSession(session); logger.info("刪除的會話:"+entry.getKey()); // 2.2 從sessionMap中踢除會話 iterator.remove(); // 2.3 從sessionRegistry中踢除會話 sessionRegistry.removeSessionInformation(session.getId()); } } /*//以下這種方法會引起java.util.ConcurrentModificationException: null 錯誤, HashMap // 2.先判斷使用者是否重複登入 for (Entry<String, HttpSession> entry : sessionMap.entrySet()) { HttpSession session = entry.getValue(); // 2.1 判斷session中所包含的使用者名稱稱是否有當前登入使用者 String username = SessionUtil.getUserName(session); if (currentuser.equals(username)) { logger.info("使用者:" + currentuser + "已經在其它地方登入過,將踢除!"); SessionUtil.expireSession(session); logger.info(entry.getKey()); sessionMap.remove(entry.getKey());//這裡會引起同步錯誤 sessionRegistry.removeSessionInformation(session.getId()); } }*/ // 3.將當前session儲存到sessionMap中 logger.info("將當前會話:" + request.getSession().getId() + ",儲存到sessionMap"); sessionMap.put(request.getSession().getId(), request.getSession()); for (Entry<String, HttpSession> entry : sessionMap.entrySet()) { logger.info("顯示已經儲存的sessionMap:Key: " + entry.getKey() + " Value: " + entry.getValue()); } // 4.列印所有認證通過的使用者(包含重複登入的,不過上面已經踢除了) List<Object> principals = sessionRegistry.getAllPrincipals(); List<String> usersNamesList = new ArrayList<String>(); for (Object principal: principals) { if (principal instanceof SysUser) { usersNamesList.add(((SysUser) principal).getUsername()); } } logger.info("已經認證通過的使用者數:"+usersNamesList.size()+", 已經認證通過使用者:"+usersNamesList.toString()); // response.sendRedirect("/"); super.onAuthenticationSuccess(request, response, authentication); } }
package com.mayocase.util; import java.util.Enumeration; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.User; import com.mayocase.domain.SysUser; public class SessionUtil { private static SecurityContext attribute; /** * 根據當前session獲取當前登入使用者物件 * @param session * @return guser */ public static SysUser getUser(HttpSession session) { try { attribute = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT"); SysUser principal = (SysUser) attribute.getAuthentication().getPrincipal(); return principal; } catch (Exception e) { } return null; } /** * 根據當前session獲取當前登入使用者ID * @param session * @return guser */ public static Integer getUserId(HttpSession session) { try { attribute = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT"); SysUser principal = (SysUser) attribute.getAuthentication().getPrincipal(); return principal.getId(); } catch (Exception e) { } return null; } /** * 根據session獲取使用者名稱稱 * @param session * @return void */ public static String getUserName(HttpSession session) { try { attribute = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT"); SysUser principal = (SysUser) attribute.getAuthentication().getPrincipal(); return principal.getUsername(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 根據session獲取count * session中包含一個count鍵預設為null,可以用來統計登入次數 * @param session * @return void */ public static void count(HttpSession session) { ServletContext context = session.getServletContext(); System.out.println("sessionid:"+session.getId()+",的count是:"+context.getAttribute("count")); } /** * 辨別使用者是否已經登入,如果已經登入就不能登入了。 * * @param request * @param sessionRegistry * @param loginedUser */ public static void deleteSameUser(HttpServletRequest request, SessionRegistry sessionRegistry, User loginedUser) { SecurityContext sc = (SecurityContext) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); List<SessionInformation> sessionsInfo; sessionsInfo = sessionRegistry.getAllSessions(sc.getAuthentication().getPrincipal(), true); String currentSessionId; if (null != sessionsInfo && sessionsInfo.size() == 0) { sessionRegistry.registerNewSession(request.getSession().getId(), sc.getAuthentication().getPrincipal()); sessionsInfo = sessionRegistry.getAllSessions(sc.getAuthentication().getPrincipal(), false); } currentSessionId = sessionsInfo.get(0).getSessionId(); List<Object> o = sessionRegistry.getAllPrincipals(); for (Object principal : o) { if (principal instanceof User && (loginedUser.getUsername().equals(((User) principal).getUsername()))) { List<SessionInformation> oldSessionsInfo = sessionRegistry.getAllSessions(principal, false); if (null != oldSessionsInfo && oldSessionsInfo.size() > 0 && !oldSessionsInfo.get(0).getSessionId().equals(currentSessionId)) { for (SessionInformation sessionInformation : sessionsInfo) { //當前session失效 sessionInformation.expireNow(); sc.setAuthentication(null); sessionRegistry.removeSessionInformation(currentSessionId); //throw new GeneralServerExistException(ErrorMessage.ALONG_LOGIN_ERROTR.toString()); } } } } } /** * 會話銷燬(剔除前一個使用者) * * @param request * @param sessionRegistry * @param loginedUser * @param , SysMessageService sysMessageService */ public static void expireSession(HttpSession session) { session.invalidate(); } /** * 剔除前一個使用者 * * @param request * @param sessionRegistry * @param loginedUser * @param , SysMessageService sysMessageService */ public static void dropPreviousUser2(HttpServletRequest request, SessionRegistry sessionRegistry, SysUser loginedUser) { List<SessionInformation> sessionsInfo = null; //登入以後session裡才會加入鍵名為"SPRING_SECURITY_CONTEXT"的欄位 SecurityContext sc = (SecurityContext) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); if(sc!=null) { System.out.println("!!!!!!!!!!!!"+sc.getAuthentication().getPrincipal().toString()); //獲取當前登入使用者的會話資訊集合 sessionsInfo = sessionRegistry.getAllSessions(sc.getAuthentication().getPrincipal(), false); if (sessionsInfo.size() > 0) { //當前會話ID String currentSessionId = sessionsInfo.get(0).getSessionId(); //獲取所有已經登入的使用者 List<Object> o = sessionRegistry.getAllPrincipals(); for (Object principal : o) { //當登入使用者的名字和已經登入使用者的名字相同,也就是登入使用者已經登入過了。 if (principal instanceof User && (loginedUser.getUsername().equals(((User) principal).getUsername()))) { //獲取已經登入使用者的會話資訊集合 List<SessionInformation> oldSessionsInfo = sessionRegistry.getAllSessions(principal, false); //如果會話資訊不為空且會話資訊的ID不等於當前會話ID if (null != oldSessionsInfo && oldSessionsInfo.size() > 0 && !oldSessionsInfo.get(0).getSessionId().equals(currentSessionId)) { //遍歷已經登入使用者的會話資訊,並設定過期,即刪除session for (SessionInformation sessionInformation : oldSessionsInfo) { //舊使用者的session失效 //send message //sysMessageService.sendMessage(((User) principal).getUsername(), new SysMessage(null, Consts.NOTIFICATION_TYPE_HADLOGIN_CONTENT, 5, Consts.NOTIFICATION_ACCEPT_TYPE_HADLOGIN)); sessionInformation.expireNow(); } } } } } } } /** * session 失效 * * @param request * @param sessionRegistry */ public static void expireSession(HttpServletRequest request, User user, SessionRegistry sessionRegistry) { List<SessionInformation> sessionsInfo = null; if (null != user) { List<Object> o = sessionRegistry.getAllPrincipals(); for (Object principal : o) { if (principal instanceof User && (user.getUsername().equals(((User) principal).getUsername()))) { sessionsInfo = sessionRegistry.getAllSessions(principal, false); } } } else if (null != request) { SecurityContext sc = (SecurityContext) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); if (null != sc.getAuthentication().getPrincipal()) { sessionsInfo = sessionRegistry.getAllSessions(sc.getAuthentication().getPrincipal(), false); sc.setAuthentication(null); } } if (null != sessionsInfo && sessionsInfo.size() > 0) { for (SessionInformation sessionInformation : sessionsInfo) { //當前session失效 sessionInformation.expireNow(); sessionRegistry.removeSessionInformation(sessionInformation.getSessionId()); } } } public void showsession(HttpServletRequest request) { //獲取session HttpSession session = request.getSession(); // 獲取session中所有的鍵值 Enumeration<String> attrs = session.getAttributeNames(); // 遍歷attrs中的 while(attrs.hasMoreElements()){ // 獲取session鍵值 String name = attrs.nextElement().toString(); // 根據鍵值取session中的值 Object vakue = session.getAttribute(name); // 列印結果 System.out.println("--sessionID"+session.getId()); System.out.println("--名字:" + name +"-----\n"); System.out.println("--值:" + vakue +"--------\n"); } } }
SessionRegistry引入的方法
package com.mayocase.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource(locations={"classpath:spring-security-context.xml"})
public class XMLConfigClass {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<bean id="mySessionAuthenticationStrategy"
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
<property name="maximumSessions" value="1" />
</bean>
<bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
</beans>
新增一個監聽器,用於控制session過期的情況,
package com.mayocase.listener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import com.mayocase.domain.SysUser;
import com.mayocase.handler.MyAuthenctiationSuccessHandler;
//啟動類加上註解@ServletComponentScan,這樣才能掃描到監聽器
@WebListener
public class MySessionListner implements HttpSessionListener {
private static final Logger logger = LoggerFactory.getLogger(MySessionListner.class);
/**
* 新建session時(開啟瀏覽器訪問登入頁面時,伺服器會建立一個新的session)
*/
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
}
/**
* 刪除session時(退出系統)
*/
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
logger.info("銷燬session時");
MyAuthenctiationSuccessHandler.sessionMap.remove(httpSessionEvent.getSession().getId());
}
}