集群化部署,Spring Security 要如何处理 session 共享?

前面和你们聊了 Spring Security 如何像 QQ 同样,自动踢掉已登陆用户(Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?),可是前面咱们是基于单体应用的,若是咱们的项目是集群化部署,这个问题该如何解决呢?java

今天咱们就来看看集群化部署,Spring Security 要如何处理 session 并发。nginx

本文是 Spring Security 系列第 17 篇,阅读前面的文章有助于更好的理解本文:git

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登陆
  4. Spring Security 作先后端分离,咱就别作页面跳转了!通通 JSON 交互
  5. Spring Security 中的受权操做原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登陆功能
  9. Spring Boot 自动登陆,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登陆用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登陆用户,一个配置搞定!
  14. Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?
  15. Spring Security 自带防火墙!你都不知道本身的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防护会话固定攻击?

1.集群会话方案

在传统的单服务架构中,通常来讲,只有一个服务器,那么不存在 Session 共享问题,可是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:github

在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上以后,被 Nginx 转发到 Tomcat A 上,而后在 Tomcat A 上往 session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有以前的数据。web

1.1 session 共享

对于这一类问题的解决,目前比较主流的方案就是将各个服务之间须要共享的数据,保存到一个公共的地方(主流方案就是 Redis):redis

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aaFMeebv-1589763397129)(http://img.itboyhub.com/2020/...]spring

当全部 Tomcat 须要往 Session 中写数据时,都往 Redis 中写,当全部 Tomcat 须要读数据时,都从 Redis 中读。这样,不一样的服务就可使用相同的 Session 数据了。数据库

这样的方案,能够由开发者手动实现,即手动往 Redis 中存储数据,手动从 Redis 中读取数据,至关于使用一些 Redis 客户端工具来实现这样的功能,毫无疑问,手动实现工做量仍是蛮大的。后端

一个简化的方案就是使用 Spring Session 来实现这一功能,Spring Session 就是使用 Spring 中的代理过滤器,将全部的 Session 操做拦截下来,自动的将数据 同步到 Redis 中,或者自动的从 Redis 中读取数据。浏览器

对于开发者来讲,全部关于 Session 同步的操做都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 同样。

1.2 session 拷贝

session 拷贝就是不利用 redis,直接在各个 Tomcat 之间进行 session 数据拷贝,可是这种方式效率有点低,Tomcat A、B、C 中任意一个的 session 发生了变化,都须要拷贝到其余 Tomcat 上,若是集群中的服务器数量特别多的话,这种方式不只效率低,还会有很严重的延迟。

因此这种方案通常做为了解便可。

1.3 粘滞会话

所谓的粘滞会话就是将相同 IP 发送来的请求,经过 Nginx 路由到同一个 Tomcat 上去,这样就不用进行 session 共享与同步了。这是一个办法,可是在一些极端状况下,可能会致使负载失衡(由于大部分状况下,都是不少人用同一个公网 IP)。

因此,Session 共享就成为了这个问题目前主流的解决方案了。

2.Session共享

2.1 建立工程

首先 建立一个 Spring Boot 工程,引入 Web、Spring Session、Spring Security 以及 Redis:

建立成功以后,pom.xml 文件以下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

2.2 配置

spring.redis.password=123
spring.redis.port=6379
spring.redis.host=127.0.0.1

spring.security.user.name=javaboy
spring.security.user.password=123

server.port=8080

配置一下 Redis 的基本信息;Spring Security 为了简化,我就将用户名密码直接配置在 application.properties 中了,最后再配置一下项目端口号。

2.3 使用

配置完成后 ,就可使用 Spring Session 了,其实就是使用普通的 HttpSession ,其余的 Session 同步到 Redis 等操做,框架已经自动帮你完成了:

@RestController
public class HelloController {
    @Value("${server.port}")
    Integer port;
    @GetMapping("/set")
    public String set(HttpSession session) {
        session.setAttribute("user", "javaboy");
        return String.valueOf(port);
    }
    @GetMapping("/get")
    public String get(HttpSession session) {
        return session.getAttribute("user") + ":" + port;
    }
}

考虑到一会 Spring Boot 将以集群的方式启动 ,为了获取每个请求究竟是哪个 Spring Boot 提供的服务,须要在每次请求时返回当前服务的端口号,所以这里我注入了 server.port 。

接下来 ,项目打包:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqUwnFQc-1589763397136)(http://img.itboyhub.com/2020/...]

打包以后,启动项目的两个实例:

java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8081

而后先访问 localhost:8080/set8080 这个服务的 Session 中保存一个变量,第一次访问时会自动跳转到登陆页面,输入用户名密码进行登陆便可。访问成功后,数据就已经自动同步到 Redis 中 了 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HKNFuzWj-1589763397137)(http://img.itboyhub.com/2020/...]

而后,再调用 localhost:8081/get 接口,就能够获取到 8080 服务的 session 中的数据:

此时关于 session 共享的配置就已经所有完成了,session 共享的效果咱们已经看到了。

2.4 Security 配置

Session 共享已经实现了,可是咱们发现新的问题,在Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?一文中咱们配置的 session 并发管理失效了。

也就是说,若是我添加了以下配置:

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest()
            ...
            .sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true);
}

