整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Servlet Session 跟踪

TTP 是一种"无状态"协议,这意味着每次客户端检索网页时,客户端打开一个单独的连接到 Web 服务器,服务器会自动不保留之前客户端请求的任何记录。

但是仍然有以下三种方式来维持 Web 客户端和 Web 服务器之间的 session 会话:

Cookies

一个 Web 服务器可以分配一个唯一的 session 会话 ID 作为每个 Web 客户端的 cookie,对于客户端的后续请求可以使用接收到的 cookie 来识别。

这可能不是一个有效的方法,因为很多浏览器不支持 cookie,所以我们建议不要使用这种方式来维持 session 会话。

隐藏的表单字段

一个 Web 服务器可以发送一个隐藏的 HTML 表单字段,以及一个唯一的 session 会话 ID,如下所示:

<input type="hidden" name="sessionid" value="12345">

该条目意味着,当表单被提交时,指定的名称和值会被自动包含在 GET 或 POST 数据中。每次当 Web 浏览器发送回请求时,session_id 值可以用于保持不同的 Web 浏览器的跟踪。

这可能是一种保持 session 会话跟踪的有效方式,但是点击常规的超文本链接(<A HREF...>)不会导致表单提交,因此隐藏的表单字段也不支持常规的 session 会话跟踪。

URL 重写

您可以在每个 URL 末尾追加一些额外的数据来标识 session 会话,服务器会把该 session 会话标识符与已存储的有关 session 会话的数据相关联。

例如,http://w3cschool.cn/file.htm;sessionid=12345,session 会话标识符被附加为 sessionid=12345,标识符可被 Web 服务器访问以识别客户端。

URL 重写是一种更好的维持 session 会话的方式,它在浏览器不支持 cookie 时能够很好地工作,但是它的缺点是会动态生成每个 URL 来为页面分配一个 session 会话 ID,即使是在很简单的静态 HTML 页面中也会如此。

HttpSession 对象

除了上述的三种方式,Servlet 还提供了 HttpSession 接口,该接口提供了一种跨多个页面请求或访问网站时识别用户以及存储有关用户信息的方式。

Servlet 容器使用这个接口来创建一个 HTTP 客户端和 HTTP 服务器之间的 session 会话。会话持续一个指定的时间段,跨多个连接或页面请求。

您会通过调用 HttpServletRequest 的公共方法 getSession() 来获取 HttpSession 对象,如下所示:

HttpSession session = request.getSession();

你需要在向客户端发送任何文档内容之前调用 request.getSession()。下面总结了 HttpSession 对象中可用的几个重要的方法:

Session 跟踪实例

本实例说明了如何使用 HttpSession 对象获取 session 会话创建时间和最后访问时间。如果不存在 session 会话,我们将通过请求创建一个新的 session 会话。

// 导入必需的 java 库
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
 
// 扩展 HttpServlet 类
public class SessionTrack extends HttpServlet {
 
