Java实现不同用户从不同设备登录的会话管理

有时用户从不同的设备登录服务器,新登录的会话应该要踢掉旧会话。在我们的产品中,用户主要可以通过网页前端登录和从收银机POS登录。用户可以在浏览器和一台POS收银机同时登录,两个会话互不影响。用户在客户端登录前,需要先让POS收银机登录,POS收银机也需要一个会话信息,以便服务器区分不同的POS收银客户端。本文将介绍POS收银机的登录过程和用户在网页端或POS客户端登录后的会话管理,包括:“POS收银机登录”、“用户在网页登录后设置POS会话”、“用户在浏览器、POS收银机不同设备登录后的会话管理”。

1、POS收银机登录。

(1)POS客户端请求服务器的getToken接口,获取RSA公钥的模数和指数。

POS收银机请求服务器,请求携带着自己的posID信息:

//pos GetToken
RequestBody body = new FormBody.Builder()
        .add(pos.field.getFIELD_NAME_ID(), String.valueOf(pos.getID()))
        .build();
Request request = new Request.Builder()
        .url(Configuration.HTTP_IP + "pos/getTokenEx.bx")
        .post(body)
        .build();
httpEvent.setEventProcessed(false);
httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
HttpRequestUnit hru1 = new PosGetToken();
hru1.setRequest(request);
hru1.setTimeout(TIME_OUT);
hru1.setbPostEventToUI(true);
hru1.setEvent(httpEvent);
HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru1);
//
long lTimeOut = TIME_OUT;
while (httpEvent.getStatus() != BaseEvent.EnumEventStatus.EES_Http_Done && lTimeOut-- > 0) {
    Thread.sleep(1000);
}
if (httpEvent.getStatus() != BaseEvent.EnumEventStatus.EES_Http_Done) {
    log.info("pos getToken超时!");
    return;
}
if (httpEvent.getLastErrorCode() != ErrorInfo.EnumErrorCode.EC_NoError) {
    return;
}

(2)服务器收到客户端的getToken请求,生成RSA公私钥,把公钥的模数和指数发送给客户端。

	@RequestMapping(value = "/getTokenEx", produces = "plain/text; charset=UTF-8", method = RequestMethod.POST)
	@ResponseBody
	protected String getTokenEx(@ModelAttribute("SpringWeb") Pos pos, ModelMap model, HttpSession session) throws Exception {
		if (!canCallCurrentAction(session, BaseAction.EnumUserScope.ANYONE.getIndex())) {
			logger.debug("无权访问本Action");
			return null;
		}
		
		logger.info("====Pos token SessionID===" + session.getId());
		if (pos.getOperationType() == 0) { // int3为0代表是login操作,需要清空会话,1为其他操作,不需要清空会话
			EnumSession.clearAllSession(session);
		}
		logger.info("POS获取登录密码的公钥,pos" + pos);

		logger.info("posBO hash1 = " + posBO.hashCode());
		RSAInfo rsa = generateRSA(String.valueOf(pos.getID()));
		logger.info("当前的方法名称:" + new Exception().getStackTrace()[0].getMethodName() + "\t\tFiddler的http头的cookie字符串为:\nCookie: JSESSIONID=" + session.getId());
		session.setAttribute(EnumSession.SESSION_POS_ID.getName(), String.valueOf(pos.getID()));
		Map<String, Object> params = getDefaultParamToReturn(true);
		params.put("rsa", rsa);
		params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_NoError.toString());

		params.put(KEY_HTMLTable_Parameter_msg, posBO.getLastErrorMessage());
		logger.info("getTokenEx返回的数据=" + params);

		return JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();
	}

generateRSA生成RSA公私钥,放到RSAInfo类的对象存储。

mapRSA是hashmap结构,存储着本次请求的公私钥信息,key为posID,value为公私钥信息,主要是为了后续的解密客户端的密文密码:

	public RSAInfo generateRSA(String id) throws Exception {
		HashMap<String, Object> map = RSAUtils.getKeys();
		RSAPublicKey publicKey = (RSAPublicKey) map.get("public");

		mapRSA.put(id, map);

		String modulus = publicKey.getModulus().toString(16);
		String exponent = publicKey.getPublicExponent().toString(16);

		RSAInfo rsa = new RSAInfo();
		rsa.setExponent(exponent);
		rsa.setModulus(modulus);

		return rsa;
	}

