返回顶部
首页 > 资讯 > 精选 >如何模仿J2EE的session机制实现App后端会话信息管理
  • 401
分享到

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

j2eesessionapp 2023-05-30 20:05:07 401人浏览 八月长安
摘要

这篇文章主要为大家展示了“如何模仿J2EE的session机制实现App后端会话信息管理”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何模仿J2EE的session机制实现App后端会话信息管

这篇文章主要为大家展示了“如何模仿J2EE的session机制实现App后端会话信息管理”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何模仿J2EE的session机制实现App后端会话信息管理”这篇文章吧。

背景

在传统的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": "客户端串号(手机)",    "body": {    "key1": "value1",    "key2": {      "key21": "value21"    },    "key3": [      1,    ]  }}

响应的报文

{        "success": false,        "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中参数类型            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;public class TokenMapPoolBean {      private static final Log log = LogFactory.getLog(TokenMapPoolBean.class);      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>();  }      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;  }      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;  }      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);  }      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);  }      public String getToken() {    if (currentToken.get() == null) {      //初始化一次token      verifyToken(null);    }    return currentToken.get();  }      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字符串。

注意事项:

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

客户端一定要在封装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;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);    }  }      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")@ResponseBodypublic 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;public class ServiceResponseBuilder {    public static ServiceResponse buildSuccess(Object body) {    return new ServiceResponse(        ((TokenMapPoolBean) SpringContextUtil.getBean("tokenMapPool"))            .getToken(),        "操作成功", body);  }    public static ServiceResponse buildSuccess(String token, Object body) {    return new ServiceResponse(token, "操作成功", body);  }    public static ServiceResponse buildFail(int failCode, String msg) {    return buildFail(failCode, msg, null);  }    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后端会话信息管理”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注编程网精选频道!

--结束END--

本文标题: 如何模仿J2EE的session机制实现App后端会话信息管理

本文链接: https://lsjlt.com/news/221027.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 如何模仿J2EE的session机制实现App后端会话信息管理
    这篇文章主要为大家展示了“如何模仿J2EE的session机制实现App后端会话信息管理”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何模仿J2EE的session机制实现App后端会话信息管...
    99+
    2023-05-30
    j2ee session app
  • 如何实现基于Java SpringBoot的前后端分离信息管理系统
    这篇文章主要介绍了如何实现基于Java SpringBoot的前后端分离信息管理系统,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。主要功能说明用户登录、修改密码、首...
    99+
    2023-06-21
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作