上一章简单介绍了SpringBoot整合Ehcache缓存(二十二),如果没有看过,请观看上一章
关于 Spring-Session 共享,参考了 阿飞云 大牛的文章:
Spring-Session实现Session共享入门教程
为了便于用户查看,将其中的理论部分摘抄下来.
一. 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("") 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
首先要在 pom.xml 添加 fastjson的依赖
<!--解决spring-session处理缓存时乱码的问题--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.69</version> </dependency>
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; } }
三.五 配置静态资源,添加过滤器
以前的 MvcConfig 静态资源配置
@Configuration public class MvcConfig extends WebMvcConfigurerAdapter { /** * 配置静态的资源信息 * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); //映射 static 目录 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); //放置其他 业务页面资源 registry.addResourceHandler("/**").addResourceLocations("classpath:/templates/"); } @Override public void addInterceptors(InterceptorRegistry registry) { //注册自己的拦截器并设置拦截的请求路径 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**"); super.addInterceptors(registry); } }
三.六 配置静态资源
三.六.一 static 目录下添加库
三.六.二 templates 目录下 添加静态页面
login.html 是登录页, main.html 是登录后的展示主页 noPrivilege 是没有权限展示的页面
add delete select update 是 添加,删除,查询,修改的页面,表示 员工的权限。
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录页面</title> <link rel="StyleSheet" href="https://www.ctyun.cn/portal/link.html?target=static%2Fbootstrap%2Fcss%2Fbootstrap.css" type="text/css"> <script type="text/javascript" src="static/jquery-1.11.3.min.js"></script> <script type="text/javascript" src="static/bootstrap/js/bootstrap.js"></script> </head> <script> $(function(){ $("#submit").click(function(){ var code=$("#code").val(); var password=$("#password").val(); var info=new Object(); //传入进去,员工的id编号 info.account=code; info.password=password; $.post("User/login",info,function(data){ if(data.code==200){ alert("登录成功"); window.location.href="https://www.ctyun.cn/portal/link.html?target=toMain"; }else{ alert("用户名或者密码错误"); } },"json") }) }) </script> <body> <div class="col-sm-6 col-sm-offset-3"> <div style="margin-top:40px;"> <div class="row col-md-offset-3 "> <h3>登录页面</h3> </div> <div class="row" style="margin-top:30px;"> <form class="form-horizontal" role="form"> <div class="form-group"> <label for="code" class="col-md-3 control-label">用户名:</label> <div class="col-md-4"> <input type="text" class="form-control" id="code" name="code" value=""/> </div> </div> <div class="form-group"> <label for="password" class="col-md-3 control-label">密码:</label> <div class="col-md-4"> <input type="password" class="form-control" id="password" name="password"/> </div> </div> <div class="form-group"> <div class="col-sm-offset-4"> <input type="button" value="登录" id="submit" class="btn btn-success"/> </div> </div> </form> </div> </div> </div> </body> </html>
main.html
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录成功,展示首页</title> </head> <body> 登录成功 <span> <a href="https://www.ctyun.cn/portal/link.html?target=javascript%3Avoid%280%29%3B" onclick="window.location.href='https://www.ctyun.cn/portal/link.html?target=User%2Flogout'">退出</a> </span> </body> </html>
其他的页面,都是一句话描述
<body> 抱歉,您没有权限访问!!! </body>
<body> 添加用户 </body>
<body> 删除用户 </body>
<body> 查询用户 </body>
<body> 修改用户 </body>
三.七 页面跳转 Controller PageController
@Controller public class PageController { @RequestMapping("/toLogin") //跳转到登录页面 public String toLogin(){ return "login"; } @RequestMapping("/toMain") //跳转到登录页面 public String toMain(){ return "main"; } @RequestMapping("/add") //跳转到登录页面 public String add(){ return "add"; } @RequestMapping("/update") //跳转到登录页面 public String update(){ return "update"; } @RequestMapping("/select") //跳转到登录页面 public String select(){ return "select"; } @RequestMapping("/delete") //跳转到登录页面 public String delete(){ return "delete"; } }
三.八 员工的登录和注销
UserController.java
@Controller @RequestMapping("/User") public class UserController { @Autowired private UserService userService; @RequestMapping("/login") @ResponseBody public OutputResult login(User userInfo, HttpSession session){ //将请求信息,封装到对象里面。 Map<String,Object> map=new HashMap<String,Object>(); //从数据库中查询密码 User user=userService.findByAccountAndPassword(userInfo.getAccount(), userInfo.getPassword()); if(null==user){ return OutputResult.fail(); } //说明登录成功,放置到session里面 session.setAttribute("loginUser",user); //模拟设置权限 List<String> privileges=getPrivilegeByAccount(user.getAccount()); session.setAttribute("privilegeList_"+user.getId(),privileges); //登录成功 return OutputResult.success(user); } /** * 模拟权限 yzl 有 添加的 权限, zxh有 delete的权限。 都没有 update 的权限 * @date 2021/10/21 17:39 * @author zk_yjl * @param account * @return java.util.List<java.lang.String> */ private List<String> getPrivilegeByAccount(String account) { List<String> privileges = new ArrayList<>(); privileges.add("/select"); privileges.add("/toMain"); privileges.add("/logout"); if("yzl".equals(account)){ privileges.add("/add"); }else{ privileges.add("/delete"); } return privileges; } @RequestMapping("/logout") //退出登录 public String logout(HttpSession session){ //注销 session.invalidate(); return "login"; }
其中 OutputResult 与以前是一样的.
/** * @ClassName:OutputResult * @Description 返回的响应实体信息 * @Author 岳建立 * @Date 2021/1/1 10:09 * @Version 1.0 **/ @Data public class OutputResult implements Serializable { /** * @param code 响应代码 * @param message 响应信息 * @param data 响应的数据 */ private Integer code; private String message; private Map<String,Object> data=new HashMap<String,Object>(); /** * 构造方法 私有。 避免外部构造 */ private OutputResult(){ } /** * 成功 * @return */ public static OutputResult fail(){ OutputResult outputResult=new OutputResult(); outputResult.code=500; outputResult.message="失败"; return outputResult; } /** * 成功 * @return */ public static OutputResult success(){ OutputResult outputResult=new OutputResult(); outputResult.code=200; outputResult.message="成功"; return outputResult; } /** * 成功 * @param data 要响应的数据 * @return */ public static OutputResult success(Object data){ OutputResult outputResult=new OutputResult(); outputResult.code=200; outputResult.message="成功"; outputResult.data.put("result",data); return outputResult; } }
三.九 模拟验证
将项目进行打包, 然后启动项目.
启动 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 项目上 直接 请求 /select 进行查询, 会跳转到登录的页面
输入正确的用户名和密码, yzl/12345 会登录进去
可以进行查询
但是如果是修改和删除的话
是没有权限的
直接访问 8082 项目,查询 select 用户, 是可以直接访问的
如果在 8081 项目上 退出登录
那么在 8081项目上会退出,
同时8082 项目上,也将无法访问
这是整合 Spring-Session的基本用法。