• SpringSecurity(八)【会话管理】


    九、会话管理


    简介

    当浏览器调用登录接口登录成功之后,服务端会和浏览器之间创建一个会话(Session),浏览器在每次发送请求时都会携带一个 SessionId,服务端则根据这个 SessionId 来判断用户身份。当浏览器关闭之后,服务端的 Session 并不会自动销毁,需要开发者手动在服务端调用 Session 销毁方法,或者等 Session 过期时间到了自动销毁。在 Spring Security 中,与 HttpSession 相关的功能由 SessionManagementFilter 和 SessionAuthenticationStrategy 接口来处理,Session 相关操作委托给 SessionAuthenticationStrategy 接口去完成

    9.1 会话并发管理

    简介

    会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在 SpringSecurity 中对此进行配置

    • 代码配置
    package com.vinjcent.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.session.HttpSessionEventPublisher;
    
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf()
                    .disable()
                    .sessionManagement()    // 开启会话管理
                    .maximumSessions(1);    // 允许会话最大并发只能一个客户端
        }
        
        // 用于监听会话的创建和销毁
        @Bean
        public HttpSessionEventPublisher httpSessionEventPublisher() {
            return new HttpSessionEventPublisher();
        }
    }
    
    • 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
    • 使用两个浏览器进行登录,观察一个登录之后,在进行另外一个登录,前者再次访问系统资源将会被提醒该用户正在被使用

    在这里插入图片描述

    1. sessionManagement() 用来开启会话管理、sessionManagement() 用来指定会话的并发数
    2. HttpSessionEventPublisher 提供一个 HttpSessionEventPublisher 实例。SpringSecurity 中通过一个 Map 集合来维护当前的 HttpSession 记录,进而实现会话的并发管理。当用户登陆成功时,就向集合中添加一条 HttpSession 记录;当会话销毁时,就从集合中移除一条 HttpSession 记录。HttpSessionEventPublisher 实现了 HttpSessionListener 接口,可以监听到 HttpSession 的创建和销毁事件,并将 HttpSession 的 创建/销毁 事件发布出去,这样,当有 HttpSession 销毁时,SpringSecurity 就可以感知到该事件了

    9.2 会话失效

    传统 web 开发处理

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
                    ...
                    .sessionManagement()    // 开启会话管理
                    .maximumSessions(1)     // 允许会话最大并发只能一个客户端
                    .expiredUrl("/toLogin");        // 会话过期处理
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    前后端分离

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable()
                .sessionManagement()    // 开启会话管理
                .maximumSessions(1)     // 允许会话最大并发只能一个客户端
                // .expiredUrl("/toLogin");        // 传统架构的会话过期处理方案
                .expiredSessionStrategy(event -> {      // 前后端分离架构会话过期处理方案
                    HttpServletResponse response = event.getResponse();
                    HashMap<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效,请重新登录");
                    String str = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(str);
                    response.flushBuffer();
                });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 测试访问

    在这里插入图片描述

    9.3 禁止再次登录

    默认的效果是一种被 “挤下线” 的效果,后面登录的用户会把前面登录的用户 “挤下线”。还有一种禁止后来者登录,即一旦当前用户登陆成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录,配置如下

    在这里插入图片描述

    • 代码配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()   // 需要开启退出登录
                .and()
                .csrf()
                .disable()
                .sessionManagement()    // 开启会话管理
                .maximumSessions(1)
                // .expiredUrl("/toLogin")
                .expiredSessionStrategy(event -> {
                    HttpServletResponse response = event.getResponse();
                    HashMap<String, Object> result = new HashMap<>();
                    result.put("status", 500);
                    result.put("msg", "当前会话已经失效,请重新登录");
                    String str = new ObjectMapper().writeValueAsString(result);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println(str);
                    response.flushBuffer();
                })
                .maxSessionsPreventsLogin(true);    // 一旦登录,禁止再次登录
    }
    
    • 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

    9.4 会话共享

    前面所有的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案就会失效。此时可以利用 spring-session 结合 redis 实现 session 共享

    • 实战
      1. 引入依赖pom.xml
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.sessiongroupId>
        <artifactId>spring-session-data-redisartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 配置application.yml文件
    # 端口号
    server:
      port: 3035
      servlet:
        session:
          # 设置session过期时间
          timeout: 1
    # 服务应用名称
    spring:
      application:
        name: SpringSecurity10security
      # The Redis settings
      redis:
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器连接端口
        port: 6379
    
    # 日志处理,为了展示 mybatis 运行 sql 语句
    logging:
      level:
        com:
          vinjcent:
            debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    1. 配置 Security 适配器
    • WebSecurityConfigurerAdapter
    package com.vinjcent.config.security;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.session.SessionRegistry;
    import org.springframework.session.FindByIndexNameSessionRepository;
    import org.springframework.session.security.SpringSessionBackedSessionRegistry;
    
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 注入 session 仓库
        private final FindByIndexNameSessionRepository sessionRepository;
    
        @Autowired
        public WebSecurityConfiguration(FindByIndexNameSessionRepository sessionRepository) {
            this.sessionRepository = sessionRepository;
        }
    
        // 注册 session 同步到 redis 中
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SpringSessionBackedSessionRegistry(sessionRepository);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .successForwardUrl("/test")
                    .and()
                    .logout()   // 需要开启退出登录
                    .and()
                    .csrf()
                    .disable()
                    .sessionManagement()    // 开启会话管理
                    .maximumSessions(1)
                    // .expiredUrl("/toLogin");
                    .expiredSessionStrategy(event -> {
                        HttpServletResponse response = event.getResponse();
                        HashMap<String, Object> result = new HashMap<>();
                        result.put("status", 500);
                        result.put("msg", "当前会话已经失效,请重新登录");
                        String str = new ObjectMapper().writeValueAsString(result);
                        response.setContentType("application/json;charset=UTF-8");
                        response.getWriter().println(str);
                    })
                    .maxSessionsPreventsLogin(true)    // 一旦登录,禁止再次登录
                    .sessionRegistry(sessionRegistry());     // 将 session 交给谁管理,前后端分离自定义过滤器需要配setSessionAuthenticationStrategy
        }
    }
    
    • 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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    1. 将 redis 作为全局 HttpSession(不开启redis作为HttpSession会使得前面的配置失效,导致出现每个服务有各自的 session 情况
    package com.vinjcent.config.redis;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
    
    @Configuration
    @EnableRedisHttpSession // 将整个应用中使用session的数据全部交给redis处理
    public class RedisSessionManager {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 模拟分布式服务
    • 将当前的服务复制一份运行,并设置运行vm参数

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    1. 启动本地 redis-server 服务,测试两个服务进行登录操作,结果如图所示

    在这里插入图片描述

  • 相关阅读:
    领英高级会员LinkedIn Sales Navigator是什么?有什么作用?看完这篇文章你就彻底明白了
    Python协程(asyncio)(四)同步原语
    docker安装mysql8和mysql5.7
    【java_wxid项目】【第二章】【Spring Cloud Alibaba Nacos集成】
    探索Vue 3和Vue 2的区别
    Linux进程信号的处理
    利用已存在的conda环境
    【数据库查询表结构】
    数据处理及跳转
    solidity入门
  • 原文地址:https://blog.csdn.net/Wei_Naijia/article/details/128170161