当前位置: 首页 > news >正文

session共享问题

一、  问题概述

session共享问题出现于集群或分布式环境中。

在最简单的一主一备、负载均衡的集群下,比如两台tomcat服务器和一台nginx负载均衡服务器。当用户访问时,nginx分配给tomcat1服务器处理登陆业务,用户登陆成功,在tomcat1记录了其登陆信息,当页面刷新时,nginx将用户请求分配给tomcat2服务器,在tomcat2服务器上没有用户登陆session,这样就需要用户再次登陆,如果足够巧合,刚好再次登陆的请求转到tomcat1服务器,显示用户登陆,再次刷新刚好又分配给tomcat2服务器,又没有登陆,甚至形成既登陆又没有登陆的矛盾局面。这就造成了不好的体验。

一般的解决办法是,tomcat服务器之间开启session共享广播,当tomcat1服务器记录了session数据后,就广播给其他tomcat服务器。但是,tomcat的session共享的节点数是有上限的。当集群中配置的tomcat节点机到达一定数量后(一般是5个),节点内部通信的流量可能被session广播占满,导致无法顺畅的处理其他业务,特别是难以适应高并发的场景。

避免session广播形成节点上限的解决办法是,配置单点登录的session服务器,适应redis缓存模拟session保存登陆信息。

分布式环境中,本身就是根据业务拆分成的不同的系统,比如商品管理、搜索功能、首页展示、商品详情等,登陆功能也是一个独立的系统。使用单点登录服务器解决session共享问题。

二、  搭建sso工程

single sign  on,单点登陆。

使用maven搭建sso服务层系统和表现层系统。

在登陆业务中,如果登陆通过验证,生成一个唯一性质的token,模拟sessionId。将token作为redis缓存中的key,以用户信息作为value,设置缓存的有效期模拟session的过期时间。返回登陆成功,将token保存到cookie,模拟sessionId保存到cookie。

判断登陆状态,比如当用户添加商品到购物车或者访问订单页面时,从cookie中取token,然后调用sso服务根据token查询用户信息。在sso系统中,接收token,根据token查询redis。判断token是否有值,如果没有,返回用户登陆界面;如果有,重置过期时间,返回用户登陆已登陆,并显示请求页面结果。

服务层用户登陆的业务。

@Service

publicclass LoginServiceImpl implements LoginService {

    @Autowired

    private TbUserMapper userMapper;

    @Autowired

    private JedisClient jedisClient;

    @Value("${SESSION_EXPIRE_TIME}")

    private Integer SESSION_EXPIRE_TIME;

    @Value("${SEESION_PRE}")

    private String SEESION_PRE;



    /**

     * 用户注册

     */

    @Override

    public ServiceResultuserLogin(String username, String password) {

        // 非空判断

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {

            return ServiceResult.build(400,"用户名或密码不能为空");

        }

        // 密码md5加密

        password = DigestUtils.md5DigestAsHex(password.getBytes());

        // 设置查询条件

        TbUserExample example = new TbUserExample();

        Criteria criteria = example.createCriteria();

        criteria.andUsernameEqualTo(username);

        criteria.andPasswordEqualTo(password);

        // 查询是否用户是否存在

        List<TbUser> list = userMapper.selectByExample(example);

        if (list != null && list.size() > 0) {

            TbUser user = list.get(0);

            // 生成token

            String token = UUID.randomUUID().toString().replaceAll("-", "");

            // 将token和用户信息保存到缓存

            user.setPassword(null);

            StringuserInfo = JsonUtils.objectToJson(user);

            jedisClient.set(SEESION_PRE + ":" + token, userInfo);

            // 设置session的过期时间

            jedisClient.expire(SEESION_PRE + ":" + token, SESSION_EXPIRE_TIME);

            // 返回带有token的服务业务结果对象

            return ServiceResult.ok(token);

        }

        return ServiceResult.build(400,"用户名或密码有误");

    }



}

表现层用户登陆的业务。 

        public ServiceResultlogin(String username, String password, HttpServletRequest request,

                            HttpServletResponseresponse) {

                   // 调用登陆服务

                   ServiceResultresult = loginService.userLogin(username, password);

                   // 判断是否登陆成功

                   if(result.getStatus() == 200) {

                            // 成功,将token写入cookie

                            Stringtoken = result.getData().toString();

                            CookieUtils.setCookie(request,response, TOKEN_KEY, token);

                   }

                   // 跳转到网站首页

                   return result;

         }

登录跳转到网站页面时,需要显示用户登陆成功的信息,就是需要从cookie中获取token,再根据token从sso系统中获取用户信息。如果在表现层中调用sso中的方法获取用户信息,在分布式系统中就需要每个相关系统的表现层都调用sso系统,实现起来比较重复。第二种方案是在页面加载完成后使用js的ajax取token中的用户信息,这就涉及 到js跨域ajax请求。但是js不可以跨域请求,跨域是域名不同;或者域名相同端口不同。解决js的跨域问题可以使用jsonp。