(3)客户端收到模数和指数后,对要发送给服务器的登录密码进行加密。

//pos Login
httpEvent.setEventProcessed(false);
Response response = httpEvent.getResponse();
//解析modulus和exponent //...model.parse1
String responseData = httpEvent.getResponseData();
JSONObject jsonObject = new JSONObject(responseData);
String str = jsonObject.getString("rsa");
JSONObject rsaData = new JSONObject(str);
String modulus = rsaData.getString("modulus");
String exponent = rsaData.getString("exponent");
//生成公钥
modulus = new BigInteger(modulus, 16).toString();
exponent = new BigInteger(exponent, 16).toString();
//
RSAPublicKey publicKey = RSAUtils.getPublicKey(modulus, exponent);
//...加密密码
pwdEncrypted = RSAUtils.encryptByPublicKey(pos.getPasswordInPOS(), publicKey);

(4)客户端请求服务器loginEx接口,进行登录,把posID和加密后的密码发给服务器。

    //解析Session
    Headers headers = response.headers();
    List<String> cookies = headers.values("Set-Cookie");
    String cookie = cookies.get(0);
    GlobalController.getInstance().setSessionID(cookie.substring(0, cookie.indexOf(";")));
    log.info("tempSessionID: " + GlobalController.getInstance().getSessionID());
    httpEvent.setTempSessionID(GlobalController.getInstance().getSessionID());
    httpEvent.setPwdEncrypted(pwdEncrypted);
    httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
    //
    RequestBody loginBody = new FormBody.Builder()
            .add(pos.field.getFIELD_NAME_ID(), String.valueOf(pos.getID()))
            .add(pos.field.getFIELD_NAME_pwdEncrypted(), pwdEncrypted)
            .add(pos.field.getFIELD_NAME_companySN(), Constants.MyCompanySN) //... TODO
            //
            .add(pos.field.getFIELD_NAME_resetPasswordInPos(), String.valueOf(pos.getResetPasswordInPos()))
            .build();
    //
    Request requestLogin = new Request.Builder()
            .url(Configuration.HTTP_IP + "pos/loginEx.bx")
            .addHeader(BaseHttpBO.COOKIE, GlobalController.getInstance().getSessionID())
            .post(loginBody)
            .build();
    //
    httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
    HttpRequestUnit hru = new PosLogin();
    hru.setRequest(requestLogin);
    hru.setTimeout(TIME_OUT);
    hru.setbPostEventToUI(true);
    hru.setEvent(httpEvent);
    HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru);
    //
    while (httpEvent.getStatus() != BaseEvent.EnumEventStatus.EES_Http_Done && lTimeOut-- > 0) {
        Thread.sleep(1000);
    }
    if (httpEvent.getStatus() != BaseEvent.EnumEventStatus.EES_Http_Done) {
        log.info("pos login超时!");
        return;
    }
    if (httpEvent.getLastErrorCode() != ErrorInfo.EnumErrorCode.EC_NoError) {
        return;
    }
} catch (Exception e) {
    e.printStackTrace();
    httpEvent.setLastErrorCode(ErrorInfo.EnumErrorCode.EC_OtherError); //
    log.info("POS登录失败,错误信息:" + e.getMessage());
}

(5)服务器收到请求客户端的login请求。

用私钥解密密文密码,并与数据库的账号密码进行对比,完成登录验证:

	public BaseAuthenticationModel login(String dbName, int iUseCaseID, BaseModel bmIn, String sPasswordEncrypted) {
		bmIn.setReturnSalt(RETURN_SALT);
		DataSourceContextHolder.setDbName(dbName);
		BaseModel bm = retrieve1Object(BaseBO.SYSTEM, iUseCaseID, bmIn);
		if (getLastErrorCode() != EnumErrorCode.EC_NoError || bm == null) {
			lastErrorCode = ErrorInfo.EnumErrorCode.EC_NoSuchData;

			return null;
		}

		if (!isActiveObject(bm)) {
			return null;
		}

		String pwd = decrypt(((BaseAuthenticationModel) bmIn).getKey(), sPasswordEncrypted);
		if (pwd == null) {
			return null;
		}

		String md5 = MD5Util.MD5(pwd + BaseAction.SHADOW);
		if (md5 == null) {
			lastErrorCode = ErrorInfo.EnumErrorCode.EC_OtherError;
			return null;
		}
		if (md5.equals(((BaseAuthenticationModel) bm).getSalt())) {
			return (BaseAuthenticationModel) bm;
		} else {
			lastErrorCode = ErrorInfo.EnumErrorCode.EC_NoSuchData;
		}

		return null;
	}

       登录验证通过后,将登录信息保存到session中:

		DataSourceContextHolder.setDbName(company.getDbName());
		posInOut = (Pos) authenticate(company.getDbName(), BaseBO.INVALID_CASE_ID, posInOut, pos.getPwdEncrypted(), ec);
		if (ec.getErrorCode() == EnumErrorCode.EC_NoError && posInOut != null) {
			session.setAttribute(EnumSession.SESSION_POS.getName(), posInOut);
			Pos posReturn = (Pos) posInOut.clone();
			posReturn.clearSensitiveInfo();// 盐值不返回给请求者以免引起安全问题

			params.put(KEY_Object, posReturn);
		}

