当前位置: 移动技术网 > IT编程>开发语言>Java > 模仿J2EE的session机制的App后端会话信息管理实例

模仿J2EE的session机制的App后端会话信息管理实例

2019年07月19日  | 移动技术网IT编程  | 我要评论

效果器,工商营业执照不年检,石贞善照片

此文章只将思想,不提供具体完整实现(博主太懒,懒得整理),有疑问或想了解的可以私信或评论

背景

在传统的java web 中小型项目中,一般使用session暂存会话信息,比如登录者的身份信息等。此机制是借用http的cookie机制实现,但是对于app来说每次请求都保存并共享cookie信息比较麻烦,并且传统的session对集群并不友好,所以一般app后端服务都使用token来区分用户登录信息。

j2ee的session机制大家都很了解,使用非常方便,在传统java web应用中很好用,但是在互联网项目中或用得到集群的一些项目就有些问题,比如序列化问题,同步的延时问题等等,所以我们需要一个使用起来类似session的却能解决得了集群等问题的一个工具。

方案

我们使用cache机制来解决这个问题,比较流行的redis是个nosql内存数据库,而且带有cache的失效机制,很适合做会话数据的存储。而token字符串需要在第一次请求时服务器返回给客户端,客户端以后每次请求都使用这个token标识身份。为了对业务开发透明,我们把app的请求和响应做的报文封装,只需要对客户端的http请求工具类做点手脚,对服务端的mvc框架做点手脚就可以了,客户端的http工具类修改很简单,主要是服务端的协议封装。

实现思路

一、制定请求响应报文协议。

二、解析协议处理token字符串。

三、使用redis存储管理token以及对应的会话信息。

四、提供保存、获取会话信息的api。

我们逐步讲解下每一步的实现方案。

一、制定请求响应报文协议。

既然要封装报文协议,就需要考虑什么是公共字段,什么是业务字段,报文的数据结构等。

请求的公共字段一般有token、版本、平台、机型、imei、app来源等,其中token是我们这次的主角。

响应的公共字段一般有token、结果状态(success,fail)、结果码(code)、结果信息等。

报文数据结构,我们选用json,原因是json普遍、可视化好、字节占用低。

请求报文如下,body中存放业务信息,比如登录的用户名和密码等。

{
  "token": "客户端token",
  /**客户端构建版本号*/
  "version": 11,
  /**客户端平台类型*/
  "platform": "ios",
  /**客户端设备型号*/
  "machinemodel": "iphone 6s",
  "imei": "客户端串号(手机)",
  /**真正的消息体,应为map*/
  "body": {
    "key1": "value1",
    "key2": {
      "key21": "value21"
    },
    "key3": [
      1,

    ]
  }
}

响应的报文

{
    /**是否成功*/
    "success": false,
    /**每个请求都会返回token,客户端每次请求都应使用最新的token*/
    "token": "服务器为当前请求选择的token",
    /**失败码*/
    "failcode": 1,
    /**业务消息或者失败消息*/
    "msg": "未知原因",
    /**返回的真实业务数据,可为任意可序列化的对象*/
    "body": null
  }
}

二、解析协议处理token字符串。

服务端的mvc框架我们选用的是springmvc框架,springmvc也比较普遍,不做描述。

暂且不提token的处理,先解决制定报文后怎么做参数传递。

因为请求信息被做了封装,所以要让springmvc框架能正确注入我们在controller需要的参数,就需要对报文做解析和转换。

要对请求信息做解析,我们需要自定义springmvc的参数转换器,通过实现handlermethodargumentresolver接口可以定义一个参数转换器

requestbodyresolver实现resolveargument方法,对参数进行注入,以下代码为示例代码,切勿拿来直用。

