Cookie
Cookie
是网络编程中使用最广泛的一项技术,主要用于辨识用户身份。
客户端(浏览器)与网站服务端通讯的过程如下图所示:
从图中看,服务端既要返回 Cookie
给客户端,也要读取客户端提交的 Cookie
。所以本节课主要学习服务端 Spring
工程是如何使用 Cookie
的,有读、写两种操作。
浏览器如何使用
Cookie
,在《Java 网络编程》课程中讲解。
读 Cookie
为 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
的名字,就可以通过注解的方式读取,不需要再遍历 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,使用此注解就会报错。必须谨慎使用。
写 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;@PostConstructpublic 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); // 获取现有会话,如果不存在则返回nullif (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 {@Beanpublic 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 {@Beanpublic 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;}// 当前存在内存中@Beanpublic MapSessionRepository sessionRepository() {return new MapSessionRepository(new ConcurrentHashMap<>());}
}
想必大家已经了解了 Cookie
各属性值的作用,这里就不赘述了。
这段代码有些长,是 Spring 官方推荐的比较标准的写法:点此阅读官方文档。当前的重点是学习如何使用。
Spring Request 拦截器
在上节课的练习中我们模拟了用户登录以及提供了查询登录状态的方法。
在实际的项目中,会有大量的页面功能是需要判断用户是否登录的。例如电商的网站,订单、购物车、管理收货地址等等,都需要登录。那么让每一个页面都判断是否登录、未登录跳转到登录页,就太繁琐了,也不利于维护。
所以需要一种统一处理相同逻辑的机制,Spring
提供了 HandlerInterceptor
(拦截器)满足这种场景的需求。
实现拦截器也不同负责,有三个步骤:
一、创建拦截器
拦截器必须实现 HandlerInterceptor
接口。可以在三个点进行拦截:
- Controller方法执行之前。这是最常用的拦截点。例如是否登录的验证就要在
preHandle()
方法中处理。 - Controller方法执行之后。例如记录日志、统计方法执行时间等,就要在
postHandle()
方法中处理。 - 整个请求完成后。不常用的拦截点。例如统计整个请求的执行时间的时候用,在
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方法执行之前@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 只有返回true才会继续向下执行,返回false取消当前请求return true;}//Controller方法执行之后@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {}// 整个请求完成后(包括Thymeleaf渲染完毕)@Overridepublic 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 {@Overridepublic 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方法执行之前@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {long startTime = System.currentTimeMillis();request.setAttribute("startTime", startTime);return true;}//Controller方法执行之后@Overridepublic 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渲染完毕)@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}