OA第六天的内容是从第42集开始到63集,一共22集。这是所有天中学的视频集数最多的,当初学的时候,本来那天下午用来总结,就不会看这么多,奈何一是看的太起劲了,二是不想总结了,导致看的太多了.不知道这22集,要弄出多少东西来,不过,好在很多东西前面都介绍了,后面需要单独介绍的应该不多了.
看了第五天的学习内容,上面写着"应该再有4天就差不多学完了",现在想来,当时以为的是只要再按照计划学习4天就能学完,并且总结完.可惜啊,我没有按照原来的设想,一边看视频一边总结,导致视频是看完了,总结不想做了.哎,要不是写了第一篇雄心壮阔的博客,我估计也没有现在这几篇博客了.所以,以后看视频还是试试学完当天总结下吧.
不过,照例还是先来介绍下第六天的学习内容:首先就是接着做权限功能,其中各种和权限有关的功能基本都有介绍,如登录注销,权限的分配,功能的是否显示.还有论坛模块的功能,需求分析、设计实体,增删改查不用说了,里面实现上下移动的功能,这个是通过操作后台来处理的。唉,看起来内容好像挺多的!
感觉实现上下移动功能,有两种做法,一种是前台控制,一种是后台控制。前台的就是通过js控制上下移动,然后你真要保存,在点击保存,保存下修改,否则刷新页面,就回到最初;还有就是这种,每次点击上移,就更新一次数据库,然后页面上的显示就可以改变了,看起来就像上下移动了。
分配权限(TreeView)
首先对于权限模块,最重要的功能就是权限分配,以及权限的控制(包括控制菜单是否显示,页面元素是否显示,以及是否登录控制).其中分配权限的效果,如下图所示.
其中需要实现,1.树形结构显示,这样权限之间的关系会很明确;2.全选的选中和取消,影响下面所有权限的选中和取消;3.选中任意子节点,其所有直系父类都要选中;4.选中任意父结点,则其下所有子节点都要选中.而树形结构使用treeview就可以实现;其他都是靠jquery就能实现.
首先看jsp页面的代码,引入treeview的js和css,以及文件夹图标的css.
<script language="javascript"
src="${pageContext.request.contextPath}/script/jquery_treeview/jquery.treeview.js"></script>
<link type="text/css" rel="stylesheet"
href="${pageContext.request.contextPath}/style/blue/file.css" />
<link type="text/css" rel="stylesheet"
href="${pageContext.request.contextPath}/script/jquery_treeview/jquery.treeview.css" />
然后整个树形结构的表单页面代码就是这样,使用ul和li来控制缩进.其中,最外层的ul的id为tree.而所有的checkbox复选框的name都为privilegeIds.回显是通过s:property标签使用ognl的in效果是实现的.而class="folder",则是文件夹图标的效果.
<!-- 显示树状结构内容 -->
<ul id="tree">
<s:iterator value="#application.topPrivilegeList">
<li>
<input type="checkbox" name="privilegeIds" value="${id}" id="cb_${id}" <s:property value="%{id in privilegeIds ? 'checked' : ''}"/> />
<label for="cb_${id}"><span class="folder">${name}</span></label>
<ul>
<s:iterator value="children">
<li>
<input type="checkbox" name="privilegeIds" value="${id}" id="cb_${id}" <s:property value="%{id in privilegeIds ? 'checked' : ''}"/> />
<label for="cb_${id}"><span class="folder">${name}</span></label>
<ul>
<s:iterator value="children">
<li>
<input type="checkbox" name="privilegeIds" value="${id}" id="cb_${id}" <s:property value="%{id in privilegeIds ? 'checked' : ''}"/> />
<label for="cb_${id}"><span class="folder">${name}</span></label>
</li>
</s:iterator>
</ul>
</li>
</s:iterator>
</ul>
</li>
</s:iterator>
</ul>
最后加上这段js,树形结构的效果就可以了.
<script language="javascript">
$("#tree").treeview();
</script>
其中,3和4的效果,用这段js就能实现.
<script type="text/javascript">
$(function(){
// 指定事件处理函数
$("[name=privilegeIds]").click(function(){
// 当选中或取消一个权限时,也同时选中或取消所有的下级权限
$(this).siblings("ul").find("input").attr("checked", this.checked);
// 当选中一个权限时,也要选中所有的直接上级权限
if(this.checked == true){
$(this).parents("li").children("input").attr("checked", true);
}
});
});
</script>
而全选的效果也一样,找到所有复选框,然后他们的选中/取消和全选的保持一致.而label标签的for效果,只要指定一个id,那么点击label的效果和点击该id对象的效果是一样.也就是点击"全选"的文字,和点击复选框的效果是一样一样的.
<input type="checkbox" id="cbSelectAll" onClick="$('[name=privilegeIds]').attr('checked', this.checked)"/>
<label for="cbSelectAll">全选</label>
最后,只要点击最下面的保存,提交form,就可以将privilegeIds,更新到数据库中.登录和注销
前面已经完成了分配权限的功能,现在可以对于登录和注销有一些处理.首先权限分为几种,大体分为两种,需要控制的功能和不需要控制的功能;其中需要控制又可以分为:登录功能,只要没有登录就能使用;以及不需要控制的功能,这部分功能,只要登录了,谁都能用,如使用主页和注销退出;而需要控制的功能,这部分的功能,只有有权限的才能操作,若删除/添加用户.
对于登录和注销没有太复杂,都是跳转到一个页面就可以了.登录要求,输入用户名和密码,验证用户名和密码是否正确,错误显示错误信息,正确将用户信息加到值栈的session中,方便后面的使用,然后跳转到主页面.然后注销的话,先要将放到值栈的session中的用户对象移除.
代码就是这样,在登录中先验证用户名和面是否正确,不正确的话,将错误信息添加到错误字段login中,而jsp页面用
<s:fielderror fieldName="login"></s:fielderror>
,就能接收到错误信息。若是成功了,就添加到session中,然后result为toIndex.然后注销的话,就是从session中remove,然后跳到logout.jsp页面.
@Controller //交给容器管理
@Scope("prototype") //多例
public class UserAction extends BaseAction<User> {
/** 登录UI **/
public String loginUI() throws Exception {
return "loginUI";
}
/** 登录 **/
public String login() throws Exception {
//2个请求
User user = userService.findByLoginNameAndPassword(model.getLoginName(),model.getPassword());
if(user == null) {
addFieldError("login","用户名或密码不正确!");
return "loginUI";
}else {
//登录用户
ActionContext.getContext().getSession().put("user", user);
return "toIndex";
}
}
/** 注销 **/
public String logout() throws Exception {
//1个请求
ActionContext.getContext().getSession().remove("user");
return "logout";
}
...
}
其中,findByLoginNameAndPassword,验证用户名和密码.由于数据库中的密码加密了,所以要比较的话,传入的password也要进行加密处理.
@Service
@Transactional
public class UserServiceImpl extends DaoSupportImpl<User> implements UserService {
public User findByLoginNameAndPassword(String loginName, String password) {
//使用密码的MD5摘要进行对比,将传入的密码进行加密,将加密的密码查找user
String md5Digest = DigestUtils.md5Hex(password);
return (User)getSession().createQuery(//
"FROM User u WHERE u.loginName=? AND u.password=?")//
.setParameter(0,loginName)//
.setParameter(1,md5Digest)//
.uniqueResult();
}
}
再次说明下,若错误要显示到jsp页面,,1.action要添加错误;2.jsp要接收显示错误.
而对于result,为toIndex和logout和loginUI,在struts.xml中配置.开始的时候配置的是toIndex的type为redirectAction.但是后来报错,(There is no Action mapped for namespace / and action name index.jsp -[unknown location]).因为在Action中,没有写“/index.jsp”的方法,所以找不到对应的Action。所以这里type应该直接就是redirect,就直接是重定向到/index.jsp页面,而不是重定向到Action。
而loginUI,由于登录只要没有登录都能登录,所以放到全局result配置中。
<package name="default" namespace="/" extends="struts-default">
...
<!-- 全局的result配置 -->
<global-results>
<result name="loginUI">/WEB-INF/jsp/userAction/loginUI.jsp</result>
...
</global-results>
...
<!-- 用户管理 -->
<action name="user_*" class="userAction" method="{1}">
...
<result name="logout">/WEB-INF/jsp/userAction/logout.jsp</result>
<result name="toIndex" type="redirect">index.jsp</result>
</action>
</package>
现在登录没有问题了,那么显示的主页面,该如何处理?
主页面
首先主页面的框架是这样子的.
第一个frameset中,给第一行分配100px,第三行分配30px,剩下的都是中间的.然后第二个frameset,给第一列分配150px,剩下的都是第二列的.上面的比较好懂,下面的就是实际应用了.
然后要求效果:在"left"中点击超链接,但在"right"中显示页面,这样写<a href="xxx.html" target="right">显示</a> 就可以了.
然后看效果,输入地址写上home_index.action,就可以看到效果了.
只是如何只输入index.jsp就能跳转到主页?如下,在index.jsp页面中,让他重定向到/home_index.action中就可以了.
现在可以看到左侧菜单是没有的,如何写呢?这里准备数据代码,以及显示的代码都不是很难.
左侧菜单
需要注意,左侧的菜单都是权限,若是没有权限,应该看不到那个菜单.并且点击上级菜单,应该是可以展开和收缩菜单的.
给menu的下一个对象写上toggle(),顶级菜单就可以收缩和展开了.
然后有权限的菜单才显示,只要加上if的判断,从session中获取user信息,(用#session,因为session在Map栈中),然后将功能name传入,检查是否有这个功能,有就可以显示.
写监听器就是先写一个自定义的监听器,该监听器要实现监听器接口,然后再在web.xml文件中配置监听器,由于该监听器需要使用Spring容器来注入对象,所以要写在Spring的监听器的下面.写完监听器之后,运行发现有懒加载异常.
之前不用用OpenSessionInViewFilter解决了这个问题吗?因为之前的做法是,将session的关闭推迟到一个request请求之后结束,但是Tomcat启动的时候,初始化的这个,不是一个请求,所以将相关的懒加载去掉.
然后,对于代码中注释掉的注解.现在没注释掉的写法是,serviceImpl的获取是通过获取到Spring容器,然后从容器中获取的.之前本来应该可以通过@Resource这样注入的,为什么在这里不行呢?
首先对于serviceImpl来说,满足了2个条件,Spring能扫描到以及写了@Serivice的注解,所以Spring容器中是一定有的.而在InitListener中要注入service,InitListener也要写上注解@Component,交由容器来管理,这样才能顺利注入.但是即使这样做了,可以看看,在web.xml中配置的监听器,写的是InitListener的全名,而Tomcat实例化对象是通过new来实现的.尽管写上了注解,但是Tomcat不会用Spring中的,只会用它自己实例化的.所以,既然InitListener用的不是Spring的,那么注入service就不行了.所以只能手动获取.
而自己再new一个spring容器的话,本身已经有一个了,这样就有2个spring容器.所以如何获取那个spring容器?
使用这个WebApplicationContextUtils工具,类,传入一个指定的Key值就可以获取了,WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext()).
最后效果就是这样
现在左侧菜单已经显示了,并且还做了权限控制,没有权限的菜单就看不到.但是除了这个,还需要控制页面元素,对于某些功能没有权限的用户,应该控制一些链接看不到,如没有删除权限,则页面上的删除的链接应该看不到.
使用权限--显示有权限的链接
权限的控制比较好做,只要和判断权限Name一样,判断url就可以.但是由于要控制的话,所有的a标签都需要进行权限的控制,所有的都要写这么一份代码,太费劲了,若是修改最原始的a标签,就可以去掉代码的重复了,但是如何修改源码?
首先在struts-tags.tld,这个在struts-core的jar中,然后找到a标签,对应的类.
不过源码肯定是改不了的,我们要如何做才能不改源码,但是达到该源码的效果?
按照一个规则,若是jar包中和项目中有一个一样的名的类,那么会优先加载class文件中的,也就是我们项目中的.所以,在项目中添加一个包org.apache.struts2.views.jsp.ui和一个文件AnchorTag,然后重写doEndTag方法,在这里面,获取路径,并进行判断.
public class AnchorTag extends AbstractClosingTag {
...
@Override
public int doEndTag() throws JspException {
//当前登录用户
User user = (User)pageContext.getSession().getAttribute("user");
//当前准备显示的链接对应的权限URL
//>>在开头加上'/'
String privUrl = "/"+ action;
if(user.hasPrivilegeByUrl(privUrl)) {
return super.doEndTag(); //正常的生成并显示超链接标签,并继续执行页面中后面的代码
} else {
return EVAL_PAGE;//什么都不做,只是继续执行页面中后面的代码 SKIP_PAGE:跳过页面中剩余的其他代码
}
}
...
其中hasPrivilegeByUrl,是将url链接如
<s:acssClass="ForumPageTopic"action="forum_show?id=%{id}">${name}</s:a>,要去掉后面的id参数,只保留forum_show和数据库中的链接进行比较.如果不在数据库中,则可以登录,其中若是乱写一个action,因为在struts.xml中没有对应的配置,会直接报错;若在,就检查下该用户的角色是否有该权限.有就可以显示链接,否则就不显示.
/**
* 判断本用户是否有指定url的权限
* @param privUrl
* @return
*/
public boolean hasPrivilegeByUrl(String privUrl) {
//超级管理员有所有的权限
if(isAdmin()) {
return true;
}
//>>去掉后面的参数
int pos = privUrl.indexOf("?");
if(pos > -1) {
privUrl = privUrl.substring(0,pos);
}
//>>去掉UI后缀
if(privUrl.endsWith("UI")) {
privUrl = privUrl.substring(0,privUrl.length()-2);
}
//如果本URL不需要控制,则登录用户就可以使用
Collection<String> allPrivilegeUrls =(Collection<String>) ActionContext.getContext().getApplication().get("allPrivilegeUrls");
if(!allPrivilegeUrls.contains(privUrl)) {
return true;
} else {
//普通用户要判断是否含有这个权限
for(Role role:roles) {
for(Privilege priv:role.getPrivileges()) {
if(privUrl.equals(priv.getUrl())) {
return true;
}
}
}
}
return false;
}
到目前为止,权限已经控制的很好了.可以控制菜单的是否显示,以及页面链接元素的是否显示.还有一种需要控制,就是是否登录的控制,对于所有没有登录的,必须要让其去登录,登录成功才能访问。
拦截验证所有请求的权限
所以,要拦截所有的请求,进行是否登录的判断,没有登录的跳转到登录页面,让其登录,登录了就要看请求是否有权限,没有权限就跳转到“没有权限的”提示页面。当然这个拦截器要放在所有拦截器的最外面,第一个进行判断。整个原理是这样的,
先写一个拦截器,然后配置到struts.xml中,由于需要Action在的请求最开始就拦截,所以要重新用一个默认拦截器栈,将验证权限放在最上面.
代码,拦截器要继承AbstractInterceptor 抽象的拦截器,然后获取到当前登录用户,以及从invocation中获取url,如果还没有登录,正要登录,就不用权限控制,放行;如果不是就去登录;如果已经登录,就判断是否有权限,有就放行,没有就转到noPrivilegeError.jsp页面,提示"没有权限访问此功能".
public class CheckPrivilegeInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
//获取信息
User user = (User)ActionContext.getContext().getSession().get("user");//当前登录用户
String namespace = invocation.getProxy().getNamespace();
String actionName = invocation.getProxy().getActionName();
String privUrl = namespace+actionName;//对应的权限URL
//如果未登录,就转到登录页面
if(user == null) {
//如果是去登录,则放行
if(privUrl.startsWith("/user_login")) {
return invocation.invoke();
}else {
//如果不是就转到登录页面
return "loginUI";
}
}
//如果已登录,就判断权限
else {
if(user.hasPrivilegeByUrl(privUrl)) {
//如果有权限,就放行
return invocation.invoke();
}else {
//如果没有权限,就转到提示页面
return "noPrivilegeError";
}
}
}
}
然后配置struts.xml,配置拦截器栈.
<interceptors>
<!-- 声明有一个拦截器 -->
<interceptor name="checkPrivilege" class="cn.itcast.oa.util.CheckPrivilegeInterceptor"></interceptor>
<!-- 重新定义默认的拦截器栈 -->
<interceptor-stack name="defaultStack">
<interceptor-ref name="checkPrivilege"></interceptor-ref>
<interceptor-ref name="defaultStack"></interceptor-ref>
</interceptor-stack>
</interceptors>
效果就是这样.没有登录的效果就是跳到登录页面,没有权限的,就跳到提示页面。
权限的功能基本就完了。下面的论坛管理中的上下移动功能。
上下移动
对于版块需要有排序,并且可以修改顺序。所以,有上移和下移的功能。效果就是这样
最上面的不能上移,最下面的不能下移;并且点击上移和下移就可以修改位置。做法一般有两种,1种是前台JS修改顺序,然后再进行保存;还有就是这种,上移和下移是直接和后台交互。
下面先上JSP页面的代码,其中iterator有一个属性为status状态,然后key为status,然后用first和last就能判断是不是iterator中的第一条或者最后一条记录.
<style type="text/css">
.disabled{
color: gray;
cursor: pointer;
}
</style>
<s:iterator value="#forumList" status="status">
<tr class="TableDetail1 template">
<td>${name} </td>
<td>${description} </td>
<td>
<s:a action="forumManage_delete?id=%{id}" οnclick="return delConfirm()">删除</s:a>
<s:a action="forumManage_editUI?id=%{id}">修改</s:a>
<!-- 最上面的不能上移 -->
<s:if test="#status.first">
<span class="disabled">上移</span>
</s:if>
<s:else>
<s:a action="forumManage_moveUp?id=%{id}">上移</s:a>
</s:else>
<!-- 最下面的不能下移 -->
<s:if test="#status.last">
<span class="disabled">下移</span>
</s:if>
<s:else>
<s:a action="forumManage_moveDown?id=%{id}">下移</s:a>
</s:else>
</td>
</tr>
</s:iterator>
而status,对应的是IteraotrStatus,可以看到first和last的大概说明。
现在前台代码准备好了,写后台的代码。Action中的代码,就是调用service中的moveUp和moveDown,所以直接上serviceImpl中的moveUp和moveDown代码.思路就是:对于上移,获取当前选中的,然后通过sql语句,找到当前上面的那个,然后交换他们的position字段的值.
/**
* 上移
*/
public void moveUp(Long id) {
//上移,找出相关的Forum,
Forum forum = getById(id);//当前要移动的Forum
//找到当前Forum上面的Forum,
Forum other = (Forum) getSession().createQuery(//
"FROM Forum f WHERE f.position<? ORDER BY f.position DESC")//
.setParameter(0, forum.getPosition())//
.setFirstResult(0)//
.setMaxResults(1)//
.uniqueResult(); //我上面的那个Forum
//最上面的不能上移
if(other == null) {
return;
}
//交换position的值
int temp = forum.getPosition();
forum.setPosition(other.getPosition());
other.setPosition(temp);
//更新到数据库中,可以不写,因为对象现在是持久化状态,会自动更新
getSession().update(forum);
getSession().update(other);
}
/**
* 下移
*/
public void moveDown(Long id) {
//下移,找出相关的Forum,
Forum forum = getById(id);//当前要移动的Forum
//找到当前Forum的下面一个Forum
Forum other = (Forum) getSession().createQuery(//
"FROM Forum f WHERE f.position>? ORDER BY f.position ASC")//
.setParameter(0, forum.getPosition())//
.setFirstResult(0)//
.setMaxResults(1)//
.uniqueResult(); //我上面的那个Forum
//最上面的不能上移
if(other == null) {
return;
}
//交换position的值
int temp = forum.getPosition();
forum.setPosition(other.getPosition());
other.setPosition(temp);
//更新到数据库中,可以不写,因为对象现在是持久化状态,会自动更新
getSession().update(forum);
getSession().update(other);
}
这样上下移动也就可以完成了.
第六天的总结,写完了,深刻了解了不少权限控制的操作,还有上下移动,还有最后一篇就完了,加油!