在上文中有意埋了几个安全彩蛋,以便后面在聊网络安全时使用。

书接前言,对上文做过实践的朋友肯定会发现:当用户注册/登录成功后页面跳转到了系统首页,但首页的导航菜单并没有显示用户名。本文重点实现这个特性,同时也谈谈系统的编码问题。


六、注册/登录成功后导航菜单显示当前用户名

   与JSP不同之处在于,《斗医》本着Web本质特点,让不了解Web的朋友在脑海中有一个整体思路,不要把Web应用想的过于神秘,所以页面展示部分放到了HTML中,服务端部分仅提供查询页面和提供数据,它们之间通过HTTP协议贯通。

   正是这个原因当用户注册/登录成功后跳转到系统首页,浏览器开始渲染main.html页面,由于此时还没有让Javascript去服务端读取用户信息,所以当前用户名没有显示。

   下面实现这个特性:


1、系统首页调用common.js的公共接口

(function(window){

   $(document).ready(function(){

       // 生成系统菜单

       generateSystemMenu();

       // 设置首页菜单被选中

       selectSystemMenu("system_home_menu");

       // 获取用户信息

       getBreifUserInfo();

   });

})(window);


2、common.js中定义getBreifUserInfo()方法,以实现异步向服务端获取数据

/**

* 获取用户的信息:用户名

*/

function getBreifUserInfo(){

   asyncRequest("userBrief.data", null, function(result){

       var briefUser = eval(result); // 其中eval是JS的不安全方法,不建议使用,这里留个彩蛋

       $("#system_login_user_name").text(briefUser.userId);

   });

}


3、配置获取用户信息的业务

在war\WEB-INF\config\sm下定义system-data.xml文件,里面配置如下业务:

<?xml version="1.0" encoding="UTF-8" ?>

<business-config>

      <!--获取用户信息,导航菜单使用-->

      <business name="userBrief" business-class="com.medical.server.data.UserBriefDataAction" />

</business-config>


4、定义com.medical.server.data.UserBriefDataAction.java类,它继承FrameDefaultAction类,同时重写execute()方法

@Override

public String execute() throws FrameException

{

   UserDAO loginUser = FrameCache.getInstance().getUserBySession(session);

   if(loginUser == null)

   {

        loginUser = new UserDAO();

        loginUser.setUserId("游客");

   }

   return gson.toJson(loginUser);

}

   这个方法中使用了FrameCache.getInstance().getUserBySession(session)方法,这个方法是从系统全局缓存中读取会话中的用户。既然有读取那么也有对应的设置,方法如下:

public class FrameCache

{

    public UserDAO getUserBySession(HttpSession session)

    {

         return (UserDAO)session.getAttribute(FrameConstant.SYSTEM_CURRENT_LOGIN_USER);

    }


    public void setUserBySession(HttpSession session, UserDAO currentUser)

    {

         session.setAttribute(FrameConstant.SYSTEM_CURRENT_LOGIN_USER, currentUser);

    }

}

    这里又涉及到一个常量定义,其具体为:FrameConstant.SYSTEM_CURRENT_LOGIN_USER = “systemCurrentLoginUser”;


5、在《【斗医】【11】Web应用开发50天》的注册和登录中没有把当前用户放入全局缓存,下面进行修改:

(1)修改UserUtil.isValideUser()方法,把它更名为getUserDAO(),同时返回值由原来的boolean改为UserDAO

public static UserDAO getUserDAO(String userName, String userAuth)

{

    Session session = FrameDBUtil.openSession();

    Criteria criteria = session.createCriteria(UserDAO.class);

    criteria.add(Restrictions.eq("userId", userName)).add(Restrictions.eq("userAuth", userAuth));

    List<?> userList = criteria.list();

    FrameDBUtil.closeSession();


    if(FrameUtil.isEmpty(userList))

    {

        return null;

    }

    return (UserDAO)userList.get(0);

}


(2)修改UserLoginDataAction的用户注册方法

private String doRegistAction(String userName, String userAuth)

{

   // 1. 判断数据库中是否已存在该用户名

   UserDAO user = UserUtil.getUserByName(userName);

   if (user != null)

   {

       UserLoginBean loginBean = new UserLoginBean();

       loginBean.setErrorCode(FrameErrorCode.USER_SAME_ERROR);

       loginBean.setErrorDesc(FrameUtil.getErrorDescByCode(loginBean.getErrorCode()));

       return gson.toJson(loginBean);

   }


   // 2. 把用户入库

   UserUtil.insertUser(userName, userAuth);


   // 3. 存入会话对应的内存

   user = new UserDAO();

   user.setUserId(userName);

   FrameCache.getInstance().setUserBySession(session, user);


   // 4. 返回用户注册成功JSON对象

   UserLoginBean loginBean = new UserLoginBean();

   loginBean.setErrorCode(FrameErrorCode.USER_LOGIN_SUCCESS);

   loginBean.setErrorDesc(FrameUtil.getErrorDescByCode(loginBean.getErrorCode()));

   loginBean.setForwardPath("index.act");

   return gson.toJson(loginBean);

}


