Session管理
一、场景描述:
在你已经清晰的了解了不进行Session管理的环境下,进行普通页面的开发以及进行页面之间的跳转的前提下,你或许会考虑让你的服务器进行Session管理,以控制关键页面的访问和关键数据接口的调用。
二、具体需求:
使用Session进行会话管理,进行Session验证。
三、解决方案:
A、实现思路:
首先想到,需要一个自定义的ContextData(上下文数据),用来缓存会话中产生的业务数据。再需要一个自定义的SessionManager(会话管理者),来进行自定义的Session校验并管理ContextData。在自定义Session校验的逻辑中,可以使用Utility.error(paramString)或error(paramString)来抛出会话异常。当Session异常时,怎么办呢?此时便需要一个自定义的ExceptionHandler(异常管理者),来处理Session异常。在ExceptionHandler中,可以使用MobileServerException.class.isInstance(e)和((MobileServerException)e).getCode()=="-100"来判断是否为Session异常,并作出相应处理。在登陆时,我们可以使用IpuSessionManager.getInstance().createSession(contextData)来创建Session。此方法会返回一个SessionId,我们应将这个SessionId返回给前端JS。前端JS获取到此SessionId后,保存到内存中。通过在common.js中重写核心API(如openPage,callSvc),让前端在访问需要进行Session校验的页面时,自动在传入参数中携带好本次会话的SessionId。最后,别忘了在server-config.xml中配置好你的异常处理类和会话管理类(配置参数为exceptionHandler和sessionManager)。
B、具体实现:
1.自定义上下文数据,类名IpuContextData。如下,在上下文数据中存入了当前登陆用户的用户名。
package com.ipu.server.core.context; import com.ailk.mobile.frame.context.impl.DefaultContextData; public class IpuContextData extends DefaultContextData { private static final long serialVersionUID = 1L; public String getUserName(){ return contextData.getString("USER_NAME"); } public void setUserName(String name){ contextData.put("USER_NAME", name); } }
2.自定义会话管理者,类名IpuSessionManager。如下,用来书写Session校验逻辑,进行登陆状态的检测。由于下面进行了用户名的校验,故需要在每次请求时,除了携带SessionId外,还需要携带当前登陆用户的用户名,并且要求保证本次请求传入的用户名与Context中保存的用户名(登陆时传入的用户名)一致。
package com.ipu.server.core.session; import com.ailk.common.data.IData; import com.ailk.common.util.Utility; import com.ailk.mobile.frame.context.IContextData; import com.ailk.mobile.frame.session.impl.AbstractSessionManager; import com.ipu.server.core.context.IpuContextData; public class IpuSessionManager extends AbstractSessionManager { @Override public void customVerify(String paramString, IData paramIData, IContextData paramIContextData) throws Exception { System.out.println("IpuSessionManager.customVerify()"); String userName = paramIData.getString("USER_NAME"); String contextUserName = ((IpuContextData)paramIContextData).getUserName(); if(userName == null || !userName.equals(contextUserName)){ Utility.error("非法操作,请重新登陆!"); } } }
3.自定义异常管理类,类名IpuExceptionHandler。如下,用来处理Session校验异常。此处需要注意,当在浏览器上访问时,ServletManager.openPage("SessionErr")会生效,当客户端访问时,由于新页面并非来自服务器端,上面的页面跳转不会生效。所以更明智的做法是,在此处书写其它业务逻辑,而是否进行页面跳转的判断在js端去执行。
package com.ipu.server.core.handle; import com.ailk.mobile.frame.handle.impl.DefaultExceptionHandler; import com.ailk.mobile.servlet.ServletManager; import com.ailk.mobile.util.MobileServerException; public class IpuExceptionHandler extends DefaultExceptionHandler { @Override public void pageErrorHandle(Exception e, String pageAction, String data) throws Exception { if(MobileServerException.class.isInstance(e)){ if(((MobileServerException)e).getCode()=="-100"){ ServletManager.openPage("SessionErr"); } } super.pageErrorHandle(e, pageAction, data); //执行父类的逻辑 } }
4.自定义数据接口的父类,类名IpuAppBean。如下,由于一般的数据接口类都是继承自AbstractBean,他的getContextData方法返回的数据类型为IContext,为了避免每次获取时主动的强制类型转换,可以自定义一个数据接口的父类,将强制类型转换的逻辑写入父类中。
package com.ipu.server.core.bean; import com.ailk.mobile.frame.bean.AbstractBean; import com.ipu.server.core.context.IpuContextData; public class IpuAppBean extends AbstractBean { @Override protected IpuContextData getContextData() throws Exception { return (IpuContextData)(getContext().getContextData()); } }
5.创建登陆验证的数据接口,类名Login,方法名checkLogin。如下,从前端传来用户名和密码之后,可以进行用户名和密码的校验,如若校验通过,则首先创建一个新的上下文数据,作为创建Session时的参数,进而创建了一个新的Session,同时,将新的Session对应的SessionId返回给JS端。
package com.ipu.server.bean; import com.ailk.common.data.IData; import com.ailk.common.data.impl.DataMap; import com.ipu.server.core.bean.IpuAppBean; import com.ipu.server.core.context.IpuContextData; import com.ipu.server.core.session.IpuSessionManager; public class Login extends IpuAppBean{ public IData checkLogin(IData param){ String userName = param.getString("USER_NAME"); @SuppressWarnings("unused") String passWord = param.getString("PASS_WORD"); if(userName == null){ userName = "无名氏"; } String sessionId="ERR"; try { IpuContextData contextData = new IpuContextData(); contextData.setUserName(userName); sessionId = IpuSessionManager.getInstance().createSession(contextData); } catch (Exception e) { e.printStackTrace(); } IData resultData = new DataMap(); resultData.put("SESSION_ID",sessionId); return resultData; } }
6.配置Login.checkLogin数据接口。找到etc/server-data.xml配置文件,在datas节点下,添加或修改如下配置。
<action name="Login.checkLogin" class="com.ipu.server.bean.Login" method="checkLogin" verify="false" > </action>
7.配置会话管理类和异常管理类。找到etc/server-config.xml配置文件,添加或修改如下两项配置(exceptionHandler和sessionManager)。
<config name="exceptionHandler" value="com.ipu.server.core.handle.IpuExceptionHandler"/> <config name="sessionManager" value="com.ipu.server.core.session.IpuSessionManager"/>
8.创建自己的登陆页面,文件名Login.html。
<!DOCTYPE html> <html> <head> <title>登录</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <link rel="apple-touch-icon" href="touch-icon-iphone.png" /> <link rel="apple-touch-icon-precomposed" href="touch-icon-iphone.png" /> <!--1,引入CSS文件 --> <link rel="stylesheet" href="biz/ipu/theme/hum.css"> </head> <body> <header class="ui-toolbar" id="header" > <a class="ui-icon icon-left-nav fn-left" href="javascript:;" action="Index" ></a> <h1 class="ui-toolbar-title">登录</h1> </header> <div class="ui-panel-content"> <form> <input type="text" placeholder="用户名" id="userName" /> <input type="text" placeholder="密码" id="passWord" /> <button type="button" class="ui-btn ui-btn-block" id="loginBtn" >登录</button> </form> </div> </body> {%>template/common/Head.html%} <script type="text/javascript" src="biz/js/Login.js"></script> </html>
9.配置自己的登陆页面。找到etc/server-page配置文件,在pages节点下,添加或修改如下配置项。
<action name="Login" template="template/webapp/ipu/Login.html" ></action>
10.书写业务JS逻辑,文件名Login.js。如下,通过Mobile的callSvc方法,访问数据接口Login.checkLogin。在回调方法中,获取到返回的SessionId,并调用Mobile的setMemoryCache方法,将SessionId和UserName存入内存中(以便后续需要时取用,{注:在common.js中需要})。
require(["mobile","zepto","jcl"],function(Mobile,$,Wade){ $("#header").children().click(function(){ Mobile.openPage("Index"); }); $("#loginBtn").click(function(){ Mobile.loadingStart("登陆中,请稍等……","等待"); setTimeout(function(){ Mobile.loadingStop(); var userName = $("#userName").val(); var passWord = $("#passWord").val(); var data = Wade.DataMap(); data.put("USER_NAME",userName); data.put("PASS_WORD",passWord); Mobile.callSvc("Login.checkLogin",data,function(resultData){ var myData = new Wade.DataMap(resultData); var sessionId = myData.get("SESSION_ID"); if(sessionId){ Mobile.setMemoryCache("SESSION_ID",sessionId); Mobile.setMemoryCache("USER_NAME",userName); Mobile.tip("登陆成功!"); Mobile.openPage("Index"); }else{ alert("登陆异常,请联系管理员!"); } }); },600); }); });
11.在web/biz/js/common/common.js中,重写核心Api(如:openPage,callSvc)。如下,从内存中获取到之前存入的SessionId和UserName,将这两个数据合并到即将发往服务端的参数中。即,通过common.js中的方法发送数据请求时,会自动带上SessionId和UserName这两个参数。
this.openPage = function(action,param){ param = param?param:new Wade.DataMap(); Mobile.getMemoryCache(function(data){ data = new Wade.DataMap(data); param.put("SESSION_ID",data.get("SESSION_ID")); param.put("USER_NAME",data.get("USER_NAME")); Mobile.openPage(action,param); },[‘SESSION_ID‘,‘USER_NAME‘]); }; this.callSvc = function(action,param,callback,err){ param = param?param:new Wade.DataMap(); Mobile.getMemoryCache(function(data){ data = new Wade.DataMap(data); param.put("SESSION_ID",data.get("SESSION_ID")); param.put("USER_NAME",data.get("USER_NAME")); Mobile.callSvc(action,param,callback,err); },[‘SESSION_ID‘,‘USER_NAME‘]); };
12.创建一个普通的html页面,文件名Test.html。如下,此页面会将获取到的UserName显示到页面上。注意,此UserName是从上下文数据中获取到,并返回到页面上的。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd "> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> {%>template/common/Head.html%} </head> <body> <h1> Session数据:{%USER_NAME%} </h1> <input type="button" value="打开照相" id="getPhoto" > <script type="text/javascript" src="biz/js/Test.js"></script> </body> </html>
13.创建一个普通的数据接口,类名Test,方法名getData。如下,他继承自自定义的IpuAppBean,并调用了父类中的getContextData方法来获取上下文数据。
package com.ipu.server.bean; import com.ailk.common.data.IData; import com.ailk.common.data.impl.DataMap; import com.ipu.server.core.bean.IpuAppBean; import com.ipu.server.core.context.IpuContextData; public class Test extends IpuAppBean { public IData getData(IData param) throws Exception{ IData data = new DataMap(); IpuContextData contextData = getContextData(); String name = contextData.getUserName(); data.put("USER_NAME", name); return data; } }
14.书写普通html页面对应的JS业务逻辑,文件名Test.js。
require(["mobile","zepto","wadeMobile"],function(Mobile,$,WadeMobile){ $("#getPhoto").click(function(){ Mobile.tip("测试成功!!!"); WadeMobile.getPhoto(function(result){ alert(result); },1); }); });
15.配置普通的数据接口。找到etc/server-data.xml配置文件,在datas节点下,添加或修改如下配置项。注意,此处的verify="true"。说明,访问这个数据接口时,会进行Session校验。
<action name="Test.getData" class="com.ipu.server.bean.Test" method="getData" verify="true" > </action>
16.配置普通的html页面。找到etc/server-page.xml配置文件,在pages节点下,添加或修改如下配置项。
<action name="Test" template="template/webapp/ipu/Test.html" data="Test.getData" > </action>
17.关键点。上述工作完成后,如果需要从某个页面(如index.html页面),跳转到需要进行Session校验的页面(或仅仅是访问需要进行Session校验的数据接口),应当(也必须)使用common.js中重写的核心Api(如,Common.openPage,Common.callSvc),否则会产生Session异常。原因是,使用普通的打开新页面的方法(如:Mobile.openPage,Mobile.callSvc),不会自动将SessionId(以及UserName)带到服务器端,故服务器端的Session校验无法通过。写法,请参看如下示例。
require(["mobile","jquery","common"],function(Mobile,$,Common){ $("#openTestPage").click(function(){ Common.openPage("Test"); }); });