UsernamePasswordAuthenticationFilter主要用来处理用户登录时的验证操作. 它的一般用法请参考Spring Security学习笔记之整体配置
ConcurrentSessionFilter的作用比较简单, 它会对每一个请求都作判断:
1) 如果session没过期, 就会更新session里的"last update" date/time;
2) 如果session过期, 就会调用logout handlers(一般是LogoutFilter)去销毁session, 然后跳转到expiredUrl;
(注意这里的session是指储存在SessionRegistry里的SessionInformation实例, 不是HttpSession)
ConcurrentSessionFilter的构造函数需要两个参数(第二个可以省略)
sessionRegistry: 一般是SessionRegistryImpl的实例
expiredUrl: session过期后跳转的页面
这里主要介绍一下如何使用这两个过滤器来防止用户重复登录的问题.
UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter有一个属性sessionStrategy, 就是用它来指定具体的防止重复登录的策略. 它的默认值是NullAuthenticatedSessionStrategy. NullAuthenticatedSessionStrategy只是一个抽象类, 不做任何操作, 源码如下:
public final class NullAuthenticatedSessionStrategy implements SessionAuthenticationStrategy {
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
}
}
注意, delegateStrategies 是一个集合, 可绑定多个SessionAuthenticationStrategy的实例:
public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
private final Log logger = LogFactory.getLog(getClass());
private final List delegateStrategies;
...
}
绑定的配置如下:
...
现在让我们先来看看AbstractAuthenticationProcessingFilter在用户登录时做了什么.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 这里调用了子类UsernamePasswordAuthenticationFilter.attemptAuthentication()方法来验证用户是否存在
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed authentication
return;
}
// 如果用户信息正确, 则继续做下一步验证(如防止重复登录验证)
sessionStrategy.onAuthentication(authResult, request, response);
} catch(InternalAuthenticationServiceException failed) {
logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
先看RegisterSessionAuthenticationStrategy的onAuthentication()方法:
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
它只是简单的调用了实现类SessionRegistryImpl的registerNewSession()方法:
public void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (logger.isDebugEnabled()) {
logger.debug("Registering session " + sessionId +", for principal " + principal);
}
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
Set sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet();
// 把用户信息和session信息作对应关系, 并保存起来
Set prevSessionsUsedByPrincipal = principals.putIfAbsent(principal, sessionsUsedByPrincipal);
if (prevSessionsUsedByPrincipal != null) {
sessionsUsedByPrincipal = prevSessionsUsedByPrincipal;
}
}
sessionsUsedByPrincipal.add(sessionId);
if (logger.isTraceEnabled()) {
logger.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal);
}
}
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
// 找出当前用户所对应的所有session信息
final List 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);
}
但这里有一个问题, 它是怎样找出当前用户所对应的所有session信息的呢? 上面我们说到, 它把对应信息存在了SessionRegistryImpl的principals属性里. 下面我们看看SessionRegistryImpl的getAllSessions()方法, 通过它来找出用户的所有session信息.
public List getAllSessions(Object principal, boolean includeExpiredSessions) {
// 找出用户的所有session信息
final Set sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
}
List list = new ArrayList(sessionsUsedByPrincipal.size());
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);
if (sessionInformation == null) {
continue;
}
if (includeExpiredSessions || !sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
return list;
}
所以, 我们要重写User类的equals()和hashCode()方法, 来区分什么情况下是同一个用户在登录(这里我判断如果用相同的username和password来登录, 就是同一个用户).
@Override
public boolean equals(Object obj) {
if (obj instanceof User && this.hashCode() == obj.hashCode()) {
return true;
} else {
return false;
}
}
@Override
public int hashCode() {
return this.userName.hashCode() + this.password.hashCode();
}
org.springframework.security.web.session.HttpSessionEventPublisher
理由是:
Adding the listener to web.xml
causes an ApplicationEvent
to be published to the Spring ApplicationContext
every time a HttpSession
commences or terminates. This is critical, as it allows the SessionRegistryImpl
to be notified when a session ends. Without it, a user will never be able to log back in again once they have exceeded their session allowance, even if they log out of another session or it times out.
意思是说, 每当一个session结束的时候, 该监听器都会通知SessionRegistryImpl来删除这个session的信息. 这是因为SessionRegistryImpl实现了ApplicationListener
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener {
...
public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
removeSessionInformation(sessionId);
}
}
如果不加这个监听器, 那么当一个用户的session超时后, 他将永远不能再登录.
现在, 防止用户重复登录的功能已经实现了. 但是,, 上面我们设置ConcurrentSessionControlAuthenticationStrategy的属性exceptionIfMaximumExceeded为true:
意思是, 当用户第一次登录的session未过期时, 他的第二次登录会失败. 那么会引发一个问题: 如果用户没有点击Logout按钮进行登出, 而是直接关闭浏览器. 那么, 在他第一个session超时之前, 他都不能正常登录...
解决的方法是, 把exceptionIfMaximumExceeded设为false. 它的意思是, 允许用户进行第二次登录, 登录成功后, 会销毁第一次登录的session, 以保证每个用户同一时间只有一个有效的session.
但是, 如果把exceptionIfMaximumExceeded设为false, 我们要额外设置多一个过滤器 -- ConcurrentSessionFilter:
...
这个过滤器会销毁第一次登录时的session.
那么它是如何实现的呢? 通过观察上面ConcurrentSessionControlAuthenticationStrategy的onAuthentication()方法我们发现, 如果当前用户的session数大于设置的最大数时, 它会调用allowableSessionsExceeded() 方法:
protected void allowableSessionsExceeded(List sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.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;
}
}
// 把旧的session设成过期
leastRecentlyUsed.expireNow();
}
这个方法会把旧的session设成过期.
(注意这里的leastRecentlyUsed是SessionInformation的实例, 不是HttpSession的实例, 所以我们才要配多一个ConcurrentSessionFilter. 如果不配的话, 则旧的session会仍然有效)
下面是ConcurrentSessionFilter的doFilter()方法:
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) {
SessionInformation info = sessionRegistry.getSessionInformation(session.getId());
if (info != null) {
// 如果session已过期, 则logout, 并且跳转到expiredUrl
if (info.isExpired()) {
// Expired - abort processing
doLogout(request, response);
String targetUrl = determineExpiredUrl(request, info);
if (targetUrl != null) {
redirectStrategy.sendRedirect(request, response, targetUrl);
return;
} else {
response.getWriter().print("This session has been expired (possibly due to multiple concurrent " +
"logins being attempted as the same user).");
response.flushBuffer();
}
return;
} else {
// Non-expired - update last request date/time
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}
logout操作会交给我们配置的LogoutFilter来做, 它会把对应的session销毁. 具体请参考 Spring Security学习笔记之LogoutFilter
-------------------------------------------
补充
接口Authentication和UserDetails的区别:
Authentication: 它存储安全实体的标识, 密码以及认证请求的上下文信息. 它还包含用户认证后的信息(可能会包含一个UserDetails的实例). 通常不会被扩展, 除非是为了支持某种特定类型的认证.
UserDetails: 为了存储一个安全实体的概况信息, 包含名字, e-mail, 电话号码等. 通常会被扩展以支持业务需求.