一. Spring-Session介绍
一.一 Spring-Session使用的场景
HttpSession是通过Servlet容器进行创建和管理的,在单机环境中。通过Http请求创建的Session信息是存储在Web服务器内存中,如Tomcat/Jetty。
假如当用户通过浏览器访问应用服务器,session信息中保存了用户的登录信息,并且session信息没有过期失,效那么用户就一直处于登录状态,可以做一些登录状态的业务操作
但是现在很多的服务器都采用分布式集群的方式进行部署,一个Web应用,可能部署在几台不同的服务器上,通过LVS或者Nginx等进行负载均衡(一般使用Nginx+Tomcat实现负载均衡)。此时来自同一用户的Http请求将有可能被分发到不同的web站点中去(如:第一次分配到A站点,第二次可能分配到B站点)。那么问题就来了,如何保证不同的web站点能够共享同一份session数据呢?假如用户在发起第一次请求时候访问了A站点,并在A站点的session中保存了登录信息,当用户第二次发起请求,通过负载均衡请求分配到B站点了,那么此时B站点能否获取用户保存的登录的信息呢?答案是不能的,因为上面说明,Session是存储在对应Web服务器的内存的,不能进行共享,此时Spring-session就出现了,来帮我们解决这个session共享的问题!
一.二 如何进行Session共享
简单点说就是请求http请求经过Filter职责链,根据配置信息过滤器将创建session的权利
由tomcat交给了Spring-session中的SessionRepository,通过Spring-session创建会话,
并保存到对应的地方。
实际上实现Session共享的方案很多,其中一种常用的就是使用Tomcat、Jetty等服务器提供的Session共享功能,将Session的内容统一存储在一个数据库(如MySQL)或缓存(如Redis,Mongo)中
二. SpringBoot 整合 Spring-Session 使用
按照上一章节的方式 创建对应的项目 SpringBoot_Session
二.一 pom.xml 添加依赖
<!--依赖 data-redis的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--不能忘记这个依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--添加cache的依赖信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--添加 session的依赖--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
二.二 application.yml 配置redis和session
采用多环境配置
server: servlet: context-path: /Session # 引入 数据库的相关配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true username: root password: abc123 # 配置thymeleaf的相关信息 thymeleaf: # 开启视图解析 enabled: true #编码格式 encoding: UTF-8 #前缀配置 prefix: classpath:/templates/ # 后缀配置 suffix: .html #是否使用缓存 开发环境时不设置缓存 cache: false # 格式为 HTML 格式 mode: HTML5 # 配置类型 servlet: content-type: text/html # 配置Redis的使用 redis: database: 15 # 所使用的数据库 默认是0 host: 127.0.0.1 #所使用的redis的主机地址 port: 6379 # 端口号 默认是 6379 password: zk123 # 密码 timeout: 5000 # 超时时间 5000毫秒 # 连接池 lettuce 的配置 lettuce: pool: max-active: 100 min-idle: 10 max-wait: 100000 profiles: active: 8081 # 默认启用的端口号是 8081 # 配置session的相关信息 session: store-type: redis # 配置存储的类型 timeout: 3600 # 配置过期时间 redis: flush-mode: on_save # 保存时刷新 namespace: springSession # 命令空间 #整合mybatis时使用的 mybatis: #包别名 type-aliases-package: top.yueshushu.learn.pojo #映射文件路径 mapper-locations: classpath:mybatis/mapper/**/*.xml configuration: #日志信息 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 多环境配置,默认为 8081 --- server: port: 8081 spring: profiles: 8081 --- server: port: 8082 spring: profiles: 8082
二.三 启动类 SessionApplication
@MapperScan("top.yueshushu.learn.mapper") @SpringBootApplication //开启缓存 @EnableCaching public class SessionApplication { public static void main(String[] args) { SpringApplication.run(SessionApplication.class,args); System.out.println("共享 Session"); } }
不要忘记 CacheConfig 和 RedisConfig 的配置信息
二.四 SessionController 创建和获取Session
创建一个Controller 类, 用于创建和获取对应的Session 信息
@RestController public class SessionController { @Value("${server.port}") private String port; @RequestMapping("/createSession") public String createSession(HttpSession httpSession){ String sessionId=httpSession.getId(); httpSession.setAttribute("name",port+",两个蝴蝶飞"+sessionId); httpSession.setAttribute("sname",port+":abc"); return sessionId+"创建端口号是:"+port+"的应用创建Session,属性是:"+httpSession.getAttribute("name").toString(); } @RequestMapping("/getSession") public String getSession(HttpSession httpSession){ return "访问端口号是:"+port+",获取Session属性是:"+httpSession.getAttribute("name").toString(); } }
二.五 测试Session共享
将项目进行打包, 然后启动项目.
启动 8081 端口的项目
java -jar learn-1.0-SNAPSHOT.jar --spring.profiles.active=8081
启动8082的端口的项目
java -jar learn-1.0-SNAPSHOT.jar --spring.profiles.active=8082
8081 项目 创建Session
8082 项目 试图获取Session (不是同一个项目,按照以前的逻辑是获取不到的。 但是添加了 Spring-session 是可以获取到的)
可以获取到 Session
查看Redis 的存储信息
会存储相关的信息, 但是值是乱码.
可以通过 fastjson 进行序列化,解决这个问题。
在config 包下,添加关于 Spring-Session的配置信息, RedisSessionConfig
1.首先要在 pom.xml 添加 fastjson的依赖
<!--解决spring-session处理缓存时乱码的问题--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.69</version> </dependency>
2.RedisSessionConfig.java 中进行配置序列化方式
@Configuration @EnableRedisHttpSession public class RedisSessionConfig { @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { // 使用 FastJsonRedisSerializer 来序列化和反序列化redis 的 value的值 FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class); ParserConfig.getGlobalInstance().addAccept("com.muzz"); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setCharset(StandardCharsets.UTF_8); serializer.setFastJsonConfig(fastJsonConfig); return serializer; } }
删除以前的Redis数据,重新访问 createSession , 生成新的Session
解决乱码问题。
但是,不建议这么做. 老蝴蝶只是演示一下,项目里面,不用这一个
三. SpringSession 的详细使用
关于访问权限的问题,可以看老蝴蝶以前写的文章: RBAC
关于 静态资源的问题, 可以看老蝴蝶以前写的文章: SpringBoot静态资源整合Bootstrap(十)
三.一 pom.xml 添加依赖
<!--依赖 data-redis的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--不能忘记这个依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--添加cache的依赖信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--添加 session的依赖--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!--解决spring-session处理缓存时乱码的问题--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.69</version> </dependency> <!--引入 spring-boot-starter-thymeleaf的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--添加一个webjar jquery--> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency> <!--引入bootstrap--> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.4.1</version> </dependency>
三.二 数据库信息
在 springboot 数据库里面 添加 user 表, 创建两个用户
/*!40101 SET NAMES utf8 */; -- 创建员工 user 表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(15) DEFAULT NULL, `sex` varchar(20) DEFAULT NULL, `age` int(6) DEFAULT NULL, `description` varchar(50) DEFAULT NULL, `account` varchar(100) DEFAULT NULL, `password` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `user_un_account` (`account`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 插入员工信息数据 insert into user(name,sex,age,description,account,password) values ('两个蝴蝶飞','男',26,'一个快乐的程序员','yzl','123456'), ('周小欢','女',22,'一个小坏蛋','zxh','123456');
三.三 其他的相关信息
application.yml 配置信息不需要改变, 启动类不需要改变,
创建User 类和 对应的 mapper, service 等基础的服务( 与Mybatis一样)
提供一个 根据员工的 账号 account 和 密码 password 的查询服务
UserServiceImpl.java
@Override public User findByAccountAndPassword(String name, String password) { return userMapper.findByAccountAndPassword(name,password); }
UserMapper.xml
<select id="findByAccountAndPassword" resultType="top.yueshushu.learn.pojo.User"> select * from user where account=#{account} and password=#{password} </select>
三.四 添加登录和权限拦截的过滤器 LoginInterceptor
放置在 interceptor 包下.
LoginInterceptor.java类:
package top.yueshushu.learn.interceptor; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import top.yueshushu.learn.pojo.User; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.ArrayList; import java.util.List; /** * * @author 两个蝴蝶飞 * * 登录和授权拦截器 */ public class LoginInterceptor implements HandlerInterceptor { //不需要登录验证的url private static List<String> noLoginValidateUrl; //不需要权限验证的url private static List<String> noPriValidateUrl; //跳转到的登录页面 private static String LOGIN_URL; //没有权限的界面 private static String NO_PRIVILEGE_URL; static{ noLoginValidateUrl=new ArrayList<String>(); //静态资源 noLoginValidateUrl.add("/static/"); noLoginValidateUrl.add("/webjars/"); noLoginValidateUrl.add("/templates/"); noLoginValidateUrl.add("/login.html"); noLoginValidateUrl.add("/noPrivilege.html"); //登录页面 noLoginValidateUrl.add("/toLogin"); //登录方法 noLoginValidateUrl.add("/login"); noPriValidateUrl=new ArrayList<String>(); LOGIN_URL="/login.html"; NO_PRIVILEGE_URL="/noPrivilege.html"; } @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { // TODO 自动生成的方法存根 } @Override public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception { // TODO 自动生成的方法存根 } @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object arg2) throws Exception { //获取Session HttpSession session=req.getSession(); //请求路径 String realPath=req.getRequestURI(); System.out.println("地址是:"+realPath); //验证是否在 不需要验证登录的url里面 if(isContain(realPath,1)){ return true; } //如果为空,表示没有登录 if(session.getAttribute("loginUser")==null){ req.getRequestDispatcher(LOGIN_URL).forward(req,resp); return false; }else{ //最后一个 //为权限 String privilegeUrl=realPath.substring(realPath.lastIndexOf("/")); //如果不为空,表示登录了。 //重新获取全部权限 , 需要缓存, 这儿不用缓存。 User user=(User)session.getAttribute("loginUser"); List<String> privileges=(List<String>)session.getAttribute("privilegeList_"+user.getId()); boolean isHavePri=true; if(CollectionUtils.isEmpty(privileges)||!privileges.contains(privilegeUrl)){ isHavePri=false; } if(isHavePri){ //放行 return true; }else{ req.getRequestDispatcher(NO_PRIVILEGE_URL).forward(req,resp); return false; } } } private boolean isContain(String realPath,int type){ List<String> urls; if(type==1){ urls=noLoginValidateUrl; }else{ urls=noPriValidateUrl; } boolean flag=false; for(String url:urls){ //包括,返回-1 if(realPath.indexOf(url)!=-1){ flag=true; break; } } return flag; } }