@override
  public object resolveargument(methodparameter parameter,
      modelandviewcontainer mavcontainer, nativewebrequest webrequest,
      webdatabinderfactory binderfactory) throws exception {
    string requestbodystr = webrequest.getparameter(requestbodyparamname);//获取请求报文,可以使用任意方式传递报文,只要在这获取到就可以
    if(stringutils.isnotblank(requestbodystr)){
      string paramname = parameter.getparametername();//获取controller中参数名
      class<?> paramclass = parameter.getparametertype();//获取controller中参数类型
      /* 通过json工具类解析报文 */
      jsonnode jsonnode = objectmapper.readtree(requestbodystr);
      if(paramclass.equals(servicerequest.class)){//servicerequest为请求报文对应的vo
        servicerequest servicerequest = objectmapper.readvalue(jsonnode.traverse(),servicerequest.class);
        return servicerequest;//返回这个object就是注入到参数中了,一定要对应类型,否则异常不容易捕获
      }
      if(jsonnode!=null){//从报文中查找controller中需要的参数
        jsonnode paramjsonnode = jsonnode.findvalue(paramname);
        if(paramjsonnode!=null){
          return objectmapper.readvalue(paramjsonnode.traverse(), paramclass);
        }
        
      }
    }
    return null;
  }

将自己定义的参数转换器配置到srpingmvc的配置文件中<mvc:argument-resolvers>

<mvc:argument-resolvers>
  <!-- 统一的请求信息处理,从servicerequest中取数据 -->
     <bean id="requestbodyresolver" class="com.niuxz.resolver.requestbodyresolver">
       <property name="objectmapper"><bean class="com.shoujinwang.utils.json.objectmapper"></bean></property>
       <!-- 配置请求中servicerequest对应的字段名,默认为requestbody -->
       <property name="requestbodyparamname"><value>requestbody</value></property>
     </bean>
</mvc:argument-resolvers>

这样就可以使报文中的参数能被springmvc正确识别了。

接下来我们要对token做处理了,我们需要添加一个srpingmvc拦截器将每次请求都拦截下来,这属于常用功能,不做细节描述

matcher m1 =pattern.compile("\"token\":\"(.*?)\"").matcher(requestbodystr);
  
if(m1.find()){
  token = m1.group(1);
}
tokenmappool.verifytoken(token);//对token做公共处理,验证

这样就简单的获取到了token了,可以做公共处理了。

三、使用redis存储管理token以及对应的会话信息。

其实就是写一个redis的操作工具类,因为使用了spring作为项目主框架,而且我们用到redis的功能并不多,所以直接使用spring提供的cachemanager功能

配置org.springframework.data.redis.cache.rediscachemanager

<!-- 缓存管理器 全局变量等可以用它存取-->
<bean id="cachemanager" class="org.springframework.data.redis.cache.rediscachemanager">
  <constructor-arg>
    <ref bean="redistemplate"/>
  </constructor-arg>
  <property name="useprefix" value="true" />
  <property name="cacheprefix">
    <bean class="org.springframework.data.redis.cache.defaultrediscacheprefix">
      <constructor-arg name="delimiter" value=":@webserviceinterface"/>
    </bean>
  </property>
  <property name="expires"><!-- 缓存有效期 -->
    <map>
      <entry>
        <key><value>tokenpoolcache</value></key><!-- tokenpool缓存名 -->
        <value>2592000</value><!-- 有效时间 -->
      </entry>
    </map>
  </property>
</bean>

四、提供保存、获取会话信息的api。

通过以上前戏我们已经把token处理的差不多了,接下来我们要实现token管理工作了

我们需要让业务开发方便的保存获取会话信息,还要使token是透明的。

import java.util.hashmap;
import java.util.map;

import org.apache.commons.logging.log;
import org.apache.commons.logging.logfactory;
import org.springframework.cache.cache;
import org.springframework.cache.cache.valuewrapper;
import org.springframework.cache.cachemanager;

/**
 * 
 * 类      名:  tokenmappoolbean
 * 描      述:  token以及相关信息调用处理类
 * 修 改 记 录:  
 * @version  v1.0
 * @date  2016年4月22日
 * @author  niuxz
 *
 */
public class tokenmappoolbean {
  
  
  private static final log log = logfactory.getlog(tokenmappoolbean.class);
  
  /** 当前请求对应的token*/
  private threadlocal<string> currenttoken;
  
  private cachemanager cachemanager;
  
  private string cachename;
  
  private tokengenerator tokengenerator;
  
  public tokenmappoolbean(cachemanager cachemanager, string cachename, tokengenerator tokengenerator) {
    this.cachemanager = cachemanager;
    this.cachename = cachename;
    this.tokengenerator = tokengenerator;
    currenttoken = new threadlocal<string>();
  }
  
