【CSII-PE】重复登录控制分析

重复登录控制分析

重复登录控制指的是同一帐号在同一时间只能在一个客户端上登录。后登陆的会将先登录的踢出去。下面分析这个功能是怎么实现的,首先踢出先前登录用户的操作当然也是在登录的过程中实现的,如果当前用户登录成功,那么就把该帐号之前的登录注销掉。代码实现主要在LoginAction中:

public void execute(Context context) throws PeException {

    //为当前登录获取一个新的User实例
    User user = getUserManager().getUser(context);

    if (user == null){
      throw new ValidationException("validation.login_failed");
    }
    
    //验证权限
    if (!getUserManager().authenticate(user, context)){

      registerUserAfterFailure(user, context.getClientInfo());
      
      throw new ValidationException("validation.login_failed");
    }
    
    //设置User数据
    getUserManager().initUser(user, context);   

    //设置到用户到Context
    context.setUser(user);

    //注册已登录成功的用户
    registerUserAfterSuccess(user, context.getClientInfo());
}

首先调用UserManager来获取一个初始化的User对象,之前的分析中也说过,UserManager有两种类型,一种差不多起转发作用,一种是实际处理。比如个人网银中实际处理作用的有一个CertUserManager:

<bean id="CertUserManager" class="com.csii.mcs.ibs.common.login.CertUserManager">
    <param name="userBeanName">user</param>
    <ref name="uniqueIdResolver">uniqueUserIdResolver</ref>
    <ref name="rolePool">rolePool</ref>       
    <ref name="pinModuleDelegate">PinModuleDelegate</ref>
    <ref name="transportBean">mca.TransformerTransport</ref>   
    <ref name="cachedRulePool">CachedRulePool</ref>   
    <ref name="bankProductDefResolver">BankProductDefResolver</ref>   
    <ref name="securityAccessControl">CertAccessControl</ref>   
    <ref name="idFactory">idFactory</ref>
    <ref name="userRuleUtil">UserRuleUtil</ref>
</bean>

我们看getUser方法,在父类AbstractUserManager中:

public User getUser(Context context) throws PeException {
    User user = (User)getApplicationContext().getBean(getUserBeanName());
    
    String uniqueId = getUniqueIdResolver().resolve(context);
    user.setUniqueId(uniqueId);
    user.setClientCertificate(getClientCertificate(context));

    //清空了当前Context的User对象
    context.setUser(null);
    
    return getUserInternal(user, context);
}

拿到User实例之后设置了一个uniqueId,这个Id是有UniqueIdResolver生成的,可以看到CertUserManager中注入的uniqueIdResolver是UniqueUserIdResolver:

<bean id="uniqueUserIdResolver" class="com.csii.pe.accesscontrol.lc.MultiFieldsUniqueUserIdResolver">
  <param name="variableNames">LoginId</param>
</bean>

这里设置了一个variableNames属性,并且值为LoginId,我们知道在登录时报文中LoginId指的是登录网银的用户名,登录网银的用户名肯定不会重复,那么可以猜测这个uniqueUserIdResolver就是使用网银登录Id来作为唯一标识的:

public String resolve(Context context) {
    String firstField = (String)context.getData(variableNames[0]);
    StringBuffer sb = firstField != null ? new StringBuffer(firstField) : new StringBuffer();
    
    for (int i = 1; i < variableNames.length; i++) {
      sb.append('/');
      sb.append(context.getData(variableNames[i]));
    }
    return sb.toString();
}

public void setVariableNames(String variableNames) 
    this.variableNames = variableNames.split(",");
}

查看代码后发现和我们想的基本一样,这个类产生的唯一标识是从Context中读取指定字段,并且使用'/'来拼接。

获取到User对象后,接下来就是验证用户登录密码和状态。如果验证通过,用户可以登录。那么就将User对象设置到Context中:

public void setUser(User user){

    if (user == super.getUser()){
      if ((session != null) && (user != null))
        session.setAttribute("_USER", user);
      return;
    }
    
    if (session == null) {
      session = request.getSession();

    } else if (reservedSessionAttributes != null){

      Map map = new HashMap(reservedSessionAttributes.size());
      for (Iterator it = reservedSessionAttributes.iterator(); it.hasNext();){
        String key = (String)it.next();
        map.put(key, session.getAttribute(key));
      }
      
      //让当前session失效,
      session.invalidate();
      //创建了新的Session
      session = request.getSession();
      
      for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
        Map.Entry entry = (Map.Entry)it.next();
        session.setAttribute((String)entry.getKey(), entry.getValue());
      }
      
    } else {

      session.invalidate();
      session = request.getSession();
    }
    
    if (user != null) {
      //将User对象设置到Session中
      session.setAttribute("_USER", user);
    } else {
      session.removeAttribute("_USER");
    }
    

    //设置User对象为Context的属性
    super.setUser(user);
   
}

代码中可以看到,在调用setUser时如果设置的User对象和之前存在与context中的User对象不是同一个,那么会将当前的Session销毁,获取一个新的Session,并且将User对象设置到新的Session中。这就是为什么登录前后服务器中SessionID不一样的原因。

