主页
Target="right" 而这个right对应name="right"的地方.这样一来,在第二行左边点击超链接,就可以在右边显示页面了.
先将静态页面的五个页面(主页,上下左右页面)复制到新建在jsp中的homeAction文件夹中,
原本的静态页面都是html,要将html改为jsp:将jsp的约束代码和导入公共页面的代码复制进html中,再将后缀名都改为.jsp
新建一个HomeAction:
@Controller public class HomeAction extends ActionSupport{ public String index() throws Exception { return "index"; } public String top() throws Exception { return "top"; } public String bottom() throws Exception { return "bottom"; } public String left() throws Exception { return "left"; } public String right() throws Exception { return "right"; } } |
Tips:这个action只做跳转的作用,所以只需要继承ActionSupport.
开多例@Scope("prototype")是为了线程安全,而在这个action中没有公共的资源,所以不存在线程安全问题,也就不用开多例了.
Struts.sml:
<!-- 首页 --> <action name="home_*" class="homeAction" method="{1}"> <result name="{1}">/WEB-INF/jsp/homeAction/{1}.jsp</result> </action> |
Tips:因为action中每个方法的返回值都和方法同名,所以可以这么写.
一般用户访问的是放在外面的index.jsp,然后再由这个页面跳转到被深藏起来的index.jsp
所以放在外面的index.jsp只有跳转的作用.
(放在外面的)Index.jsp:
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% response.sendRedirect(request.getContextPath() + "/home_index.do"); %> |
(放在homeAction中的)Index.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <title>ItcastOA</title> <%@ include file="/WEB-INF/jsp/public/header.jspf" %> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head>
<frameset rows="100,*,25" framespacing="0" border="0" frameborder="0"> <frame src="home_top.do" name="TopMenu" scrolling="no" noresize /> <frameset cols="180,*" id="resize"> <frame noresize name="menu" src="home_left.do" scrolling="yes" /> <frame noresize name="right" src="home_right.do" scrolling="yes" /> </frameset> <frame noresize name="status_bar" scrolling="no" src="home_bottom.do" /> </frameset>
<noframes> <body> </body> </noframes> </html> |
这里的链接,如home_top.do,还要经过action,再跳到相应jsp页面.
修改下top.jsp:
<div id="Head1Right"> <div id="Head1Right_UserName"> <img border="0" width="13" height="14" src="style/images/top/user.gif" /> 您好,<b>${user.name }</b> </div> <div id="Head1Right_SystemButton"> <s:a target="_parent" action="loginout_logout"> <img width="78" height="20" alt="退出系统" src="style/blue/images/top/logout.gif" /> </s:a> </div> |
- target="_parent"
这行代码在这里的作用是:当退出系统时,注销页面会占用整个页面,而不是只在top(第一行)显示!!!
显示左侧菜单
显示左侧菜单(一)
左侧菜单是二级菜单,权限数据不会被改变,所以可以采用两重遍历的方式来实现左侧菜单.
Left.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <title>导航菜单</title> <%@ include file="/WEB-INF/jsp/public/header.jspf"%> <link type="text/css" rel="stylesheet" href="style/blue/menu.css" /> </head> <body style="margin: 0"> <div id="Menu"> <ul id="MenuUl"> <!-- 第一级权限 --> <s:iterator value="topPrivilegeList"> <li class="level1"> <div onClick="menuClick(this)" class="level1Style"> <img src="style/images/MenuIcon/FUNC20001.gif" class="Icon" />${name } </div> <ul style="display: none;" class="MenuLevel2"> <!-- 第二级权限 --> <s:iterator value="children"> <li class="level2"> <div class="level2Style"> <img src="style/images/MenuIcon/menu_arrow_single.gif" /> ${name } </div> </li> </s:iterator> </ul> </s:iterator> </ul> </div> </body> </html> |
- 因为权限数据一旦被初始化就不能被改变了,所以每次查询的顶级权限集合topPrivilegeList都是一样的.可以采用这样的方式:查一次topPrivilegeList,将其放入最大作用域:application作用域中(如果是放入session作用域中,那只有登陆用户能用,而放到application作用域中的话,所以用户都能用),再利用监听器Listener,(效果是)启动应用程序时,在所有请求到来之前就将topPrivilegeList装好.
在util包中新建一个OAInitListener:
@Component
public class OAInitListener implements ServletContextListener {
private Log log =LogFactory.getLog(OAInitListener.class);
@Resource
private PrivilegeService privilegeService;
//初始化
public void contextInitialized(ServletContextEvent sec) {
List<Privilege> topPrivilegeList = privilegeService.findtopPrivilegeList();
ActionContext.getContext().put("topPrivilegeList", topPrivilegeList);
log.info("======= topPrivilegeList已经放到application作用域中了! =======");
}
//销毁
public void contextDestroyed(ServletContextEvent sec) {
}
}
创建监听器之后,还得在web.xml中配置:
<!-- 配置Spring的用于初始化ApplicationContext对象的监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext*.xml</param-value> </context-param>
<!-- 配置自己的用于初始化的监听器,一定要配置到spring的ContextLoaderListener后面,因为要用到spring容器对象 (先写的先配置) --> <listener> <listener-class>cn.itcast.oa.util.OAInitListener</listener-class> </listener>
<!-- 配置Spring的OpenSessionInViewFilter以解决懒加载异常的问题 --> <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> </filter-mapping>
<!-- 配置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>/*</url-pattern> </filter-mapping> |
虽然把OAInitListener放到了spring容器中,也注入了service,但是web.xml和spring没有关联,其中配置监听器的代码不是从容器中拿对象,而是用类全名通过反射的方式创建实例对象,所以OAInitListener根本拿不到service.
正确的OAInitListener:
public class OAInitListener implements ServletContextListener {
private Log log =LogFactory.getLog(OAInitListener.class);
//初始化 public void contextInitialized(ServletContextEvent sce) { ServletContext application = sce.getServletContext();
//从spring的容器中取出privilegeService的对象实例 WebApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(application); PrivilegeService privilegeService = (PrivilegeService) ac.getBean("privilegeServiceImpl");
//查询所有顶级的权限列表,并放到application作用域中 List<Privilege> topPrivilegeList = privilegeService.findtopPrivilegeList(); application.setAttribute("topPrivilegeList", topPrivilegeList); log.info("======= topPrivilegeList已经放到application作用域中了! =======");
}
//销毁 public void contextDestroyed(ServletContextEvent sce) { } } |
- Tips:不能自己new一个service对象,不然spring不会帮忙管理,一定要用spring已经创建好的对象.而spring会将所有创建好的对象都放到application作用域中,并且提供了一个类WebApplicationContextUtils方便我们取出实例对象.
因为在left.jsp中是这样获取topPrivilegeList的: <s:iterator value="topPrivilegeList">.
这是ognl表达式,这么写并拿不到存在application作用域中的topPrivilegeList.应该改为
Left.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <title>导航菜单</title> <%@ include file="/WEB-INF/jsp/public/header.jspf"%> <link type="text/css" rel="stylesheet" href="style/blue/menu.css" /> </head> <body style="margin: 0"> <div id="Menu"> <ul id="MenuUl"> <!--一级菜单 --> <s:iterator value="#application.topPrivilegeList"> <li class="level1"> <div onClick="menuClick(this)" class="level1Style"> <img src="style/images/MenuIcon/FUNC20001.gif" class="Icon" />${name } </div> <ul style="display: none;" class="MenuLevel2"> <!-- 二级菜单 --> <s:iterator value="children"> <li class="level2"> <div class="level2Style"> <img src="style/images/MenuIcon/menu_arrow_single.gif" /> ${name } </div> </li> </s:iterator> </ul> </s:iterator> </ul> </div> </body> </html> |
但是改成这样之后却会报懒加载异常:
懒加载:对象被加载的时候,其属性不会被加载,只有在真正需要这些资源的时候才会加载。其思想简单来说就是拖到最后一刻,直到万不得已才加载,才开始占用资源。
为什么在web.xml中加上了解决懒加载问题的代码,却还是会出现懒加载问题?
<!-- 配置Spring的OpenSessionInViewFilter以解决懒加载异常的问题 --> <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> </filter-mapping> |
因为web.xml中的这段代码解决的只是同一个请求的懒加载问题,当有多个请求出现懒加载问题时,它是解决不了的.
详解:
- 在同一个请求中,service开启了事务,打开session,拿到对象,却在提交事务,关闭session之后才用到对象的懒加载属性,这样就会出现懒加载异常.所以得延迟关闭session.而web.xml中的那段代码正是为了延迟关闭session.
- 当有多个请求时,比如:
- 现在有两个请求(每个请求都有自己的事务和session),第一个请求拿到对象之后,没用到对象的懒加载属性就关闭了,该对象被存到某个地方.此时第二个请求直接使用对象的懒加载属性,就会出现懒加载异常的问题.
- 因为被加载的对象之和第一个请求有关联,和第二个请求无关.
在此正是多个请求导致的懒加载异常,启动应用程序时topPrivilegeList就被加载了(Listener的特性),过两个小时之后再做第一次访问(第一个请求?),左侧菜单为一个请求,这时就要用到topPrivilegeList的懒加载属性(菜单中的二级菜单:children属性),而他们根本不是同一个请求,所以会出现懒加载异常.解决方案:直接关掉懒加载.
Privilege.hbm.xml:
<!-- children属性,表达的是本对象与Privilege(children)的一对多关系 --> <set name="children" order-by="id ASC" lazy="false"> <key column="parentId"></key> <one-to-many class="Privilege"/> </set> |
同理,登录时(第一个请求),会将user放到session中,而加载user对象时却没有加载user下的roles(懒加载),所以当要用到user下的所有roles时(第二个请求),又会出现懒加载问题.Roles下的privileges也一样,所以都要改.
User.hbm.xml:
<!-- roles属性,表达的是本对象与Role的多对多关系 --> <set name="roles" table="itcast_user_role" lazy="false"> <key column="userId"></key> <many-to-many class="Role" column="roleId"></many-to-many> </set> |
Role.hbm.xml:
<!-- privileges属性,表达的是本对象与Privilege的多对多关系 --> <set name="privileges" table="itcast_role_privilege" lazy="false"> <key column="roleId"></key> <many-to-many class="Privilege" column="privilegeId"></many-to-many> </set> |
修改完成之后的效果图:
因为left.jsp中遍历二级菜单时有这样一行代码style="display: none;",这就导致左侧菜单默认不显示二级菜单,将这行代码删去之后,就会默认显示所有菜单.
1.给左侧菜单添加超链接.因为url已经被存进数据库中
所以可以这么写:
<a href="${pageContext.request.contextPath }${url}.do" target="right">${name }</a>
2.给一级菜单更换图片:
可以把要用到的图片复制一份,将副本名字改成数据库中权限(即菜单)的id值:
然后导入图片的代码改成这样:
<img src="style/images/MenuIcon/${id }.gif" class="Icon" />
3.当点击一级菜单时,会显示他所有的二级菜单,再点击一下就会关闭他所有的二级菜单:
onClick=" $(this).next().toggle() "
此时left.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <title>导航菜单</title> <%@ include file="/WEB-INF/jsp/public/header.jspf"%> <link type="text/css" rel="stylesheet" href="style/blue/menu.css" /> </head> <body style="margin: 0"> <div id="Menu"> <ul id="MenuUl"> <!-- 一级菜单 --> <s:iterator value="#application.topPrivilegeList"> <li class="level1"> <div onClick=" $(this).next().toggle() " class="level1Style"> <img src="style/images/MenuIcon/${id }.gif" class="Icon" /> ${name } </div> <ul class="MenuLevel2"> <!-- 二级菜单 --> <s:iterator value="children"> <li class="level2"> <div class="level2Style"> <img src="style/images/MenuIcon/menu_arrow_single.gif" /> <a href="${pageContext.request.contextPath }${url}.do" target="right">${name }</a> </div> </li> </s:iterator> </ul> </s:iterator> </ul> </div> </body> </html> |
Tips: target="right" 将页面指定在name="right"的地方显示(即右侧)
显示左侧菜单(二):显示有权限的菜单
思路:可以在left.jsp遍历topPrivilegeList的同时,取出当前存在session中的user(正在登录的用户),判断user是否有当前正在遍历的权限,有则显示权限,没有则进行下一次遍历,直到将topPrivilegeList遍历完成.
Left.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <title>导航菜单</title> <%@ include file="/WEB-INF/jsp/public/header.jspf"%> <link type="text/css" rel="stylesheet" href="style/blue/menu.css" /> </head> <body style="margin: 0"> <div id="Menu"> <ul id="MenuUl"> <!-- 一级菜单 --> <s:iterator value="#application.topPrivilegeList"> <s:if test=" #session.user.hasPrivilegeByName(name) "> <li class="level1"> <div onClick=" $(this).next().toggle() " class="level1Style"> <img src="style/images/MenuIcon/${id }.gif" class="Icon" /> ${name } </div> <ul class="MenuLevel2"> <!-- 二级菜单 --> <s:iterator value="children"> <s:if test=" #session.user.hasPrivilegeByName(name) "> <li class="level2"> <div class="level2Style"> <img src="style/images/MenuIcon/menu_arrow_single.gif" /> <a href="${pageContext.request.contextPath }${url}.do" target="right">${name }</a> </div> </li> </s:if> </s:iterator> </ul> </li> </s:if> </s:iterator> </ul> </div> </body> </html> |
Tips:<s:if test=" #session.user.hasPrivilegeByName(name) ">
hasPrivilegeByName()是user中的方法,参数name是当前正在遍历的权限的名字.
Ognl表达式中可以调用方法(而el表达式不能).
User.java中得新增方法:
/** * 判断是否有权限 * @param priviName 权限的名称 */ public boolean hasPrivilegeByName(String privName){ for (Role role : roles) { for (Privilege p : role.getPrivileges()) { if(p.getName().equals(privName)){ return true; } } } return false; } |
然而,测试运行却会报错,因为当前没有登录用户.
这个target是指拥有当前方法的对象实例,在此为user.
当登录admin后,却什么也看不到.
这是因为admin是特殊用户,没有与任何岗位关联.
所以这时还得继续判断是否为admin.
User.java:
/** * 判断是否有权限 * @param priviName 权限的名称 */ public boolean hasPrivilegeByName(String privName){ //如果是超级管理员,就有所有权限 if(isAdmin()){ return true; }
for (Role role : roles) { for (Privilege p : role.getPrivileges()) { if(p.getName().equals(privName)){ return true; } } } return false; }
/* * 判断当前用户是否为admin */ public boolean isAdmin(){ return "admin".equals(loginName); } |
Tips:用equals()进行比较时,一般把一定不为null的值放在左边.因为:(不为nul)l.equals(任何值(包括null))其返回值都不为空,此时就算右边为空,他也会返回false.而如果(null).equals(任何值(包括不为null的值))会抛空指针异常.
三.显示右侧的链接--思路:改jar包中源码的方式
打个比方,如果程序员没有删除用户的权限,那么身为程序员的用户在用户列表页面,要么看到的"用户删除"链接是灰色的,要么根本看不到"用户删除"链接.在此用的是第二种方式.有一下几种方案:
方案1太麻烦,不是用struts标签,而是用其他标签(如原始标签:<a/>)时适合用方案三,在此我们选择用方案四.
首先要找到<s:a/>所在源码:随便找到个<s:a/>标签,按住ctrl,左键单击,就进入了这里:
由此得知该标签所在类为:
<tag-class>org.apache.struts2.views.jsp.ui.AnchorTag</tag-class> |
复制类名,按住CTRL + SHIFT + T :跳转到该类.但是该类是只读形式.
修改方法:新建一个包,包名和该类所在包的包名相同,创建一个类,类名和该类一样,全选复制该类内容,粘贴过来,再修改相应方法.
原理:1.虽然这两个类包名和类名都一样,但他们在不同jar包里,所有这是可行的.2.启动应用程序的时候,所有jar包都会被加载然后被放到缓存中,之后就不会再被加载了,重点是,哪个类先被加载,哪个类就会被使用.项目中,它都是先加载我们自己的类,找不到的话才会去加载lib中的类.
用继承的方式不太好,因为万一该类是final修饰的,那就不可继承,又或者我们要修改的方法是private修饰的,那也不能继承.
找该类所在包名的方法:
具体修改:
AnchorTag.java:
@Override public int doEndTag() throws JspException { //获取当前登录的用户 User user = (User) pageContext.getSession().getAttribute("user"); if(user == null){ throw new RuntimeException("当前没有登陆用户!"); }
//获取所需要的权限url(在action属性值中,但需要处理一下) String privUrl = "/" + action;
//a.去掉后面的参数字符串(如果有) int pro = privUrl.indexOf("?"); if(pro > -1){ privUrl = privUrl.substring(0, pro); } //b.去掉后面的UI后缀(如果有) if(privUrl.endsWith("UI")){ privUrl = privUrl.substring(0, privUrl.length()-2); }
//根据权限决定是否显示超链接 if(user.hasPrivilegeByUrl(privUrl)){ return super.doEndTag(); //输出<a/>标签,并继续执行此标签后面的jsp代码 }else{ return BodyTagSupport.EVAL_PAGE; //不输出<a/>,并继续执行此标签后面的jsp代码 } } |
- Tips: 1.doEndTag()是被继承过来的方法,所有需要重载;
2. BodyTagSupport.EVAL_PAGE; //不输出<a/>,并继续执行此标签后面的jsp代码.如果没有权限,就不输出<a/>,也就看不到超链接了;
3.在这个类中,Struts框架对action进行了定义,页面上的超链接地址
就是保存到了action中,所以在这里可以直接String privUrl = "/" + action;加"/"是因为数据库中的url地址
4.不确定privUrl的截取是否正确的话,可以在 if(pro > -1){ 这行打断点.首先登录用户,然后它会自动跳到如下界面:
点击一下页面上的用户管理(即让程序进入对privUrl的判断),对String privUrl = "/" + action;中的privUrl右键单击,选择"Watch",会出现如下窗口:
然后多次单击
进入下一行代码,多次观察Expressions窗口中的privUrl的值是否正确.此时再点击
进入下一次判断,再点击…进入下一行代码,观察…如此反复操作.
前面都加了"/";
此时Uer.java再新增一个方法(通过url来判断是否有权限):
/** * 判断是否有权限 * @param priviUrl 权限的url */ public boolean hasPrivilegeByUrl(String privUrl) { //如果是超级管理员,就有所有的权限 if(isAdmin()){ return true; } //如果是一般用户,就得判断是否有权限 for (Role role : roles) { for (Privilege p : role.getPrivileges()) { if(privUrl.equals(p.getUrl())){ return true; } } } return false; } |
Tips: privUrl.equals(p.getUrl())将privUrl放在前面是因为p.getUrl有可能为null.若是顶级菜单就没有url.
以后还可能需要通过判断url来判断是否有权限,那么就可以将AnchorTag.java中对url的判断语句放到user.java里,修改后
AnchorTag.java:
@Override public int doEndTag() throws JspException { //获取当前登录的用户 User user = (User) pageContext.getSession().getAttribute("user"); if(user == null){ throw new RuntimeException("当前没有登陆用户!"); }
//获取所需要的权限url(在action属性值中,但需要处理一下) String privUrl = "/" + action;
//根据权限决定是否显示超链接 if(user.hasPrivilegeByUrl(privUrl)){ return super.doEndTag(); //输出<a/>标签,并继续执行此标签后面的jsp代码 }else{ return BodyTagSupport.EVAL_PAGE; //不输出<a/>,并继续执行此标签后面的jsp代码 } } |
User.java:
/** * 判断是否有权限 * @param priviUrl 权限的url */ public boolean hasPrivilegeByUrl(String privUrl) { //如果是超级管理员,就有所有的权限 if(isAdmin()){ return true; }
//a.去掉后面的参数字符串(如果有) int pro = privUrl.indexOf("?"); if(pro > -1){ privUrl = privUrl.substring(0, pro); } //b.去掉后面的UI后缀(如果有) if(privUrl.endsWith("UI")){ privUrl = privUrl.substring(0, privUrl.length()-2); }
//如果是一般用户,就得判断是否有权限 for (Role role : roles) { for (Privilege p : role.getPrivileges()) { if(privUrl.equals(p.getUrl())){ return true; } } } return false; }
/* * 判断当前用户是否为admin */ public boolean isAdmin(){ return "admin".equals(loginName); } |
拦截验证每一个请求的权限
作用:避免用户在没有登录的情况下,却能通过直接输入url的方式来进行某些操作.比如没有登录时输入http://localhost:8080/ItcastOA/user_list.do来查看用户列表,这是不允许的,必须得先登录,然后再判断用户是否具有进行该操作的权限.
(一)测试拦截器
(这集没声音,光知道操作,理解不深,所以谋得解释啊啊啊啊啊)
先在util包中创建一个类
CheckPrivilegeInterceptor:
package cn.itcast.oa.util;
import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class CheckPrivilegeInterceptor extends AbstractInterceptor {
@Override public String intercept(ActionInvocation invocation) throws Exception { System.out.println("======>拦截器(前)<======"); String result = invocation.invoke(); // 放行 System.out.println("======>拦截器(后)<======"); return result; } } |
接着是到struts.xml中进行配置:
<interceptors> <!-- 声明拦截器 --> <interceptor name="CheckPrivilege" class="cn.itcast.oa.util.CheckPrivilegeInterceptor"></interceptor> <!-- 配置我们自己的拦截器栈 --> <interceptor-stack name="myDefaultStack"> <interceptor-ref name="CheckPrivilege"></interceptor-ref> <interceptor-ref name="defaultStack"></interceptor-ref> </interceptor-stack> </interceptors> <!-- 配置默认的拦截器栈 --> <default-interceptor-ref name="myDefaultStack"></default-interceptor-ref> |
一定要引入默认拦截器.这段配置代码应该是要放到前面的.
(二)
CheckPrivilegeInterceptor:
package cn.itcast.oa.util;
import cn.itcast.oa.domain.User;
import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class CheckPrivilegeInterceptor extends AbstractInterceptor {
@Override public String intercept(ActionInvocation invocation) throws Exception {
//准备数据 //a.获取当前登录的用户 User user = (User) ActionContext.getContext().getSession().get("user"); //b.获取当前访问的url String namespace = invocation.getProxy().getNamespace(); String actionName = invocation.getProxy().getActionName(); if(null == namespace || "".equals(namespace)){ namespace = "/"; } if(!namespace.endsWith("/")){ namespace += "/"; } String url = namespace + actionName;
//一,如果用户未登录,则转到登录页面 if(user == null){ //a.如果当前访问的是登录页面:loginout_loginUI,loginout_login,则放行 if(url.startsWith("/loginout_login")){ return invocation.invoke();
//b.如果访问的不是登录登录功能,就转到登录页面 }else{ return "loginUI"; }
//二,如果用户已登录,则判断权限 }else{ //a.如果有权限访问当前url,则放行 if(user.hasPrivilegeByUrl(url)){ return invocation.invoke();
//b.如果没有权限访问当前url,则转到提示消息的页面 }else{ return "noPrivilegeUI"; } } } } |
创建noPrivilegeUI.jsp(作用:提示没有权限),并套用静态页面:
noPrivilegeUI.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"%> <HTML> <HEAD> <TITLE>没有权限</TITLE> <%@ include file="/WEB-INF/jsp/public/header.jspf" %> <SCRIPT TYPE="text/javascript"> </SCRIPT> </HEAD> <BODY>
<DIV ID="Title_bar"> <DIV ID="Title_bar_Head"> <DIV ID="Title_Head"></DIV> <DIV ID="Title"><!--页面标题--> <IMG BORDER="0" WIDTH="13" HEIGHT="13" SRC="${pageContext.request.contextPath}/style/images/title_arrow.gif"/> 提示 </DIV> <DIV ID="Title_End"></DIV> </DIV> </DIV>
<!--显示表单内容--> <DIV ID="MainArea"> <DIV CLASS="ItemBlock_Title1"> </DIV>
<DIV CLASS="ItemBlockBorder" STYLE="margin-left: 15px;"> <DIV CLASS="ItemBlock" STYLE="text-align: center; font-size: 16px;"> 出错了,您没有权限访问此功能! </DIV> </DIV>
<!-- 操作 --> <DIV ID="InputDetailBar"> <A HREF="javascript:history.go(-1);"><IMG SRC="${pageContext.request.contextPath}/style/images/goBack.png"/></A> </DIV>
</DIV>
</BODY> </HTML> |
在struts.xml中配置全局变量(拦截器中的返回值和action中的返回值效果是一样的):
<!-- 全局配置 --> <global-results> <result name="loginUI">/WEB-INF/jsp/loginoutAction/loginUI.jsp</result> <result name="noPrivilegeUI">/noPrivilegeUI.jsp</result> </global-results> |
到此结束的话,系统会存在一个很大的漏洞:因为还有很多登录用户都可以访问的url(也就是用户登录之后,不需要再进行控制的功能(如首页,注销))没被存进数据库中.这就导致用户被系统误判为是没有权限!!
思路:将所有权限的url查询出来放到一个集合中,如果是超级管理员,则它有所有权限(返回true).如果当前要访问的权限url不在权限url集合中,返回true(代表只要是登录用户都可以访问);如果当前权限url在权限url集合中,则要继续判断当前用户是否具有该权限.
由于权限url是固定的(从初始化权限数据开始就固定下来了),所有可以利用监听器的思想.在启动应用程序,所有请求到来之前,就将url集合放入最大作用域application中(这样就只需要查询一次,以后都不用再查了).
User.java:
/** * 判断是否有权限 * @param priviUrl 权限的url */ public boolean hasPrivilegeByUrl(String privUrl) { //如果是超级管理员,就有所有的权限 if(isAdmin()){ return true; }
//a.去掉后面的参数字符串(如果有) int pro = privUrl.indexOf("?"); if(pro > -1){ privUrl = privUrl.substring(0, pro); } //b.去掉后面的UI后缀(如果有) if(privUrl.endsWith("UI")){ privUrl = privUrl.substring(0, privUrl.length()-2); }
//如果是一般用户,就得判断是否有权限 //a.如果这个url是不需要控制的功能(登录后就可以直接使用的,如首页,注销),这时应该直接返回ture Collection<String> allPrivilegeUrls = (Collection<String>) ActionContext.getContext().getApplication().get("allPrivilegeUrls"); if(!allPrivilegeUrls.contains(privUrl)){ return true;
//b.如果这个url是需要控制的功能(登录还得有对应权限才能使用),这时应该判断权限 }else{ for (Role role : roles) { for (Privilege p : role.getPrivileges()) { if(privUrl.equals(p.getUrl())){ return true; } } } return false; } } /* * 判断当前用户是否为admin */ public boolean isAdmin(){ return "admin".equals(loginName); } |
OAInitListener.java:
package cn.itcast.oa.util;
import java.util.List;
import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils;
import cn.itcast.oa.domain.Privilege; import cn.itcast.oa.service.PrivilegeService;
import com.opensymphony.xwork2.ActionContext; @SuppressWarnings("unused") public class OAInitListener implements ServletContextListener {
private Log log =LogFactory.getLog(OAInitListener.class);
//初始化 public void contextInitialized(ServletContextEvent sce) { ServletContext application = sce.getServletContext();
//从spring的容器中取出privilegeService的对象实例 WebApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(application); PrivilegeService privilegeService = (PrivilegeService) ac.getBean("privilegeServiceImpl");
//查询所有顶级的权限列表,并放到application作用域中 List<Privilege> topPrivilegeList = privilegeService.findtopPrivilegeList(); application.setAttribute("topPrivilegeList", topPrivilegeList); log.info("======= topPrivilegeList已经放到application作用域中了! =======");
//查询所有权限列表,并放到application作用域中 List<String> allPrivilegeUrls = privilegeService.getAllPrivilegeUrls(); application.setAttribute("allPrivilegeUrls", allPrivilegeUrls); log.info("======= allPrivilegeUrls已经放到application作用域中了! =======");
}
//销毁 public void contextDestroyed(ServletContextEvent sce) { }
} |
privilegeServiceImpl:
/** * 查询所有权限的url */ public List<String> getAllPrivilegeUrls() { return getSession().createQuery(// "select distinct p.url from Privilege p where p.url is not null")// .list(); } |
查出来的url要避免空值和重复的值,因为数据库中就有空值和重复的值:
(三)一些优化
1).用户登录后,在Tomcat重启后,还应是登录状态.
原理: tomcat在(正常)关闭的时候,会把内存中的session序列化到本地硬盘上,再启动服务器时,会把反序列化拿到硬盘中还原session信息.
演示:
(此时服务器开着)正常关闭服务器,会看到session被序列化到了本地硬盘上:
再启动服务器,session的序列化信息又会被还原到内存中:
前提:User.java和它所关联的所有实体都实现序列化: implements Serializable.
效果:
打开服务器,以超级管理员的身份登录系统:
此时右键TomcatàRestart:
刷新页面,原本系统会跳到登录页面,要求重新登录,可实现序列化后,session中的user还在,所以用户还是保持着登录状态,不需要再重新登录了:
2)页面嵌套的问题.
演示问题:
- 先退出当前用户(有两种方法):
第一种方法是启动服务器,登录用户,右键退出按钮,单击"在新窗口中打开链接"
在新窗口中刷新一次.(即退出了当前用户)
第二种方法是直接等待session过期(期间不要进行任何操作),session默认过期时间是半小时.
- 当退出用户后,在页面上点击任何一个超链接,系统都会转到登录界面.此时就会出现页面嵌套的情况:
登录后也还有页面嵌套的问题:
此时手动刷新一下,页面就会恢复正常了.
我们要做的就是避免页面嵌套的问题,应在loginUI.jsp中加入自动刷新的代码:
<script type="text/javascript"> if(window.parent != window){ window.parent.location.href = window.location.href; } </script> |
Window指的就是整个页面(整个大框),所以window是没有上级的,它的parent就是它自己.当window.parent != window,说明出现了页面嵌套的情况,这时页面会自动刷新window.parent.location.href = window.location.href;
- 总结:
1.显示左侧菜单需要在left.jsp中进行两次遍历(第一次是TopPrivilegeList,第二次是children.其中TopPrivilegeList是利用监听器放到最大作用域application中的), 在两次遍历的同时还分别调用了User中的hasPrivilegeByName()来判断是否有权限;
2.显示右侧超链接是用了修改jar源码的方式,还调用了User中的hasPrivilegeByUrl()来判断是否有权限;
3.拦截每一次请求是专门写了一个拦截器,其中也调用了User中的hasPrilegeByUrl()来判断是否有权限.拦截器的思路:用户未登录的时候有两种情况:要是当前要访问的url是登录页面的话,就放行,否则就跳转到登录页面.用户已登陆的时候,也有两种情况:要是没有当前访问的url权限,则提示没有权限,要是有权限就放行.
而因为有很多登录用户都能访问的url没有被存进数据库中(比如首页,注销),所以当用户访问这些url时也会被认为是没有权限而无法访问.这是我们就应该修改User中的hasPrilegeByUrl().hasPrilegeByUrl()的思路是:若是超级管理员则有所有权限(返回true),处理被传递过来的url,对url进行判断.若是当前要访问的url不在所有权限url集合中,则返回true.(这说明所有登录用户都能访问这个url).否则就要进一步判断用户是否有权限了.至于url集合:因为权限url是固定的,所以它也可以利用监听器存到最大作用域application中(好处是查一次就好了,查过之后它会被放入缓存中,以后用的时候就直接从缓存中拿,而不用查第二次了).
- 小技巧:
当这个双箭头被选中时,右边打开任何一个文件,在左边都会实时显示该文件所在的位置.如:
快捷键:CTRL+SHIFT+T 查找所有java文件,按回车后会跳到那个页面.比如按CTRL+SHIFT+T后打入UserService,按回车之后就跳到了UserService页面.
按home,光标会跳到当前行的行首,按end,光标会跳到当前行的行末.