  /**
   * 如果token合法就返回token,不合法就创建一个新的token并返回,
   * 将token放入threadlocal中 并初始化一个tokenmap
   * @param token
   * @return token
   */
  public string verifytoken(string token) {
    //    log.info("校验token:\""+token+"\"");
    string verifyedtoken = null;
    if (tokengenerator.checktokenformat(token)) {
      //      log.info("校验token成功:\""+token+"\"");
      verifyedtoken = token;
    }
    else {
      verifyedtoken = newtoken();
    }
    currenttoken.set(verifyedtoken);
    cache cache = cachemanager.getcache(cachename);
    if (cache == null) {
      throw new runtimeexception("获取不到存放token的缓存池,chachename:" + cachename);
    }
    valuewrapper value = cache.get(verifyedtoken);
    //token对应的值为空,就创建一个新的tokenmap放入缓存中
    if (value == null || value.get() == null) {
      verifyedtoken = newtoken();
      currenttoken.set(verifyedtoken);
      map<string, object> tokenmap = new hashmap<string, object>();
      cache.put(verifyedtoken, tokenmap);
    }
    return verifyedtoken;
  }
  
  /**
   * 生成新的token
   * @return token
   */
  private string newtoken() {
    cache cache = cachemanager.getcache(cachename);
    if (cache == null) {
      throw new runtimeexception("获取不到存放token的缓存池,chachename:" + cachename);
    }
    string newtoken = null;
    int count = 0;
    do {
      count++;
      newtoken = tokengenerator.generatortoken();
    }
    while (cache.get(newtoken) != null);
    //    log.info("创建token成功:\""+newtoken+"\" 尝试生成:"+count+"次");
    return newtoken;
  }
  
  /**
   * 获取当前请求的tokenmap中对应key的对象
   * @param key
   * @return 当前请求的tokenmap中对应key的属性,模拟session
   */
  public object getattribute(string key) {
    cache cache = cachemanager.getcache(cachename);
    if (cache == null) {
      throw new runtimeexception("获取不到存放token的缓存池,chachename:" + cachename);
    }
    valuewrapper tokenmapwrapper = cache.get(currenttoken.get());
    map<string, object> tokenmap = null;
    if (tokenmapwrapper != null) {
      tokenmap = (map<string, object>) tokenmapwrapper.get();
    }
    if (tokenmap == null) {
      verifytoken(currenttoken.get());
      tokenmapwrapper = cache.get(currenttoken.get());
      tokenmap = (map<string, object>) tokenmapwrapper.get();
    }
    return tokenmap.get(key);
  }
  
  /**
   * 设置到当前请求的tokenmap中,模拟session<br>
   * todo:此种方式设置attribute有问题:<br>
   * 1、可能在同一token并发的情况下执行cache.put(currenttoken.get(),tokenmap);时,<br>
   *   tokenmap可能不是最新,会导致丢失数据。<br>
   * 2、每次都put整个tokenmap,数据量太大,需要优化<br>
   * @param key value
   */
  public void setattribute(string key, object value) {
    cache cache = cachemanager.getcache(cachename);
    if (cache == null) {
      throw new runtimeexception("获取不到存放token的缓存池,chachename:" + cachename);
    }
    valuewrapper tokenmapwrapper = cache.get(currenttoken.get());
    map<string, object> tokenmap = null;
    if (tokenmapwrapper != null) {
      tokenmap = (map<string, object>) tokenmapwrapper.get();
    }
    if (tokenmap == null) {
      verifytoken(currenttoken.get());
      tokenmapwrapper = cache.get(currenttoken.get());
      tokenmap = (map<string, object>) tokenmapwrapper.get();
    }
    log.info("tokenmap.put(key=" + key + ",value=" + value + ")");
    tokenmap.put(key, value);
    cache.put(currenttoken.get(), tokenmap);
  }
  
  /** 
   * 获取当前线程绑定的用户token
   * @return token
   */
  public string gettoken() {
    if (currenttoken.get() == null) {
      //初始化一次token
      verifytoken(null);
    }
    return currenttoken.get();
  }
  
