App back end session information management instance that mimics J2EE's session mechanism

  • 2020-11-20 06:06:45
  • OfStack

This article will only provide the idea, not the specific complete implementation (the blogger is too lazy, too lazy to sort out), have any questions or want to know can be private messages or comments

background

In traditional SMALL and medium-sized java web projects, session is generally used to store session information, such as the identity information of the logon. This mechanism is implemented by borrowing http's cookie mechanism, but it is cumbersome for app to save and share cookie information on each request, and traditional session is not cluster-friendly, so app backend services generally use token to distinguish user login information.

j2ee session mechanism everyone know, use very convenient, in the traditional java web applications useful, but in Internet project or cluster 1 project has some problems, such as serialization problems, the time delay of synchronous problem and so on, so we need to use 1 similar session can resolve the problems such as got a cluster of a tool.

plan

We use the cache mechanism to solve this problem. The popular redis is an nosql in-memory database with the cache invalidation mechanism, which is suitable for session data storage. The token string needs to be returned to the client on the first request, and the client will use this token identity for each subsequent request. In order to be transparent to the business development, we encapsulate the app request and response message. We only need to make some changes to the client's http request tool class and the server's mvc framework. The modification of the client's http tool class is very simple, mainly the protocol encapsulation of the server.

Implementation approach

1. Formulate the request response message protocol.

2. The parsing protocol handles token strings.

3. Use redis to store and manage token and corresponding session information.

4. API for saving and obtaining session information.

Let's walk through the implementation of each step step by step.

1. Formulate the request response message protocol.

Since we want to encapsulate the message protocol, we need to consider what is the common field, what is the business field, the data structure of the message, etc.

The common field 1 of the request generally includes token, version, platform, model, imei, app source, etc., among which token is our protagonist this time.

Common fields for responses are token, result status (success,fail), result code (code), result information, and so on.

json is chosen for message data structure because of its universality, good visualization and low byte occupancy.

The request message is as follows: body stores business information such as login username and password.


{
  "token": " The client token",
  /** The client build version number */
  "version": 11,
  /** Client platform type */
  "platform": "IOS",
  /** Client device model */
  "machineModel": "Iphone 6s",
  "imei": " Client string number ( Mobile phone )",
  /** The true message body should map*/
  "body": {
    "key1": "value1",
    "key2": {
      "key21": "value21"
    },
    "key3": [
      1,

    ]
  }
}

Response message


{
    /** The success of */
    "success": false,
    /** Each request is returned token , the client should use the latest on each request token*/
    "token": " Selected by the server for the current request token",
    /** The failure code */
    "failCode": 1,
    /** Business message or failure message */
    "msg": " Unknown reason ",
    /** Returns real business data that can be any serializable object */
    "body": null
  }
}

2. The parsing protocol handles token strings.

The mvc framework for the server is the SpringMVC framework. SpringMVC is also common and does not describe.

Leaving aside the processing of token for the moment, we will first solve the problem of how to pass parameters after making the message.

Because the request information is encapsulated, the message needs to be parsed and transformed in order for the springmvc framework to correctly inject the parameters we need in Controller.

To parse the request information, we need to customize the parameter converter for springmvc, one of which can be defined by implementing the HandlerMethodArgumentResolver interface

RequestBodyResolver implements the resolveArgument method and injects the parameters. The following code is the sample code and should not be used directly.


@Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
    String requestBodyStr = webRequest.getParameter(requestBodyParamName);// To get the request message, you can send the message any way you want, as long as you get it here 
    if(StringUtils.isNotBlank(requestBodyStr)){
      String paramName = parameter.getParameterName();// To obtain Controller The parameter name 
      Class<?> paramClass = parameter.getParameterType();// To obtain Controller Medium parameter type 
      /*  through json The utility class parses the message  */
      JsonNode jsonNode = objectMapper.readTree(requestBodyStr);
      if(paramClass.equals(ServiceRequest.class)){//ServiceRequest Is corresponding to the request message VO
        ServiceRequest serviceRequest = objectMapper.readValue(jsonNode.traverse(),ServiceRequest.class);
        return serviceRequest;// Return to the object It's just injected into the parameter, 1 Make sure to match the type, otherwise the exception is not easy to catch 
      }
      if(jsonNode!=null){// Lookup from the message Controller The parameters required in 
        JsonNode paramJsonNode = jsonNode.findValue(paramName);
        if(paramJsonNode!=null){
          return objectMapper.readValue(paramJsonNode.traverse(), paramClass);
        }
        
      }
    }
    return null;
  }

Configure your self-defined parameter converters to the configuration file of SrpingMVC < mvc:argument-resolvers >