首先在sso系统中提供根据token获取用户信息的接口和类。

服务层

         public ServiceResultgetUserByToken(String token) {

                   // 从缓存中获取token建对应的值

                   if (StringUtils.isBlank(token)){

                            returnServiceResult.build(201, "未登录");

                   }

                   String result = jedisClient.get(SEESION_PRE+ ":" + token);

                   // 如果没有,返回提示未登录的信息

                   if (StringUtils.isBlank(result)){

                            returnServiceResult.build(201, "登录过期");

                   }

                   // 并重置token的有效期为初始值

                   jedisClient.expire(SEESION_PRE+ ":" + token, SESSION_EXPIRE_TIME);

                   // 如果有,将值封装到服务结果pojo,返回服务结果pojo

                   returnServiceResult.ok(JsonUtils.jsonToPojo(result, TbUser.class));

         }

支持jsonp的表现层 

  @RequestMapping("/user/token/{token}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

         @ResponseBody

         public StringgetUserByToken(@PathVariable String token, String callback) {

                   ServiceResult result= tokenService.getUserByToken(token);

                   // 判断是否为jsonp请求

                   if (StringUtils.isNoneBlank(callback)){

                            return callback+ "(" + JsonUtils.objectToJson(result) + ");";

                   }

                   returnJsonUtils.objectToJson(result);

         }

spring4.1后的支持的表现层的方法

         @RequestMapping(value = "/user/token/{token}")

         @ResponseBody

         public ObjectgetUserByToken(@PathVariable String token, String callback) {

                   ServiceResult result= tokenService.getUserByToken(token);

                   // 判断是否为jsonp请求

                   if (StringUtils.isNoneBlank(callback)){

                            MappingJacksonValuevalue = new MappingJacksonValue(result);

                            value.setJsonpFunction(callback);

                            return value;

                   }

                   return result;

         }

三、  使用jsonp实现ajax跨域请求

jsonp的原理,虽然js不能跨域请求,但是js可以跨域请求js文件。jsonp根据这一特点。编写一个jsonp跨域请求的js文件,以便可以在需要的系统中直接引用。

var SHOP = {

         checkLogin : function(){

                   var _ticket =$.cookie("shop-token");

                   if(!_ticket){

                            return;

                   }

                   $.ajax({

                            url : "http://localhost:8099/user/token/"+ _ticket,

                            dataType: "jsonp",

                            type : "GET",

                            success: function(data){

                                     if(data.status== 200){

                                               varusername = data.data.username;

                                               varhtml = username + ",欢迎来到网!<a href=\"http://www.exx.com/user/logout.html\"class=\"link-logout\">[退出]</a>";

                                               $("#loginbar").html(html);

                                     }

                            }

                   });

         }

}



$(function(){

         // 查看是否已经登录,如果已经登录查询登录信息

         SHOP.checkLogin();

});

 

相关文章:

  • springmvc中ajax响应json报406错误的两种原因
  • 数据库切片
  • tomcat热部署
  • c++
  • linux系统编程
  • proc*c/c++简介
  • [one_demo_14]一个简单的easyui的demo
  • ztree
  • PowerDesigner
  • POI简单介绍
  • shiro
  • ehcache
  • highcharts
  • spring框架和jdk的版本兼容
  • 系统中日志的作用
  • Angular Elements 及其运作原理
  • JavaScript 基本功--面试宝典
  • JS实现简单的MVC模式开发小游戏
  • Linux下的乱码问题
  • php面试题 汇集2
  • Python进阶细节
  • spring boot 整合mybatis 无法输出sql的问题
  • Spring Cloud中负载均衡器概览
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • Webpack 4 学习01(基础配置)
  • 大快搜索数据爬虫技术实例安装教学篇
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 工程优化暨babel升级小记
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 聊聊flink的TableFactory
  • 马上搞懂 GeoJSON
  • 前端之React实战:创建跨平台的项目架构
  • 驱动程序原理
  • 微信支付JSAPI,实测!终极方案
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 一个项目push到多个远程Git仓库
  • 字符串匹配基础上
  • C# - 为值类型重定义相等性
  • ​Python 3 新特性:类型注解
  • #《AI中文版》V3 第 1 章 概述
  • #includecmath
  • #大学#套接字
  • (11)MATLAB PCA+SVM 人脸识别
  • (175)FPGA门控时钟技术
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (七)Java对象在Hibernate持久化层的状态
  • (强烈推荐)移动端音视频从零到上手(下)
  • (十八)用JAVA编写MP3解码器——迷你播放器
  • (实战篇)如何缓存数据
  • (译) 理解 Elixir 中的宏 Macro, 第四部分:深入化
  • (转)VC++中ondraw在什么时候调用的
  • (转)详解PHP处理密码的几种方式
  • ***php进行支付宝开发中return_url和notify_url的区别分析