2、用户在网页登录后设置POS会话。

用户登录过程在另一篇文章有详细介绍。→《Java使用RSA算法实现安全登录》

用户在网页登录成功后,也会在session设置一个ID为-1的收银机Pos对象存到session中。

这个是为了客户端同步服务器数据而设置,如网页端修改了数据,其它客户端都需要同步这个数据。而POS客户端修改了数据上传到服务器后,其它POS需要同步服务器数据,而自己不需要同步:

			//
			Pos pos = (Pos) session.getAttribute(EnumSession.SESSION_POS.getName());
			if (pos == null) {
				pos = new Pos();
				pos.setID(BaseAction.INVALID_POS_ID);
				session.setAttribute(EnumSession.SESSION_POS.getName(), pos);
			}

3、用户在浏览器、POS收银机不同设备登录后的会话管理。

关键的类为 LoginGuard。

LoginGuard有一个内部类LoginDevice,登录设备类,主要存储登录设备数据库表ID和session会话:

	public class LoginDevice {
		public LoginDevice() {
			// 本系统内部的POSID=INVALID_POS_ID=-1时,代表用户在网页端登录。
			// POSID>0时,代表用户在POS机登录。
			// POSID=0时,代表用户未在网页端和POS机登录。
			clientSessionOfWeb = null;
			posIDOfPos = 0;
		}

		protected HttpSession clientSessionOfWeb;

		public HttpSession getClientSessionOfWeb() {
			return clientSessionOfWeb;
		}

		public void setClientSessionOfWeb(HttpSession clientSessionOfWeb) {
			this.clientSessionOfWeb = clientSessionOfWeb;
		}

		protected int posIDOfPos;

		public int getPosIDOfPos() {
			return posIDOfPos;
		}

		public void setPosIDOfPos(int posIDOfPos) {
			this.posIDOfPos = posIDOfPos;
		}
	}

LoginGuard还定义了一个hashmap变量mapStaffLoginDevice,它的key为公司编号+用户ID,value为LoginDevice:

protected Map<String, LoginDevice> mapStaffLoginDevice;

用户登录后,会进入LoginGurad的login方法:

/**
	 * 用户(不是POS)登录成功后,可能存在其它同类设备的登录会话,需要用本次登录会话替换掉旧的登录会话。旧的登录会话在发送请求到服务器时将被阻止
	 * @param staffID 当前登录的用户ID
	 * @param posID 当前登录的POSID。如果是BaseAction.INVALID_POS_ID,表明是网页端登录。如果是>0,表明是在POS端登录
	 * @param session 用户登录会话的对象,用来区分不同的连接及其浏览器/设备
	 */
	public void login(String companySN, int staffID, int posID, HttpSession session) {
		if ((posID != BaseAction.INVALID_POS_ID && posID <= 0) || staffID <= 0) {
			return;
		}

		lock.writeLock().lock();

		LoginDevice currentLoginDevice = mapStaffLoginDevice.get(generateKey(companySN, staffID)); 
		if (currentLoginDevice != null) {
			if (posID == BaseAction.INVALID_POS_ID) {
				if (currentLoginDevice.getClientSessionOfWeb() != null && currentLoginDevice.getClientSessionOfWeb().hashCode() != session.hashCode()) {
					logger.debug("踢走旧的网页端登录会话,old session=" + currentLoginDevice.getClientSessionOfWeb());
				}
				currentLoginDevice.setClientSessionOfWeb(session);
			} else {
				if (currentLoginDevice.getPosIDOfPos() != posID) {
					logger.debug("踢走旧的POS端登录会话,old posID=" + currentLoginDevice.getPosIDOfPos());
				}
				currentLoginDevice.setPosIDOfPos(posID);
			}
		} else {
			// 之前未有任何登录操作,网页,pos都未登录过
			LoginDevice ld = new LoginDevice();
			if (posID == BaseAction.INVALID_POS_ID) {
				ld.setClientSessionOfWeb(session); // 网页端登录
			} else {
				ld.setPosIDOfPos(posID); // POS机上登录
			}
			mapStaffLoginDevice.put(generateKey(companySN, staffID), ld);
		}

		lock.writeLock().unlock();
	}