<mvc:argument-resolvers>
  <!--  system 1 Request information processing from ServiceRequest In the data  -->
     <bean id="requestBodyResolver" class="com.niuxz.resolver.RequestBodyResolver">
       <property name="objectMapper"><bean class="com.shoujinwang.utils.json.ObjectMapper"></bean></property>
       <!--  In the configuration request ServiceRequest The corresponding field name, by default requestBody -->
       <property name="requestBodyParamName"><value>requestBody</value></property>
     </bean>
</mvc:argument-resolvers>

This enables the parameters in the message to be correctly identified by springmvc.

Next we need to deal with token, we need to add an SrpingMVC interceptor to intercept every request, this is a common function, do not describe in detail


Matcher m1 =Pattern.compile("\"token\":\"(.*?)\"").matcher(requestBodyStr);
  
if(m1.find()){
  token = m1.group(1);
}
tokenMapPool.verifyToken(token);// right token Do public processing, validation 

This is a simple way to get to token and do the public processing.

3. Use redis to store and manage token and corresponding session information.

In fact, it is to write an redis operation tool class. Since spring is used as the main framework of the project, and we do not use many functions of redis, so we directly use the CacheManager functions provided by spring

Configuration org. springframework. data. redis. cache. RedisCacheManager


<!--  Cache manager   Global variables and so on can be accessed using it -->
<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"><!--  Cache expiration date  -->
    <map>
      <entry>
        <key><value>tokenPoolCache</value></key><!-- tokenPool Cache name  -->
        <value>2592000</value><!--  Valid time  -->
      </entry>
    </map>
  </property>
</bean>

4. API for saving and obtaining session information.

We have dealt with token almost through the above foreplay, and now we are going to implement the management of token

We need to make it easy for business development to save access session information and make token transparent.


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;

/**
 * 
 *  class        The name :  TokenMapPoolBean
 *  tracing        above :  token And the associated information to invoke the processing class 
 *  repair   change   remember   record :  
 * @version  V1.0
 * @date  2016 years 4 month 22 day 
 * @author  NiuXZ
 *
 */
public class TokenMapPoolBean {
  
  
  private static final Log log = LogFactory.getLog(TokenMapPoolBean.class);
  
  /**  Corresponding to the current request 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>();
  }
  
  /**
   *  if token Return if it's legal token , illegal to create 1 A new one token And return ,
   *  will token In the ThreadLocal In the   And initialize 1 a tokenMap
   * @param token
   * @return token
   */
  public String verifyToken(String token) {
    //    log.info(" check Token:\""+token+"\"");
    String verifyedToken = null;
    if (tokenGenerator.checkTokenFormat(token)) {
      //      log.info(" check Token successful :\""+token+"\"");
      verifyedToken = token;
    }
    else {
      verifyedToken = newToken();
    }
    currentToken.set(verifyedToken);
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      throw new RuntimeException(" Failed to access storage token The buffer pool ,chacheName:" + cacheName);
    }
    ValueWrapper value = cache.get(verifyedToken);
    //token If the corresponding value is null, it is created 1 A new one tokenMap Put it in the cache 
    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;
  }
  
  /**
   *  Generate a new token
   * @return token
   */
  private String newToken() {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      throw new RuntimeException(" Failed to access storage token The buffer pool ,chacheName:" + cacheName);
    }
    String newToken = null;
    int count = 0;
    do {
      count++;
      newToken = tokenGenerator.generatorToken();
    }
    while (cache.get(newToken) != null);
    //    log.info(" create Token successful :\""+newToken+"\"  Try to generate :"+count+" time ");
    return newToken;
  }
  
  /**
   *  Gets the current request tokenMap In the corresponding key The object of 
   * @param key
   * @return  Current requested tokenMap In the corresponding key Attributes of simulation session
   */
  public Object getAttribute(String key) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      throw new RuntimeException(" Failed to access storage token The buffer pool ,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);
  }
  
  /**
   *  Set to the current request tokenMap In the simulation session<br>
   * TODO: Set in this way attribute There was a problem: <br>
   * 1 , may be in the same 1token Concurrent execution cache.put(currentToken.get(),tokenMap); When, <br>
   *   tokenMap It may not be up to date, resulting in lost data. <br>
   * 2 Every time, put The whole tokenMap , the amount of data is too large and needs to be optimized <br>
   * @param key value
   */
  public void setAttribute(String key, Object value) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      throw new RuntimeException(" Failed to access storage token The buffer pool ,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);
  }
  
  /** 
   *  Gets the user of the current thread binding token
   * @return token
   */
  public String getToken() {
    if (currentToken.get() == null) {
      // Initialize the 1 time token
      verifyToken(null);
    }
    return currentToken.get();
  }
  
  /**
   *  delete token As well as tokenMap
   * @param token
   */
  public void removeTokenMap(String token) {
    if (token == null) {
      return;
    }
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      throw new RuntimeException(" Failed to access storage token The buffer pool ,chacheName:" + cacheName);
    }
    log.info(" delete 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();
  }
  
}