(3)修改UserLoginDataAction的用户登录方法

private String doLoginAction(String userName, String userAuth)

{

   // 1. 判断数据库中是否已存在该用户名

   UserDAO cacheUser = UserUtil.getUserDAO(userName, userAuth);

   if (cacheUser == null)

   {

       UserLoginBean loginBean = new UserLoginBean();

       loginBean.setErrorCode(FrameErrorCode.USER_NOT_EXIST_ERROR);

       loginBean.setErrorDesc(FrameUtil.getErrorDescByCode(loginBean.getErrorCode()));

       return gson.toJson(loginBean);

   }


   // 2. 存入会话对应的内存

   cacheUser.setUserAuth(null);

   FrameCache.getInstance().setUserBySession(session, cacheUser);


   // 3. 返回用户登录成功JSON对象

   UserLoginBean loginBean = new UserLoginBean();

   loginBean.setErrorCode(FrameErrorCode.USER_LOGIN_SUCCESS);

   loginBean.setErrorDesc(FrameUtil.getErrorDescByCode(loginBean.getErrorCode()));

   loginBean.setForwardPath("index.act");

   return gson.toJson(loginBean);

}


【备注】:由于客户端并不需要用户的密码,因此没有必要把用户信息暴露,增加网络安全风险



(4)用例验证

用例1:

前提:系统中没有qingkechina用户

操作:进入系统的登录页面,注册名为qingkechina的用户

期望:注册成功且系统的菜单右上角能显示qingkechina用户名


用例2:

前提:系统中已有qingkechina用户

操作:进入系统的登录页面,以qingkechina用户登录

期望:登录成功且系统的菜单右上角能显示qingkechina用户名


用例3:

前提:系统中没有“陈许诺”用户

操作:进入系统的登录页面,注册名为“陈许诺”的用户

期望:注册成功且系统的菜单右上角能显示“陈许诺”用户名


打开Eclipse启动Tomcat成功后,在浏览器中输入http://localhost:8080/medical回车,按上面的用户验证,会发现前两个英文用例成功,但中文用例存在乱码问题,如下图:  

wKioL1M5JCPgxu-WAAAe6wn9Xh4060.png



七、系统的编码与乱码

   系统出现乱码的原因简而言之是由于:输入与输出编码不一致,比如说浏览器在Windows中文操作系统下运行,Chrome、FireFox缺省是以GBK编码显示,此时若服务端传给浏览器的编码是UTF-8,则就会形成乱码。

   各个国家的程序开发人员为了让系统支持各国的语言,都会采用UTF-8编码来解决全球化的问题。今天在网上搜索时也发现一个比较好玩的文章《大话编码》,感兴趣的可以看一看,就本系统来言存在如下边界:

wKiom1M5KiCy38apAAAbDg1Siy0785.png


解释:

IE浏览器根据操作系统缺省选择编码,可通过“查看 > 编码”来查看;FireFox浏览器在中文windows操作系统下缺省使用unicode编码,可通过“查看 > 字符编码”查看;Chrome浏览器在中文windows操作系统下缺省使用GBK编码,可通过选择“设置 > 高级设置 > 网络内容 > 自定义字体 > 编码”查看;

Tomcat在中文windows操作系统下缺省GBK编码;但Java依赖于JVM的具体环境,一般都是以unicode编码;

mysql安装时缺省以latin1编码;

properties文件当时设置时以utf-8编码;


看着是不是有点晕了?所以若想不让其乱码,最好统一成同一个编码,这里使用UTF-8。


1、浏览器显示使用UTF-8编码

这一点在前面写HTML页面时已指定页面的编码为UTF-8,如打开main.html在它的<head>中已说明

<!--设置字符集-->

<meta http-equiv="content-type" content="text/html;charset=utf-8" />


2、浏览器与Tomcat之间可以通过filter过滤器的方式,让所有的请求和响应都使用UTF-8编码

(1)在war\WEB-INF\web.xml配置编码过滤器

<filter>

   <filter-name>encoder</filter-name>

   <filter-class>com.medical.frame.FrameEncoderFilter</filter-class>        

</filter>

