Spring Security 实现用户单设备登录控制
在许多系统中,为了安全性考虑,需要限制用户只能在一个设备上登录。Spring Security 提供了完善的会话管理机制,可以轻松实现这一需求,主要通过控制用户并发会话数量来实现。
两种单设备登录策略
1. 后来者登录,顶掉之前的登录者(默认行为)
这种策略允许新登录挤掉旧登录,适用于用户可能在不同设备间切换的场景。
1 2 3 4 5 6 7 8 9 10 11 12
| @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable() .sessionManagement() .maximumSessions(1); }
|
效果:当用户在新设备登录时,系统会自动使该用户在其他设备上的旧会话失效,旧会话的用户在后续操作时会被要求重新登录。
2. 不允许后来者登录,保留当前登录
这种策略更严格,一旦用户在某个设备登录,其他设备的登录尝试会被拒绝。
1 2 3 4 5 6 7 8 9 10 11 12
| @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable() .sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true); }
|
效果:如果用户已经在一个设备登录,当尝试在第二个设备登录时,会收到登录失败的提示,无法成功登录。
源码解析:并发会话控制的实现原理
Spring Security 通过 ConcurrentSessionControlAuthenticationStrategy 类实现并发会话控制,核心逻辑在 onAuthentication 和 allowableSessionsExceeded 方法中。
1. onAuthentication 方法:判断是否允许新登录
该方法在用户认证成功后执行,用于检查当前用户的会话数量是否超过限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
final List<SessionInformation> sessions = sessionRegistry.getAllSessions( authentication.getPrincipal(), false);
int sessionCount = sessions.size(); int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (sessionCount < allowedSessions) { return; }
if (allowedSessions == -1) { return; }
if (sessionCount == allowedSessions) { HttpSession session = request.getSession(false); if (session != null) { for (SessionInformation si : sessions) { if (si.getSessionId().equals(session.getId())) { return; } } } }
allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); }
|
2. allowableSessionsExceeded 方法:处理会话超额
当会话数超过限制时,该方法决定是拒绝新登录还是使旧会话失效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| protected void allowableSessionsExceeded(List<SessionInformation> 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")); }
SessionInformation leastRecentlyUsed = null;
for (SessionInformation session : sessions) { if ((leastRecentlyUsed == null) || session.getLastRequest() .before(leastRecentlyUsed.getLastRequest())) { leastRecentlyUsed = session; } }
leastRecentlyUsed.expireNow(); }
|
扩展配置:会话失效后的处理
可以进一步配置会话失效后的行为,如跳转页面或返回提示信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable() .sessionManagement() .maximumSessions(1) .expiredUrl("/login?expired") .maxSessionsPreventsLogin(true) .sessionRegistry(sessionRegistry()); }
@Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); }
|
前端处理建议
- 被顶掉的用户处理:
- 对于被顶掉的用户,在其进行下一次请求时,会收到会话过期的响应
- 前端可以监听 401 响应,提示用户 “您的账号已在其他设备登录,请重新登录”
- 登录被拒绝的用户处理:
- 对于登录被拒绝的情况,后端会返回登录失败的响应
- 前端可以显示 “您的账号已在其他设备登录,是否强制登录?”(需要额外开发强制登出功能)
实现强制登出功能
如果需要允许用户主动踢掉其他设备的登录,可以结合 SessionRegistry 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Service public class SessionService {
@Autowired private SessionRegistry sessionRegistry;
public void forceLogoutOtherSessions(String username) { List<Object> principals = sessionRegistry.getAllPrincipals(); for (Object principal : principals) { if (principal instanceof UserDetails && username.equals(((UserDetails) principal).getUsername())) { List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false); HttpSession currentSession = SecurityContextHolder.getContext().getAuthentication().getDetails(); for (SessionInformation session : sessions) { if (!session.getSessionId().equals(currentSession.getId())) { session.expireNow(); } } } } } }
|
通过 Spring Security 提供的会话管理机制,我们可以灵活地实现单设备登录控制,既可以选择 “顶掉旧登录” 的宽松策略,也可以选择 “拒绝新登录” 的严格策略,满足不同系统的安全需求
v1.3.10