 public void doGet(HttpServletRequest request,
 HttpServletResponse response)
 throws ServletException, IOException
 {
 // 如果不存在 session 会话,则创建一个 session 对象
 HttpSession session = request.getSession(true);
 // 获取 session 创建时间
 Date createTime = new Date(session.getCreationTime());
 // 获取该网页的最后一次访问时间
 Date lastAccessTime = 
 new Date(session.getLastAccessedTime());
 String title = "欢迎回到我的网站";
 Integer visitCount = new Integer(0);
 String visitCountKey = new String("visitCount");
 String userIDKey = new String("userID");
 String userID = new String("ABCD");
 // 检查网页上是否有新的访问者
 if (session.isNew()){
 title = "欢迎来到我的网站";
 session.setAttribute(userIDKey, userID);
 } else {
 visitCount = (Integer)session.getAttribute(visitCountKey);
 visitCount = visitCount + 1;
 userID = (String)session.getAttribute(userIDKey);
 }
 session.setAttribute(visitCountKey, visitCount);
 // 设置响应内容类型
 response.setContentType("text/html");
 PrintWriter out = response.getWriter();
 String docType =
 "<!doctype html public \"-//w3c//dtd html 4.0 " + "transitional//en\">\n";
 out.println(docType +
 "<html>\n" +
 "<head><title>" + title + "</title></head>\n" +
 "<body bgcolor=\"#f0f0f0\">\n" +
 "<h1 align=\"center\">" + title + "</h1>\n" +
 "<h2 align=\"center\">Session 信息</h2>\n" +
 "<table border=\"1\" align=\"center\">\n" +
 "<tr bgcolor=\"#949494\">\n" +
 " <th>Session 信息</th><th>值</th></tr>\n" +
 "<tr>\n" +
 " <td>id</td>\n" +
 " <td>" + session.getId() + "</td></tr>\n" +
 "<tr>\n" +
 " <td>Creation Time</td>\n" +
 " <td>" + createTime + 
 " </td></tr>\n" +
 "<tr>\n" +
 " <td>Time of Last Access</td>\n" +
 " <td>" + lastAccessTime + 
 " </td></tr>\n" +
 "<tr>\n" +
 " <td>User ID</td>\n" +
 " <td>" + userID + 
 " </td></tr>\n" +
 "<tr>\n" +
 " <td>Number of visits</td>\n" +
 " <td>" + visitCount + "</td></tr>\n" +
 "</table>\n" +
 "</body></html>");
 }
}

编译上面的 Servlet SessionTrack,并在 web.xml 文件中创建适当的条目。在浏览器地址栏输入 http://localhost:8080/SessionTrack,当您第一次运行时将显示如下结果:

欢迎来到我的网站

Session 信息

Session 信息值id0AE3EC93FF44E3C525B4351B77ABB2D5Creation TimeTue Jun 08 17:26:40 GMT+04:00 2014Time of Last AccessTue Jun 08 17:26:40 GMT+04:00 2014User IDABCDNumber of visits0

再次尝试运行相同的 Servlet,它将显示如下结果:

欢迎回到我的网站

Session 信息

Session 信息值id0AE3EC93FF44E3C525B4351B77ABB2D5Creation TimeTue Jun 08 17:26:40 GMT+04:00 2014Time of Last AccessTue Jun 08 17:26:40 GMT+04:00 2014User IDABCDNumber of visits1

删除 Session 会话数据

当您完成了一个用户的 session 会话数据,您有以下几种选择:

  • 移除一个特定的属性:您可以调用 public void removeAttribute(String name) 方法来删除与特定的键相关联的值。 to delete the value associated with a particular key.
  • 删除整个 session 会话:您可以调用 public void invalidate() 方法来丢弃整个 session 会话。
  • 设置 session 会话过期时间:您可以调用 public void setMaxInactiveInterval(int interval) 方法来单独设置 session 会话超时。
  • 注销用户:如果使用的是支持 servlet 2.4 的服务器,您可以调用 logout 来注销 Web 服务器的客户端,并把属于所有用户的所有 session 会话设置为无效。
  • web.xml 配置:如果您使用的是 Tomcat,除了上述方法,您还可以在 web.xml 文件中配置 session 会话超时,如下所示:
 <session-config>
 <session-timeout>15</session-timeout>
 </session-config>

上面实例中的超时时间是以分钟为单位,将覆盖 Tomcat 中默认的 30 分钟超时时间。

在一个 Servlet 中的 getMaxInactiveInterval() 方法会返回 session 会话的超时时间,以秒为单位。所以,如果在 web.xml 中配置 session 会话超时时间为 15 分钟,那么 getMaxInactiveInterval() 会返回 900。

pringSecurity-10-Session会话管理

理解Session

Http协议是一种无状态协议所以当服务端需要记录用户的状态时,需要某种机制用于识别用户,这个机制就是Session。服务器通过和用户约定每一个请求携带一个id信息,用于统一用户的请求有了管理,并且区分不同用户。基于session方案,为让用户请求都携带同一个id,并且不妨碍用户体验的情况下,选择cookie作为载体是一个不错的选择,用户第一次访问服务器的时候,没有携带id,服务器端会生成sessionid:session键值对,并且发送sessionid给客户端添加到cookie中。然后该用户在之后的访问中,每一次请求都会将sessionid放到cookie中,使得服务端可以很容易识别用户。