将User对象设置到Session中之后,调用了registerUserAfterSuccess方法来将用户放置到已经登录用户的列表中:

protected void registerUserAfterSuccess(User user, ClientInfo clientInfo) {
    UserRegistryItem item = new UserRegistryItemImpl();
    
    item.setCounter(1);
    item.setAccessDate(System.currentTimeMillis());

    //携带上此次登录成功的User对象,方便后续调用
    item.setUser(user);
    item.setClientInfo(clientInfo); 

    getFailureResourceList().remove(user.getUniqueId());
    
    getSuccessResourceList().add(user.getUniqueId(), item);
}

查看LoginAction的配置:

<action id="LoginAction" class="com.csii.mcs.ibs.action.IbsLoginAction">
    <ref name="successResourceList">successUserRegistry</ref>
    <ref name="failureResourceList">failureUserRegistry</ref>
    <ref name="userManager">userManager</ref>
</action>

这里的successResourceList使用的是successUserRegistry:

扫描二维码关注公众号,回复: 2765985 查看本文章
<userRegistry id="successUserRegistry" class="com.csii.pe.accesscontrol.lc.OnlineUserRegistry" /> 

查看其实现类OnlineUserRegistry的add方法(在父类AbstractHashMapResourceList中):

public void add(String uniqueId, ResourceItem item){
    synchronized (hashMap){
   
      //获取之前已经登录的用户(userId相同的)
      ResourceItem currentItem = (ResourceItem)hashMap.get(uniqueId);
      //抽象方法,子类中实现
      preAdd(uniqueId, currentItem, item);
      //将当前登录成功的用户放入(覆盖之前登录的用户)
      hashMap.put(uniqueId, item);
    }
}

protected void preAdd(String uniqueId, ResourceItem currentItem, ResourceItem newItem){
    logoutCurrentUser(currentItem);
}

private void logoutCurrentUser(ResourceItem currentItem){
    if (currentItem != null) {
      //获取到之前登录成功是保存的User对象
      User user = ((UserRegistryItem)currentItem).getUser();
      if (user != null) {
        //状态改为已退出
        user.logout();
      }
    }
}

从这里可以看到,当用户登录时,在他之前登录的用户(存在于不同的session中的User对象)的状态就会设置为已签退。那么之前登录的用户再操作时,程序检测Session中的User对象的状态已经是退出,那么就会将用户强制退出。强制签退这一功能在MainController中实现:

<bean id="mainController" class="com.csii.pe.channel.http.servlet.MainController" >
    <!--强制签退时展示的视图名称-->

    <param name="loginView">forceout</param>
    <ref name="coreController">coreController</ref>
    <ref name="constantsMap">constantsMap</ref>

    <ref name="idResolver">extendedIdResolver</ref>
    <ref name="contextResolver">extendedContextResolver</ref>

    <ref name="exceptionHandler">exceptionHandler</ref>
</bean>

查看MainController的源码:

public Object process(HttpServletRequest request, HttpServletResponse response, Locale locale) throws PeException {
    Context context = null;
    try {
      if ((contextResolver instanceof ExtendedContextResolver)) {
        context = ((ExtendedContextResolver)contextResolver).resolveContext(request, response, locale, idResolver, streamMIMETypes);
      } else {
        context = contextResolver.resolveContext(request, locale, idResolver);
      }
      
      String viewName = context.getString("_viewReferer");
      if (viewName == null) {
        viewName = request.getParameter("_viewReferer");
      }
      
      context.setTrsFlowContext(contextResolver.resolveTrsFlowContext(request));
      
      request.setAttribute("_viewReferer", viewName);
      
      User user = context.getUser();
      
      //其他地点登录,强制签退当前用户
      if ((user != null) &&  (user.isLogout())) {
        try {
          context.setUser(null);
        }catch (NullPointerException localNullPointerException) {
        }
       
        //设置展示页面为签退页面
        request.setAttribute("_viewReferer", loginView);
        return null;
      }
           
      preExecute(request, response, context, locale);
      
      coreController.execute(context);
      
      TransactionConfig tc = context.getTransactionConfig();
      
      request.setAttribute(Constants.TRANSACTION_APPLICATION_CONTEXT_ATTRIBUTE, tc.getApplicationContext());
      
      afterExecute(request, response, context, locale);
      
      String viewName = resolveViewName(context);
      
      request.setAttribute("_viewReferer", viewName);
      return context.getDataMap();
      
    }catch (Exception e){
        //省略代码
    }finally{
        //省略代码
    }
}

之前以为同一用户同时只能登录一个的功能是由chain中的loginControlCommand实现

<chain id="chainForLoginControl">
    <commands>
        <ref>validationCommand</ref>
        <ref>tokenControlVercodeCommand</ref>
        <ref>loginControlCommand</ref>
        <ref>delegateCommand</ref>
    </commands>
</chain>

看了代码之后发现这个Command不是用来实现上述功能的,而是检测用户是否频繁登录失败,防止别人用程序来强制破解密码。

by CSII@王大仙

猜你喜欢

转载自blog.csdn.net/joxlin/article/details/81626543
PE