先上一张流程图
主要配置源码:
package com.example.demo.sms;
import com.example.demo.core.AbstractSecurityConfigurerAdapter;
import com.example.demo.core.MySimpleRedirectSessionInformationExpiredStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
public class SmsLoginConfig extends AbstractSecurityConfigurerAdapter {
@Resource
private SessionRegistry sessionRegistry;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationProcessingFilter processingFilter = new SmsAuthenticationProcessingFilter("/sms/login");
processingFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
processingFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
processingFilter.setAuthenticationManager(authenticationManager);
//注册新session策略
RegisterSessionAuthenticationStrategy registerSessionAuthenticationStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry);
registerSessionAuthenticationStrategy = postProcess(registerSessionAuthenticationStrategy);
//session 管理策略,
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry);
// -------------一个用户登录一次设置 start--------------------------
//设置 限制的同一个用户登录的次数
concurrentSessionControlStrategy.setMaximumSessions(1);
//作用是当session超过次数限制时,是当前用户无法登陆,还是之前登录过期的策略设置,默认是之前过期
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(false);
List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<>();
delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,registerSessionAuthenticationStrategy));
//compositeSessionAuthenticationStrategy是存放SessionAuthenticationStrategy的集合
CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy = new CompositeSessionAuthenticationStrategy(delegateStrategies);
//设置 session 策略
processingFilter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy);
//ConcurrentSessionFilter的作用是判断session是否过期,如果过期就执行过期方法(是requst中的session无效,清除凭证)
http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry,new SimpleRedirectSessionInformationExpiredStrategy()), ConcurrentSessionFilter.class);
// -------------一个用户登录一次设置 end--------------------------
http.addFilterBefore(processingFilter, UsernamePasswordAuthenticationFilter.class);
//MyAuthenticationProvider 真正认证
http.authenticationProvider(new SmsAuthenticationProvider());
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
}
介绍下各个类的作用:
ConcurrentSessionFilter:是判断session是否过期,如果过期就执行过期方法
SessionRegistryImpl:存放认证通过的session消息
concurrentSessionControlStrategy:判断是否超过设置的session限制
registerSessionAuthenticationStrategy:注册新session
CompositeSessionAuthenticationStrategy:存放上两个策略
关键源码如下,如果有不懂可以提问。
1.设置策略。可以通过自定义实现类AbstractAuthenticationProcessingFilter#setSessionAuthenticationStrategy方法设置策略
public void setSessionAuthenticationStrategy(
SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
入口方法(认证成功会掉调用):
AbstractAuthenticationProcessingFilter#sessionStrategy.onAuthentication(authResult, request, response);
# CompositeSessionAuthenticationStrategy 是个可以存放多个sessionStrategy的封装类
CompositeSessionAuthenticationStrategy存放两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy会循环调用,顺序不能变
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException {
#遍历每个sessionStrategy执行onAuthentication方法,
#我的配置中设置了两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy,
#并且构造方法都用了同一个SessionRegistryImpl
for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Delegating to " + delegate);
}
delegate.onAuthentication(authentication, request, response);
}
}
两个策略concurrentSessionControlStrategy,registerSessionAuthenticationStrategy的关键源码如下:
//ConcurrentSessionControlAuthenticationStrategy作用:判断当前登录用户名所存在的session是否在允许的最大登录数内,如果是直接返回,否就会执行
//registerSessionAuthenticationStrategy 注册新session
concurrentSessionControlStrategy类执行如下,
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
int sessionCount = sessions.size();
#可以通过 concurrentSessionControlStrategy.setMaximumSessions(1)改变同一用户登录次数
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
//第一次登录会执行如下方法,因为是同一个sessionId
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
}
//如果超过设置,就会执行sessions是之前该用户登录的session集合,allowableSessions是运行的个数,registry是SessionRegistryImpl,第二次登录会调用
allowableSessionsExceeded(List<SessionInformation> sessions,
int allowableSessions, SessionRegistry registry)
}
protected void allowableSessionsExceeded(List<SessionInformation> sessions,
int allowableSessions, SessionRegistry registry)
throws SessionAuthenticationException {
}
//属性exceptionIfMaximumExceeded是决定如果登录超过限制,是限制当前登录用户异常,还是将之前登录用户强制退出策略(默认是false),
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(messages.getMessage(
"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] {allowableSessions},
"Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used sessions, and mark them for invalidation
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session: sessionsToBeExpired) {
//超过的session就设置为无效
session.expireNow();
}
}
RegisterSessionAuthenticationStrategy作用是增加新sessionInfo
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
sessionRegistry.registerNewSession(request.getSession().getId(),
authentication.getPrincipal());
}
SessionRegistryImpl
registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
if (logger.isDebugEnabled()) {
logger.debug("Registering session " + sessionId + ", for principal "
+ principal);
}
//放置sessionId-SessionInformation(principal, sessionId, new Date())
sessionIds.put(sessionId,
new SessionInformation(principal, sessionId, new Date()));
//放置principal--sessionIds
principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
sessionsUsedByPrincipal.add(sessionId);
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + principal + "' : "
+ sessionsUsedByPrincipal);
}
return sessionsUsedByPrincipal;
});
}
SessionRegistryImpl 部分源码
public class SessionRegistryImpl implements SessionRegistry,
ApplicationListener<SessionDestroyedEvent> {
// ~ Instance fields
// ================================================================================================
protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);
#存放的key是用户名 ,value 是 sessionId集合
private final ConcurrentMap<Object, Set<String>> principals;
private final Map<String, SessionInformation> sessionIds;
public SessionRegistryImpl() {
this.principals = new ConcurrentHashMap<>();
this.sessionIds = new ConcurrentHashMap<>();
}
//ConcurrentSessionFilter源码:判断是否过期,如果过期执行过期方法,如果没有过期刷新请求时间
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session != null) {
//根据sessionId去获取SessionInformation
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
//判断是否过期,只有登录次数限制才设置了过期,所以一定是其他设备上登录导致的过期
if (info.isExpired()) {
// Expired - abort processing
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " has expired.");
}
//设置了tomcat的session失效,清除凭证
doLogout(request, response);
//回调过期处理方法,可以自定义,该类构造方法就可以自定义设置
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
// Non-expired - update last request date/time
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}