前段时间,公司要求基于现有系统新增限制单用户登录的功能。就是说:同一个用户在不同的地方(浏览器)不能同时登录,后登录的要踢掉先登录的用户,同时给出友好的提示。
 首先,服务器环境是nginx1.6.2+Tomcat8.5+JAVA8,同时部署在多台服务器上,LBS负载均衡。
 开发框架使用SSM(Spring,SpringMvc,Mybatis)。
 Redis作为缓存数据库。
 登录验证方面,使用强大并且可定制的SpringSecurity4.1(无xml的使用方式)。
 既然是基于现有系统新增的功能,那么首先我们考虑在现有SpringSecurity配置下,通过新增配置来实现这个功能,查询SpringSecurity的相关文档后,得到这样一种解决办法:
 在SecuredConfig的配置方法configure(HttpSecurity http)中添加这样一段代码,(注:这里省略了其他配置)
1  
2  
3  
4  
5  
6  
7  
8  
9  
@Configuration  
@EnableWebSecurity  
public class SecuredConfig extends WebSecurityConfigurerAdapter {  
 @Autowired  
 SessionRegistry sessionRegistry;  
 @Override  
 protected void configure(HttpSecurity http) throws Exception {         http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login?expired");  
 }  
}  
1
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login?expired");
 maximumSessions(1):最大登录人数为1。
 expiredUrl(“/login?expired”):被踢出时跳转的Url。
 sessionManagement():获取系统当前的sessionManagement。
 sessionRegistry(sessionRegistry):设置处理登录所用类。
 我们主要了解一下加粗部分。maximumSessions(1) 配置同一个用户最多只能在系统登录一次(后登陆的会踢出之前登录的),expiredUrl(“/login?expired”) 配置被踢出方下一次访问服务器的时候将跳转到的Url(在这里我们可以做一些友好的提示)。
 配置好后,剩下步骤的都由框架本身搞定了。启动调试,第二个用户登录时,前一个用户确实被踢出了系统。
 好了,到这里,单体架构环境下,我们单用户登录的功能就已经实现。
 我们有多台服务器。
 配置完成后开启多台服务器进行测试时发现:多个用户可以同时进行登录,并且最大可登录人数=服务器个数,这个时候,基于简单配置的解决办法显然是不可行的。
 在这种情况下我们考虑对SpringSecurity进行改造,以Redis作为媒介使多台服务器的用户登录状态同步。
 改造开始前要说明几点:
 1.SpringSecurity的大部分功能是通过一条过滤器链来实现的。链上有多个过滤器,根据配置分别担任登录用户认证,权限认证,csrf验证等等不同的功能。
 2.每个用户登录的详细信息存储在服务器session中,
 所有用户的登录状态存储在一个静态map(concurrentHashMap)中。
 3.其中一个过滤器(SessionManagementFilter)会查找静态map中当前sessionid的登录状态,如果状态是被踢出的(expired==true)时就对当前sessionid进行登出(清空cookie,跳转到登出页面等)操作。
 根据以上几点,我们不难发现,
因为每一次访问都回去查询静态map的登录状态,将之与session中的登录状态进行同步,所以单用户登录验证的关键在于这个静态map,所以改造登出验证的关键在于改造静态map。
 由本地静态map改存到redis
 ————————————-部分代码(不不关心代码部分可以直接拉到结尾)——————————
这里静态map存储于前面提到的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  
31  
32  
33  
34  
35  
36  
public class MySessionRegistryImpl extends SessionRegistryImpl{  
......  
 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.removeSessionInfo(sessionId);  
 Set<String> sessionsUsedByPrincipal = (Set) this.getPrincipals(info.getPrincipal().toString());  
 if (sessionsUsedByPrincipal != null) {  
 if (this.logger.isDebugEnabled()) {  
 this.logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");  
 }  
sessionsUsedByPrincipal.remove(sessionId);  
 if (sessionsUsedByPrincipal.isEmpty()) {  
 if (this.logger.isDebugEnabled()) {  
 this.logger.debug("Removing principal " + info.getPrincipal() + " from registry");  
 }  
this.removePrincipal((info.getPrincipal().toString()));  
 }  
if (this.logger.isTraceEnabled()) {  
 this.logger.trace("Sessions used by '" + info.getPrincipal() + "' : " + sessionsUsedByPrincipal);  
 }  
}  
 }  
 }  
......  
}  
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  
public class MyUsernamePasswordAuthenticationFilter  extends UsernamePasswordAuthenticationFilter {  
......  
 private boolean postOnly = true;  
@Resource  
 private SessionRegistry sessionRegistry;  
@Override  
 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {  
 if(this.postOnly && !request.getMethod().equals("POST")) {  
 throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());  
 } else {  
 String username = this.obtainUsername(request);  
 String password = this.obtainPassword(request);  
 if(username == null) {  
 username = "";  
 }  
if(password == null) {  
 password = "";  
 }  
username = username.trim();  
 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);  
 this.setDetails(request, authRequest);  
 Authentication authentication = this.getAuthenticationManager().authenticate(authRequest);  
 if(authentication!=null){
sessionRegistry.registerNewSession(request.getSession().getId(),authRequest.getPrincipal());  
 }  
 return authentication;  
 }  
 }  
 ......  
}  
1  
2  
3  
4  
5  
6  
7  
8  
9  
public class MyConcurrentSessionFilter extends GenericFilterBean {  
......  
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {  
 this.redirectStrategy = redirectStrategy;  
 }  
......  
}  
 通过配置将我们改造过的SessionRegistryImpl替换进去。
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
http.addFilterAt(new MyConcurrentSessionFilter(sessionRegistry, "/login?expired"), ConcurrentSessionFilter.class);  
 http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);  
@Bean  
public MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {  
 MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter();  
 myUsernamePasswordAuthenticationFilter.setAuthenticationManager(this.authenticationManager());  
 myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MySimpleUrlAuthenticationSuccessHandler());  
 myUsernamePasswordAuthenticationFilter.setSessionAuthenticationStrategy(new MyConcurrentSessionControlAuthenticationStrategy(sessionRegistry));  
 myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MySimpleUrlAuthenticationFailureHandler());  
 return myUsernamePasswordAuthenticationFilter;  
}  
————————————-部分代码(不关心代码部分可以直接看后面)——————————
 至此,我们将静态map替换为redis的工作大体上就完成了。整个流程大概如下图所示。
 NoExpiredConfig单独作为配置类来筛选不需要作单用户登录验证的用户。
 至此,我们的SpringSecurity改造就完成了。
 在单服务器情况下,我们的单用户登录限制可以简单通过增加配置来实现。
 在多服务器情况下,我们需要对SpringSecurity进行改造,将存储用户登录信息的静态map交由redis进行管理。然后修改相关过滤器来实现多服务器上用户登录状态的同步。
 本文已在版权印备案,如需转载请访问版权印39852703
Original url: Access
Created at: 2019-06-24 10:51:58
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论