加入收藏 | 设为首页 | 会员中心 | 我要投稿 安卓应用网 (https://www.0791zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Java > 正文

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

发布时间:2020-05-23 15:55:54 所属栏目:Java 来源:互联网
导读:此文章只将思想,不提供具体完整实现(博主太懒,懒得整理),有疑问或想了解的可以私信或评论

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

背景

在传统的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的缓存池,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字符串。

注意事项:

(编辑:安卓应用网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读