但是有时候用户为了保护个人信息或者安全考虑会禁用cookie,这时候cookie就无法使用。因此有时候服务还支持用url重写来实现,比如:

http://www.baidu.com;jessionid=xxx

URL重写原本是为了兼容禁用cookie的浏览器而设计的,但也容易被黑客利用。黑客只需访问一 次系统,将系统生成的sessionId提取并拼凑在URL上,然后将该URL发给一些取得信任的用户。只要 用户在session有效期内通过此URL进行登录,该sessionId就会绑定到用户的身份,黑客便可以轻松享 有同样的会话状态,完全不需要用户名和密码,这就是典型的会话固定攻击。

防御会话固定攻击

sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种:

  • none:用户登录后session不发生变化
  • newSession:用户登录以后创建新的session
  • migrateSession:用户登录后创建新的session,但是会将旧的session中数据复制到新的session中。
  • changeSessionId:不创建新的会话,而是使用selert容器提供的会话固定保护,每次登录访问之后都更换sessionid,但是没有新建session会话。默认启动此策略
 http.sessionManagement().sessionFixation().changeSessionId();

会话过期

除了防御会话固定攻击,还可以通过SpringSecurity配置会话过期策略,比如会话过期跳转到某个URL。在Springboot应用中有两种会话超时设置的方式,当会话超时之后用户需要重写登录才可以访问应用:

  1. server.servlet.session.timeout=1m
  2. spring.session.timeout=1m

方式1是springboot应用自带的session超时设置,方式2是使用Spring Session之后。提供的session超时配置,方式2优先级高

在Spring Boot中Session超时最短的时间是一分钟,当你的设置小于一分钟的时候,默认为一分钟默认超时时长是30分钟

默认情况下session失效以后会跳转到认证页面,我们可以自定义session失效后,响应结果,有以下两种方式。

invalidSessionUrl

invalidSessionUrl作用是session失效后跳转的url,配置如下,在安全配置中心的 configure(HttpSecurity http)方法中添加代码如下:

  1. 在src\main\resources\templates路径下添加invalidSession.html
<!--suppress ALL-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>springboot葵花宝典登录页面</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>springboot葵花宝典登录页面</h1>
<h2>session会话失效</h2>
</body>
</html>
  1. 在controller中添加
    @RequestMapping("/invalidSession")
    public String invalidSession() {
        return "invalidSession"; // classpath: /templates/login.html
    }
  1. 在LearnSrpingSecurity的configure(HttpSecurity http)添加配置
http.sessionManagement().invalidSessionUrl("/invalidSession")

具体配置如图

注意要以上路径需要配置permitAll()权限,即无需授权即可访问

测试

启动项目登录后,再次登录,结果如下

invalidSessionStrategy

session失败后的策略,配置如下:

  1. 创建com.security.learn.sessionStrategy.CustomInvalidSessionStrategy代码如下
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
    private  static ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        Cookie cookie = new Cookie("JSESSIONID", null);
        cookie.setMaxAge(0);
        String contextPath = request.getContextPath();
        String c= contextPath.length() > 0 ? contextPath : "/";
        cookie.setPath(c);
        response.addCookie(cookie);
        // 当认证失败后,响应 JSON 数据给前端
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString("策略失效"));
    }
}
  1. 将CustomInvalidSessionStrategy注入容器
@Configuration
public class Myconfig {
    @Bean
    @ConditionalOnMissingBean(InvalidSessionStrategy.class)
    public CustomInvalidSessionStrategy customInvalidSessionStrategy(){
        return  new CustomInvalidSessionStrategy();
    }
}
  1. 添加session失效处理

在LearnSrpingSecurity的configure(HttpSecurity http)添加配置,代码如下

测试

启动项目登录后,再次登录,结果如下

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!

公众号 springboot葵花宝典 主要分享JAVA技术,主要包含SpringBoot、SpingCloud、Docker、中间件等技术

原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!

