自動擠掉前一個用戶
1.配置一個用戶只允許一個會話
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .rememberMe() .key("system") .and() .formLogin() .authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/hello") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable() .sessionManagement() .maximumSessions(1); }
2.重寫userDetail的hashCode和quals
public class UserInfoDto implements UserDetails { //....省略部分代碼 @Override public String toString() { return this.username; } @Override public int hashCode() { return username.hashCode(); } @Override public boolean equals(Object obj) { return this.toString().equals(obj.toString()); } }
3.分別用同一個賬號2個瀏覽器登錄。然后再訪問第一次登錄成功的用戶則出現提示
禁止新的賬號登錄
1.配置
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .rememberMe() .key("system") .and() .formLogin() .authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/hello") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable() .sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true); }
2.增加一個監聽的bean
spring事件使用參考<spring源碼閱讀(一)-附錄例子>
@Bean HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); }
public class HttpSessionEventPublisher implements HttpSessionListener { private static final String LOGGER_NAME = org.springframework.security.web.session.HttpSessionEventPublisher.class.getName(); public HttpSessionEventPublisher() { } /** * 獲得當前ServletContext的spring容器 * @param servletContext * @return */ ApplicationContext getContext(ServletContext servletContext) { return SecurityWebApplicationContextUtils.findRequiredWebApplicationContext(servletContext); } /** * 創建session的 spring事件發送 * @param event */ public void sessionCreated(HttpSessionEvent event) { HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession()); Log log = LogFactory.getLog(LOGGER_NAME); if (log.isDebugEnabled()) { log.debug("Publishing event: " + e); } this.getContext(event.getSession().getServletContext()).publishEvent(e); } /** * session銷毀的spring事件發送 * @param event */ public void sessionDestroyed(HttpSessionEvent event) { HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession()); Log log = LogFactory.getLog(LOGGER_NAME); if (log.isDebugEnabled()) { log.debug("Publishing event: " + e); } this.getContext(event.getSession().getServletContext()).publishEvent(e); } }
3.如果有賬號登錄另外一個賬號登錄則會提示
源碼
1.UsernamePasswordAuthenticationFilter的父類AbstractAuthenticationProcessingFilter
sessionStrategy默認為org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { //省略部分代碼...... // 調用子類的attemptAuthentication 處理登錄認證 authResult = this.attemptAuthentication(request, response); if (authResult == null) { return; } //認證成功走session校驗 this.sessionStrategy.onAuthentication(authResult, request, response); //省略部分代碼...... }
2.org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy#onAuthentication
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException { SessionAuthenticationStrategy delegate; //遍歷delegateStrategies 調用onAuthentication方法 for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext(); delegate.onAuthentication(authentication, request, response)) { delegate = (SessionAuthenticationStrategy)var4.next(); if (this.logger.isDebugEnabled()) { this.logger.debug("Delegating to " + delegate); } } }
3.真正處理登錄剔除和攔截的是SessionAuthenticationStrategy的實現類
org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { //獲得指定用戶名所有的session authentication.getPrincipal()就是登陸名 List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false); int sessionCount = sessions.size(); //獲取我們配置的最大數 int allowedSessions = this.getMaximumSessionsForThisUser(authentication); //如果大於最大配置數 if (sessionCount >= allowedSessions) { if (allowedSessions != -1) { if (sessionCount == allowedSessions) { HttpSession session = request.getSession(false); if (session != null) { Iterator var8 = sessions.iterator(); while(var8.hasNext()) { SessionInformation si = (SessionInformation)var8.next(); if (si.getSessionId().equals(session.getId())) { return; } } } } this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry); } } } protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException { //是否配置了禁止多個賬號登錄 if (!this.exceptionIfMaximumExceeded && sessions != null) { sessions.sort(Comparator.comparing(SessionInformation::getLastRequest)); int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1; List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy); Iterator var6 = sessionsToBeExpired.iterator(); //將登錄的剔除 while(var6.hasNext()) { SessionInformation session = (SessionInformation)var6.next(); session.expireNow(); } } else { //拋出異常 禁止多端登陸 throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded")); } }
獲取登錄失敗原因
可以通過設置攔截器獲取 保存到當前會話
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登陸失敗"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception));//將異常寫入response中,顯示在頁面上 }
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .successHandler(myAuthenticationSuccessHandler)//配置successHandler .failureHandler(myAuthenticationFailureHandler)//配置failureHandler .and() .authorizeRequests() .antMatchers( "/loginPage.html", "/myLoginPage.html", "/authentication/require" ).permitAll() .anyRequest() .authenticated() .and() .csrf().disable();