zoukankan      html  css  js  c++  java
  • spring boot项目17:session

    JAVA 8

    Spring Boot 2.5.3

    Google Chrome 92+

    ---

    授人以渔:

    1、Spring Boot Reference Documentation

    This document is also available as Multi-page HTML, Single page HTML and PDF.

    有PDF版本哦,下载下来!

    2、Spring Session

    Spring Session provides an API and implementations for managing a user’s session information.

    PDF版本哦,下载下来!

    项目:web0920(Web项目)

    目录

    1、普通Web项目的Session

    请求中的session调试

    Session及Cookie配置

    session存储到本地

    2、Security的Web项目的Session

    3、使用Spring Session

    试验1:使用Redis作为session存储

    试验2:多应用共享Session

    参考文档

    1、普通Web项目的Session

    建立项目,依赖:spring-boot-starter-web

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    开发端点/home/get:

    @RestController
    @RequestMapping(value="/home")
    @SpringBootApplication
    public class Web0920Application {
    
    	// 注入请求对象
        @Autowired
    	private HttpServletRequest req;
        
        // 新增端点:输出请求、请求中的session信息
    	@GetMapping(value="/get")
    	public String get() {
    		cs.accept("req=" + req);
    		cs.accept("req=" + req.getClass());
            
    		HttpSession hsess = req.getSession();
    		cs.accept("req sess=" + hsess);
            
    		String sid = req.getRequestedSessionId();
    		cs.accept("req sid=" + sid);
            
    		cs.accept("req=" + req.getSession(true));
            
    		sid = req.getRequestedSessionId();
    		cs.accept("req sid=" + sid);
    		
    		return new Date().toString();
        }
    
    }

    调用/home/get端点:

    - 在Chrome中按下 F12,进入 Application->Storage->Cookies,可以看到其中存在一个Cookie,名称为JSESSION

    - 检查应用的日志

    请求req的类型 是 动态代理类;

    req.getSession() 返回HttpSession,类型为 org.apache.catalina.session.StandardSessionFacade,位于 tomcat-embed-core-x.x.x.jar包中,这样来看,SESSION是由嵌入式Tomcat管理的;

    执行 req.getSession(true) 前后,SessionId 发生了变化,最后在浏览器上展示的是 新建的session id——Cookie JSESSION的值;

    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sess=org.apache.catalina.session.StandardSessionFacade@2f463540
    req sid=C05EFABC37A3F027E64F7F63A2006FFE
    req=org.apache.catalina.session.StandardSessionFacade@2f463540
    req sid=C05EFABC37A3F027E64F7F63A2006FFE
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sess=org.apache.catalina.session.StandardSessionFacade@2f463540
    req sid=D48B21C607B1B98A9BBFCDD990278732
    req=org.apache.catalina.session.StandardSessionFacade@2f463540
    req sid=D48B21C607B1B98A9BBFCDD990278732

    补充说明:

    清理掉浏览器的cookie时,上面请求中的req sid=null;但是,返回到浏览器时,又生成了Cookie JSESSION。这样看来,SESSION是在端点返回时生成的?下面调试时会介绍。

    测试代码及执行日志:

    端点代码:
    	@GetMapping(value="/get")
    	public String get() {
    		cs.accept("req=" + req);
    		cs.accept("req=" + req.getClass());
    		
    		HttpSession hsess = req.getSession();
    		cs.accept("req sess=" + hsess);
    		
    		String sid = req.getRequestedSessionId();
    		cs.accept("req sid 1=" + sid);
    		
    		sid = req.getRequestedSessionId();
    		cs.accept("req sid 2=" + sid);
    		
    		return new Date().toString();
    	}
    
    清理浏览器中的cookie后,执行结果日志:
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sess=org.apache.catalina.session.StandardSessionFacade@5f0de3eb
    req sid 1=null
    req sid 2=null
    
    此时,浏览器得到的Cookie JSESSION = 636A9BB7D3E507E0D2555F3A60877D53
    
    再次访问端点,执行结果日志:
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sess=org.apache.catalina.session.StandardSessionFacade@5f0de3eb
    req sid 1=636A9BB7D3E507E0D2555F3A60877D53
    req sid 2=636A9BB7D3E507E0D2555F3A60877D53
    
    此后多次执行,Cookie JSESSION不变。

    请求中的session调试

    session是怎么产生的呢?调试(DEBUG)可知。

    在嵌入式Tomcat的包中,有一个 org.apache.catalina.connector.Request 类,

    package org.apache.catalina.connector;
    ...
    public class Request implements HttpServletRequest {
    
        @Override
        public HttpSession getSession() {
            Session session = doGetSession(true);
            if (session == null) {
                return null;
            }
    
            return session.getSession();
        }
        
        @Override
        public HttpSession getSession(boolean create) {
            Session session = doGetSession(create);
            if (session == null) {
                return null;
            }
    
            return session.getSession();
        }
    }

    其中有两个getSession函数,在第二个有参数的 getSession(boolean create) 中添加断点——doGetSession,再执行调试(调试前先清理浏览器的cookie),可以看到 session 在一个请求中新建的过程:

    在 org.apache.catalina.session.ManagerBase 中 有一个 createSession(String sessionId) 函数,继续调试,调用 generateSessionId() 函数时产生了 sessionID。

    浏览器得到响应后的Cookie值:和上面创建的相同

    继续调试,进入了 org.apache.catalina.connector.CoyoteAdapter 的 postParseRequest函数:

    其中有一个 SessionConfig 类 可以看下,

    CoyoteAdapter 中的parseSessionCookiesId函数会 获取请求的Cookies,其中就包含JSESSIONID的:

    调试到代码中调用 req.getRequestedSessionId() 的地方:

    通过上面的调试,可以看到在Web请求中Session的创建、获取过程。

    疑问:嵌入式的Tomcat是这么管理session的,Jetty、Undertow的呢?

    Session及Cookie配置

    在本文的Web应用(Embedded Servlet Container)中,Session、Cookie是可以配置的。

    官文中展示了下面的配置:

    server.servlet.session.persistent

    Whether to persist session data between restarts.

    server.servlet.session.store-dir

    Directory used to store session data.

    server.servlet.session.timeout

    server.servlet.session.cookie.* // cookie的配置

    ……

    HttpSession对象 是有很多方法可以调用的:

    调用下面的程序测试:

    	@GetMapping(value="/get")
    	public String get() {
    		cs.accept("
    req=" + req);
    		cs.accept("req=" + req.getClass());
    
    		// 请求中的session id:旧值
    		String sid = req.getRequestedSessionId();
    		cs.accept("req sid 1=" + sid);
    		
    		// 请求处理后的,新值
    		HttpSession hsess = req.getSession();
    		cs.accept("req sess=" + hsess);
    		cs.accept("createTime=" + new Date(hsess.getCreationTime()));
    		cs.accept("LastAccessedTime=" + new Date(hsess.getLastAccessedTime()));
    		cs.accept("MaxInactiveInterval=" + hsess.getMaxInactiveInterval() + "秒");
    		
    		cs.accept("hsess.getId()=" + hsess.getId());
    		cs.accept("hsess.isNew()=" + hsess.isNew());
    		
    		return new Date().toString();
    	}

    原来,req.getRequestedSessionId() 获取的是 请求中的session——根据cookie解析得来,难怪之前会显示null!

    而req.getSession()获取的HttpSession hsess对象 是请求中的 实时值。

    这样的话,前面的一个介绍是错误的——session 其实是在请求处理过程中新建的,而不是 端点处理完后,返回过程中创建。纠正!

    执行结果:

    # 浏览器有 旧cookie时
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sid 1=5307455366ED81818A4BF23DF1B60D87
    req sess=org.apache.catalina.session.StandardSessionFacade@4da148de
    createTime=Wed Sep 22 22:56:10 CST 2021
    LastAccessedTime=Wed Sep 22 22:56:10 CST 2021
    MaxInactiveInterval=1800秒
    hsess.getId()=128190BB325A6235595E9292BCDEF464
    hsess.isNew()=true
    
    # 注意 最后一行的 isNew()=false 不是新的了
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy50
    req sid 1=128190BB325A6235595E9292BCDEF464
    req sess=org.apache.catalina.session.StandardSessionFacade@4da148de
    createTime=Wed Sep 22 22:56:10 CST 2021
    LastAccessedTime=Wed Sep 22 22:56:10 CST 2021
    MaxInactiveInterval=1800秒
    hsess.getId()=128190BB325A6235595E9292BCDEF464
    hsess.isNew()=false

    配置session及cookie:

    # session超时配置:默认30m
    server.servlet.session.timeout=60s
    
    # cookie配置
    server.servlet.session.cookie.name=mysession
    server.servlet.session.cookie.max-age=60s

    配置前的Cookie样式:

    配置后的Cookie:可以看到多了一个名为mysesion的cookie,其属性也和配置一致(官文中还有更多配置)。

    若是清理了cookie再访问,就没有旧的JSESSIONID了。

    检查日志:session 的 MaxInactiveInterval 变成 60秒了,也就是说 最少60秒更新一次——只要有请求进来。

    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy52
    req sid 1=null
    req sess=org.apache.catalina.session.StandardSessionFacade@5eb81a04
    createTime=Wed Sep 22 23:08:11 CST 2021
    LastAccessedTime=Wed Sep 22 23:08:11 CST 2021
    MaxInactiveInterval=60秒
    hsess.getId()=7AB5481E512D71C5EC2520A0F39E6E0D
    hsess.isNew()=true
    
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy52
    req sid 1=7AB5481E512D71C5EC2520A0F39E6E0D
    req sess=org.apache.catalina.session.StandardSessionFacade@5eb81a04
    createTime=Wed Sep 22 23:08:11 CST 2021
    LastAccessedTime=Wed Sep 22 23:08:11 CST 2021
    MaxInactiveInterval=60秒
    hsess.getId()=7AB5481E512D71C5EC2520A0F39E6E0D
    hsess.isNew()=false

    更多配置,待解锁。

    思考:Web应用的需要session,Reactive Web需要吗?

    session存储到本地

    添加下面的配置:

    server.servlet.session.persistent=true
    # 错误的地址
    #server.servlet.session.store-dir=D:datasessionweb0920
    # 正确的配置
    server.servlet.session.store-dir=/data/session/web0920

    启动应用,此时,目录为空。

    访问/home/get端点:

    在项目web0920的根目录下出现了 /data/session/web0920目录,访问期间,其下没有内容。

    关闭应用,此时出现一个SESSIONS.ser文件:

    前面步骤浏览器中记录的session值:C232AABAE7917EE147F6CB18945C5170 ,有效期1800秒

    再次重启应用,看看浏览器的这个session是否继续有效——访问时不会建新的?旧的仍然有效,不会生成新的!

    只不过,应用启动后,/data/session/web0920 下的文件没了:来自博客园

    疑问

    1、正常停止程序的时候 会来得及保存session,要是程序异常关闭了呢——更严重的导致关闭的原因,比如断电?

    2、怎么设置保存到 某个指定目录,而不是 项目根目录下呢?

    session虽然存储到了本地,但从前面的测试来看,这种方式的session也是无法在 多个项目中共享的。

    要实现session在多个应用的共享,需要用到Spring Session技术,第三章介绍。来自博客园

    2、Security的Web项目的Session

    在第一章的基础上,添加依赖包:spring-boot-starter-security

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    启动项目:启动后,存在一个DefaultSecurityFilterChain,其中包含一个 SessionManagementFilter对象,这个 SessionManagementFilter有什么用呢?

    访问前面的 /home/get,跳转到登录页,使用 user + 启动日志中的密码 登录,此时,/home/get端点输出:

    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy53
    req sid 1=EBBF5F23FE2D77D5BCE3860A998D80A9
    req sess=org.apache.catalina.session.StandardSessionFacade@3c17eb29
    createTime=Wed Sep 22 23:16:07 CST 2021
    LastAccessedTime=Wed Sep 22 23:16:32 CST 2021
    MaxInactiveInterval=1800秒
    hsess.getId()=EBBF5F23FE2D77D5BCE3860A998D80A9
    hsess.isNew()=false

    打开登录页面时,显示的是SESSION-A,点击登录,调用/login 端点,此时,请求中的cookie为SESSION-A,登录成功,生产了新的SESSION-B

    之后访问 /home/get 时,都是使用的 SESSION-B 了。来自博客园

    SESSION-B 是怎么产生的?难道是前面的 SessionManagementFilter中?

    调试 SessionManagementFilter 的 doFilter 应该可以知道答案。

    结论:

    调试上面的 doFilter,和 session的创建没有关系,还是要调试 前面的Request的getSession函数才可以一步一步找到。

    SessionManagementFilter 到底有什么用呢?在安全中可以发挥什么作用?TODO

    存在一个 SessionAuthenticationStrategy接口:其中的 onAuthentication 方法 是用来做安全处理的?

    public interface SessionAuthenticationStrategy {
    
    	/**
    	 * Performs Http session-related functionality when a new authentication occurs.
    	 * @throws SessionAuthenticationException if it is decided that the authentication is
    	 * not allowed for the session. This will typically be because the user has too many
    	 * sessions open at once.
    	 */
    	void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
    			throws SessionAuthenticationException;
    
    }

    ---210922 2357--

    3、使用Spring Session

    前面的应用中,session都是有应用自行管理的,无法在应用间共享、也无法持久化。

    session无法共享,会导致依赖session存储数据的服务无法轻易地横向扩展,会导致端点提供的服务是 有状态服务。

    session无法持久化,会导致数据丢失,比如,用户登录后,但应用重启了,此时,用户就需要再次登录——因为session没了。

    因此,Spring框架提供了Spring Session,Spring Boot提供了它的自动配置(auto-configuration)

    对于 Servlet web application(本文使用的项目),提供了下面的存储方式:

    • JDBC
    • Redis
    • Hazelcast
    • MongoDB

    而对于reactive web application,Spring Session提供了两种存储方式:来自博客园

    • Redis
    • MongoDB

    依赖包:spring-session-core

    <dependency>
    	<groupId>org.springframework.session</groupId>
    	<artifactId>spring-session-core</artifactId>
    </dependency>

    对应 https://start.spring.io/ 的 Spring Session:

    如果在建立项目时,选择了Spring Session、Spring Data Redis,此时的依赖包会有两个:

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

    其中,spring-session-data-redis 结果如下:包含了前面的 spring-session-core

    疑问

    在上面选择了Redis、Session后,再选择JDBC相关包会怎样?来自博客园

    官文的说法,

    如果只有单个的Spring Session module在classpath中,S.B.自动选择存储实现方式。

    要是有多个存储实现方式,就需要配置StoreType,比如,下面配置为 jdbc:

    spring.session.store-type=jdbc

    设置为none则会禁止Spring Session。

    每次存储方式都有额外的配置,可以看S.B.的官文了解:spring.session.*、spring.session.jdbc.、spring.session.mongodb.、spring.session.hazelcast.、spring.session.redis.。

    对于spring session的配置,还可以使用 @Enable*Session 相关注解。下面是使用Redis作为实现时找到的4个@Enable*注解:来自博客园

    HttpSession结尾的是 Servlet Web,而 WebSession结尾的是 Reactive Web应用的。

    这个方式的配置会导致S.B.的自动配置、前面的spring.session.* 配置优先级降低。

    还有一个 spring.session.timeout 属性,在 Servlet Web应用中,如果没有的话,就会使用之前的 server.servlet.session.timeout。

    试验1:使用Redis作为session存储

    引入依赖包(见上文):spring-boot-starter-data-redis、spring-session-data-redis

    添加Redis配置:

    #
    # Redis
    # mylinux 是虚拟机的本地域名,配置到 hosts文件中
    spring.redis.host=mylinux
    spring.redis.port=6379
    

    启动项目:成功。

    检查redis服务器中的键:没有session相关的。来自博客园

    127.0.0.1:6379> keys *
    1) "xacxedx00x05tx00x05test1"
    2) "xacxedx00x05tx00x04set1"
    3) "xacxedx00x05tx00x05test3"
    127.0.0.1:6379>
    

    访问/home/get端点:成功。但是,此时浏览器有一个name=SESSION的cookie——而不是 JSESSION。

    再次检查Redis:多了很多 spring:session 开头的键。

    127.0.0.1:6379> keys *
    1) "spring:session:sessions:expires:a51d5f9f-5e70-4797-a5e6-e9f42eea0008"
    2) "spring:session:sessions:6978552e-8f6b-4bb3-903c-9d3f56d8dcd0"
    3) "spring:session:sessions:expires:6978552e-8f6b-4bb3-903c-9d3f56d8dcd0"
    4) "spring:session:expirations:1632369240000"
    6) "spring:session:sessions:a51d5f9f-5e70-4797-a5e6-e9f42eea0008"
    8) "spring:session:expirations:1632369120000"
    127.0.0.1:6379>
    

    重启项目,检查在session有效期内,是否需要重新生成新session?结果是,不需要!符合预期。

    试验2:多应用共享Session

    需要结合Nginx使用——转发请求到多个应用。来自博客园

    两个应用:8080、9090端口。

    配置hosts文件:后面都访问 session.com

    127.0.0.1 session.com

    配置Nginx转发请求 session.com的请求到 两个应用:

    # upstreamssession.conf文件
    upstream session_com {
    	server localhost:8080;
    	server localhost:9090;
    }
    
    # serverssession.conf文件
    server {
    	listen       80;
    	server_name  session.com;
    
    	sendfile        on;
    
    	access_log  logs/xq.access.log main;
    
    	location / {
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   Host      $http_host;
            proxy_set_header X-NginX-Proxy true;
    
    		proxy_pass http://session_com;
    	}
    }
    

    测试多次访问 http://session.com/home/get:来自博客园

    请求成功。并且,由两个应用均衡地处理请求。并且,实现了session共享。

    奇怪的是,Cookie的值 和 req.getRequestedSessionId()、缓存中的值 不一样

    日志 和 redis-cli中:

    日志:
    req=Current HttpServletRequest
    req=class com.sun.proxy.$Proxy62
    req sid 1=2079631a-8efb-4a17-860d-9e729b0c31a8
    req sess=org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper@6e2d8002
    createTime=Thu Sep 23 11:33:25 CST 2021
    LastAccessedTime=Thu Sep 23 11:35:54 CST 2021
    MaxInactiveInterval=1800秒
    hsess.getId()=2079631a-8efb-4a17-860d-9e729b0c31a8
    hsess.isNew()=false
    
    redis-cli:部分
    127.0.0.1:6379> keys *
     6) "spring:session:sessions:expires:2079631a-8efb-4a17-860d-9e729b0c31a8"
     7) "spring:session:sessions:2079631a-8efb-4a17-860d-9e729b0c31a8"
    127.0.0.1:6379>
    

    在这个过程中发生了什么?TODO

    本文还有更多内容需要进一步学习:

    1、spring.session.redis.*的配置

    2、@EnableRedisHttpSession的配置

    3、Cookie的设置呢?

    4、Spring Session + Spring Security

    以便得到spring session的最佳实践。

    》》》全文完《《《来自博客园

    Spring Session在大规模系统中用的多吗?

    在本文的项目中用起来还行,可是,存在什么隐患?要怎么研究?

    Session+Security 不算是主流解决方案了吧?Oauth2什么的才是吧?

    需要更多了解。来自博客园

    ---210923 1149---

    参考文档

    1、Spring Security Web 5.1.2 源码解析 -- SessionManagementFilter

    2、

  • 相关阅读:
    组合数学
    组合数学
    组合数学
    组合数学 + STL --- 利用STL生成全排列
    组合数学
    数论
    给xcode项目重命名
    iOS中动态注入JavaScript方法。动态给html标签添加事件
    swift
    swift
  • 原文地址:https://www.cnblogs.com/luo630/p/15321807.html
Copyright © 2011-2022 走看看