基于redis的分布式Spring Security改造 | 小码农潇|博客

[](#%E5%89%8D%E8%A8%80 "前言")前言

​ 前段时间,公司要求基于现有系统新增限制单用户登录的功能。就是说:同一个用户在不同的地方(浏览器)不能同时登录,后登录的要踢掉先登录的用户,同时给出友好的提示。

[](#%E7%8E%AF%E5%A2%83 "环境")环境

​ 首先,服务器环境是nginx1.6.2+Tomcat8.5+JAVA8,同时部署在多台服务器上,LBS负载均衡。

​ 开发框架使用SSM(Spring,SpringMvc,Mybatis)。

Redis作为缓存数据库。

​ 登录验证方面,使用强大并且可定制的SpringSecurity4.1(无xml的使用方式)。

[](#%E5%8E%86%E7%A8%8B "历程")历程

[](#%E5%8D%95%E4%BD%93%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95 "单体解决办法")单体解决办法

​ 既然是基于现有系统新增的功能,那么首先我们考虑在现有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(在这里我们可以做一些友好的提示)。

​ 配置好后,剩下步骤的都由框架本身搞定了。启动调试,第二个用户登录时,前一个用户确实被踢出了系统。

​ 好了,到这里,单体架构环境下,我们单用户登录的功能就已经实现。

​ 我们有多台服务器。

​ 配置完成后开启多台服务器进行测试时发现:多个用户可以同时进行登录,并且最大可登录人数=服务器个数,这个时候,基于简单配置的解决办法显然是不可行的。

[](#%E5%88%86%E5%B8%83%E5%BC%8F%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95 "分布式解决办法")分布式解决办法

​ 在这种情况下我们考虑对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改造就完成了。

[](#%E6%80%BB%E7%BB%93 "总结")总结

​ 在单服务器情况下,我们的单用户登录限制可以简单通过增加配置来实现。

​ 在多服务器情况下,我们需要对SpringSecurity进行改造,将存储用户登录信息的静态map交由redis进行管理。然后修改相关过滤器来实现多服务器上用户登录状态的同步。

​ 本文已在版权印备案,如需转载请访问版权印39852703


Original url: Access
Created at: 2019-06-24 10:51:58
Category: default
Tags: none

请先后发表评论
  • 最新评论
  • 总共0条评论