  /**
   * 删除token以及tokenmap
   * @param token
   */
  public void removetokenmap(string token) {
    if (token == null) {
      return;
    }
    cache cache = cachemanager.getcache(cachename);
    if (cache == null) {
      throw new runtimeexception("获取不到存放token的缓存池,chachename:" + cachename);
    }
    log.info("删除token:token=" + token);
    cache.evict(token);
  }
  
  public cachemanager getcachemanager() {
    return cachemanager;
  }
  
  public void setcachemanager(cachemanager cachemanager) {
    this.cachemanager = cachemanager;
  }
  
  public string getcachename() {
    return cachename;
  }
  
  public void setcachename(string cachename) {
    this.cachename = cachename;
  }
  
  public tokengenerator gettokengenerator() {
    return tokengenerator;
  }
  
  public void settokengenerator(tokengenerator tokengenerator) {
    this.tokengenerator = tokengenerator;
  }
  
  public void clear() {
    currenttoken.remove();
  }
  
}

这里用到了threadlocal变量是因为servlet容器一个请求对应一个线程,在一个请求的生命周期内都是处于同一个线程中,而同时又有多个线程共享token管理器,所以需要这个线程本地变量来保存token字符串。

注意事项:

1、verifytoken方法的调用,一定要在每次请求最开始调用。并且在请求结束后调用clear做清除,以免下次有未知异常导致verifytoken未被执行,却在返回时从threadlocal里取出token返回。(这个bug困扰我好几天,公司n个开发检查代码也没找到,最后我经过测试发现是在发生404的时候没有进入拦截器,所以就没有调用verifytoken方法,导致返回的异常信息中的token为上一次请求的token,导致诡异的串号问题。嗯,记我一大锅)。

2、客户端一定要在封装http工具的时候把每次token保存下来,并用于下一次请求。公司ios开发请的外包,但是外包没按要求做,在未登录时,不保存token,每次传递的都是null,导致每次请求都会创建一个token,服务器创建了大量的无用token。

使用

使用方式也很简单,以下是封装的登录管理器,可以参考一下token管理器对于登陆管理器的应用

import org.apache.commons.logging.log;
import org.apache.commons.logging.logfactory;
import org.springframework.cache.cache;
import org.springframework.cache.cache.valuewrapper;
import org.springframework.cache.cachemanager;

import com.niuxz.base.constants;

/**
 * 
 * 类      名:  loginmanager
 * 描      述:  登录管理器
 * 修 改 记 录:  
 * @version  v1.0
 * @date  2016年7月19日
 * @author  niuxz
 *
 */
public class loginmanager {
  
  
  private static final log log = logfactory.getlog(loginmanager.class);
  
  private cachemanager cachemanager;
  
  private string cachename;
  
  private tokenmappoolbean tokenmappool;
  
  public loginmanager(cachemanager cachemanager, string cachename, tokenmappoolbean tokenmappool) {
    this.cachemanager = cachemanager;
    this.cachename = cachename;
    this.tokenmappool = tokenmappool;
  }
  public void login(string userid) {
    log.info("用户登录:userid=" + userid);
    cache cache = cachemanager.getcache(cachename);
    valuewrapper valuewrapper = cache.get(userid);
    string token = (string) (valuewrapper == null ? null : valuewrapper.get());
    tokenmappool.removetokenmap(token);//退出之前登录记录
    tokenmappool.setattribute(constants.logged_user_id, userid);
    cache.put(userid, tokenmappool.gettoken());
  }
  
  public void logoutcurrent(string phonetel) {
    string curuserid = getcurrentuserid();
    log.info("用户退出:userid=" + curuserid);
    tokenmappool.removetokenmap(tokenmappool.gettoken());//退出登录
    if (curuserid != null) {
      cache cache = cachemanager.getcache(cachename);
      cache.evict(curuserid);
      cache.evict(phonetel);
    }
  }
  
  /**
   * 获取当前用户的id
   * @return
   */
  public string getcurrentuserid() {
    return (string) tokenmappool.getattribute(constants.logged_user_id);
  }
  
  public cachemanager getcachemanager() {
    return cachemanager;
  }
  
