二八佳人体似酥,腰间仗剑斩愚夫。虽然不见人头落,暗里教君骨髓枯。 愿世人皆有所爱,皆可以爱。
上一章简单介绍了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("${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
- 首先要在 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="static/bootstrap/css/bootstrap.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="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="javascript:void(0);" onclick="window.location.href='User/logout'">退出</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的基本用法。
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/SpringBoot_Session
谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!