面试官问我:分布式架构下怎么维持会话

1 前言大家或多或少都使用过系统,或者访问过网站。就拿京东淘宝来说,我们在初次访问网站时,可以浏览商品。当我们将商品添加购物车,或者结算支付时,这个时候网站就必须让咱们进行登录;又或者在一些早期的网站,比如我们在查成绩时,这个时候和室友出去

1 前言

大家或多或少都使用过系统,或者访问过网站。就拿京东淘宝来说,我们在初次访问网站时,可以浏览商品。当我们将商品添加购物车,或者结算支付时,这个时候网站就必须让咱们进行登录;又或者在一些早期的网站,比如我们在查成绩时,这个时候和室友出去吃饭,当回来时发现网站又回到了登录页面。这些场景我相信大家或多或少都遇到过,在咱们开发的时候,很多功能都需要用户登录授权后才能访问,这个时候就需要用到咱们今天讲得会话知识了。

2 为什么需要会话控制

面试官问我:分布式架构下怎么维持会话

保持用户登录状态,就是当用户在登录之后,会在服务器中保存该用户的登录状态,当该用户后续访问该项目中的其它动态资源(Servlet或者Thymeleaf)的时候,能够判断当前是否是已经登录过的。而从用户登录到用户退出登录这个过程中所发生的所有请求,其实都是在一次会话范围之内

3 域对象的范围

3.1 应用域的范围

面试官问我:分布式架构下怎么维持会话

整个项目部署之后,只会有一个应用域对象,所有客户端都是共同访问同一个应用域对象,在该项目的所有动态资源中也是共用一个应用域对象

3.2 请求域的范围

面试官问我:分布式架构下怎么维持会话

每一次请求都有一个请求域对象,当请求结束的时候对应的请求域对象也就销毁了;好比张三和李四在说话,张三问李四答,每一次来回就是一次Http交互,也就是一次请求范围。

比如:张三问你年龄是多少,李四回答22。这个时候会创建一个request请求域来保存他们在这一次请求范围中共享的数据,在这一次请求中,请求域的数据是有效的。再比如张三又问你家是哪儿的,这个时候是一次新的请求,上一次请求在李四给出相应后就结束了,对应的请求域也就被销毁掉了,这时候再从请求域去拿年龄,显然是获取不到的。就好比一个记忆力很差的人,你只有每次跟他说他才能记住,说完之后又立马忘掉。

3.3 会话域的范围

面试官问我:分布式架构下怎么维持会话

会话域是从客户端连接上服务器开始,一直到客户端关闭,这一整个过程中发生的所有请求都在同一个会话域中;而不同的客户端是不能共用会话域的。同样是张三跟李四说话,一次会话的意思就是把他们这一次谈话看成一个整体,在这一次谈话过程中,session域里面的数据都是有效的。

比如张三问李四年龄,李四回复;是这一次会话中的一次请求,张三又问住址,李四回复;在第二次请求中,会话域中的年龄仍然可以获得。一次会话可以包括多次请求,就好比你跟你朋友说话一样,一次会话你跟你朋友可以相互说多次话,在一次谈话中,所有的内容都可以记得,直到你们下一次谈话,这次谈话结束后这一次session才会被销毁。

4 Cookie技术

4.1 Cookie的概念

Cookie是一种客户端的会话技术,它是服务器存放在浏览器的一小份数据,浏览器以后每次访问该服务器的时候都会将这小份数据携带到服务器去。

面试官问我:分布式架构下怎么维持会话

4.2 Cookie的作用

  1. 在浏览器中存放数据
  2. 将浏览器中存放的数据携带到服务器

4.3 Cookie的应用场景

1.记住用户名当我们在用户名的输入框中输入完用户名后,浏览器记录用户名,下一次再访问登录页面时,用户名自动填充到用户名的输入框.

2.保存电影的播放进度

在网页上播放电影的时候,如果中途退出浏览器了,下载再打开浏览器播放同一部电影的时候,会自动跳转到上次退出时候的进度,因为在播放的时候会将播放进度保存到cookie中

4.4 Cookie的时效性

如果我们不设置Cookie的时效性,默认情况下Cookie的有效期是一次会话范围内,我们可以通过cookie的setMaxAge()方法让Cookie持久化保存到浏览器上

  • 会话级Cookie
    • 服务器端并没有明确指定Cookie的存在时间
    • 在浏览器端,Cookie数据存在于内存中
    • 只要浏览器还开着,Cookie数据就一直都在
    • 浏览器关闭,内存中的Cookie数据就会被释放
  • 持久化Cookie
    • 服务器端明确设置了Cookie的存在时间
    • 在浏览器端,Cookie数据会被保存到硬盘上
    • Cookie在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
    • 持久化Cookie到达了预设的时间会被释放

5 Session技术

5.1 session概述

session是服务器端的技术。服务器为每一个浏览器开辟一块内存空间,即session对象。由于session对象是每一个浏览器特有的,所以用户的记录可以存放在session对象中

5.2 Session的工作机制

