Spring Session

发布于:2024-11-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

Cookie

Cookie 是网络编程中使用最广泛的一项技术,主要用于辨识用户身份。

客户端(浏览器)与网站服务端通讯的过程如下图所示:

从图中看,服务端既要返回 Cookie 给客户端,也要读取客户端提交的 Cookie。所以本节课主要学习服务端 Spring 工程是如何使用 Cookie 的,有两种操作。

浏览器如何使用 Cookie ,在《Java 网络编程》课程中讲解。

为 control 类的方法增加一个 HttpServletRequest 参数,通过 request.getCookies() 取得 cookie 数组。然后再循环遍历数组即可。(下列演示代码省略循环代码)

系统会自动传入方法参数所需要的 HttpServletRequest 对象哦

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

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

  Cookie[] cookies = request.getCookies();
  returnData.put("cookies", cookies);

  return returnData;
}

从浏览器输出结果可以看到,打印出了所有的 Cookie 数据。

cookie 有很多属性值,各属性值的作用:

name     名称
value    值
domain   表示 cookie 生效的域。为 null 表示此 cookie 只对当前域名有效。如果设置了域名,表示当 
         前域名和子域名都有效。ds011.agent.youkeda.com 是 youkeda.com 的子域名
path     表示 cookie 生效的目录。为 null 表示此 cookie 只对当前请求的 URL 路径有效。如果设置 
         了域名,表示当前 URL 路径和所有下级路径都有效。
/        表示整个网站都生效
maxAge   有效时间,默认值为-1。负数表示关闭浏览器就删除cookie,0表示立即浏览器立即删除此cookie
         正数表示多少秒后过期自动失效
httpOnly 安全性设置。值为 true 表示通过 js 脚本将无法读取到cookie信息。false 表示不限制

如果知道 cookie 的名字,就可以通过注解的方式读取,不需要再遍历 cookie 数组了,更加方便。

为 control 类的方法增加一个 @CookieValue("xxxx") String xxxx 参数即可,注意使用时要填入正确的 cookie 名字。

系统会自动解析并传入同名的 cookie

import org.springframework.web.bind.annotation.CookieValue;

@RequestMapping("/songlist")
public Map index(@CookieValue("JSESSIONID") String jSessionId) {
  Map returnData = new HashMap();
  returnData.put("result", "this is song list");
  returnData.put("author", songAuthor);
  returnData.put("JSESSIONID", jSessionId);

  return returnData;
}

请看代码演示:

注意:如果系统解析不到指定名字的 cookie,使用此注解就会报错。必须谨慎使用。

同样,也很简单。为 control 类的方法增加一个 HttpServletResponse 参数,调用 response.addCookie() 方法添加 Cookie 实例对象即可。

系统会自动传入方法参数所需要的 HttpServletResponse 对象哦

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

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

  Cookie cookie = new Cookie("sessionId","CookieTestInfo");
  // 设置的是 cookie 的域名,就是会在哪个域名下生成 cookie 值
  cookie.setDomain("youkeda.com");
  // 是 cookie 的路径,一般就是写到 / ,不会写其他路径的
  cookie.setPath("/");
  // 设置cookie 的最大存活时间,-1 代表随浏览器的有效期,也就是浏览器关闭掉,这个 cookie 就失效了。
  cookie.setMaxAge(-1);
  // 设置是否只能服务器修改,浏览器端不能修改,安全有保障
  cookie.setHttpOnly(false);
  response.addCookie(cookie);

  returnData.put("message", "add cookie successful");
  return returnData;
}

Cookie 的各个属性的作用,注意看代码注释哦

注意,Cookie 类的构造函数,第一个参数是 cookie 名称,第二个参数是 cookie 值。而且其他的属性,需要根据实际情况和具体的业务需求决定。

Spring Session API

上节课我们学习了 Cookie 放在客户端,可以存储用户登录信息,主要用于辨识用户身份。

但如果真的把用户ID登录状态等重要信息放入 cookie,会带来安全隐患,因为网络上很不安全,cookie可能会拦截、甚至伪造。

采用 Session 会话机制可以解决这个问题,用户ID登录状态等重要信息不存放在客户端,而是存放在服务端,从而避免安全隐患。通讯过程如下图所示:

使用会话机制时,Cookie 作为 session id 的载体与客户端通信。上一节课演示代码中,Cookie 中的 JSESSIONID 就是这个作用。

名字为 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 successful");

  return returnData;
}

额外知识点

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

package fm.douban.app.control;

import fm.douban.model.UserLoginInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserControl {
    private static final Logger LOG = LoggerFactory.getLogger(UserControl.class);

    @Value("${loginmock.userName}")
    private String mockedName;

    @Value("${loginmock.password}")
    private String mockedPassword;

    @PostConstruct
    public void init() {
        LOG.info("UserControl 启动啦");
    }

    @GetMapping("/loginmock")
    public Map<String, Object> loginMock(@RequestParam("userName") String userName, @RequestParam("password") String password,
                                         HttpServletRequest request, HttpServletResponse response) {
        Map<String, Object> returnData = new HashMap<>();
        HttpSession session = request.getSession();
        UserLoginInfo userLoginInfo = (UserLoginInfo) session.getAttribute("userLoginInfo");
        if (userLoginInfo!= null && userLoginInfo.getUserName().equals(userName) && password.equals(mockedPassword)) {
            returnData.put("result", "true");
            returnData.put("message", "login sucessful");
        } else {
            returnData.put("result", "false");
            returnData.put("message", "userName or password not correct");
        }
        return returnData;
    }

    @RequestMapping("/status")
    public Map<String, Object> status(HttpServletRequest request, HttpServletResponse response) {
        Map<String, Object> returnData = new HashMap<>();
        HttpSession session = request.getSession(false); // 获取现有会话,如果不存在则返回null
        if (session!= null) {
            UserLoginInfo userLoginInfo = (UserLoginInfo) session.getAttribute("userLoginInfo");
            if (userLoginInfo!= null) {
                returnData.put("isLogin", true);
            } else {
                returnData.put("isLogin", false);
            }
        } else {
            returnData.put("isLogin", false);
        }
        return returnData;
    }
}

Spring Session 配置

上节课讲解了 Session 的操作,在操作中,没有涉及到 cookie。系统会自动把默认的 JSESSIONID 放在默认的 cookie 中。

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

前置知识点:配置

第 6 章我们讲了 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 官方推荐的比较标准的写法:点此阅读官方文档。当前的重点是学习如何使用。

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() 方法的参数中有 HttpServletRequest 和 HttpServletResponse,可以像 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)里。

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

package fm.douban.app.interceptor;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * 用户信息拦截器
 */
public class UserInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserInterceptor.class);

    // Controller方法执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        return true;
    }

    //Controller方法执行之后
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long executeTime = endTime - startTime;

        LOGGER.info("CostTime : " + executeTime + " ms");
    }

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

    }
}


网站公告

今日签到

点亮在社区的每一天
去签到