项目第六天(系统登录)
1:struts2的validator校验(后台校验)
项目中进行数据校验的方式:
Js校验(前台校验)
Ajax校验(后台校验)
Struts2的validator校验(后台校验)
项目开发的时候,针对需求:提供校验机制,项目经理要求,即做前台校验又做后台校验(保证数据安全),同时由于校验,查询性能也相应降低。
所以要求:
如果数据不是很重要,可以只做前台校验
如果数据很重要,即做前台校验又要做后台校验
第一步:在Action中定义:如果出现校验问题,使用:
if(elecUser==null){
this.addFieldError("error", "用户名输入有误!");
return "error";
}
第
二步:在struts.xml中定义:
<action name="elecMenuAction_*" class="elecMenuAction" method="{1}">
<result name="error">/WEB-INF/page/menu/index.jsp</result>
</action>
第三步:在menu/index.jsp中,定义:
使用struts2的标签,输出错误信息:
<s:fielderror/>
第四步:效果:
第五步:修改错误的样式:字体变红;去掉前面的圆圈
改变错误的样式
<s:fielderror>
标签的封装在:struts2的核心包下
只需要在项目的src下添加2个文件夹template/simple,将fielderror.ftl的文件放置到该文件夹下,此时启动的时候,就会覆盖struts核心包的下加载的内容
修改fielderror.ftl中的内容:
<#list eKeys as eKey><#t/>
<#assign eValue = fieldErrors[eKey]><#t/>
<#list eValue as eEachValue><#t/>
<font color='red'><span>
<#if parameters.escape>${eEachValue!?html}<#else>${eEachValue!}</#if>
</span></font>
</#list><#t/>
</#list><#t/>
2:hibernate的懒加载问题
产生:
当使用hibernate查询一个对象的时候,如果Session关闭,再调用该对象关联的集合或者对象的时候,会产生懒加载异常!
解决方案:
方案一:
在Session关闭之前,查询对象关联的集合或者对象,所有在业务层的方法上添加:
public ElecUser findUserByLogonName(String name) {
String condition = " and o.logonName = ?";
Object [] params = {name};
List<ElecUser> list = elecUserDao.findCollectionByConditionNoPage(condition, params, null);
//数据库表中存在该用户,返回ElecUser对象
ElecUser elecUser = null;
if(list!=null && list.size()>0){
elecUser = list.get(0);
}
/***
* 解决懒加载异常
除了OID之外的其他属性
*/
elecUser.getElecRoles().size();
return elecUser;
}
方案二:在Service层的方法中(Session关闭之前),初始化对象关联的集合或者对象
public ElecUser findUserByLogonName(String name) {
String condition = " and o.logonName = ?";
Object [] params = {name};
List<ElecUser> list = elecUserDao.findCollectionByConditionNoPage(condition, params, null);
//数据库表中存在该用户,返回ElecUser对象
ElecUser elecUser = null;
if(list!=null && list.size()>0){
elecUser = list.get(0);
}
/***
* 解决懒加载异常
*/
Hibernate.initialize(elecUser.getElecRoles());
return elecUser;
}
方案三:在ElecUser.hbm.xml中,添加lazy=”false”,查询用户的同时,立即检索查询用户关联的角色集合:
<set name="elecRoles" table="elec_user_role" inverse="true" lazy="false">
<key>
<column name="userID"></column>
</key>
<many-to-many class="cn.itcast.elec.domain.ElecRole" column="roleID"/>
</set>
表示查询用户的时候,立即检索用户所关联的角色
建议项目开发中不要在.hbm.xml中添加过多的lazy=false,这样如果表关联比较多,不需要查询的对象也被加载了,性能会出现问题。
方案四:使用spring提供的过滤器OpenSessionInViewFilter,在web容器中添加该过滤器
在web.xml中添加:
要求:该过滤器一定要放置到strtus2的过滤器的前面,先执行该过滤器。
<!-- 添加spring提供的过滤器,解决hibernate的懒加载问题 -->
<filter>
<filter-name>OpenSessionInViewFilter</filter-name>
<filter-class>
org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>OpenSessionInViewFilter</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- 配置struts2的过滤器,这是struts2运行的核心 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
表示:OpenSessionInViewFilter过滤器实现的原理:
1:事务提交:spring提供的声明式事务控制,仍然在业务层的方法进行处理,方法执行完毕后,事务会自动提交,如果出现异常,事务就会回滚。但是它延迟了Session关闭的时间。
2:Session关闭:Session在页面上进行关闭,此时当页面上的数据加载完成之后,再关闭Session。
问题:如果你开发的系统对页面数据加载比较大的时候,不适合使用
OpenSessionInViewFilter,这样Session不能及时关闭,另一个Session就无法访问,连接不够使用,就会产生“假死”现象。
3:验证码
作用:防止恶意的测试系统的用户名和密码(利用循环输入用户名和密码测试),采用验证码,每次到登录页面的时候,验证码的值是不同的,需要重新输入。
第一步:index.jsp页面:
<tr>
<td width="100"><img border="0" src="${pageContext.request.contextPath}/images/check.jpg" width="75" height="20"></td>
<td>
<table>
<tr>
<td>
<input type="text" name="checkNumber" id="checkNumber" value="" maxlength="4" size="7">
</td>
<td>
<img src="${pageContext.request.contextPath}/image.jsp" name="imageNumber" id="imageNumber" style="cursor:hand" title="点击可更换图片" height="20" onclick="checkNumberImage()"/>
</td>
</tr>
</table>
</td>
</tr>
添加:
Js方法
function checkNumberImage(){
var imageNumber = document.getElementById("imageNumber");
imageNumber.src = "${pageContext.request.contextPath}/image.jsp?timestamp="+new Date().getTime();
}
image.jsp(生成4位随机数字验证码)
Random random = new Random();
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
}
// 将认证码存入SESSION
session.setAttribute("CHECK_NUMBER_KEY", sRand);
// 输出图象到页面
try {
ImageIO.write(image, "JPEG", response.getOutputStream());
} catch (Exception e) {
}
第二步:在Action中进行校验,创建LoginUtils类
public class LogonUtils {
/**验证验证码输入是否正确*/
public static boolean checkNumber(HttpServletRequest request) {
//从页面中获取输入框的值
String checkNumber = request.getParameter("checkNumber");
if(StringUtils.isBlank(checkNumber)){
return false;
}
//从Session中获取验证码的值
String CHECK_NUMBER_KEY = (String)request.getSession().getAttribute("CHECK_NUMBER_KEY");
if(StringUtils.isBlank(CHECK_NUMBER_KEY)){
return false;
}
return checkNumber.equalsIgnoreCase(CHECK_NUMBER_KEY);
}
}
4:记住我
作用:记住当前用户名和密码,下次登录名不需要用户再次输入
第一步:index.sp页面
<tr>
<td width="100"><img border="0" src="${pageContext.request.contextPath}/images/remeber.jpg" width="75" height="20"></td>
<td>
<input type="checkbox" name="remeberMe" id="remeberMe" value="yes"/>
</td>
</tr>
第二步:Action代码的处理,创建LoginUtils类
public class LogonUtils {
/**记住我功能*/
public static void remeberMe(String name, String password,
HttpServletRequest request, HttpServletResponse response) {
//1:创建2个Cookie,存放指定值
Cookie nameCookie = new Cookie("name",name);
Cookie passwordCookie = new Cookie("password",password);
//2:设置Cookie的有效路径(指定当前项目)
nameCookie.setPath(request.getContextPath()+"/");
passwordCookie.setPath(request.getContextPath()+"/");
//3:设置Cookie的有效时间(1周)
//获取页面复选框的值(用作判断)
String remeberMe = request.getParameter("remeberMe");
//此时表示复选框选中
if(remeberMe!=null && remeberMe.equals("yes")){
nameCookie.setMaxAge(7*24*60*60);
passwordCookie.setMaxAge(7*24*60*60);
}
//此时表示复选框没有被选中
else{
nameCookie.setMaxAge(0);
passwordCookie.setMaxAge(0);
}
//4:将Cookie存放到response中
response.addCookie(nameCookie);
response.addCookie(passwordCookie);
}
}
第三步:在index.jsp页面中读取Cookie中的数据,jsp中嵌套java代码
<%
String name = "";
String password = "";
String checked = "";
Cookie [] cookies = request.getCookies();
if(cookies!=null && cookies.length>0){
for(Cookie cookie:cookies){
if(cookie.getName().equals("name")){
name = cookie.getValue();
checked = "checked";
}
if(cookie.getName().equals("password")){
password = cookie.getValue();
}
}
}
%>
缺点:将java代码放置到jsp上,要求jsp先要执行编译java代码,然后再执行。效率会降低,能否将java代码抽取出去呢?
分析:在跳转到index.jsp页面之前先从Cookie中获取数据,放置到HttpRequest对象中进行显示,这样可以使用过滤器(filter)完成:
第四步:添加过滤器
public class SystemFilter implements Filter {
/**web容器启动的时候,执行的方法*/
public void init(FilterConfig config) throws ServletException {
}
/**每次访问URL连接的时候,先执行过滤器的doFilter的方法*/
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//获取访问的连接地址
String path = request.getServletPath();
//在访问首页index.jsp页面之前,先从Cookie中获取name,password的值,并显示在页面上(完成记住我)
this.forwordIndexPage(path,request);
//放行
chain.doFilter(request, response);
}
/**销毁*/
public void destroy() {
}
/**在访问首页index.jsp页面之前,先从Cookie中获取name,password的值,并显示在页面上(完成记住我)*/
private void forwordIndexPage(String path, HttpServletRequest request) {
if(path!=null && path.equals("/index.jsp")){
String name = "";
String password = "";
String checked = "";
Cookie [] cookies = request.getCookies();
if(cookies!=null && cookies.length>0){
for(Cookie cookie:cookies){
if(cookie.getName().equals("name")){
name = cookie.getValue();
/**
* 如果name出现中文,对中文进行解码
*/
try {
name = URLDecoder.decode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
checked = "checked";
}
if(cookie.getName().equals("password")){
password = cookie.getValue();
}
}
}
request.setAttribute("name", name);
request.setAttribute("password", password);
request.setAttribute("checked", checked);
}
}
}
第五步:在web.xml中添加:
<!-- 自定义过滤器,要求添加到struts2过滤器的前面 -->
<filter>
<filter-name>SystemFilter</filter-name> <filter-class>cn.itcast.elec.util.SystemFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SystemFilter</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
测试后:发现问题:如果name中存在中文,此时中文字符是不能存放到Cookie对象中,使用HttpResponse对象添加Cookie会抛出异常。
解决方案:使用URLEncode类和URLDecode类进行编码和解码
在LogonUtils类对name进行编码:
try {
name = URLEncoder.encode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//1:创建2个Cookie,存放指定值
Cookie nameCookie = new Cookie("name",name);
Cookie passwordCookie = new Cookie("password",password);
在过滤器SystemFilter类对name进行解码:
if(cookie.getName().equals("name")){
name = cookie.getValue();
/**
* 如果name出现中文,对中文进行解码
*/
try {
name = URLDecoder.decode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
checked = "checked";
}
5:jquery的ztree插件的使用(完成动态加载树型结构)
第一步:在left.jsp中
<script language="JavaScript" src="${pageContext.request.contextPath }/script/jquery-1.4.2.js"></script>
<script language="JavaScript" src="${pageContext.request.contextPath }/script/jquery-ztree-2.5.js"></script>
<script language="JavaScript" src="${pageContext.request.contextPath }/script/treeMenu.js"></script>
<link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath }/css/menu.css" />
<link rel="stylesheet" href="${pageContext.request.contextPath }/css/zTreeStyle/zTreeStyle.css" type="text/css">
Left.jsp中使用<ul>
<TABLE border=0 width="20">
<TR>
<TD width=340px align=center valign=top>
<div class="zTreeDemoBackground">
<ul id="menuTree" class="tree" ></ul>
</div>
</TD>
</TR>
</TABLE>
第二步:在treeMenu.js中定义:
var menu = {
setting: {
isSimpleData: true,
treeNodeKey: "mid",
treeNodeParentKey: "pid",
showLine: true,
root: {
isRoot: true,
nodes: []
}
},
loadMenuTree:function(){
$.post("elecMenuAction_showMenu.do",{},function(data){
$("#menuTree").zTree(menu.setting, data);
});
}
};
$().ready(function(){
menu.loadMenuTree();
});
第三步:在Action中添加:
public String showMenu(){
//获取Session中存放的权限字符串(格式:aa@ab@ac)
String popedom = (String) request.getSession().getAttribute("globle_popedom");
//1:查询当前用户所具有的功能权限,使用权限,查询权限表,返回List<ElecPopedom>
List<ElecPopedom> list = elecRoleService.findPopedomListByUser(popedom);
//2:将list放置到栈顶,栈顶的对象转换成json数组的形式
ValueStackUtils.setValueStack(list);
return "showMenu";
}
第四步:(hql语句嵌套查询),Service类定义:
public List<ElecPopedom> findPopedomListByUser(String popedom) {
//hql语句和sql语句的嵌套查询
String condition = " and o.mid IN('"+popedom.replace("@", "','")+"') AND isMenu = ?";
Object [] params = {true};
Map<String, String> orderby = new LinkedHashMap<String, String>();
orderby.put("o.mid", "asc");
List<ElecPopedom> list = elecPopedomDao.findCollectionByConditionNoPage(condition, params, orderby);
return list;
}
第五步:在struts.xml中添加:
<!-- 将集合压入到栈顶,集合返回页面的时候,转换成json的形式 -->
<result name="showMenu" type="json"></result>
6:自定义标签
使用当前用户具有的权限,控制页面上的按钮或者链接是否可见。
详情请见技术资料【技术资料\自定义标签+struts2标签控制访问链接权限】中的《自定义标签(帮助).doc》
7:粗颗粒度权限控制(使用过滤器完成)
分析:
精确到Session的权限控制(判断Session是否存在)
使用过滤器完成粗颗粒的权限控制,如果Session不存在就跳转到首页,如果存在可以通过URL链接访问到对应的操作。
第一步:定义一个过滤器:
public class SystemFilter implements Filter {
/**web容器启动的时候,执行的方法*/
//存放没有Session之前,需要放行的连接
List<String> list = new ArrayList<String>();
public void init(FilterConfig config) throws ServletException {
list.add("/index.jsp");
list.add("/image.jsp");
list.add("/system/elecMenuAction_menuHome.do");
}
/**每次访问URL连接的时候,先执行过滤器的doFilter的方法*/
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//获取访问的连接地址
String path = request.getServletPath();
//在访问首页index.jsp页面之前,先从Cookie中获取name,password的值,并显示在页面上(完成记住我)
this.forwordIndexPage(path,request);
//如果访问的路径path包含在放行的List的存放的连接的时候,此时需要放行
if(list.contains(path)){
chain.doFilter(request, response);
return;
}
//获取用户登录的Session
ElecUser elecUser = (ElecUser)request.getSession().getAttribute("globle_user");
//放行
if(elecUser!=null){
chain.doFilter(request, response);
return;
}
//重定向到登录页面
response.sendRedirect(request.getContextPath()+"/index.jsp");
}
/**销毁*/
public void destroy() {
}
/**在访问首页index.jsp页面之前,先从Cookie中获取name,password的值,并显示在页面上(完成记住我)*/
private void forwordIndexPage(String path, HttpServletRequest request) {
if(path!=null && path.equals("/index.jsp")){
String name = "";
String password = "";
String checked = "";
Cookie [] cookies = request.getCookies();
if(cookies!=null && cookies.length>0){
for(Cookie cookie:cookies){
if(cookie.getName().equals("name")){
name = cookie.getValue();
/**
* 如果name出现中文,对中文进行解码
*/
try {
name = URLDecoder.decode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
checked = "checked";
}
if(cookie.getName().equals("password")){
password = cookie.getValue();
}
}
}
request.setAttribute("name", name);
request.setAttribute("password", password);
request.setAttribute("checked", checked);
}
}
}
第二步:在web容器中添加对应的过滤器:
<!-- 自定义过滤器,要求添加到struts2过滤器的前面 -->
<filter>
<filter-name>SystemFilter</filter-name>
<filter-class>cn.itcast.elec.util.SystemFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SystemFilter</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
问题:不够友好,访问链接,最好提示【非法操作,系统将会5秒后跳转到登录页面】
修改过滤器类中的操作:
(1)在过滤器中的init方法中添加2个放行的连接:
list.add("/error.jsp");
list.add("/system/elecMenuAction_logout.do");
(2)在doFilter的方法重定向到error.jsp
将:
//重定向到登录页面
response.sendRedirect(request.getContextPath()+"/index.jsp");
修改成:
//重定向到error.jsp(5秒跳转到登录名页面)
response.sendRedirect(request.getContextPath()+"/error.jsp");
(3)error.jsp的内容:
<script>
var i=6;
var t;
function showTimer(){
if(i==0){//如果秒数为0的话,清除t,防止一直调用函数,对于反应慢的机器可能实现不了跳转到的效果,所以要清除掉 setInterval()
parent.location.href="${pageContext.request.contextPath }/system/elecMenuAction_logout.do";
window.clearInterval(t);
}else{
i = i - 1 ;
// 秒数减少并插入 timer 层中
document.getElementById("timer").innerHTML= i+"秒";
}
}
// 每隔一秒钟调用一次函数 showTimer()
t = window.setInterval(showTimer,1000);
</script>
注意:Session不应该在服务器一直不清空,如果Session过多,会导致Session压力大,系统变慢,于是要求10分钟如果不操作系统,将Session自动清空。在web.xml中配置
<session-config>
<session-timeout>10</session-timeout>
</session-config>
粗颗粒的权限控制的面试:
使用过滤器
在过滤器中定义放行的连接,因为不是每个操作都会存在Session
在过滤器中获取登录后存放的Session,如果Session不为空,则放行,即可以操作定义的业务功能,如果Session为空,则跳转到登录页面。
控制访问的系统必须要存在Session
8:系统中的异常处理+日志备份(使用struts2的拦截器)
操作:
添加异常处理,详情请见【技术资料\自定义拦截器(实现异常处理+细颗粒权限控制)\异常处理】中的《struts2的拦截器实现异常处理.doc》
项目中的异常处理面试:
使用struts2的拦截器
在拦截器中的doIntercept()方法定义try… catch异常
如果Action、Service、Dao没有抛出异常,则在try模块中指定正确操作的页面,例如:
result = actioninvocation.invoke();
return result;
result跳转到正确的页面
如果Action、Service、Dao抛出异常,则在catch模块中,获取异常,使用log4j存放到指定的日志文件中,通过return “errorMsg”;跳转到错误页面。
9:细颗粒权限控制(使用struts2的拦截器)
操作:
详情参考【技术资料\自定义拦截器(实现异常处理+细颗粒权限控制)\细颗粒度权限控制】中的《struts2的拦截器实现细颗粒度权限控制.doc》
要求:登录操作ElecMenuAction类中的方法,要求不需要添加到struts2的自定义拦截器中
此时可以在struts.xml中定义:
问题:为什么在struts2的拦截器中都要使用roleID,mid,pid去查询一遍数据库,而为什么不从Session中获取mid的值和注解上定义的mid的值进行比较呢?
回答:此时不安全,如果盗用账户,登录系统(一定有Session),即可以操作每一个执行的方法。但是由于挂失后,数据库存放的数据发生变化,操作每一个功能之前都会先查询权限,这样查询才能保证数据安全。
细颗粒的权限控制的面试:
使用struts2的拦截器
定义一个注解(mid和pid),对应权限code和父级权限的code,将注解添加到Action类中方法的上面
每个Action类的方法上添加注解(mid=””,pid=””),表示方法的惟一标识(即该方法所具有的权限)
在struts2的拦截器中,从Session中获取角色ID,获取Action类方法上的注解(mid和pid),使用角色ID,mid和pid查询角色权限表,判断当前用户是否可以操作该方法。
10:今天知识点总结
系统上线前的准备:
权限表的数据:
11:需要掌握的知识点总结
重点:登录操作,项目中后台校验和前台校验,hibernate的懒加载处理
了解:验证码,记住我
必须掌握的分析问题的能力思想:
例如:粗颗粒权限控制
异常处理+日志备份
细颗粒权限控制