Spring Session

Spring Session API

我们知道Cookie放在客户端,可以存储用户登录信息,主要用于辨别用户身份。但如果真的把用户ID、登录状态等重要信息放入cookie,会带来安全隐患。
采用Session会话机制可以解决这个问题,将这些重要信息存在服务端,从而避免安全隐患。


image.png

使用会话机制时,Cookie作为session id的载体与客户端通信。

  • 名字为JSESSIONID 的 cookie,是专门用来记录用户session的。JSESSIONID 是标准的、通用的名字。

在了解 Session 与 Cookie 之间的关系后,我们来学习如何使用 Session,也分为读、写两种操作。

读操作

与 cookie 相似,从 HttpServletRequest 对象中取得 HttpSession 对象,使用的语句是 request.getSession()

但不同的是,返回结果不是数组,是对象。在 attribute 属性中用 key -> value 的形式存储多个数据。

假设存储登录信息的数据 key 是 userLoginInfo,那么语句就是 session.getAttribute("userLoginInfo")。(一个映射)

登录信息类

登录信息实例对象因为要在网络上传输,就必须实现序列化接口 Serializable ,否则不实现的话会报错。

登录信息类需要根据具体的需要设计属性字段。下列代码的两个属性仅供演示。

import java.io.Serializable;

public class UserLoginInfo implements Serializable {
  private String userId;
  private String userName;
}

操作代码

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@RequestMapping("/songlist")
public Map index(HttpServletRequest request, HttpServletResponse response) {
  Map returnData = new HashMap();
  returnData.put("result", "this is song list");

  // 取得 HttpSession 对象
  HttpSession session = request.getSession();
  // 读取登录信息
  UserLoginInfo userLoginInfo = (UserLoginInfo)session.getAttribute("userLoginInfo");
  if (userLoginInfo == null) {
    // 未登录
    returnData.put("loginInfo", "not login");
  } else {
    // 已登录
    returnData.put("loginInfo", "already login");
  }

  return returnData;
}

写操作

假设登录成功,怎么记录登录信息到 Session 呢?

既然从 HttpSession 对象中读取登录信息用的是 getAttribute() 方法,那么写入登录信息就用 setAttribute() 方法。

下列代码演示了使用 Session 完成登录的过程,略去了校验用户名和密码的步骤(实际项目中需要):

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@RequestMapping("/loginmock")
public Map loginMock(HttpServletRequest request, HttpServletResponse response) {
  Map returnData = new HashMap();

  // 假设对比用户名和密码成功
  // 仅演示的登录信息对象
  UserLoginInfo userLoginInfo = new UserLoginInfo();
  userLoginInfo.setUserId("12334445576788");
  userLoginInfo.setUserName("ZhangSan");
  // 取得 HttpSession 对象
  HttpSession session = request.getSession();
  // 写入登录信息
  session.setAttribute("userLoginInfo", userLoginInfo);
  returnData.put("message", "login successfule");

  return returnData;
}
  • PS:
    Cookie 存放在客户端,一般不能超过 4kb ,要特别注意,放太多的内容会导致出错;而 Session 存放在服务端,没有限制,不过基于服务端的性能考虑也不能放太多的内容。

Spring Session 配置

  • CookCookie 作为 session id 的载体,也可以修改属性。

前置知识点:配置

application.properties 是 SpringBoot 的标准配置文件,配置一些简单的属性。同时,SpringBoot 也提供了编程式的配置方式,主要用于配置 Bean 。

基本格式:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringHttpSessionConfig {
  @Bean
  public TestBean testBean() {
    return new TestBean();
  }
}

在类上添加@Configuration 注解,就表示这是一个配置类,系统会自动扫描并处理。

在方法上添加 @Bean 注解,表示把此方法返回的对象实例注册成 Bean。

  • @Service 等写在类上的注解一样,都表示注册 Bean

Session 配置

依赖库

先在 pom.xml 文件中增加依赖库:

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

配置类

在类上额外添加一个注解:@EnableSpringHttpSession ,开启 session 。然后,注册两个 bean

  • CookieSerializer:读写 Cookies 中的 SessionId 信息
  • MapSessionRepository:Session 信息在服务器上的存储仓库。
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

import java.util.concurrent.ConcurrentHashMap;

@Configuration
@EnableSpringHttpSession
public class SpringHttpSessionConfig {
  @Bean
  public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setCookieName("JSESSIONID");
        // 用正则表达式配置匹配的域名,可以兼容 localhost、127.0.0.1 等各种场景
    serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
    serializer.setCookiePath("/");
    serializer.setUseHttpOnlyCookie(false);
    // 最大生命周期的单位是分钟
    serializer.setCookieMaxAge(24 * 60 * 60);
    return serializer;
  }

  // 当前存在内存中
  @Bean
  public MapSessionRepository sessionRepository() {
    return new MapSessionRepository(new ConcurrentHashMap<>());
  }
}

想必大家已经了解了 Cookie 各属性值的作用,这里就不赘述了。

代码有些长,想探究为什么这么用,可以 点此阅读官方文档


Spring Request 拦截器

实际的项目中,会有大量的页面功能是需要判断用户是否登录的。让每个页面都判断是否登录过于繁琐,不利于维护。
所以需要一种统一处理相同逻辑的机制,Spring提供了HandlerInterceptor(拦截器)满足这种场景的需求。
实现拦截器有三个步骤

一、创建拦截器

HandlerInterceptor 接口。可以在三个点进行拦截:

  • 1.Controller方法执行之前。这是最常用的拦截点。例如是否登录的验证就要在 preHandle()方法中处理。
  • 2.Controller方法执行之后。例如记录日志、统计方法执行时间等,就要在 postHandle() 方法中处理。
  • 3.整个请求完成后。不常用的拦截点。例如统计整个请求的执行时间的时候用,在 afterCompletion 方法中处理。
    请看下列示例代码:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class InterceptorDemo implements HandlerInterceptor {

  // Controller方法执行之前
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    // 只有返回true才会继续向下执行,返回false取消当前请求
    return true;
  }

  //Controller方法执行之后
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
      ModelAndView modelAndView) throws Exception {

  }

  // 整个请求完成后(包括Thymeleaf渲染完毕)
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

  }
}

preHandle()方法的参数中有 HttpServletRequestHttpServletResponse,可以像 control 中一样使用 Session

二、实现 WebMvcConfigurer

创建一个类实现 WebMvcConfigurer,并实现 addInterceptors() 方法。这个步骤用于管理拦截器。

  • 注意:实现类要加上 @Configuration 注解,让框架能自动扫描并处理。

管理拦截器,比较重要的是为拦截器设置拦截范围。常用 addPathPatterns("/**") 表示拦截所有的 URL 。

当然也可以调用 excludePathPatterns() 方法排除某些 URL,例如登录页本身就不需要登录,需要排除。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfigurerDemo implements WebMvcConfigurer {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    // 多个拦截器组成一个拦截器链
    // 仅演示,设置所有 url 都拦截
    registry.addInterceptor(new UserInterceptor()).addPathPatterns("/**");
  }
}

这样拦截器就添加完毕了。

*学习拦截器,要注意理解和体会 拦截器 与 管理拦截器 分开的思想。

  • 思考一下:如果不分开处理,由拦截器本身决定在什么情况下进行拦截,是否更好?

通常拦截器,会放在一个包(例如interceptor)里。而用于管理拦截器的配置类,会放在另一个包(例如config)里。

这种按功能划分子包的方式,可以让阅读者比较直观、清晰的了解各个类的作用。

推荐阅读更多精彩内容