如今这个配置不起做用,用户依然能够在多个浏览器上同时登陆。

这是怎么回事呢?

首先建议你们回忆一下Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?一文。

在该文中,咱们提到,会话注册表的维护默认是由 SessionRegistryImpl 来维护的,而 SessionRegistryImpl 的维护就是基于内存的维护。如今咱们虽然启用了 Spring Session+Redis 作 Session 共享,可是 SessionRegistryImpl 依然是基于内存来维护的,因此咱们要修改 SessionRegistryImpl 的实现逻辑。

修改方式也很简单,实际上 Spring Session 为咱们提供了对应的实现类 SpringSessionBackedSessionRegistry,具体配置以下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    FindByIndexNameSessionRepository sessionRepository;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest()
                ...
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry());
    }
    @Bean
    SpringSessionBackedSessionRegistry sessionRegistry() {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }
}

咱们在这里只须要提供一个 SpringSessionBackedSessionRegistry 的实例,而且将其配置到 sessionManagement 中去便可。之后,session 并发数据的维护将由 SpringSessionBackedSessionRegistry 来完成,而不是 SessionRegistryImpl,如此,咱们关于 session 并发的配置就生效了,在集群环境下,用户也只能够在一台设备上登陆。

为了让咱们的案例看起更完美一些,接下来咱们来引入 Nginx ,实现服务实例自动切换。

3.引入 Nginx

很简单,进入 Nginx 的安装目录的 conf 目录下(默认是在 /usr/local/nginx/conf),编辑 nginx.conf 文件:

在这段配置中:

  1. upstream 表示配置上游服务器
  2. javaboy.org 表示服务器集群的名字,这个能够随意取名字
  3. upstream 里边配置的是一个个的单独服务
  4. weight 表示服务的权重,意味者将有多少比例的请求从 Nginx 上转发到该服务上
  5. location 中的 proxy_pass 表示请求转发的地址,/ 表示拦截到全部的请求,转发转发到刚刚配置好的服务集群中
  6. proxy_redirect 表示设置当发生重定向请求时,nginx 自动修正响应头数据(默认是 Tomcat 返回重定向,此时重定向的地址是 Tomcat 的地址,咱们须要将之修改使之成为 Nginx 的地址)。

配置完成后,将本地的 Spring Boot 打包好的 jar 上传到 Linux ,而后在 Linux 上分别启动两个 Spring Boot 实例:

nohup java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8080 &
nohup java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8081 &

其中

  • nohup 表示当终端关闭时,Spring Boot 不要中止运行
  • & 表示让 Spring Boot 在后台启动

配置完成后,重启 Nginx:

/usr/local/nginx/sbin/nginx -s reload

Nginx 启动成功后,咱们首先手动清除 Redis 上的数据,而后访问 192.168.66.128/set 表示向 session 中保存数据,这个请求首先会到达 Nginx 上,再由 Nginx 转发给某一个 Spring Boot 实例:

如上,表示端口为 8081Spring Boot 处理了这个 /set 请求,再访问 /get 请求:

能够看到,/get 请求是被端口为 8080 的服务所处理的。

4.总结

本文主要向你们介绍了 Spring Session 的使用,另外也涉及到一些 Nginx 的使用 ,虽然本文较长,可是实际上 Spring Session 的配置没啥,涉及到的配置也都很简单。

若是你们没有在 SSM 架构中用过 Spring Session ,可能不太好理解咱们在 Spring Boot 中使用 Spring Session 有多么方便,由于在 SSM 架构中,Spring Session 的使用要配置三个地方 ,一个是 web.xml 配置代理过滤器,而后在 Spring 容器中配置 Redis,最后再配置 Spring Session,步骤仍是有些繁琐的,而 Spring Boot 中直接帮咱们省去了这些繁琐的步骤!

好了 ,本文就说到这里,本文相关案例我已经上传到 GitHub ,你们能够自行下载:https://github.com/lenve/spring-security-samples

若是以为有收获,记得点个在看鼓励下松哥哦~

相关文章
相关标签/搜索