分布式Session解决方案
1.保存Session,进入商品列表页面
1.保存Session
1.编写工具类
1.MD5Util.java
java
复制代码
package com.sxs.seckill.utils;
import org.apache.commons.codec.digest.DigestUtils;
/**
* Description: MD5加密工具类
*
* @Author sun
* @Create 2024/5/5 14:23
* @Version 1.0
*/
public class MD5Util {
/**
* 将一个字符串转换为MD5
* @param src
* @return
*/
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
// 固定的salt
public static final String SALT = "4tIY5VcX";
// 第一次加密加盐
public static String inputPassToMidPass(String inputPass) {
// 加盐
String str = SALT.charAt(0) + inputPass + SALT.charAt(6);
return md5(str);
}
/**
* 第二次加密加盐
* @param midPass
* @param salt
* @return
*/
public static String midPassToDBPass(String midPass, String salt) {
String str = salt.charAt(0) + midPass + salt.charAt(5);
return md5(str);
}
/**
* 两次加密
* @param input
* @param saltDB
* @return
*/
public static String inputPassToDBPass(String input, String saltDB) {
String midPass = inputPassToMidPass(input);
String dbPass = midPassToDBPass(midPass, saltDB);
return dbPass;
}
}
2.CookieUtil.java
java
复制代码
package com.sxs.seckill.utils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Description: Cookie工具类
*
* @Author sun
* @Create 2024/5/6 16:04
* @Version 1.0
*/
public class CookieUtil {
/**
* 得到 Cookie 的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String
cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到 Cookie 的值, *
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String
cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到 Cookie 的值, *
*
* @param request
* @param cookieName
* @param encodeString
* @return
*/
public static String getCookieValue(HttpServletRequest request, String
cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置 Cookie 的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse
response, String cookieName, String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置 Cookie 的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse
response, String cookieName, String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage,
false);
}
/**
* 设置 Cookie 的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse
response, String cookieName, String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置 Cookie 的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse
response, String cookieName, String cookieValue, int cookieMaxage, boolean
isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage,
isEncode);
}
/**
* 设置 Cookie 的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse
response, String cookieName, String cookieValue, int cookieMaxage, String
encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除 Cookie 带 cookie 域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置 Cookie 的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie 生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue,
int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
// if (null != request) {// 设置域名的 cookie
// String domainName = getDomainName(request);
// if (!"localhost".equals(domainName)) {
// cookie.setDomain(domainName);
// }
// }
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置 Cookie 的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie 生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request,
HttpServletResponse response, String cookieName, String cookieValue,
int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {// 设置域名的 cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到 cookie 的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
// 通过 request 对象获取访问的 url 地址
String serverName = request.getRequestURL().toString();
if ("".equals(serverName)) {
domainName = "";
} else {
// 将 url 地下转换为小写
serverName = serverName.toLowerCase();
// 如果 url 地址是以 http://开头 将 http://截取
if (serverName.startsWith("http://")) {
serverName = serverName.substring(7);
}
int end = serverName.length();
// 判断 url 地址是否包含"/"
if (serverName.contains("/")) {
// 得到第一个"/"出现的位置
end = serverName.indexOf("/");
}
// 截取
serverName = serverName.substring(0, end);
// 根据"."进行分割
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.abc.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." +
domains[len - 1];
} else if (len > 1) {
// abc.com or abc.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
2.关于session和cookie关系的回顾
- 当浏览器请求到服务端时cookie会携带sessionid
- 然后在服务端getSession时会得到当前用户的session
- cookie-sessionid 连接到session
3.修改UserServiceImpl.java的doLogin方法,增加保存信息到session的逻辑
4.测试,用户票据成功保存到cookie中
2.访问到商品列表页面
1.编写GoodsController.java 验证用户登录后进入商品列表页
java
复制代码
package com.sxs.seckill.controller;
import com.sxs.seckill.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
/**
* Description:
*
* @Author sun
* @Create 2024/5/6 18:16
* @Version 1.0
*/
@Controller
@Slf4j
@RequestMapping("/goods")
public class GoodsController {
// 进入到商品首页
@RequestMapping("/toList")
public String toList(HttpSession session, Model model, @CookieValue("userTicket") String ticket) {
// 首先判断是否有票据
if (null == ticket) {
return "login";
}
// 根据票据来获取用户信息
User user = (User) session.getAttribute(ticket);
if (null == user) {
return "login";
}
// 将用户信息存入model中,返回到前端
model.addAttribute("user", user);
return "goodsList";
}
}
2.商品列表页goodsList.html
html
复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品列表</title>
</head>
<body>
<h1>商品列表</h1>
<p th:text="'hi: ' + ${user.nickname}"></p>
</body>
</html>
3.测试登录成功后进入商品列表页
2.分布式session解决方案
1.session绑定/粘滞(不常用)
2.session复制
3.前端存储
4.后端集中存储
3.方案一:SpringSession实现分布式Session
1.安装使用redis-desktop-manager
1.一直下一步,安装到D盘
2.首先要确保redis集群的端口是开放的并使其支持远程访问(之前配置过)
3.使用telnet指令测试某个服务是否能够连接成功
sh
复制代码
telnet 140.143.164.206 7489
4.连接Redis,先测试连接然后确定
5.在redis命令行设置两个键
6.在可视化工具查看
2.项目整合Redis并配置分布式session
1.pom.xml引入依赖
xml
复制代码
<!--spring data redis 依赖, 即 spring 整合 redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.5</version>
</dependency>
<!--pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<!--实现分布式 session, 即将 Session 保存到指定的 Redis-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2.application.yml配置Redis
yaml
复制代码
spring:
redis:
password: # Redis服务器密码
database: 0 # 默认数据库为0号
timeout: 10000ms # 连接超时时间是10000毫秒
lettuce:
pool:
max-active: 8 # 最大活跃连接数,使用负值表示没有限制,最佳配置为核数*2
max-wait: 10000ms # 最大等待时间,单位为毫秒,使用负值表示没有限制,这里设置为10秒
max-idle: 200 # 最大空闲连接数
min-idle: 5 # 最小空闲连接数
cluster:
nodes:
-
-
3.启动测试
1.登录
2.Redis可视化工具发现session成功存到redis
4.方案二:统一存放用户信息到Redis
1.修改pom.xml,去掉分布式springsession的依赖
2.将用户信息放到Redis
1.添加Redis配置类 com/sxs/seckill/config/RedisConfig.java
java
复制代码
package com.sxs.seckill.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Description:
*
* @Author sun
* @Create 2024/4/29 21:29
* @Version 1.0
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template =
new RedisTemplate<>();
System.out.println("template=>" + template);
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
// key 序列化方式
template.setKeySerializer(redisSerializer);
// value 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap 序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间 600 秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
2.修改 com/sxs/seckill/service/impl/UserServiceImpl.java
1.注入RedisTemplate
2.修改doLogin方法,将用户信息放到Redis中
3.启动测试
1.登录
2.可视化工具查看用户信息
3.实现使用Redis + Cookie实现登录,可以访问商品列表页面
1.刚才已经实现了Redis记录信息的功能,但是校验还没实现,修改GoodsController.java完成校验
1.注入RedisTemplate
2.从Redis中获取校验信息,进行校验
2.测试
1.登录成功后访问商品列表页面
5.扩展:自定义参数解析器,直接获取User
1.修改 com/sxs/seckill/controller/GoodsController.java 使参数直接为User
java
复制代码
// 进入到商品首页
@RequestMapping("/toList")
public String toList(Model model, User user) {
// 判断是否有用户信息
if (null == user) {
return "login";
}
// 将用户信息存入model中,返回到前端
model.addAttribute("user", user);
return "goodsList";
}
2.service层添加方法,通过票据从Redis中获取User对象
1.UserService.java
java
复制代码
/**
* 根据cookie获取用户
* @param userTicket
* @param request
* @param response
* @return
*/
public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response);
2.UserServiceImpl.java
- 这里需要注意,每次获取完User,需要重新设置Cookie,来刷新Cookie的时间
- 原因是,调用这个的目的是为了校验,而用户访问每个页面都要进行校验,如果每次校验之后都不刷新Cookie的时间,一旦Cookie失效了,用户就要重新登陆
java
复制代码
@Override
public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {
// 判断是否有票据
if (null == userTicket) {
return null;
}
// 根据票据来获取用户信息,从redis中获取
User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
if (null == user) {
return null;
}
// 重新设置cookie的有效时间
CookieUtil.setCookie(request, response, "userTicket", userTicket);
return user;
}
3.编写自定义参数解析器对User类型参数进行解析 config/UserArgumentResolver.java
java
复制代码
package com.sxs.seckill.config;
import com.sxs.seckill.pojo.User;
import com.sxs.seckill.service.UserService;
import com.sxs.seckill.utils.CookieUtil;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Description: 自定义参数解析器
*
* @Author sun
* @Create 2024/5/7 14:39
* @Version 1.0
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Resource
private UserService userService;
/*
* 判断是否支持要转换的参数类型,简单来说,就是在这里设置要解析的参数类型
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
// 如果参数类型是User,则进行解析
Class<?> parameterType = methodParameter.getParameterType();
if (parameterType == User.class) {
return true;
}
return false;
}
/**
* 编写参数解析的逻辑
*
* @param methodParameter
* @param modelAndViewContainer
* @param nativeWebRequest
* @param webDataBinderFactory
* @return
* @throws Exception
*/
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
// 首先获取request和response
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
// 从cookie中获取票据
String userTicket = CookieUtil.getCookieValue(request, "userTicket");
// 如果票据为空,则返回null
if (null == userTicket) {
return null;
}
// 如果票据不为空,则根据票据获取用户信息
User user = this.userService.getUserByCookie(userTicket, request, response);
return user;
}
}
4.编写config/WebConfig.java 将自定义参数解析器放到 resolvers 才能生效
java
复制代码
package com.sxs.seckill.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.List;
/**
* Description:
*
* @Author sun
* @Create 2024/5/7 14:53
* @Version 1.0
*/
@Configuration
// @EnableWebMvc 使用这个注解会导致SpringBoot的自动配置失效,一切都要自己配置,所以建议不要使用这个注解
public class WebConfig implements WebMvcConfigurer {
// 注入自定义参数解析器
@Resource
private UserArgumentResolver userArgumentResolver;
/**
* 静态资源加载,静态资源放在哪里就怎么配置
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
/**
* 添加自定义参数解析器到resolvers中,才能生效
* @param resolvers
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}
5.测试
1.在登录之后,可以正常访问商品列表页面