<filter-mapping>

   <filter-name>encoder</filter-name>

   <url-pattern>/*</url-pattern>

</filter-mapping>


【备注】:由于url-pattern配置为/*,它表明所有的请求都经过FrameEncoderFilter



(2)定义FrameEncoderFilter,让其实现Filter接口

public class FrameEncoderFilter implements Filter

{

   @Override

   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)                throws IOException, ServletException

   {

       request.setCharacterEncoding("utf-8");

       response.setCharacterEncoding("utf-8");

       chain.doFilter(request, response);

   }

}


(3)查看Mysql的编码,发现它以latin1编码的(具体查看办法可以问google)

I、关闭mysql进程。以我用的windows操作系统为例,进入“任务管理器 > 进程”,右键mysqld.exe结束进程树

II、打开C:\Program Files\MySQL\MySQL Server 5.5\my.ini文件,找到如下内容修改为utf8

default-character-set=utf8

character-set-server=utf8


【备注】:因为我安装在C盘,请读者根据自己的实际情况处理


III、重启Mysql进程。进入C:\Program Files\MySQL\MySQL Server 5.5\bin,双击mysqld.exe可执行文件


(4)重创建数据库medical和数据表usertable

I、打开所有程序 > MySQL > MySQL Server 5.5 > MySQL 5.5 Command Line Client命令工具

II、在窗口中输入密码进入

III、执行如下SQL语句,如图

wKiom1M5Mxix3xs7AAA44DZYCx0047.png


(5)把Java的unicode转换为utf-8编码

/**

* 把ISO编码的字符串转换为UTF-8编码

*/

public static String convert2UTF8(String resource)

{

   String descStr = resource;

   try

   {

       byte[] resourceArray = resource.getBytes("ISO-8859-1");

       descStr = new String(resourceArray, "utf-8");

   }

   catch (UnsupportedEncodingException e)

   {

       e.printStackTrace();

   }


   return descStr;

}


/**

* 由错误码获取错误描述信息

*/

public static String getErrorDescByCode(int errorCode)

{

   String errorCodeStr = String.valueOf(errorCode);

   String errorDesc = FrameCache.getInstance().getResourceValue(errorCodeStr);

   if (isEmpty(errorDesc))

   {

       return convert2UTF8("系统异常,错误码:" + errorCode);

   }


   return convert2UTF8(errorDesc);

}

再测试一下用例三,结果如下:

wKiom1M5PJmjwRrUAAAUdNw1KrI980.png


八、全局信息提示栏

   做过C/S架构开发的肯定知道模态对话框和非模态对话框的概念,提示信息正是通过对话框来展现给用户的;当然B/S架构也不离外,它也有模态和非模态的对话框。

   但细心的您肯定关注到了:现在的网站展现形式越来越“Web化”。以前大家都用翻页突然一天各大中型网站好像都不翻页了,而改为拖拽样式,像浏览百度图片等。

   这种样式的变化不是必然的,它更符合用户的操作习惯。

   全局信息提示框的大概思路,由showSystemGlobalInfo()方法生成一个div,并把它追加到<body> Dom元素上,然后由navigation.css全局样式渲染它,当使用时直接调用showSystemGlobalInfo()方法,5钞钟之后div自动隐藏掉。

1、在common.js中定义公共方法showSystemGlobalInfo()

/**

* 全局信息提示:在屏幕最下方显示

*/

var sytemGlobalInfoDiv = null;


function showSystemGlobalInfo(message)

{

   // 只初始化一次

   if(!sytemGlobalInfoDiv)

   {

       sytemGlobalInfoDiv = $("<div />").attr("class", "system_global_info").text(message);

       sytemGlobalInfoDiv.appendTo($("body"));

   }


   // 停留5s钟后提示框自动消失

   sytemGlobalInfoDiv.text(message).show();

   setTimeout(function(){

       sytemGlobalInfoDiv.hide();

   }, 5000);

}


2、在navigation.css中定义样式

.system_global_info{

   width: 100%;

   height: 45px;

   line-height: 45px;

   color: #FFF;

   font-size: 14px;

   font-weight: 600;

   text-align: center;    

   background-color: #0767C8;

   position: absolute;

   bottom: 0;

}


3、把用户注册/登录失败的地方更换为调用此方法,涉及login.js的systemUserLogin()方法

var resultJson = eval(result);

if(resultJson.errorCode != 510)

{

   alert(resultJson.errorDesc);

   showSystemGlobalInfo(resultJson.errorDesc);

   return;

}


4、功能测试

(1)先在系统中创建名称为qingkechina的用户,确保创建成功

(2)再次创建名称为qingkechina的用户,此时应该有错误提示,如下图:

wKioL1M-JcDAUE5dAAAY4LkjVr0258.png


【备注】:截止目前登录部分算是完成,里面还涉及一些不安全的处理,一些没有考虑到的,这里暂时保留。接下来完成“下战书”部分业务。