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