login方法首先判断用户有没有登录过。

如果没有登录过,那么就新建一个LoginDevice类把posID、会话信息存起来,然后放到mapStaffLoginDevice缓存。

如果之前有登录过,那么:

如果是网页登录,判断当前的session与在缓存的mapStaffLoginDevice的session是否一致(通过hashcode比较),不一致的话就更新session(踢掉旧会话)。

如果是在POS收银机登录,判断用户在之前有无登录其它POS机。如果有,则更新用户在当前设备登录。

等下一次用户携带session会话访问服务器时,就会进入LoginGuard的needToIntercept方法:

	/**
	 * 在拦截器中检查此请求是否为旧的用户会话发出的请求。
	 * @param staffID 当前登录的用户ID
	 * @param posID 当前登录的POSID。如果是BaseAction.INVALID_POS_ID,表明是网页端登录。如果是>0,表明是在POS端登录
	 * @param session 用户登录会话的对象,用来区分不同的连接及其浏览器/设备
	 * @return true,拦截用户的请求,返回错误信息;false,允许用户继续正常操作
	 */
	public boolean needToIntercept(String companySN, int staffID, int posID, HttpSession session) {
		if ((posID != BaseAction.INVALID_POS_ID && posID <= 0) || staffID <= 0) {
			return true;
		}
		
		boolean needToIntercept = false;

		lock.writeLock().lock();

		LoginDevice currentLoginDevice = mapStaffLoginDevice.get(generateKey(companySN, staffID));
		if (currentLoginDevice != null) {
			if (posID == BaseAction.INVALID_POS_ID) {// 网页端登录
				if (currentLoginDevice.getClientSessionOfWeb().hashCode() != session.hashCode()) {
					needToIntercept = true;
				}
			} else {// POS机上登录
				if (currentLoginDevice.getPosIDOfPos() != posID) {
					needToIntercept = true;
				}
			}
		}

		lock.writeLock().unlock();
		
		return needToIntercept;
	}

needToIntercept方法从mapStaffLoginDevice缓存中获取session。如果是网页登录,判断当前session会话与缓存中的session会话是否一致,不一致的则返回true,表示需要拦截当前请求。

如果是POS收银机登录,判断当前登录的POS设备(通过posID判断)是否与mapStaffLoginDevice缓存的一致。不一致的则返回true,表示需要拦截当前请求。

返回true后,告诉请求这需要重新登录,代码为:

			if (loginGuard.needToIntercept(company.getSN(), staff.getID(), posID, session)) {
				String str = null;
				if (posID == BaseAction.INVALID_POS_ID) { // 这是网页端上的操作
					// 判断是否为ajax请求
					if (httpRequest.getHeader("x-requested-with") != null) {
						httpResponse.addHeader("staffLoginUrl", loginUrl);// 返回url
						httpResponse.addHeader("sessionStatus", "duplicatedSession"); // 返回单点登录标识
						str = "<script language='window.top.location.href='" + loginUrl + "';</script>";// ......
					} else {
						str = "<script language='javascript'>alert('你已经在其它地方登录'); window.top.location.href='" + loginUrl + "';</script>";
					}
				} else { // 这是POS端上的操作
					Map<String, Object> params = new HashMap<String, Object>();
					params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_DuplicatedSession.toString());
					params.put(BaseAction.KEY_HTMLTable_Parameter_msg, "你已经在其它地方登录");
					str = JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();
				}
				response.setContentType("text/html;charset=UTF-8");
				try {
					PrintWriter writer = response.getWriter();
					writer.write(str);
					writer.flush();
					writer.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
				return false;
			}

 

posted @ 2021-12-22 16:17  Boxin-kim  阅读(496)  评论(0)    收藏  举报
Web Analytics
Guang Zhou Boxin