分布式Session

8 篇文章 0 订阅
5 篇文章 0 订阅
本文探讨了Web开发中Session与Cookie的区别,传统Session的管理问题以及解决方案,如Spring-Session、Token和JWT的实现。内容涉及Session的内存管理、分布式Session的挑战、Redis在Session和Token管理中的应用,以及JWT的加密解密特性。总结了各种方案的优缺点,并提出了实际场景下的选择建议。
摘要由CSDN通过智能技术生成

一 . Session与Cookie区别

1.1 Cookie

简单介绍:

Cookie(小甜饼)是浏览器保存在本地的文本内容,常常搭配Session来保持用户登陆态

特性(不只一点):

  • 只能携带同一域名下或子域名(可通过修改domain)的cookie到服务端
localhost与127.0.0.1虽然意思相同但是它们却属于跨域访问:无法携带cookie到另一域下
1.2 Session

简单介绍:

Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。

特性:

  • 将数据存储在服务器端且安全性较高
  • Session的数据存储在Tomcat服务器的内存中,具有时效性

二 . 传统Session方式

1.1 传统session

场景: nginx做负载均衡,同一个应用部署到了两台服务器,实现登陆

当用户请求登陆接口时,通过nginx里配置的不同的策略将请求打到某一台服务器上。

问题 1: session存储在内存中到底是由服务器管理还是tomcat管理呢?

明确一点:session存储在服务器的内存中,是tomcat来管理的。

我们知道内存中的数据在重启的时候将丢失,如果有服务器管理session,想要session丢失是不是要重启服务器呢?事实上,在实践中我们发现想要让session丢失是重启tomcat

问题 2: 当用户请求登陆后由nginx负载到了服务器1并确认登陆信息,tomcat生成session保存在服务器上,当该用户再次登陆的时候,登陆请求有可能打到服务器2,但是 服务器2并没有该用户的session信息,因此用户就需要再次登陆,降低了用户的体验

解决方案1:

有人提出当同一个用户请求时,通过nginx的一些配置(IP HASH)将请求打到同一台服务器上,这样就不会造成上述问题,如果采用该种方案将会导致架构不是高可用的,如果此时服务器1挂掉了,那么是不是此用户就没办法访问该服务了呢?

解决方案2:

通过加入一台Redis服务器,当用户请求时,每台服务器都去同一个Redis里查询该用户是否已经登陆过了

1.2 传统Session实践1.0

用户第一次登陆的时候,将用户名和密码传到后端,后端进行校验成功后,再将该用户信息保存在session中,并且tomcat会生成一个sessionId,在响应头部加入set-cookies:sessionId,返回给浏览器,浏览器将sessionid保存在本地,以后该用户在同一域名下请求该服务其它接口时都会携带着该cookie(sessionid),后端收到sessionid,在内存中查找该session,然后在后台代码进行校验或返回信息
在这里插入图片描述

1.3 传统session实践2.0

问题1: 验证cookie的不允许跨域携带cookie的特性?
验证:

  • 创建项目的过程省略
  • Controller层代码:
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/login")
    public String login(@RequestParam String userName, @RequestParam String password, HttpSession session) {

        session.setAttribute("login_user", userName);
        return "登陆成功";
    }

    @GetMapping("/info")
    public String info(HttpSession session) {
        return "当前登陆的是:" + session.getAttribute("login_user");
    }
}
  • 启动项目
  • 访问
1.首先在浏览器中一localhos:8081/user/login?userName=ray&password=123来登录
2.然后我们来获取信息:localhost:8081/user/info       浏览器收到信息:当前登陆的是ray
3.但如果我们在同一浏览器中跨域来访问接口:以127.0.0.1:8081/user/info   浏览器收到信息:当前登陆的是null

结论:两种结果很明显验证了在从127.0.0.1:8081/user/info来访问时,事实上是没有将cookie传到服务				器,以此证明了cookie的特性

那么如果我们就是想跨域来访问这个用户的信息呢?
解决方式:将从浏览器的控制台中获取出localhost域名下的sessionId将其复制粘贴到127.0.0.1域名下随请求一并发送到服务端

**问题2:**验证session是tomcat管理而不是服务器管理的?

  • 创建项目省略
  • 上一个实践的同一套代码启动两个服务8081&8082端口
1. 首先我们以localhost:8081/user/login?userName=ray&password=123来登陆
2. 获取到sessionId并将其复制到localhost:8082/user/info的请求里,结果发现浏览器展示的信息		 	是:当前登陆的是null
3. 问题1看见我们已经将sessionId复制了,应该可以成功获取到信息了	啊,但是并没有获取到信息,可见session肯定不是服务器来管理了,如果是那么同一台服务器获取到sessionId就应该返回信息,因此session不是服务器管理的
4. 总结:springboot应用内嵌了tomcat,由于用户是在8081服务登陆的session是由8081的tomcat管理的,而8082服务中没有session信息,所	以就没办法查到

三 . Spring-Session

1.1 简述

旨在解决分布式session问题,详情参看Spring-session官网

1.2 实践SpringBoot+docker+redis

步骤:

  • 服务器用docker安装redis
docker run -d -p 6379:6379 redis:6.0
  • 创建springboot项目,在pom.xml中引入依赖
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
 </dependency>
  • application.yml写配置
spring:
  redis:
    port: 6379
    host: 47.97.214.211
  session:
    store-type: redis
  • controller 层代码依然是上文所提到的
  • 重启服务
访问login接口,打开浏览器控制台发现除了正常的cookie,session机制中有的JSESSION之外还有SESSION,并且我们通过连接服务器redis查看0号数据库中保存了spring:session键值,过期时间等
  • 再次重启服务器,不登陆直接访问info接口,发现我们仍然可以获得信息
我们启动两个不同端口的相同服务,依然会发现不再出现传统session的问题,即用户明明登陆了,确还需要再次登陆的问题
  • 参考源码:SessionRepositoryRequestWrapper类:原因在于Spring-session再http请求时加了个拦截器来寻找redis中spring-session,如果存在则证明该用户已经登陆

四 . Token+Redis

步骤:

  • docker安装redis
docker run -d -p 6379:6379 redis:6.0
  • 创建springboot项目,在pom.xml中引入依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • application.yml写配置
spring:
  redis:
    port: 6379
    host: 47.97.214.211
  • controller层
@Autowired
private StringRedisTemplate redisTemplate;

@GetMapping("loginWithToken")
public String loginWithToken(@RequestParam String userName, @RequestParam String password) {
    //        省略一些数据库校验,假设账号密码都正确
    String key = "token_" + UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(key, userName, 3600, TimeUnit.SECONDS);
    return key;
}

@GetMapping("infoWithToken")
public String infoWithToken(@RequestParam String token) {
    return "当前登陆的是:" + redisTemplate.opsForValue().get(token);
}
  • 总结:实际上原理和spring-session类似,引入三方服务器来存储用户信息,每次访问时都来看第三方服务器是否存在该用户的信息

五 . JWT

1.1 简述
  • Json Web token:实际上就是后端返回加密的token给前端,前端请求接口时,在header里携带这个token到服务端,服务端用解密算法解密即可
  • JWT不需要Redis就可以做到分布式Session,原因在于:服务端的加密解密
  • 通过网站查看加密后的token
  • JWT里的内容可以被解析,但是不能被篡改
  • JWT和Spring-Session/普通token+redis的本质区别在于:内容可以被解析,普通的token是不可以被解析的同时也不能被篡改
1.2 实践

步骤:

  • 创建项目pom.xml引入依赖
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.16.0</version>
</dependency>
  • Controller层
 @GetMapping("/loginWithJwt")
    public String loginWithJwt(@RequestParam String userName, @RequestParam String password) {
        Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
        String token = JWT.create()
                .withClaim("login_user", userName)
                .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
                .sign(algorithm);
        return success;
   }
  @GetMapping("infoWithJwt")
    public String infoWithJwt(@RequestHeader String token) {
        Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
        JWTVerifier verifier = JWT.require(algorithm)
                .build(); //Reusable verifier instance
        try {
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaim("login_user").asString();
        } catch (TokenExpiredException e) {
//            token过期
        } catch (JWTDecodeException e) {
//            解码失败,token错误
        }
        return "error";
   }
  • 测试启动两台服务8081、8082端口,先已8081访问loginWithJwt这个接口获得token,然后已8082端口访问infoWithJwt,并在header中加入之前的token,发现可以获得信息

改造JWT里的Controller层代码:

  1. 考虑在拦截器中统一处理token:
public class LoginIntercepter extends HandlerInterceptorAdapter {
    public static final String JWT_KEY = "secret";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
//        应该抛出自定义的异常
            throw new RuntimeException("token为空");
        }
        Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
        JWTVerifier verifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = verifier.verify(token);
//         这里将jwt里的数据解码出来并转接到controller中
            request.setAttribute("login_user", verify.getClaim("login_user").asString());
            return true;
        } catch (TokenExpiredException e) {
            throw new RuntimeException("token过期");
        } catch (JWTDecodeException e) {
            throw new RuntimeException("解码失败,token错误");
        }
    }
}
  1. controller层
//使用@RequestAttribute来进行获取我们在拦截器中转接属性
@GetMapping("/address")
public String address(@RequestAttribute String login_user) {
    return login_user;
}

六. 总结

JWT方案

优点:

  • 天然去中心化模式,会话状态由令牌本身自解释,简单粗暴
  • 服务器端不用存储token,节约资源

缺点:

  • 一旦服务端下发了token便不受服务端控制
  • 如果发生token泄露,服务器也只能人气蹂躏,只能等到它自己过期

问题: 如果用JWT实现可以管理下发的token呢?

可以考虑增加一张表,在表里存放当前颁发的token以及是否允许继续使用这个token,每次校验时再去表里判断即可,但是这样使用不就和传统的Session一样了么  emmm

因此:选择什么方案应该根据具体的场景
Redis+Token方案

优点:

  • 可以有效的管理服务器颁发的token

缺点:

  • 如果只有一台Redis服务器就容易出现单点故障
  • 服务器需要存储token,耗费资源
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值