  public string getcachename() {
    return cachename;
  }
  
  public tokenmappoolbean gettokenmappool() {
    return tokenmappool;
  }
  
  public void setcachemanager(cachemanager cachemanager) {
    this.cachemanager = cachemanager;
  }
  
  public void setcachename(string cachename) {
    this.cachename = cachename;
  }
  
  public void settokenmappool(tokenmappoolbean tokenmappool) {
    this.tokenmappool = tokenmappool;
  }
  
}

下面是一段常见的发送短信验证码接口,有的应用也是用session存储验证码,我不建议用这种方式,存session弊端相当大。大家看看就好,不是我写的

public void sendvalicodebyphonenum(string phonenum, string hintmsg, string logsuffix) {
    validatephonetimespace();
    // 获取6位随机数
    string code = codeutil.getvalidatecode();
    log.info(code + "------->" + phonenum);
    // 调用短信验证码下发接口
    retstatus retstatus = msgsendutils.sendsms(code + hintmsg, phonenum);
    if (!retstatus.getisok()) {
      log.info(retstatus.tostring());
      throw new throwstodataexception(serviceresponsecode.fail_invalid_params, "手机验证码获取失败,请稍后再试");
    }
    // 重置session
    tokenmappool.setattribute(constants.validate_phone, phonenum);
    tokenmappool.setattribute(constants.validate_phone_code, code.tostring());
    tokenmappool.setattribute(constants.send_code_wrongnu, 0);
    tokenmappool.setattribute(constants.send_code_time, new date().gettime());
    log.info(logsuffix + phonenum + "短信验证码:" + code);
  }

处理响应

有的同学会问了 那么响应的报文封装呢?

@requestmapping("record")
@responsebody
public serviceresponse record(string message){
  string userid = loginmanager.getcurrentuserid(); 
  messageboardservice.recordmessage(userid, message);
  return serviceresponsebuilder.buildsuccess(null);
}

其中serviceresponse是封装的响应报文vo,我们直接使用springmvc的@responsebody注解就好了。关键在于这个builder。

import org.apache.commons.lang3.stringutils;

import com.niuxz.base.pojo.serviceresponse;
import com.niuxz.utils.spring.springcontextutil;
import com.niuxz.web.server.token.tokenmappoolbean;

/**
 * 
 * 类 名: serviceresponsebuilder
 * 
 * @version v1.0
 * @date 2016年4月25日
 * @author niuxz
 *
 */
public class serviceresponsebuilder {

  /**
   * 构建一个成功的响应信息
   * 
   * @param body
   * @return 一个操作成功的 serviceresponse
   */
  public static serviceresponse buildsuccess(object body) {
    return new serviceresponse(
        ((tokenmappoolbean) springcontextutil.getbean("tokenmappool"))
            .gettoken(),
        "操作成功", body);
  }

  /**
   * 构建一个成功的响应信息
   * 
   * @param body
   * @return 一个操作成功的 serviceresponse
   */
  public static serviceresponse buildsuccess(string token, object body) {
    return new serviceresponse(token, "操作成功", body);
  }

  /**
   * 构建一个失败的响应信息
   * 
   * @param failcode
   *      msg
   * @return 一个操作失败的 serviceresponse
   */
  public static serviceresponse buildfail(int failcode, string msg) {
    return buildfail(failcode, msg, null);
  }

  /**
   * 构建一个失败的响应信息
   * 
   * @param failcode
   *      msg body
   * @return 一个操作失败的 serviceresponse
   */
  public static serviceresponse buildfail(int failcode, string msg,
      object body) {
    return new serviceresponse(
        ((tokenmappoolbean) springcontextutil.getbean("tokenmappool"))
            .gettoken(),
        failcode, stringutils.isnotblank(msg) ? msg : "操作失败", body);
  }
}

由于使用的是静态工具类的形式,不能通过spring注入tokenmappool(token管理器)对象,则通过spring提供的api获取。然后构建响应信息的时候直接调用tokenmappool的gettoken()方法,此方法会返回当前线程绑定的token字符串。再次强调在请求结束后一定要手动调用clear(我通过全局拦截器调用)。

以上这篇模仿j2ee的session机制的app后端会话信息管理实例就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网