简介
保证同一时间内只有一个账户处于登录状态
在同一个系统中,我们只允许一个用户在一个终端上登录,一般来说这可能是出于安全方面的考虑,但是也有一些情况是出于业务上的考虑
要实现一个用户不可以同时在两台设备上登录,我们有两种思路:
- 后来的登录自动踢掉前面的登录,就像大家在扣扣中看到的效果。
- 如果用户已经登录,则不允许后来者登录。
实现
方法一:踢掉前一个已登录账户
想要实现该功能,我们只需要在security中将session的最大数设置为1,即可实现
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1);}
maximumSessions 表示配置最大会话数为 1,这样后面的登录就会自动踢掉前面的登录。
方法二:已登录账户不允许再登录
如果相同的用户已经登录了,你不想踢掉他,而是想禁止新的登录操作,那也好办,配置方式如下:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);}
添加 maxSessionsPreventsLogin 配置即可。此时一个浏览器登录成功后,另外一个浏览器就登录不了了。
配置实体
我们还需要再提供一个 Bean:
@BeanHttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}
为什么要加这个 Bean 呢?
因为在 Spring Security 中,它是通过监听 session 的销毁事件,来及时的清理 session 的记录。
用户从不同的浏览器登录后,都会有对应的 session,当用户注销登录之后,session 就会失效,
但是默认的失效是通过调用 StandardSession#invalidate 方法来实现的,
这一个失效事件无法被 Spring 容器感知到,
进而导致当用户注销登录之后,Spring Security 没有及时清理会话信息表,以为用户还在线,进而导致用户无法重新登录进来
为了解决这一问题,我们提供一个 HttpSessionEventPublisher ,
这个类实现了 HttpSessionListener 接口,
在该 Bean 中,可以将 session 创建以及销毁的事件及时感知到,
并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去,进而被 Spring Security 感知到
public void sessionCreated(HttpSessionEvent event) {HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());getContext(event.getSession().getServletContext()).publishEvent(e);}public void sessionDestroyed(HttpSessionEvent event) {HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());getContext(event.getSession().getServletContext()).publishEvent(e);}
前后端分离配置
对于前后端分离的情况下,我们的用户都是存在数据库中的,为此我们上面的配置很明显不会生效,
源码分析
Spring Security 中通过 SessionRegistryImpl 类来实现对会话信息的统一管理,我们来看下这个类的源码(部分):
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {
protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);
private final ConcurrentMap<Object, Set<String>> principals;
private final Map<String, SessionInformation> sessionIds;
public SessionRegistryImpl() {
this.principals = new ConcurrentHashMap();
this.sessionIds = new ConcurrentHashMap();
}
public SessionRegistryImpl(ConcurrentMap<Object, Set<String>> principals, Map<String, SessionInformation> sessionIds) {
this.principals = principals;
this.sessionIds = sessionIds;
}
public List<Object> getAllPrincipals() {
return new ArrayList(this.principals.keySet());
}
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = (Set)this.principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
} else {
List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size());
Iterator var5 = sessionsUsedByPrincipal.iterator();
while(true) {
SessionInformation sessionInformation;
do {
do {
if (!var5.hasNext()) {
return list;
}
String sessionId = (String)var5.next();
sessionInformation = this.getSessionInformation(sessionId);
} while(sessionInformation == null);
} while(!includeExpiredSessions && sessionInformation.isExpired());
list.add(sessionInformation);
}
}
}
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
return (SessionInformation)this.sessionIds.get(sessionId);
}
public void onApplicationEvent(AbstractSessionEvent event) {
String oldSessionId;
if (event instanceof SessionDestroyedEvent) {
SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent)event;
oldSessionId = sessionDestroyedEvent.getId();
this.removeSessionInformation(oldSessionId);
} else if (event instanceof SessionIdChangedEvent) {
SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent)event;
oldSessionId = sessionIdChangedEvent.getOldSessionId();
if (this.sessionIds.containsKey(oldSessionId)) {
Object principal = ((SessionInformation)this.sessionIds.get(oldSessionId)).getPrincipal();
this.removeSessionInformation(oldSessionId);
this.registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);
}
}
}
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
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 (this.getSessionInformation(sessionId) != null) {
this.removeSessionInformation(sessionId);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal));
}
this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet();
}
((Set)sessionsUsedByPrincipal).add(sessionId);
this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
return (Set)sessionsUsedByPrincipal;
});
}
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
if (this.logger.isTraceEnabled()) {
this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
}
this.sessionIds.remove(sessionId);
this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
this.logger.debug(LogMessage.format("Removing session %s from principal's set of registered sessions", sessionId));
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
this.logger.debug(LogMessage.format("Removing principal %s from registry", info.getPrincipal()));
sessionsUsedByPrincipal = null;
}
this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", info.getPrincipal(), sessionsUsedByPrincipal));
return sessionsUsedByPrincipal;
});
}
}
}
- 首先大家看到,一上来声明了一个 principals 对象,这是一个支持并发访问的 map 集合,集合的 key 就是用户的主体(principal),正常来说,用户的 principal 其实就是用户对象,而集合的 value 则是一个 set 集合,这个 set 集合中保存了这个用户对应的 sessionid。
- 如有新的 session 需要添加,就在 registerNewSession 方法中进行添加,具体是调用 principals.compute 方法进行添加,key 就是 principal。
- 如果用户注销登录,sessionid 需要移除,相关操作在 removeSessionInformation 方法中完成,具体也是调用 principals.computeIfPresent 方法
ConcurrentMap 集合的 key 是 principal 对象,用对象做 key,一定要重写 equals 方法和 hashCode 方法,否则第一次存完数据,下次就找不到了
security内置User
public class User implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
@Override
public boolean equals(Object rhs) {
if (rhs instanceof User) {
return username.equals(((User) rhs).username);
}
return false;
}
@Override
public int hashCode() {
return username.hashCode();
}
}
数据库用户
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity(name = "t_user")
public class UserEntity implements UserDetails {
private static final long serialVersionUID = 5461613954799968213L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
private List<RoleEntity> roleEntities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(RoleEntity RoleEntity : getRoleEntities()) {
authorities.add(new SimpleGrantedAuthority(RoleEntity.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public boolean equals(Object o) {
if(this == o)
return true;
if(o == null || Hibernate.getClass(this) != Hibernate.getClass(o))
return false;
UserEntity that = (UserEntity) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return 0;
}
}
注意重写equals和hashCode方法即可实现前后端分离下的剔除用户的功能
