场景应用:利用Redis实现分布式Session
文章目录
原理:Redis实现分布式Session
web开发session
在web开发中,我们会把用户的登录信息存储在session里。而session是依赖于cookie的,即服务器创建session时会给它分配一个唯一的ID,并且在响应时创建一个cookie用于存储这个SESSIONID。当客户端收到这个cookie之后,就会自动保存这个SESSIONID,并且在下次访问时自动携带这个SESSIONID,届时服务器就可以通过这个SESSIONID得到与之对应的session,从而识别用户的身。
如下图:
分布式session同步问题
现在的互联网应用,基本都是采用分布式部署方式,即将应用程序部署在多台服务器上,并通过nginx做统一的请求分发。而服务器与服务器之间是隔离的,它们的session是不共享的,这就存在session同步的问题了,如下图:
如果客户端第一次访问服务器,请求被分发到了服务器A上,则服务器A会为该客户端创建session。如果客户端再次访问服务器,请求被分发到服务器B上,则由于服务器B中没有这个session,所以用户的身份无法得到验证,从而产生了不一致的问题。
分布式session解决方案
解决这个问题的办法有很多,比如可以协调多个服务器,让他们的session保持同步。也可以在分发请求时做绑定处理,即将某一个IP固定分配给同一个服务器。但这些方式都比较麻烦,而且性能上也有一定的消耗。
更合理的方式就是采用类似于Redis这样的高性能缓存服务器,来实现分布式session。
从上面的叙述可知,我们使用session保存用户的身份信息,本质上是要做两件事情。第一是保存用户的身份信息,第二是验证用户的身份信息。如果利用其它手段实现这两个目标,那么就可以不用session,或者说我们使用的是广义上的session了。
具体实现的思路如下图,我们在服务端增加两段程序:
第一是创建令牌的程序,就是在用户初次访问服务器时,给它创建一个唯一的身份标识,并且使用cookie封装这个标识再发送给客户端。那么当客户端下次再访问服务器时,就会自动携带这个身份标识了,这和SESSIONID的道理是一样的,只是改由我们自己来实现了。另外,在返回令牌之前,我们需要将它存储起来,以便于后续的验证。而这个令牌是不能保存在服务器本地的,因为其他服务器无法访问它。因此,我们可以将其存储在服务器之外的一个地方,那么Redis便是一个理想的场所。
第二是验证令牌的程序,就是在用户再次访问服务器时,我们获取到了它之前的身份标识,那么我们就要验证一下这个标识是否存在了。验证的过程很简单,我们从Redis中尝试获取一下就可以知道结果。
实战:Redis实现分布式Session
技术栈:Spring Session
Spring Session是Spring的项目之一,Spring Session把servlet容器实现的httpSession替换为spring-session,专注于解决session管理问题。
Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。
spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。
Spring Session支持功能
- 轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。这样可以独立于应用服务器的方式提供高质量的集群。
- 同一个浏览器同一个网站,支持多个session问题。 从而能够很容易地构建更加丰富的终端用户体验。
- Restful API,不依赖于cookie。可通过header来传递jessionID 。控制session id如何在客户端和服务器之间进行交换,这样的话就能很容易地编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie
- WebSocket和spring-session结合,同步生命周期管理。当用户使用WebSocket发送请求的时候
Spring Session实战
项目目录如下:
步骤1:依赖包
首先要添加依赖包:因为是web应用。我们加入springboot的常用依赖包web,加入SpringSession、redis的依赖包,移支持把session存储在redis。
这里因为是把seesion存储在redis,这样每个服务登录都是去查看redis中数据进行验证的,所有是分布式的。 这里要引入spring-session-data-redis和spring-boot-starter-redis
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>redis-session-project</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.3.11.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> </project>
步骤2:启动类与配置文件
编写启动类:
package com.yyl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RedisSessionApplication { public static void main(String[] args) { SpringApplication.run(RedisSessionApplication.class, args); } }
添加配置文件:
spring.application.name=spring-boot-redis server.port=9090 # 设置session的存储方式,采用redis存储 spring.session.store-type=redis # session有效时长为15分钟 server.servlet.session.timeout=PT15M ## Redis 配置 ## Redis数据库索引 spring.redis.database=1 ## Redis服务器地址 spring.redis.host=127.0.0.1 ## Redis服务器连接端口 spring.redis.port=6379 ## Redis服务器连接密码(默认为空) spring.redis.password=
步骤3:实现逻辑
创建测试实体类User
模拟初始化用户数据,用map创建,这里主要测试redis就不连数据库了
@Slf4j @RestController @RequestMapping(value = "/user") public class UserController { Map<String, User> userMap = new HashMap<>(); public UserController() { //初始化1个用户,用于模拟登录 User u1=new User(1,"user1","user1"); userMap.put("user1",u1); } }
登录:登录接口,根据用户名和密码登录,这里进行验证,如果验证登录成功,使用 session.setAttribute(session.getId(), user);把相关信息放到session中。
@GetMapping(value = "/login") public String login(String username, String password, HttpSession session) { //模拟数据库的查找 User user = this.userMap.get(username); if (user != null) { if (!password.equals(user.getPassword())) { return "用户名或密码错误!!!"; } else { session.setAttribute(session.getId(), user); log.info("登录成功{}",user); } } else { return "用户名或密码错误!!!"; } return "登录成功!!!"; }
查找用户:模拟通过用户名查找用户
/** * 通过用户名查找用户 */ @GetMapping(value = "/find/{username}") public User find(@PathVariable String username) { User user=this.userMap.get(username); log.info("通过用户名={},查找出用户{}",username,user); return user; }
获取session
/** * 拿当前用户的session */ @GetMapping(value = "/session") public String session(HttpSession session) { log.info("当前用户的session={}",session.getId()); return session.getId(); }
退出登录
/** * 退出登录 */ @GetMapping(value = "/logout") public String logout(HttpSession session) { log.info("退出登录session={}",session.getId()); session.removeAttribute(session.getId()); return "成功退出!!"; }
步骤4:编写session拦截器
session拦截器的作用:验证当前用户发来的请求是否有携带sessionid,如果没有携带,提示用户重新登录。
package com.yyl.interceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @Slf4j @Configuration public class SecurityInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 验证当前session是否存在,存在返回true true代表能正常处理业务逻辑 if (session.getAttribute(session.getId()) != null) { log.info("session拦截器,session={},验证通过", session.getId()); return true; } // session不存在,返回false,并提示请重新登录。 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.getWriter().write("请登录!!!!!"); log.info("session拦截器,session={},验证失败", session.getId()); return false; } }
步骤5:把拦截器注入到拦截器链中
package com.yyl.config; import com.yyl.interceptor.SecurityInterceptor; 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 SessionCofig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityInterceptor()) //排除拦截的2个路径 .excludePathPatterns("/user/login") .excludePathPatterns("/user/logout") //拦截所有URL路径 .addPathPatterns("/**"); } }
步骤6:测试
登录user1用户:http://127.0.0.1:9090/user/login?username=user1&password=user1
查询user1用户session:http://127.0.0.1:9090/user/session
退出登录: http://127.0.0.1:9090/user/logout
在本地浏览器我们也能找到存的session信息
测试源码
https://download.csdn.net/download/weixin_45525272/86501938