The ThreadLocal variable is used here because the servlet container has one thread for each request and is in the same thread for the lifetime of each request, and there are multiple threads sharing the token manager at the same time, so this thread-local variable is needed to hold the token string.

Notes:

1, verifyToken method call, 1 must be called at the beginning of each request. It also calls clear to clear after the request, so that the next time an unknown exception causes verifyToken not to be executed, it takes token out of ThreadLocal on return. (This bug has been bothering me for several days, and the company's n development and inspection code has not been found. Finally, After testing, I found that it did not enter the interceptor when 404 occurred, so I did not call the verifyToken method. As a result, token returned in the exception information was the token requested in the last time, resulting in the weird string number problem. Well, count me in 1 cauldron).

2. Client 1 must save token each time it packages the http tool and use it for the next request. The outsourcing of ios development of the company did not do as required. When it was not logged in, token was not saved. Each time, null was delivered, resulting in the creation of 1 token for each request and a large number of useless token were created by the server.

use

The usage is also very simple, the following is the encapsulated login manager, you can refer to the token manager for the application of the login manager in 1


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;

/**
 * 
 *  class        The name :  LoginManager
 *  tracing        above :   Login manager 
 *  repair   change   remember   record :  
 * @version  V1.0
 * @date  2016 years 7 month 19 day 
 * @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(" The user login :userId=" + userId);
    Cache cache = cacheManager.getCache(cacheName);
    ValueWrapper valueWrapper = cache.get(userId);
    String token = (String) (valueWrapper == null ? null : valueWrapper.get());
    tokenMapPool.removeTokenMap(token);// Log in before you log out 
    tokenMapPool.setAttribute(Constants.LOGGED_USER_ID, userId);
    cache.put(userId, tokenMapPool.getToken());
  }
  
  public void logoutCurrent(String phoneTel) {
    String curUserId = getCurrentUserId();
    log.info(" User exit :userId=" + curUserId);
    tokenMapPool.removeTokenMap(tokenMapPool.getToken());// Log out 
    if (curUserId != null) {
      Cache cache = cacheManager.getCache(cacheName);
      cache.evict(curUserId);
      cache.evict(phoneTel);
    }
  }
  
  /**
   *  Gets the current user's 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;
  }
  
}

The following is a common interface for sending SMS captchas. Some applications also use session to store captchas. I do not recommend using session as session has a lot of disadvantages. Just look at it. I didn't write it


public void sendValiCodeByPhoneNum(String phoneNum, String hintMsg, String logSuffix) {
    validatePhoneTimeSpace();
    //  To obtain 6 A random number 
    String code = CodeUtil.getValidateCode();
    log.info(code + "------->" + phoneNum);
    //  Call the short message verification code issuing interface 
    RetStatus retStatus = msgSendUtils.sendSms(code + hintMsg, phoneNum);
    if (!retStatus.getIsOk()) {
      log.info(retStatus.toString());
      throw new ThrowsToDataException(ServiceResponseCode.FAIL_INVALID_PARAMS, " Mobile phone captcha access failed , Please try again later ");
    }
    //  reset 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 + " SMS verification code :" + code);
  }

Process the response

Some of you are asking what about packet encapsulation of the response?


@RequestMapping("record")
@ResponseBody
public ServiceResponse record(String message){
  String userId = loginManager.getCurrentUserId(); 
  messageBoardService.recordMessage(userId, message);
  return ServiceResponseBuilder.buildSuccess(null);
}

ServiceResponse is the encapsulated response message VO, so we just use the @ResponseBody annotation of springmvc. The key is this builder.


{
    /** The success of */
    "success": false,
    /** Each request is returned token , the client should use the latest on each request token*/
    "token": " Selected by the server for the current request token",
    /** The failure code */
    "failCode": 1,
    /** Business message or failure message */
    "msg": " Unknown reason ",
    /** Returns real business data that can be any serializable object */
    "body": null
  }
}
0

Since it is in the form of a static utility class, the tokenMapPool (token manager) object cannot be injected through spring, which is obtained through api provided by spring. The getToken() method of tokenMapPool is then called directly when the response is built, which returns the token string of the current thread binding. Again, it's important to call clear manually after the request ends (I called it through the global interceptor).


Related articles: