This article only contains ideas and does not provide specific and complete implementation (the blogger is too lazy to sort it out). If you have any questions or want to know, you can send a private message or comment.
background
In traditional Java web small and medium-sized projects, session is generally used to temporarily store session information, such as the identity information of the logger. This mechanism is implemented by borrowing http's cookie mechanism, but it is more troublesome for the app to save and share cookie information every time it requests, and the traditional session is not cluster friendly, so the general app backend services use tokens to distinguish user login information.
Everyone knows the session mechanism of j2ee, which is very convenient to use and is very useful in traditional Java web applications. However, some projects that can be used in Internet projects or clusters have some problems, such as serialization problems, synchronization delay problems, etc., so we need a tool that can solve cluster problems that are similar to sessions.
plan
We use the cache mechanism to solve this problem. The more popular redis is a nosql memory database and has a cache failure mechanism, which is very suitable for storing session data. The token string needs to be returned to the client on the first request, and the client uses this token to identify the identity every time he requests in the future. In order to be transparent about business development, we encapsulate the packets made by the app's request and response. We only need to do some tricks 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 server's protocol encapsulation.
Implementation ideas
1. Formulate a request response message protocol.
2. The parsing protocol processes token strings.
3. Use redis to store to manage tokens and corresponding session information.
4. Provide an API for storing and obtaining session information.
We will explain the implementation plan of each step step.
1. Formulate a request response message protocol.
Since you want to encapsulate the message protocol, you need to consider what is a public field, what is a service field, the data structure of the message, etc.
The public fields requested generally include token, version, platform, model, imei, app source, etc., among which token is the protagonist of our this time.
The common fields of the response generally include token, result status (success, fail), result code (code), result information, etc.
For packet data structure, we choose json because json is common, has good visualization, and has low byte occupancy.
The request message is as follows, and the body stores business information, such as the logged-in username and password, etc.
{ "token": "Client token", /**Client build version number*/ "version": 11, /**Client platform type*/ "platform": "IOS", /**Client device model*/ "machineModel": "Iphone 6s", "imei": "Client string number (mobile phone)", /**Real message body should be map*/ "body": { "key1": "value1", "key2": { "key21": "value21" }, "key3": [ 1, ] }}Responsive message
{ /**Success*/ "success": false, /**Every request will return to a token, and the client should use the latest token for each request*/ "token": "The server selected token for the current request", /**Failed code*/ "failCode": 1, /**Business message or failure message*/ "msg": "Unknown cause", /**Returned real business data can be any serializable object*/ "body": null }}2. The parsing protocol processes token strings.
For the server-side mvc framework, we use the SpringMVC framework, which is also common and will not be described.
Let’s not mention the processing of tokens for the time being. First, how to pass parameters after formulating the packet.
Because the request information is encapsulated, in order for the springmvc framework to correctly inject the parameters we need in the Controller, we need to parse and convert the packets.
To parse the request information, we need to customize the parameter converter of springmvc. By implementing the HandlerMethodArgumentResolver interface, we can define a parameter converter
RequestBodyResolver implements resolveArgument method and injects parameters. The following code is sample code, and do not use it directly.
@Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String requestBodyStr = webRequest.getParameter(requestBodyParamName);//Get request message, you can use any method to pass the message, as long as you get it here, you can get it if(StringUtils.isNotBlank(requestBodyStr)){ String paramName = parameter.getParameterName();//Get parameter name in Controller Class<?> paramClass = parameter.getParameterType();//Get parameter type in Controller/* Parses packets through json tool class*/ JsonNode jsonNode = objectMapper.readTree(requestBodyStr); if(paramClass.equals(ServiceRequest.class)){//ServiceRequest is the VO corresponding to the request packet ServiceRequest serviceRequest = objectMapper.readValue(jsonNode.traverse(),ServiceRequest.class); return serviceRequest;//Return this object to inject into the parameter, it must correspond to the type, otherwise the exception will not be easily caught} if(jsonNode!=null){//Find the parameters required in the Controller from the message JsonNode paramJsonNode = jsonNode.findValue(paramName); if(paramJsonNode!=null){ return objectMapper.readValue(paramJsonNode.traverse(), paramClass); } } } return null; }Configure the parameter converter you defined into the SrpingMVC configuration file <mvc:argument-resolvers>
<mvc:argument-resolvers> <!-- Unified request information processing, fetching data from ServiceRequest--> <bean id="requestBodyResolver"> <property name="objectMapper"><bean></bean></property> <!-- The field name corresponding to ServiceRequest in the configuration request is requestBody --> <property name="requestBodyParamName"><value>requestBody</value></property> </bean></mvc:argument-resolvers>
This allows the parameters in the message to be correctly identified by springmvc.
Next we have to process the token. We need to add a SrpingMVC interceptor to intercept each request. This is a common function and will not be described in detail.
Matcher m1 =Pattern.compile("/"token/":/"(.*?)/"").matcher(requestBodyStr); if(m1.find()){ token = m1.group(1);}tokenMapPool.verifyToken(token);//Perform public processing of token and verifyIn this way, you can get the token and you can do public processing.
3. Use redis to store to manage tokens and corresponding session information.
In fact, it is just writing a redis operation tool class. Because spring is used as the main framework of the project, and we don’t use many functions of redis, we directly use the CacheManager function provided by spring.
Configure org.springframework.data.redis.cache.RedisCacheManager
<!-- Cache Manager global variables, etc. can be used to access --><bean id="cacheManager"> <constructor-arg> <ref bean="redisTemplate"/> </constructor-arg> <property name="usePrefix" value="true" /> <property name="cachePrefix"> <bean> <constructor-arg name="delimiter" value=":@WebServiceInterface"/> </bean> </property> <property name="expires"><!-- Cache Validity Period--> <map> <entry> <key><value>tokenPoolCache</value></key><!-- tokenPool cache name--> <value>2592000</value><!-- Valid time--> </entry> </map> </property></bean>
4. Provide an API for storing and obtaining session information.
Through the above foreplay, we have almost processed the token. Next, we will implement the token management work.
We need to make business development convenient to save and obtain session information, and tokens are 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.ValueWrapper;import org.springframework.cache.Cache.ValueWrapper;import org.springframework.cache.CacheManager;/** * * Class name: TokenMapPoolBean * Description: token and related information call processing class* Modification record: * @version V1.0 * @date April 22, 2016* @author NiuXZ * */public class TokenMapPoolBean { private static final Log log = LogFactory.getLog(TokenMapPoolBean.class); /** token corresponding to the current request*/ 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 the token is legal, return token. Create a new token and return, * Put the token in ThreadLocal and initialize a tokenMap * @param token * @return token */ public String verifyToken(String token) { // log.info("CheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheCheC verifyedToken = null; if (tokenGenerator.checkTokenFormat(token)) { // log.info("CheckToken successful:/"+token+"/""); verifyedToken = token; } else { verifyedToken = newToken(); } currentToken.set(verifiedToken); Cache cache = cacheManager.getCache(cacheName); if (cache == null) { throw new RuntimeException("Cache pool where token is stored cannot be obtained, chacheName:" + cacheName); } ValueWrapper value = cache.get(verifiedToken); //The corresponding value of token is empty, create a new tokenMap and put it in the cache if (value == null || value.get() == null) { verifyedToken = newToken(); currentToken.set(verifiedToken); Map<String, Object> tokenMap = new HashMap<String, Object>(); cache.put(verifiedToken, tokenMap); } return verifyedToken; } /** * Generate a new token * @return token */ private String newToken() { Cache cache = cacheManager.getCache(cacheName); if (cache == null) { throw new RuntimeException("Cache pool where the token is stored cannot be obtained, chacheName:" + cacheName); } String newToken = null; int count = 0; do { count++; newToken = tokenGenerator.generatorToken(); } while (cache.get(newToken) != null); // log.info("Create the Token successfully:/"+newToken+"/" Try to generate:"+count+" times"); return newToken; } /** * Get the object of the corresponding key in the tokenMap of the current request* @param key * @return The attribute of the corresponding key in the tokenMap of the current request, simulate session */ public Object getAttribute(String key) { Cache cache = cacheManager.getCache(cacheName); if (cache == null) { throw new RuntimeException("Cannot get the cache pool where the token is stored, 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 currently requested tokenMap and simulate session<br> * TODO: There is a problem with setting attribute in this way: <br> * 1. When cache.put(currentToken.get(),tokenMap); may be executed under concurrent conditions of the same token, <br> * tokenMap may not be the latest, which will cause data loss. <br> * 2. Put the entire tokenMap every time. The data volume is too large and needs optimization. <br> * @param key value */ public void setAttribute(String key, Object value) { Cache cache = cacheManager.getCache(cacheName); if (cache == null) { throw new RuntimeException("Cannot get the cache pool where the token is stored, 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); } /** * Get the user token bound to the current thread * @return token */ public String getToken() { if (currentToken.get() == null) { //Initialize token once verifyToken(null); } return currentToken.get(); } /** * Delete token and tokenMap * @param token */ public void removeTokenMap(String token) { if (token == null) { return; } Cache cache = cacheManager.getCache(cacheName); if (cache == null) { throw new RuntimeException("CacheName:" + 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 a request corresponds to a thread in the servlet container, and it is in the same thread during the life cycle of a request, and multiple threads share the token manager at the same time, so this thread local variable is needed to save the token string.
Notes:
1. The call to verifyToken method must be called at the beginning of each request. And after the request is completed, clear is called to clear, so as not to cause the verificationToken to be executed next time, but the token is taken out from ThreadLocal when it returns. (This bug has bothered me for several days, and the company's n development check codes were not found. Finally, after testing, I found that the interceptor was not entered when 404 occurred, so I did not call the verificationToken method, which caused the token in the returned exception information to be the token requested last time, resulting in a strange string number problem. Well, remember me a big pot).
2. The client must save each token when encapsulating the http tool and use it for the next request. The company's ios development requested outsourcing, but the outsourcing did not do as required. When not logged in, the token is not saved. Each time the token is passed, it is null, resulting in a token being created for each request, and the server creates a large number of useless tokens.
use
The usage method is also very simple. The following is the encapsulated login manager. You can refer to the application of token manager for login manager.
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 name: LoginManager * Description: LoginManager* Modify record: * @version V1.0 * @date July 19, 2016* @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("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);//Login record before logging out tokenMapPool.setAttribute(Constants.LOGGED_USER_ID, userId); cache.put(userId, tokenMapPool.getToken()); } public void logoutCurrent(String phoneTel) { String curUserId = getCurrentUserId(); log.info("User Logout: userId=" + curUserId); tokenMapPool.removeTokenMap(tokenMapPool.getToken());//Login if (curUserId != null) { Cache cache = cacheManager.getCache(cacheName); cache.evict(curUserId); cache.evict(phoneTel); } } /** * Get the id of the current user * @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; } }Below is a common SMS verification code interface. Some applications also use session to store verification codes. I do not recommend using this method. The disadvantages of storing sessions are quite large. Just look at it, it wasn't what I wrote
public void sendValiCodeByPhoneNum(String phoneNum, String hintMsg, String logSuffix) { validatePhoneTimeSpace(); // Get 6-bit random number String code = CodeUtil.getValidateCode(); log.info(code + "------>" + phoneNum); // Call the SMS verification code to send interface RetStatus retStatus = msgSendUtils.sendSms(code + hintMsg, phoneNum); if (!retStatus.getIsOk()) { log.info(retStatus.toString()); throw new ThrowsToDataException(ServiceResponseCode.FAIL_INVALID_PARAMS, "The mobile phone verification code has failed to obtain, 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); }Processing response
Some students will ask if there is such a responsive message packaging?
@RequestMapping("record")@ResponseBodypublic ServiceResponse record(String message){ String userId = loginManager.getCurrentUserId(); messageBoardService.recordMessage(userId, message); return ServiceResponseBuilder.buildSuccess(null);}Among them, ServiceResponse is the encapsulated response packet VO. We just need to use the @ResponseBody annotation of springmvc. The key is this 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;/** * Class name: ServiceResponseBuilder * * @version V1.0 * @date April 25, 2016* @author NiuXZ * */public class ServiceResponseBuilder { /** * Build a successful response message* * @param body * @return A ServiceResponse with successful operation */ public static ServiceResponse buildSuccess(Object body) { return new ServiceResponse( ((TokenMapPoolBean) SpringContextUtil.getBean("tokenMapPool")) .getToken(), "Action Success", body); } /** * Build a successful response message* * @param body * @return A ServiceResponse with successful operation */ public static ServiceResponse buildSuccess(String token, Object body) { return new ServiceResponse(token, "Action successful", body); } /** * Build a failed response message* * @param failCode * msg * @return A ServiceResponse that failed operation */ public static ServiceResponse buildFail(int failCode, String msg) { return buildFail(failCode, msg, null); } /** * Build a failed response message* * @param failCode * msg body * @return A ServiceResponse that failed operation */ public static ServiceResponse buildFail(int failCode, String msg, Object body) { return new ServiceResponse( ((TokenMapPoolBean) SpringContextUtil.getBean("tokenMapPool")) .getToken(), failCode, StringUtils.isNotBlank(msg) ? msg : "Operation failed", body); }}Since we use the form of a static tool class, we cannot inject to the tokenMapPool (token manager) object through spring, and then we can obtain it through the API provided by spring. Then, when constructing the response information, directly call the getToken() method of tokenMapPool. This method will return the token string bound by the current thread. Again, it is important to call clear manually after the request is over (I call it through the global interceptor).
The above example of the app backend session information management that imitates J2EE's session mechanism is all the content I share with you. I hope you can give you a reference and I hope you can support Wulin.com more.