前提:浏览器正常访问服务器

  • 服务器端没调用request.getSession()方法:什么都不会发生
  • 服务器端调用了request.getSession()方法
    • 有:根据JSESSIONID在服务器端查找对应的HttpSession对象
    • 无:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回
    • 能找到:将找到的HttpSession对象作为request.getSession()方法的返回值返回
    • 找不到:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回
    • 服务器端检查当前请求中是否携带了JSESSIONID的Cookie
面试官问我:分布式架构下怎么维持会话

6 Cookie、Session会话技术在分布式环境下的缺陷

通过以上的知识,我们可以知道会话技术的原理,是cookie和session相互配合来进行完成的,服务器提供了session对象来缓存用户的数据,同时会将当前对象的唯一标识id写到浏览器缓存cookie中,在下一次请求后,客户端会将cookie中的JSESSIONID携带到服务器,服务器通过唯一JSESSIONID查找出上次的session对象,进而来达到访问上次的服务器数据的目的。

但是这种情况只适用于单体服务,如果此时是分布式环境呢?服务部署在多个节点,第一次A服务器创建session,唯一JSESSIONID为a1,将数据同时保存在此session中,并将a1写在客户端cookie中。

客户端第二次发起请求,并将cookie中的JSESSIONID携带到服务器,但是因为后台是分布式部署,通过负载均衡,第二次请求被路由到B服务器,B服务器接收到JSESSIONID为a1,但是这个时候B服务器并未创建这个对象,会找不到对不对?这个时候就会创建新的session实例,比如b1。

那么从session中加载的数据自然就为空了。比如判断用户是否登录,第一次在服务器A节点登录了,第二次客户端再发起请求,如果路由的是A节点还好,如果不是,当前是不是又需要登录呢?那么是不是又造成了一个用户共享的session不唯一了呢?

7 分布式架构下的会话解决方案

通过上面的分析,我们可以知道原始的会话技术在分布式环境下显然是行不通的,造成失效的根本原因是因为多个服务器节点的session对象没有达到共享,以至于客户端的JSESSIONID路由在不同的服务器造成session数据紊乱。这个时候为了解决这种问题,实际就是解决session共享的问题,比如用户在A节点创建了session,下次路由到B服务器时,我们希望也能在B节点拿到A节点创建的那个session对象,需要实现跨服务器的session共享;

面试官问我:分布式架构下怎么维持会话

7.1 将session储存在redis中,通过redis来完成多节点共享

为了解决多服务节点session共享问题,比如用户第一次访问的是serverA服务器时,serverA创建session,除了将JSESSIONID写到客户端cookie中,我们再将该session写到redis中,

redisTemplate.opsForValue().set(RedisConst.USER_LOGIN_KEY_PREFIX + JSESSIONID, session.toJSONString());

对key的要求可以用上当前的JSESSIONID拼接上一些特定的前后缀,下次再访问时,通过客户端传入的JSESSIONID拼接对应的redisKey。首先通过key去判断redis服务器是否存在,若存在直接取出key中的session对象,反之则重新创建;

这样即使用户下次请求访问的是serverB,我们通过redis能够查找到对应的key不为空,这时候就不会创建新的session,而是直接从redis中拿到ServerA对应的session,从而达到session共享的目的。

7.2 redis代替session

第一种方式,我们是将session存在redis中,让多个服务器共享同一台redis服务器,那么同样我们也可以直接将数据写到redis中,让redis直接替换session,起到缓存效果。

第一步:双写用户信息

用户登录成功后,将用户信息保存在cookie,同时也写在redis中;如果用户登录成功,也就是通过用户名和密码能在数据库查询到用户信息,此时维持会话,我们需要生成一个token,也就是这个登录信息唯一的一个标识符,再用唯一标识作为key,用户信息作为value同步到redis中,目的是客户端下次发送请求,会携带对应的cookie中的token,通过token能够取出redis中对应的用户信息;

/实现登录的具体验证工作

@PostMapping(“/login”)

public RetVal login(@RequestBody UserInfo uiUserInfo, HttpServletRequest request){

//a.根据用户账号密码从数据库中查询用户信息

UserInfo dbUserInfo=userInfoService.queryUserFromDb(uiUserInfo);

if(dbUserInfo!=null){

//b.生成一个token/用户信息返回给页面cookie

Map<String, Object> retValMap = new HashMap<>();

String token = UUID.randomUUID().toString();

retValMap.put(“token”,token);

//用户昵称信息

String nickName = dbUserInfo.getNickName();

retValMap.put(“nickName”,nickName);

/**c.将用户信息存放到redis当中

* 用户信息的key 一个前缀+token

* 存储的信息

* 用户的id

* 用户使用机器的ip地址

*/

String userKey= RedisConst.USER_LOGIN_KEY_PREFIX+token;

JSONObject loginInfo = new JSONObject();

loginInfo.put(“userId”,dbUserInfo.getId());

loginInfo.put(“loginIp”, IpUtil.getIpAddress(request));

redisTemplate.opsForValue().set(userKey,loginInfo.toJSONString(),RedisConst.USERKEY_TIMEOUT, TimeUnit.SECONDS);

return RetVal.ok(retValMap);

}else{

return RetVal.fail().message(“登录失败”);

}

}

第二步:

1.利用SpringCloudGateWay配置网关过滤器,所有请求在访问服务器时,均会被网关所拦截: 在全局过滤器中获取当前请求request信息,通过request对象可以得到用户当前的请求路径,如果请求路径包含某个内部路径,则拦截,返回给客户端,无法访问;这也就是某些内部接口设置权限访问的原理;

2.判断用户当前登录状态,通过request从header或者cookie中获取当前token信息,如果token不为空,则拼接token为redis中的key,通过该key去redis中查询用户信息,如果从redis中取出的ip和当前客户端的ip地址不同,说明登录异常,如果匹配说明当前为登陆状态,反之则为未登录状态;

3.配置服务白名单列表,白名单表示这些服务必须要经过登录才能访问,未登录无法访问,并跳转到登录页面,登陆成功后则返回之前页面;

原理:在数据库或者配置文件中配置白名单服务地址列表,判断当前路径是否在该白名单之内且登录状态为true,如果两者满足拦截器才发行,反之则携带当前地址信息转发到到页面,登陆成功后通过原来地址重新回到原来页面;

4.由于其他服务同样需要操作userId,而过滤器与web服务的request对象不是同一个,则在web请求对象获取userId则会为空,这时候我们则需要重新构建一个request,将当前登录用户信息保存到该request对象中,让它起一个传输用户登录信息的载体,放行时将该request传递到下一个服务中去;同时在微服务之间通信也无法获取到userId,使用同样的方式在微服务通信时配置一个拦截器,将登陆用户信息重新赋值传入新的request即可。

@Component

public class AccessFilter implements GlobalFilter {

//匹配路径对象

private AntPathMatcher antPathMatcher=new AntPathMatcher();

@Autowired

private RedisTemplate redisTemplate;

@Value(“${filter.whiteList}”)

private String filterWhiteList;

/**

* @param exchange 服务网络交换器,存放着重要的请求-响应属性、请求实例和响应实例

* 不可变实例 如果我们想要修改它,需要通过mutate()方法生成一个新的实例

* @param chain 网关过滤的链表,用于过滤器的链式调用 设计模式:责任链模式

* 面试问题:谈谈你对设计模式的理解—>单例,代理,工厂,装饰模式

*/

@Override

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

//1.对于规定的内部接口 不允许外部调用

ServerHttpRequest request = exchange.getRequest();

String path = request.getURI().getPath();

//如果请求路径当中以sku开头就让他没有权限访问

if(antPathMatcher.match(“/sku/**”, path)){

//写一些数据到浏览告诉访问端 没有权限访问

return writeDataToBrowser(exchange,RetValCodeEnum.NO_PERMISSION);

}

//2.如果用户登录了拿到用户id判断该用户是否为同一IP

String userId=getUserId(request);

//获取用户的临时id

String userTempId=getUserTempId(request);

if(“-1”.equals(userId)){

return writeDataToBrowser(exchange,RetValCodeEnum.NO_PERMISSION);

}

//3.对于指定的某些(我的订单/我的购物车/我xxx)资源,必须登录

if(antPathMatcher.match(“/order/**”, path)){

if(StringUtils.isEmpty(userId)){

//写一些数据到浏览告诉访问端 没有权限访问

return writeDataToBrowser(exchange,RetValCodeEnum.NO_LOGIN);

}

}

//4.用户请求白名单 如果请求是白名单 就直接跳转到登录页面

for(String filterWhite:filterWhiteList.split(“,”)){

//用户访问路径包含白名单路径,并且用户未登录

if(path.indexOf(filterWhite)!=-1&&StringUtils.isEmpty(userId)){

//跳转到登录页面

ServerHttpResponse response = exchange.getResponse();

response.setStatusCode(HttpStatus.SEE_OTHER);

response.getHeaders().set(HttpHeaders.LOCATION,”http://passport.gmall.com/login.html?originalUrl=”+request.getURI());

return response.setComplete();

}

}

//5.把用户id需要保存到header中 传给shop-web那边的request

if(!StringUtils.isEmpty(userId)||!StringUtils.isEmpty(userTempId)){

if(!StringUtils.isEmpty(userId)){

//将用户id放入header当中

request.mutate().header(“userId”,userId).build();

}

if(!StringUtils.isEmpty(userTempId)){

request.mutate().header(“userTempId”,userTempId).build();

}

//过滤器放开拦截器 让下游请求继续执行(此时修改了exchange对像)

return chain.filter(exchange.mutate().request(request).build());

}

//过滤器放开拦截器 让下游请求继续执行

return chain.filter(exchange);

}

8 总结

简而言之,权限和登录验证在我们开发至关重要,熟悉cookie与session的原理对我们来说是必不可少的,随着最近几年分布式的兴起。应用也越来越”碎片化“,越来越微服务化,以往单体架构的技术在分布式环境下会面对全新的问题。但是只要我们对技术理解到本质,总会找到更适合的解决方式的。总得来说分布式下的会话问题,实际是session共享的问题,除了redis解决外,方法还有很多种,例如保存到数据库,让所有集群节点都共享一台数据库服务器、jwt或者springsecurity等等。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/89345.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信