于Cookie和Session的功能与工作原理,在这里我就不再叙述了,大家想要了解可以看一下我的上一篇博客,讲的还是很细致的。

但是之前讲的Session是运行在一台服务器上的,所有的访问都会到达我们的唯一服务器上,这样我们可以根据客户端传来的sessionID,来获取session,或在对应Session不存在的情况下(session 生命周期到了/用户第一次登录),创建一个新的Session;但是,如果我们在集群环境下,假设我们有两台服务器A,B,用户的请求会由Nginx服务器进行转发(别的方案也是同理),用户登录时,Nginx将请求转发至服务器A上,A创建了新的session,并将SessionID返回给客户端,用户在浏览其他页面时,客户端验证登录状态,Nginx将请求转发至服务器B,由于B上并没有对应客户端发来sessionId的session,所以会重新创建一个新的session,并且再将这个新的sessionID返回给客户端,这样,我们可以想象一下,用户每一次操作都有1/2的概率进行再次的登录,这样不仅对用户体验特别差,还会让服务器上的session激增,加大服务器的运行压力。

为了解决集群环境下的seesion共享问题,共有4种解决方案:

1.粘性session

粘性session是指Ngnix每次都将同一用户的所有请求转发至同一台服务器上,即将用户与服务器绑定。

2.服务器session复制

即每次session发生变化时,创建或者修改,就广播给所有集群中的服务器,使所有的服务器上的session相同。

3.session共享

缓存session,使用redis, memcached。

4.session持久化

将session存储至数据库中,像操作数据一样才做session。

其实,最简单的两种方案,就是方案一和方案三,都不需要对session进行任何操作,只需要将Nginx和Tomcat上的配置文件修改一下即可。由于我们做集群,访问量一定是比较大的了,对于第一种方案,如果某台服务器发生故障,此服务器上的所有用户的session都会丢失,所以今天我们采用第三种解决方案。

感谢开源项目tomcat-redis-session-manager,感谢项目的发起者jcoleman。

此方案最简单之处就在于我们无需修改项目,只需要修改Tomcat的context.xml配置文件即可,并且,redis服务器同样可以做分布式。

接下来,我们来讲解一下如何配置tomcat,首先,讲解一下注意事项,因为项目的原因,并没能用maven来管理依赖的包,而在我配置的时候,很正常的发生了包版本间的冲突,从redis中取出的序列化后的session无法转换为HttpSession,所以,大家做的时候一定要注意版本间的兼容问题。

把我使用的版本给大家做个参考吧,tomcat-redis-session-manager-1.1.jar,jedis-2.1.0.jar,commons-pool-1.6.jar

这三个包需放入 tomcat的安装目录下的lib文件夹下,IDE自带的tomcat可以在配置中查看。

需注意的是,当redis的版本‘过高’时,需要依赖commons-pool2.jar,(具体版本不写)。

接下来,我们就讲一下最核心的配置,配置context.xml文件(文件路径workspace\.metadata\.me_tcat7\conf,此处仅是myeclipse编辑器的路径)

[html] view plain copy

  1. <Valve className="com.radiadesign.catalina.session.RedisSessionHandlerValve" />
  2. <Manager className="com.radiadesign.catalina.session.RedisSessionManager"
  3. host="127.0.0.1"
  4. port="9313"
  5. database="0"
  6. maxInactiveInterval="60"/>

这个里的class路径具体看tomcat-redis-session-manager里的路径。

可能你会遇到重启tomcat配置文件会被还原的问题,那么你还需要修改另一处的context.xml文件,workspace\Servers\MyEclipse Tomcat v7.0-config文件夹中的,修改内容同上。

接下来,给大家看一下配置完成后的结果:

图1为监听到的redis服务器的状态,可以看到我们将session序列化后以sessionID为key存入了redis中,图2可以看到是客户端header中的cookie信息(若有疑问可以查看我上一篇的博客),图三是我在redis客户端get key为sessionID的记录,可以验证成功,我们配置是成功了的,因为使用的自己的电脑,配置有限,并没有开几个tomcat来测试,不过上述还是很有说服力的(线上已经测试过了)。

希望对你有帮助!